diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs b/Assets/Mirror/Authenticators/BasicAuthenticator.cs index 0fdc7e2..92ff68c 100644 --- a/Assets/Mirror/Authenticators/BasicAuthenticator.cs +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -17,7 +16,7 @@ public class BasicAuthenticator : NetworkAuthenticator public string username; public string password; - readonly HashSet connectionsPendingDisconnect = new HashSet(); + readonly HashSet connectionsPendingDisconnect = new HashSet(); #region Messages @@ -51,7 +50,7 @@ public override void OnStartServer() /// /// Called on server from StopServer to reset the Authenticator - /// Server message handlers should be registered in this method. + /// Server message handlers should be unregistered in this method. /// public override void OnStopServer() { @@ -60,7 +59,7 @@ public override void OnStopServer() } /// - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate /// /// Connection to client. public override void OnServerAuthenticate(NetworkConnectionToClient conn) @@ -153,7 +152,7 @@ public override void OnStopClient() } /// - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate /// public override void OnClientAuthenticate() { @@ -163,7 +162,7 @@ public override void OnClientAuthenticate() authPassword = password }; - NetworkClient.connection.Send(authRequestMessage); + NetworkClient.Send(authRequestMessage); } /// diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta index 4765013..d4581cf 100644 --- a/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/BasicAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs index 6723cc9..5e0ea56 100644 --- a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs +++ b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs @@ -4,7 +4,7 @@ namespace Mirror.Authenticators { /// - /// An authenicator that identifies the user by their device. + /// An authenticator 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. @@ -47,7 +47,7 @@ public override void OnStopServer() } /// - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate /// /// Connection to client. public override void OnServerAuthenticate(NetworkConnectionToClient conn) @@ -94,7 +94,7 @@ public override void OnStopClient() } /// - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate /// public override void OnClientAuthenticate() { @@ -111,7 +111,7 @@ public override void OnClientAuthenticate() } // send the deviceUniqueIdentifier to the server - NetworkClient.connection.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } ); + NetworkClient.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } ); } /// diff --git a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta index 9ca9f64..3c115ca 100644 --- a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta +++ b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/DeviceAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef index 16cdfbc..70eacf3 100644 --- a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef @@ -1,14 +1,16 @@ { "name": "Mirror.Authenticators", + "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], - "optionalUnityReferences": [], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [] + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta index 2731701..af5d5bd 100644 --- a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta index b19ddec..a2d3a37 100644 --- a/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta +++ b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/TimeoutAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta index 8b23823..a58bca4 100644 --- a/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta +++ b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs index 89867c1..26aff41 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs @@ -11,47 +11,34 @@ static class PreprocessorDefine [InitializeOnLoadMethod] public static void AddDefineSymbols() { +#if UNITY_2021_2_OR_NEWER + string currentDefines = PlayerSettings.GetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup)); +#else + // Deprecated in Unity 2023.1 string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup); +#endif + // Remove oldest when adding next month's symbol. + // Keep a rolling 12 months of symbols. 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" + "MIRROR_89_OR_NEWER", + "MIRROR_90_OR_NEWER", + "MIRROR_93_OR_NEWER", + "MIRROR_96_OR_NEWER" }; - // only touch PlayerSettings if we actually modified it. + // 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) { +#if UNITY_2021_2_OR_NEWER + PlayerSettings.SetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup), newDefines); +#else + // Deprecated in Unity 2023.1 PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines); +#endif } } } diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta index 30806d0..27860f5 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/AssemblyInfo.cs.meta b/Assets/Mirror/Components/AssemblyInfo.cs.meta index f9af1fa..565e5cb 100644 --- a/Assets/Mirror/Components/AssemblyInfo.cs.meta +++ b/Assets/Mirror/Components/AssemblyInfo.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs index ddb38ea..5fa9397 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs @@ -6,7 +6,7 @@ namespace Mirror.Discovery { [Serializable] - public class ServerFoundUnityEvent : UnityEvent {}; + public class ServerFoundUnityEvent : UnityEvent {}; [DisallowMultipleComponent] [AddComponentMenu("Network/Network Discovery")] @@ -14,27 +14,6 @@ public class NetworkDiscovery : NetworkDiscoveryBase /// Process the request from a client /// diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta index c691a61..c6c23e0 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta +++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/NetworkDiscovery.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs index ac57b75..677bad3 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs @@ -23,29 +23,46 @@ public abstract class NetworkDiscoveryBase : MonoBehaviour { 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("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")] + public bool enableActiveDiscovery = true; + + // broadcast address needs to be configurable on iOS: + // https://github.com/vis2k/Mirror/pull/3255 + [Tooltip("iOS may require LAN IP address here (e.g. 192.168.x.x), otherwise leave blank.")] + public string BroadcastAddress = ""; [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; + [Tooltip("Transport to be advertised during discovery")] + public Transport transport; + + [Tooltip("Invoked when a server is found")] + public ServerFoundUnityEvent OnServerFound; + + // 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; + + public long ServerId { get; private set; } + protected UdpClient serverUdpClient; protected UdpClient clientUdpClient; #if UNITY_EDITOR - void OnValidate() + public virtual void OnValidate() { + if (transport == null) + transport = GetComponent(); + if (secretHandshake == 0) { secretHandshake = RandomLong(); @@ -54,22 +71,31 @@ void OnValidate() } #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() { + ServerId = RandomLong(); + + // active transport gets initialized in Awake + // so make sure we set it here in Start() after Awake + // Or just let the user assign it in the inspector + if (transport == null) + transport = Transport.active; + // Server mode? then start advertising -#if UNITY_SERVER - AdvertiseServer(); -#endif + if (Utils.IsHeadless()) + { + AdvertiseServer(); + } + } + + 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); } // Ensure the ports are cleared no matter when Game/Unity UI exits @@ -144,6 +170,8 @@ public void AdvertiseServer() MulticastLoopback = false }; + //Debug.Log($"Discovery: Advertising Server {Dns.GetHostName()}"); + // listen for client pings _ = ServerListenAsync(); } @@ -162,9 +190,7 @@ public async Task ServerListenAsync() // socket has been closed break; } - catch (Exception) - { - } + catch (Exception) {} } } @@ -243,11 +269,12 @@ protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint 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")) @@ -267,7 +294,7 @@ void EndpMulticastLock() { #if UNITY_ANDROID if (!hasMulticastLock) return; - + multicastLock?.Call("release"); hasMulticastLock = false; #endif @@ -324,16 +351,16 @@ public void StopDiscovery() /// ClientListenAsync Task public async Task ClientListenAsync() { - // while clientUpdClient to fix: + // 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, + // 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 + // 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) { @@ -369,6 +396,18 @@ public void BroadcastDiscoveryRequest() IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort); + if (!string.IsNullOrWhiteSpace(BroadcastAddress)) + { + try + { + endPoint = new IPEndPoint(IPAddress.Parse(BroadcastAddress), serverBroadcastListenPort); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { writer.WriteLong(secretHandshake); @@ -381,6 +420,7 @@ public void BroadcastDiscoveryRequest() ArraySegment data = writer.ToArraySegment(); + //Debug.Log($"Discovery: Sending BroadcastDiscoveryRequest {request}"); clientUdpClient.SendAsync(data.Array, data.Count, endPoint); } catch (Exception) diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta index 7dfbaf6..cbd7616 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs index e43c3d7..a34bbe0 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Mirror.Discovery @@ -17,11 +18,20 @@ public class NetworkDiscoveryHUD : MonoBehaviour #if UNITY_EDITOR void OnValidate() { - if (networkDiscovery == null) + if (Application.isPlaying) return; + Reset(); + } + + void Reset() + { + networkDiscovery = GetComponent(); + + // Add default event handler if not already present + if (!Enumerable.Range(0, networkDiscovery.OnServerFound.GetPersistentEventCount()) + .Any(i => networkDiscovery.OnServerFound.GetPersistentMethodName(i) == nameof(OnDiscoveredServer))) { - networkDiscovery = GetComponent(); UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer); - UnityEditor.Undo.RecordObjects(new Object[] { this, networkDiscovery }, "Set NetworkDiscovery"); + UnityEditor.Undo.RecordObjects(new UnityEngine.Object[] { this, networkDiscovery }, "Set NetworkDiscovery"); } } #endif @@ -125,6 +135,8 @@ void Connect(ServerResponse info) public void OnDiscoveredServer(ServerResponse info) { + Debug.Log($"Discovered Server: {info.serverId} | {info.EndPoint} | {info.uri}"); + // 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 index f93b275..c56f65f 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta b/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta index 84f3232..84d4d68 100644 --- a/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta +++ b/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/ServerRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta b/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta index 44f23ba..3ca8f78 100644 --- a/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta +++ b/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/ServerResponse.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Experimental.meta b/Assets/Mirror/Components/Experimental.meta deleted file mode 100644 index 57cce38..0000000 --- a/Assets/Mirror/Components/Experimental.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 06a87a9..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs +++ /dev/null @@ -1,93 +0,0 @@ -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 deleted file mode 100644 index 35ef1fe..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 4989d29..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs +++ /dev/null @@ -1,361 +0,0 @@ -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 deleted file mode 100644 index 1610f0a..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index c14b260..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs +++ /dev/null @@ -1,360 +0,0 @@ -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 deleted file mode 100644 index df466bd..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 index c6acbb8..7055fe1 100644 --- a/Assets/Mirror/Components/GUIConsole.cs +++ b/Assets/Mirror/Components/GUIConsole.cs @@ -31,27 +31,38 @@ public LogEntry(string message, LogType type) public class GUIConsole : MonoBehaviour { - public int height = 150; + public int height = 80; + public int offsetY = 40; // only keep the recent 'n' entries. otherwise memory would grow forever // and drawing would get slower and slower. public int maxLogCount = 50; + // Unity Editor has the Console window, we don't need to show it there. + // unless for testing, so keep it as option. + public bool showInEditor = false; + // log as queue so we can remove the first entry easily - Queue log = new Queue(); + readonly 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; + // Default is BackQuote, because F keys are already assigned in browsers + [Tooltip("Hotkey to show/hide the console at runtime\nBack Quote is usually on the left above Tab\nChange with caution - F keys are generally already taken in Browsers")] + public KeyCode hotKey = KeyCode.BackQuote; // GUI bool visible; Vector2 scroll = Vector2.zero; + // only show at runtime, or if showInEditor is enabled + bool show => !Application.isEditor || showInEditor; + void Awake() { - Application.logMessageReceived += OnLog; + // only show at runtime, or if showInEditor is enabled + if (show) + Application.logMessageReceived += OnLog; } // OnLog logs everything, even Debug.Log messages in release builds @@ -90,7 +101,7 @@ void OnLog(string message, string stackTrace, LogType type) void Update() { - if (Input.GetKeyDown(hotKey)) + if (show && Input.GetKeyDown(hotKey)) visible = !visible; } @@ -98,7 +109,12 @@ void OnGUI() { if (!visible) return; - scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width), GUILayout.Height(height)); + // If this offset is changed, also change width in NetworkManagerHUD::OnGUI + int offsetX = 300 + 20; + + GUILayout.BeginArea(new Rect(offsetX, offsetY, Screen.width - offsetX - 10, height)); + + scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width - offsetX - 10), GUILayout.Height(height)); foreach (LogEntry entry in log) { if (entry.type == LogType.Error || entry.type == LogType.Exception) @@ -110,6 +126,8 @@ void OnGUI() GUI.color = Color.white; } GUILayout.EndScrollView(); + + GUILayout.EndArea(); } } } diff --git a/Assets/Mirror/Components/GUIConsole.cs.meta b/Assets/Mirror/Components/GUIConsole.cs.meta index 5664216..283ae43 100644 --- a/Assets/Mirror/Components/GUIConsole.cs.meta +++ b/Assets/Mirror/Components/GUIConsole.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/GUIConsole.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Distance.meta b/Assets/Mirror/Components/InterestManagement/Distance.meta index 9847902..832357e 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance.meta +++ b/Assets/Mirror/Components/InterestManagement/Distance.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: fa4cbc6b9c584db4971985cb9f369077 -timeCreated: 1613110605 \ No newline at end of file +timeCreated: 1613110605 diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs index 116051b..e4f56ca 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs @@ -8,22 +8,38 @@ namespace Mirror 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; + public int visRange = 500; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] public float rebuildInterval = 1; double lastRebuildTime; + // cache custom ranges to avoid runtime TryGetComponent lookups + readonly Dictionary CustomRanges = new Dictionary(); + // helper function to get vis range for a given object, or default. + [ServerCallback] int GetVisRange(NetworkIdentity identity) { - return identity.TryGetComponent(out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; + return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; } [ServerCallback] - public override void Reset() + public override void ResetState() { lastRebuildTime = 0D; + CustomRanges.Clear(); + } + + public override void OnSpawned(NetworkIdentity identity) + { + if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom)) + CustomRanges[identity] = custom; + } + + public override void OnDestroyed(NetworkIdentity identity) + { + CustomRanges.Remove(identity); } public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) @@ -59,9 +75,8 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet= lastRebuildTime + rebuildInterval) diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta index 1a575af..b0e89d9 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs index 25f5347..12556e5 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs @@ -10,6 +10,6 @@ namespace Mirror public class DistanceInterestManagementCustomRange : NetworkBehaviour { [Tooltip("The maximum range that objects will be visible at.")] - public int visRange = 20; + public int visRange = 100; } } diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta index 406ca78..369f702 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Match.meta b/Assets/Mirror/Components/InterestManagement/Match.meta index e429883..2cfaabc 100644 --- a/Assets/Mirror/Components/InterestManagement/Match.meta +++ b/Assets/Mirror/Components/InterestManagement/Match.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 5eca5245ae6bb460e9a92f7e14d5493a -timeCreated: 1622649517 \ No newline at end of file +timeCreated: 1622649517 diff --git a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs index ff2eadc..d741d0c 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs @@ -7,119 +7,123 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")] public class MatchInterestManagement : InterestManagement { - readonly Dictionary> matchObjects = - new Dictionary>(); + [Header("Diagnostics")] + [ReadOnly, SerializeField] + internal ushort matchCount; - readonly Dictionary lastObjectMatch = - new Dictionary(); + readonly Dictionary> matchObjects = + new Dictionary>(); readonly HashSet dirtyMatches = new HashSet(); - public override void OnSpawned(NetworkIdentity identity) + // LateUpdate so that all spawns/despawns/changes are done + [ServerCallback] + void LateUpdate() { - 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)) + // Rebuild all dirty matches + // dirtyMatches will be empty if no matches changed members + // by spawning or destroying or changing matchId in this frame. + foreach (Guid dirtyMatch in dirtyMatches) { - objects = new HashSet(); - matchObjects.Add(currentMatch, objects); + // rebuild always, even if matchObjects[dirtyMatch] is empty. + // Players might have left the match, but they may still be spawned. + RebuildMatchObservers(dirtyMatch); + + // clean up empty entries in the dict + if (matchObjects[dirtyMatch].Count == 0) + matchObjects.Remove(dirtyMatch); } - objects.Add(identity); + dirtyMatches.Clear(); + + matchCount = (ushort)matchObjects.Count; } - public override void OnDestroyed(NetworkIdentity identity) + [ServerCallback] + void RebuildMatchObservers(Guid matchId) { - lastObjectMatch.TryGetValue(identity, out Guid currentMatch); - lastObjectMatch.Remove(identity); - if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet objects) && objects.Remove(identity)) - RebuildMatchObservers(currentMatch); + foreach (NetworkMatch networkMatch in matchObjects[matchId]) + if (networkMatch.netIdentity != null) + NetworkServer.RebuildObservers(networkMatch.netIdentity, false); } - // internal so we can update from tests + // called by NetworkMatch.matchId setter [ServerCallback] - internal void Update() + internal void OnMatchChanged(NetworkMatch networkMatch, Guid oldMatch) { - // 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; + // This object is in a new match so observers in the prior match + // and the new match need to rebuild their respective observers lists. - 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; + // Remove this object from the hashset of the match it just left + // Guid.Empty is never a valid matchId + if (oldMatch != Guid.Empty) + { + dirtyMatches.Add(oldMatch); + matchObjects[oldMatch].Remove(networkMatch); + } - // Mark new/old matches as dirty so they get rebuilt - UpdateDirtyMatches(newMatch, currentMatch); + // Guid.Empty is never a valid matchId + if (networkMatch.matchId == Guid.Empty) + return; - // 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); - } + dirtyMatches.Add(networkMatch.matchId); - // rebuild all dirty matchs - foreach (Guid dirtyMatch in dirtyMatches) - RebuildMatchObservers(dirtyMatch); + // Make sure this new match is in the dictionary + if (!matchObjects.ContainsKey(networkMatch.matchId)) + matchObjects[networkMatch.matchId] = new HashSet(); - dirtyMatches.Clear(); + // Add this object to the hashset of the new match + matchObjects[networkMatch.matchId].Add(networkMatch); } - void UpdateDirtyMatches(Guid newMatch, Guid currentMatch) + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) { - // Guid.Empty is never a valid matchId - if (currentMatch != Guid.Empty) - dirtyMatches.Add(currentMatch); + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + return; - dirtyMatches.Add(newMatch); - } + Guid networkMatchId = networkMatch.matchId; - 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); + // Guid.Empty is never a valid matchId...do not add to matchObjects collection + if (networkMatchId == Guid.Empty) + return; - // Set this to the new match this object just entered - lastObjectMatch[netIdentity] = newMatch; + // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}"); + if (!matchObjects.TryGetValue(networkMatchId, out HashSet objects)) + { + objects = new HashSet(); + matchObjects.Add(networkMatchId, objects); + } - // Make sure this new match is in the dictionary - if (!matchObjects.ContainsKey(newMatch)) - matchObjects.Add(newMatch, new HashSet()); + objects.Add(networkMatch); - // Add this object to the hashset of the new match - matchObjects[newMatch].Add(netIdentity); + // Match ID could have been set in NetworkBehaviour::OnStartServer on this object. + // Since that's after OnCheckObserver is called it would be missed, so force Rebuild here. + // Add the current match to dirtyMatches for LateUpdate to rebuild it. + dirtyMatches.Add(networkMatchId); } - void RebuildMatchObservers(Guid matchId) + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) { - foreach (NetworkIdentity netIdentity in matchObjects[matchId]) - if (netIdentity != null) - NetworkServer.RebuildObservers(netIdentity, false); + // Don't RebuildSceneObservers here - that will happen in LateUpdate. + // Multiple objects could be destroyed in same frame and we don't + // want to rebuild for each one...let LateUpdate do it once. + // We must add the current match to dirtyMatches for LateUpdate to rebuild it. + if (identity.TryGetComponent(out NetworkMatch currentMatch)) + { + if (currentMatch.matchId != Guid.Empty && + matchObjects.TryGetValue(currentMatch.matchId, out HashSet objects) && + objects.Remove(currentMatch)) + dirtyMatches.Add(currentMatch.matchId); + } } + [ServerCallback] public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { // Never observed if no NetworkMatch component - if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch)) + if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch)) return false; // Guid.Empty is never a valid matchId @@ -127,7 +131,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection return false; // Never observed if no NetworkMatch component - if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch)) + if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch)) return false; // Guid.Empty is never a valid matchId @@ -137,24 +141,24 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId; } + [ServerCallback] public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) { - if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) return; - Guid matchId = networkMatch.matchId; - // Guid.Empty is never a valid matchId - if (matchId == Guid.Empty) + if (networkMatch.matchId == Guid.Empty) return; - if (!matchObjects.TryGetValue(matchId, out HashSet objects)) + // Abort if this match hasn't been created yet by OnSpawned or OnMatchChanged + if (!matchObjects.TryGetValue(networkMatch.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); + foreach (NetworkMatch netMatch in objects) + if (netMatch.netIdentity != null && netMatch.netIdentity.connectionToClient != null) + newObservers.Add(netMatch.netIdentity.connectionToClient); } } } diff --git a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta index 605d215..bc4b6da 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs index f6689f2..0d55fd0 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs +++ b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs @@ -9,7 +9,34 @@ namespace Mirror [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] public class NetworkMatch : NetworkBehaviour { + Guid _matchId; + +#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes + [SerializeField, ReadOnly] + [Tooltip("Match ID is shown here on server for debugging purposes.")] + string MatchID = string.Empty; +#pragma warning restore IDE0052 + ///Set this to the same value on all networked objects that belong to a given match - public Guid matchId; + public Guid matchId + { + get => _matchId; + set + { + if (!NetworkServer.active) + throw new InvalidOperationException("matchId can only be set at runtime on active server"); + + if (_matchId == value) + return; + + Guid oldMatch = _matchId; + _matchId = value; + MatchID = value.ToString(); + + // Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement + if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement) + matchInterestManagement.OnMatchChanged(this, oldMatch); + } + } } } diff --git a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta index 47fc70a..a7158ac 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Scene.meta b/Assets/Mirror/Components/InterestManagement/Scene.meta index 28b469f..f1fa700 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene.meta +++ b/Assets/Mirror/Components/InterestManagement/Scene.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 7655d309a46a4bd4860edf964228b3f6 -timeCreated: 1622649517 \ No newline at end of file +timeCreated: 1622649517 diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs index 8cbfa3b..c688dfd 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs @@ -17,6 +17,7 @@ public class SceneInterestManagement : InterestManagement HashSet dirtyScenes = new HashSet(); + [ServerCallback] public override void OnSpawned(NetworkIdentity identity) { Scene currentScene = identity.gameObject.scene; @@ -31,17 +32,23 @@ public override void OnSpawned(NetworkIdentity identity) objects.Add(identity); } + [ServerCallback] 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); + // Don't RebuildSceneObservers here - that will happen in LateUpdate. + // Multiple objects could be destroyed in same frame and we don't + // want to rebuild for each one...let LateUpdate do it once. + // We must add the current scene to dirtyScenes for LateUpdate to rebuild it. + if (lastObjectScene.TryGetValue(identity, out Scene currentScene)) + { + lastObjectScene.Remove(identity); + if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) + dirtyScenes.Add(currentScene); + } } - // internal so we can update from tests [ServerCallback] - internal void Update() + void LateUpdate() { // for each spawned: // if scene changed: @@ -49,7 +56,9 @@ internal void Update() // add new to dirty foreach (NetworkIdentity identity in NetworkServer.spawned.Values) { - Scene currentScene = lastObjectScene[identity]; + if (!lastObjectScene.TryGetValue(identity, out Scene currentScene)) + continue; + Scene newScene = identity.gameObject.scene; if (newScene == currentScene) continue; diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta index 9cc1ff5..99925b0 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SceneDistance.meta b/Assets/Mirror/Components/InterestManagement/SceneDistance.meta new file mode 100644 index 0000000..f010228 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SceneDistance.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f4d8c634a8103664db5f90fe8bab9544 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs new file mode 100644 index 0000000..cdd5f16 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Scene/Scene Distance Interest Management")] + public class SceneDistanceInterestManagement : InterestManagement + { + [Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")] + public int visRange = 500; + + [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] + public float rebuildInterval = 1; + double lastRebuildTime; + + // cache custom ranges to avoid runtime TryGetComponent lookups + readonly Dictionary CustomRanges = new Dictionary(); + + // helper function to get vis range for a given object, or default. + [ServerCallback] + int GetVisRange(NetworkIdentity identity) + { + return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; + } + + [ServerCallback] + public override void ResetState() + { + lastRebuildTime = 0D; + CustomRanges.Clear(); + } + + // 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(); + + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) + { + if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom)) + CustomRanges[identity] = custom; + + 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); + } + + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) + { + CustomRanges.Remove(identity); + + // Don't RebuildSceneObservers here - that will happen in LateUpdate. + // Multiple objects could be destroyed in same frame and we don't + // want to rebuild for each one...let LateUpdate do it once. + // We must add the current scene to dirtyScenes for LateUpdate to rebuild it. + if (lastObjectScene.TryGetValue(identity, out Scene currentScene)) + { + lastObjectScene.Remove(identity); + if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) + dirtyScenes.Add(currentScene); + } + } + + [ServerCallback] + void LateUpdate() + { + // for each spawned: + // if scene changed: + // add previous to dirty + // add new to dirty + // else + // if rebuild interval reached: + // rebuild all + foreach (NetworkIdentity identity in NetworkServer.spawned.Values) + { + if (!lastObjectScene.TryGetValue(identity, out Scene currentScene)) + continue; + + Scene newScene = identity.gameObject.scene; + if (newScene == currentScene) + { + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + + // no scene change, so we're done here + 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) + { + // Check for scene match first, then distance + if (identity.gameObject.scene != newObserver.identity.gameObject.scene) return false; + + int range = GetVisRange(identity); + return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range; + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // abort if no entry in sceneObjects yet (created in OnSpawned) + if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet objects)) + return; + + int range = GetVisRange(identity); + Vector3 position = identity.transform.position; + + // Add everything in the hashset for this object's current scene if within range + foreach (NetworkIdentity networkIdentity in objects) + if (networkIdentity != null && networkIdentity.connectionToClient != null) + { + // 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. + NetworkConnectionToClient conn = networkIdentity.connectionToClient; + if (conn != null && conn.isAuthenticated && conn.identity != null) + if (Vector3.Distance(conn.identity.transform.position, position) < range) + newObservers.Add(conn); + } + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs.meta new file mode 100644 index 0000000..3f74f73 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6471bc9a8f893944783fd54e9bfb6ed2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta index 00f5cd6..6a35e1e 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: cfa12b73503344d49b398b01bcb07967 -timeCreated: 1613110634 \ No newline at end of file +timeCreated: 1613110634 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs index 88f7197..d557713 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs @@ -1,11 +1,13 @@ // Grid2D from uMMORPG: get/set values of type T at any point // -> not named 'Grid' because Unity already has a Grid type. causes warnings. +// -> struct to avoid memory indirection. it's accessed a lot. using System.Collections.Generic; using UnityEngine; namespace Mirror { - public class Grid2D + // struct to avoid memory indirection. it's accessed a lot. + public struct Grid2D { // the grid // note that we never remove old keys. @@ -16,21 +18,27 @@ public class Grid2D // => 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>(); + readonly Dictionary> grid; // cache a 9 neighbor grid of vector2 offsets so we can use them more easily - Vector2Int[] neighbourOffsets = + readonly Vector2Int[] neighbourOffsets; + + public Grid2D(int initialCapacity) { - 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 - }; + grid = new Dictionary>(initialCapacity); + + neighbourOffsets = new[] { + 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) @@ -38,7 +46,15 @@ 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)) { + // each grid entry may hold hundreds of entities. + // let's create the HashSet with a large initial capacity + // in order to avoid resizing & allocations. +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have "new HashSet(capacity)" yet hashSet = new HashSet(); +#else + hashSet = new HashSet(128); +#endif grid[position] = hashSet; } diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta index f1d3cf0..b8dd422 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs new file mode 100644 index 0000000..64ca497 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs @@ -0,0 +1,106 @@ +// Grid3D based on Grid2D +// -> not named 'Grid' because Unity already has a Grid type. causes warnings. +// -> struct to avoid memory indirection. it's accessed a lot. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // struct to avoid memory indirection. it's accessed a lot. + public struct Grid3D + { + // 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! + readonly Dictionary> grid; + + // cache a 9 x 3 neighbor grid of vector3 offsets so we can use them more easily + readonly Vector3Int[] neighbourOffsets; + + public Grid3D(int initialCapacity) + { + grid = new Dictionary>(initialCapacity); + + neighbourOffsets = new Vector3Int[9 * 3]; + int i = 0; + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + for (int z = -1; z <= 1; z++) + { + neighbourOffsets[i] = new Vector3Int(x, y, z); + i += 1; + } + } + } + } + + // helper function so we can add an entry without worrying + public void Add(Vector3Int position, T value) + { + // initialize set in grid if it's not in there yet + if (!grid.TryGetValue(position, out HashSet hashSet)) + { + // each grid entry may hold hundreds of entities. + // let's create the HashSet with a large initial capacity + // in order to avoid resizing & allocations. +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have "new HashSet(capacity)" yet + hashSet = new HashSet(); +#else + hashSet = new HashSet(128); +#endif + 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(Vector3Int 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(Vector3Int position, HashSet result) + { + // clear result first + result.Clear(); + + // add neighbours + foreach (Vector3Int 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/Grid3D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs.meta new file mode 100644 index 0000000..a3268e7 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: b157c08313c64752b0856469b1b70771 +timeCreated: 1713533175 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs new file mode 100644 index 0000000..29bd6ce --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs @@ -0,0 +1,170 @@ +using UnityEngine; + +namespace Mirror +{ + internal class HexGrid2D + { + // Radius of each hexagonal cell (half the width) + internal float cellRadius; + + // Offset applied to align the grid with the world origin + Vector2 originOffset; + + // Precomputed constants for hexagon math to improve performance + readonly float sqrt3Div3; // sqrt(3) / 3, used in coordinate conversions + readonly float oneDiv3; // 1 / 3, used in coordinate conversions + readonly float twoDiv3; // 2 / 3, used in coordinate conversions + readonly float sqrt3; // sqrt(3), used in world coordinate calculations + readonly float sqrt3Div2; // sqrt(3) / 2, used in world coordinate calculations + + internal HexGrid2D(ushort visRange) + { + // Set cell radius as half the visibility range + cellRadius = visRange / 2f; + + // Offset to center the grid at world origin (2D XZ plane) + originOffset = Vector2.zero; + + // Precompute mathematical constants for efficiency + sqrt3Div3 = Mathf.Sqrt(3) / 3f; + oneDiv3 = 1f / 3f; + twoDiv3 = 2f / 3f; + sqrt3 = Mathf.Sqrt(3); + sqrt3Div2 = Mathf.Sqrt(3) / 2f; + } + + // Precomputed array of neighbor offsets as Cell2D structs (center + 6 neighbors) + static readonly Cell2D[] neighborCellsBase = new Cell2D[] + { + new Cell2D(0, 0), // Center + new Cell2D(1, -1), // Top-right + new Cell2D(1, 0), // Right + new Cell2D(0, 1), // Bottom-right + new Cell2D(-1, 1), // Bottom-left + new Cell2D(-1, 0), // Left + new Cell2D(0, -1) // Top-left + }; + + // Converts a grid cell (q, r) to a world position (x, z) + internal Vector2 CellToWorld(Cell2D cell) + { + // Calculate X and Z using hexagonal coordinate formulas + float x = cellRadius * (sqrt3 * cell.q + sqrt3Div2 * cell.r); + float z = cellRadius * (1.5f * cell.r); + + // Subtract the origin offset to align with world space and return the position + return new Vector2(x, z) - originOffset; + } + + // Converts a world position (x, z) to a grid cell (q, r) + internal Cell2D WorldToCell(Vector2 position) + { + // Apply the origin offset to adjust the position before conversion + position += originOffset; + + // Convert world X, Z to axial q, r coordinates using inverse hexagonal formulas + float q = (sqrt3Div3 * position.x - oneDiv3 * position.y) / cellRadius; + float r = (twoDiv3 * position.y) / cellRadius; + + // Round to the nearest valid cell and return + return RoundToCell(q, r); + } + + // Rounds floating-point axial coordinates (q, r) to the nearest integer cell coordinates + Cell2D RoundToCell(float q, float r) + { + // Calculate the third hexagonal coordinate (s) for consistency + float s = -q - r; + int qInt = Mathf.RoundToInt(q); // Round q to nearest integer + int rInt = Mathf.RoundToInt(r); // Round r to nearest integer + int sInt = Mathf.RoundToInt(s); // Round s to nearest integer + + // Calculate differences to determine which coordinate needs adjustment + float qDiff = Mathf.Abs(q - qInt); + float rDiff = Mathf.Abs(r - rInt); + float sDiff = Mathf.Abs(s - sInt); + + // Adjust q or r based on which has the largest rounding error (ensures q + r + s = 0) + if (qDiff > rDiff && qDiff > sDiff) + qInt = -rInt - sInt; // Adjust q if it has the largest error + else if (rDiff > sDiff) + rInt = -qInt - sInt; // Adjust r if it has the largest error + + return new Cell2D(qInt, rInt); + } + + // Populates the provided array with neighboring cells around a given center cell + internal void GetNeighborCells(Cell2D center, Cell2D[] neighbors) + { + // Ensure the array has the correct size (7: center + 6 neighbors) + if (neighbors.Length != 7) + throw new System.ArgumentException("Neighbor array must have exactly 7 elements"); + + // Populate the array by adjusting precomputed offsets with the center cell's coordinates + for (int i = 0; i < neighborCellsBase.Length; i++) + { + neighbors[i] = new Cell2D( + center.q + neighborCellsBase[i].q, + center.r + neighborCellsBase[i].r + ); + } + } + +#if UNITY_EDITOR + // Draws a 2D hexagonal gizmo in the Unity Editor for visualization + internal void DrawHexGizmo(Vector3 center, float radius, HexSpatialHash2DInterestManagement.CheckMethod checkMethod) + { + // Hexagon has 6 sides + const int segments = 6; + + // Array to store the 6 corner points in 3D + Vector3[] corners = new Vector3[segments]; + + // Calculate the corner positions based on the plane (XZ or XY) + for (int i = 0; i < segments; i++) + { + // Angle for each corner, offset by 90 degrees + float angle = 2 * Mathf.PI / segments * i + Mathf.PI / 2; + + if (checkMethod == HexSpatialHash2DInterestManagement.CheckMethod.XZ_FOR_3D) + { + // XZ plane: flat hexagon, Y=0 + corners[i] = center + new Vector3(radius * Mathf.Cos(angle), 0, radius * Mathf.Sin(angle)); + } + else // XY_FOR_2D + { + // XY plane: vertical hexagon, Z=0 + corners[i] = center + new Vector3(radius * Mathf.Cos(angle), radius * Mathf.Sin(angle), 0); + } + } + + // Draw each side of the hexagon + for (int i = 0; i < segments; i++) + { + Vector3 cornerA = corners[i]; + Vector3 cornerB = corners[(i + 1) % segments]; + Gizmos.DrawLine(cornerA, cornerB); + } + } +#endif + } + + // Struct representing a single cell in the 2D hexagonal grid + internal struct Cell2D + { + internal readonly int q; // Axial q coordinate (horizontal axis) + internal readonly int r; // Axial r coordinate (diagonal axis) + + internal Cell2D(int q, int r) + { + this.q = q; + this.r = r; + } + + public override bool Equals(object obj) => + obj is Cell2D other && q == other.q && r == other.r; + + // Generate a unique hash code for the cell + public override int GetHashCode() => (q << 16) ^ r; + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta new file mode 100644 index 0000000..83542fb --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e9b8dc0273250624c91b6681065741ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs new file mode 100644 index 0000000..b7a0fd3 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs @@ -0,0 +1,243 @@ +using UnityEngine; + +namespace Mirror +{ + internal class HexGrid3D + { + // Radius of each hexagonal cell (half the width) + internal float cellRadius; + + // Height of each cell along the Y-axis + internal float cellHeight; + + // Offset applied to align the grid with the world origin + Vector3 originOffset; + + // Precomputed constants for hexagon math to improve performance + readonly float sqrt3Div3; // sqrt(3) / 3, used in coordinate conversions + readonly float oneDiv3; // 1 / 3, used in coordinate conversions + readonly float twoDiv3; // 2 / 3, used in coordinate conversions + readonly float sqrt3; // sqrt(3), used in world coordinate calculations + readonly float sqrt3Div2; // sqrt(3) / 2, used in world coordinate calculations + + internal HexGrid3D(ushort visRange, ushort height) + { + // Set cell radius as half the visibility range + cellRadius = visRange / 2f; + + // Cell3D height is absolute...don't double it + cellHeight = height; + + // Offset to center the grid at world origin + // Cell3D height must be divided by 2 for vertical centering + originOffset = new Vector3(0, -cellHeight / 2, 0); + + // Precompute mathematical constants for efficiency + sqrt3Div3 = Mathf.Sqrt(3) / 3f; + oneDiv3 = 1f / 3f; + twoDiv3 = 2f / 3f; + sqrt3 = Mathf.Sqrt(3); + sqrt3Div2 = Mathf.Sqrt(3) / 2f; + } + + // Precomputed array of neighbor offsets as Cell3D structs (center + 6 per layer x 3 layers) + static readonly Cell3D[] neighborCellsBase = new Cell3D[] + { + // Center + new Cell3D(0, 0, 0), + // Upper layer (1) and its 6 neighbors + new Cell3D(0, 0, 1), + new Cell3D(1, -1, 1), new Cell3D(1, 0, 1), new Cell3D(0, 1, 1), + new Cell3D(-1, 1, 1), new Cell3D(-1, 0, 1), new Cell3D(0, -1, 1), + // Same layer (0) - 6 neighbors + new Cell3D(1, -1, 0), new Cell3D(1, 0, 0), new Cell3D(0, 1, 0), + new Cell3D(-1, 1, 0), new Cell3D(-1, 0, 0), new Cell3D(0, -1, 0), + // Lower layer (-1) and its 6 neighbors + new Cell3D(0, 0, -1), + new Cell3D(1, -1, -1), new Cell3D(1, 0, -1), new Cell3D(0, 1, -1), + new Cell3D(-1, 1, -1), new Cell3D(-1, 0, -1), new Cell3D(0, -1, -1) + }; + + // Converts a grid cell (q, r, layer) to a world position (x, y, z) + internal Vector3 CellToWorld(Cell3D cell) + { + // Calculate X and Z using hexagonal coordinate formulas + float x = cellRadius * (sqrt3 * cell.q + sqrt3Div2 * cell.r); + float z = cellRadius * (1.5f * cell.r); + + // Calculate Y based on layer and cell height + float y = cell.layer * cellHeight + cellHeight / 2; + + // Subtract the origin offset to align with world space and return the position + return new Vector3(x, y, z) - originOffset; + } + + // Converts a world position (x, y, z) to a grid cell (q, r, layer) + internal Cell3D WorldToCell(Vector3 position) + { + // Apply the origin offset to adjust the position before conversion + position += originOffset; + + // Calculate the vertical layer based on Y position + int layer = Mathf.FloorToInt(position.y / cellHeight); + + // Convert world X, Z to axial q, r coordinates using inverse hexagonal formulas + float q = (sqrt3Div3 * position.x - oneDiv3 * position.z) / cellRadius; + float r = (twoDiv3 * position.z) / cellRadius; + + // Round to the nearest valid cell and return + return RoundToCell(q, r, layer); + } + + // Rounds floating-point axial coordinates (q, r) to the nearest integer cell coordinates + Cell3D RoundToCell(float q, float r, int layer) + { + // Calculate the third hexagonal coordinate (s) for consistency + float s = -q - r; + int qInt = Mathf.RoundToInt(q); // Round q to nearest integer + int rInt = Mathf.RoundToInt(r); // Round r to nearest integer + int sInt = Mathf.RoundToInt(s); // Round s to nearest integer + + // Calculate differences to determine which coordinate needs adjustment + float qDiff = Mathf.Abs(q - qInt); + float rDiff = Mathf.Abs(r - rInt); + float sDiff = Mathf.Abs(s - sInt); + + // Adjust q or r based on which has the largest rounding error (ensures q + r + s = 0) + if (qDiff > rDiff && qDiff > sDiff) + qInt = -rInt - sInt; // Adjust q if it has the largest error + else if (rDiff > sDiff) + rInt = -qInt - sInt; // Adjust r if it has the largest error + + return new Cell3D(qInt, rInt, layer); + } + + // Populates the provided array with neighboring cells around a given center cell + internal void GetNeighborCells(Cell3D center, Cell3D[] neighbors) + { + // Ensure the array has the correct size + if (neighbors.Length != 21) + throw new System.ArgumentException("Neighbor array must have exactly 21 elements"); + + // Populate the array by adjusting precomputed offsets with the center cell's coordinates + for (int i = 0; i < neighborCellsBase.Length; i++) + { + neighbors[i] = new Cell3D( + center.q + neighborCellsBase[i].q, + center.r + neighborCellsBase[i].r, + center.layer + neighborCellsBase[i].layer + ); + } + } + +#if UNITY_EDITOR + + // Draws a hexagonal gizmo in the Unity Editor for visualization + internal void DrawHexGizmo(Vector3 center, float radius, float height, int relativeLayer) + { + // Hexagon has 6 sides + const int segments = 6; + + // Array to store the 6 corner points + Vector3[] corners = new Vector3[segments]; + + // Calculate the corner positions of the hexagon in the XZ plane + for (int i = 0; i < segments; i++) + { + // Angle for each corner, offset by 90 degrees + float angle = 2 * Mathf.PI / segments * i + Mathf.PI / 2; + + // Calculate the corner position based on the angle and radius + corners[i] = center + new Vector3(radius * Mathf.Cos(angle), 0, radius * Mathf.Sin(angle)); + } + + // Set gizmo color based on the relative layer for easy identification + Color gizmoColor; + switch (relativeLayer) + { + case 1: + gizmoColor = Color.green; // Upper layer (positive Y) + break; + case 0: + gizmoColor = Color.cyan; // Same layer as the reference point + break; + case -1: + gizmoColor = Color.yellow; // Lower layer (negative Y) + break; + default: + gizmoColor = Color.red; // Fallback for unexpected layers + break; + } + + // Store the current Gizmos color to restore later + Color previousColor = Gizmos.color; + + // Apply the chosen color + Gizmos.color = gizmoColor; + + // Draw each side of the hexagon as a 3D quad (wall) + for (int i = 0; i < segments; i++) + { + // Current corner + Vector3 cornerA = corners[i]; + + // Next corner (wraps around at 6) + Vector3 cornerB = corners[(i + 1) % segments]; + + // Calculate top and bottom corners to form a vertical quad + Vector3 cornerATop = cornerA + Vector3.up * (height / 2); + Vector3 cornerBTop = cornerB + Vector3.up * (height / 2); + Vector3 cornerABottom = cornerA - Vector3.up * (height / 2); + Vector3 cornerBBottom = cornerB - Vector3.up * (height / 2); + + // Draw the four lines of the quad to visualize the wall + Gizmos.DrawLine(cornerATop, cornerBTop); + Gizmos.DrawLine(cornerBTop, cornerBBottom); + Gizmos.DrawLine(cornerBBottom, cornerABottom); + Gizmos.DrawLine(cornerABottom, cornerATop); + } + + // Restore the original Gizmos color + Gizmos.color = previousColor; + } + +#endif + } + + // Custom struct for neighbor offsets (reduced memory usage) + internal struct HexOffset + { + internal int qOffset; // Offset in the q (axial) coordinate + internal int rOffset; // Offset in the r (axial) coordinate + + internal HexOffset(int q, int r) + { + qOffset = q; + rOffset = r; + } + } + + // Struct representing a single cell in the 3D hexagonal grid + internal struct Cell3D + { + internal readonly int q; // Axial q coordinate (horizontal axis) + internal readonly int r; // Axial r coordinate (diagonal axis) + internal readonly int layer; // Vertical layer index (Y-axis stacking) + + internal Cell3D(int q, int r, int layer) + { + this.q = q; + this.r = r; + this.layer = layer; + } + + public override bool Equals(object obj) => + obj is Cell3D other + && q == other.q + && r == other.r + && layer == other.layer; + + // Generate a unique hash code for the cell + public override int GetHashCode() => (q << 16) ^ (r << 8) ^ layer; + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta new file mode 100644 index 0000000..79a4425 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 9c4fe05752c9a85458b8e44611fe832b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs new file mode 100644 index 0000000..a394ce0 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs @@ -0,0 +1,345 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Hex Spatial Hash (2D)")] + public class HexSpatialHash2DInterestManagement : InterestManagement + { + [Range(1, 60), Tooltip("Time interval in seconds between observer rebuilds")] + public byte rebuildInterval = 1; + + [Range(1, 60), Tooltip("Time interval in seconds between static object rebuilds")] + public byte staticRebuildInterval = 10; + + [Range(10, 5000), Tooltip("Radius of super hex.\nSet to 10% larger than camera far clip plane.")] + public ushort visRange = 1100; + + [Range(1, 100), Tooltip("Distance an object must move for updating cell positions")] + public ushort minMoveDistance = 1; + + [Tooltip("Spatial Hashing supports XZ for 3D games or XY for 2D games.")] + public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D; + + double lastRebuildTime; + + // Counter for batching static object updates + byte rebuildCounter = 0; + + HexGrid2D grid; + + // Sparse array mapping cell indices to sets of NetworkIdentities + readonly List> cells = new List>(); + + // Tracks the last known cell position and world position of each NetworkIdentity + readonly Dictionary lastIdentityPositions = new Dictionary(); + + // Tracks the last known cell position and world position of each player's connection (observer) + readonly Dictionary lastConnectionPositions = new Dictionary(); + + // Pre-allocated array for storing neighbor cells (center + 6 neighbors) + readonly Cell2D[] neighborCells = new Cell2D[7]; + + // Maps each connection to the set of NetworkIdentities it can observe, precomputed for rebuilds + readonly Dictionary> connectionObservers = new Dictionary>(); + + // Reusable list for safe iteration over NetworkIdentities, avoiding ToList() allocations + readonly List identityKeys = new List(); + + // Pool of reusable HashSet instances to reduce allocations + readonly Stack> cellPool = new Stack>(); + + // Set of static NetworkIdentities that don't move, updated less frequently + readonly HashSet staticObjects = new HashSet(); + + // Scene bounds: 9 km (18 km total) in each dimension + const int MAX_Q = 19; // Covers -9 to 9 (~18 km) + const int MAX_R = 23; // Covers -11 to 11 (~18 km) + const ushort MAX_AREA = 9000; // Maximum area in meters + + public enum CheckMethod + { + XZ_FOR_3D, + XY_FOR_2D + } + + void Awake() + { + grid = new HexGrid2D(visRange); + // Initialize cells list with null entries up to max size (9 km bounds) + int maxSize = MAX_Q * MAX_R; + for (int i = 0; i < maxSize; i++) + cells.Add(null); + } + + // Project 3D world position to 2D grid position based on checkMethod + Vector2 ProjectToGrid(Vector3 position) => + checkMethod == CheckMethod.XZ_FOR_3D + ? new Vector2(position.x, position.z) + : new Vector2(position.x, position.y); + + void LateUpdate() + { + if (NetworkTime.time - lastRebuildTime >= rebuildInterval) + { + // Update positions of all active connections (players) in the network + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + if (conn?.identity != null) // Ensure connection and its identity exist + { + Vector2 position = ProjectToGrid(conn.identity.transform.position); + // Only update if the position has changed significantly + if (!lastConnectionPositions.TryGetValue(conn, out (Cell2D cell, Vector2 worldPos) last) || + Vector2.Distance(position, last.worldPos) >= minMoveDistance) + { + Cell2D cell = grid.WorldToCell(position); // Convert world position to grid cell + lastConnectionPositions[conn] = (cell, position); // Store the player's cell and position + } + } + + // Populate the reusable list with current keys for safe iteration + identityKeys.Clear(); + identityKeys.AddRange(lastIdentityPositions.Keys); + + // Update dynamic objects every rebuild, static objects every staticRebuildInterval + bool updateStatic = rebuildCounter >= staticRebuildInterval; + foreach (NetworkIdentity identity in identityKeys) + if (updateStatic || !staticObjects.Contains(identity)) + UpdateIdentityPosition(identity); // Refresh cell position for dynamic or scheduled static objects + + if (updateStatic) + rebuildCounter = 0; // Reset the counter after updating static objects + else + rebuildCounter++; + + // Precompute observer sets for each connection before rebuilding + connectionObservers.Clear(); + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + if (conn?.identity == null || !lastConnectionPositions.TryGetValue(conn, out (Cell2D cell, Vector2 worldPos) connPos)) + continue; + + // Get cells visible from the player's position + grid.GetNeighborCells(connPos.cell, neighborCells); + + // Initialize the observer set for this connection + HashSet observers = new HashSet(); + connectionObservers[conn] = observers; + + // Add all identities in visible cells to the observer set + for (int i = 0; i < neighborCells.Length; i++) + { + int index = GetCellIndex(neighborCells[i]); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + foreach (NetworkIdentity identity in cells[index]) + observers.Add(identity); + } + } + } + + // RebuildAll invokes NetworkServer.RebuildObservers on all spawned objects + base.RebuildAll(); + + // Update the last rebuild time + lastRebuildTime = NetworkTime.time; + } + } + + // Called when a new networked object is spawned on the server + public override void OnSpawned(NetworkIdentity identity) + { + // Register the new object's position in the grid system + UpdateIdentityPosition(identity); + + // Check if the object is statically batched (indicating it won't move) + Renderer[] renderers = identity.gameObject.GetComponentsInChildren(); + if (renderers.Any(r => r.isPartOfStaticBatch)) + staticObjects.Add(identity); + } + + // Updates the grid cell position of a NetworkIdentity when it moves or spawns + void UpdateIdentityPosition(NetworkIdentity identity) + { + // Get the current world position of the object + Vector2 position = ProjectToGrid(identity.transform.position); + + // Convert position to grid cell coordinates + Cell2D newCell = grid.WorldToCell(position); + + // Check if the object is within 9 km bounds + if (Mathf.Abs(position.x) > MAX_AREA || Mathf.Abs(position.y) > MAX_AREA) + return; // Ignore objects outside bounds + + // Check if the object was previously tracked + if (lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) previous)) + { + // Only update if the position has changed significantly or the cell has changed + if (Vector2.Distance(position, previous.worldPos) >= minMoveDistance || !newCell.Equals(previous.cell)) + { + if (!newCell.Equals(previous.cell)) + { + // Object moved to a new cell + // Remove it from the old cell's set and add it to the new cell's set + int oldIndex = GetCellIndex(previous.cell); + if (oldIndex >= 0 && oldIndex < cells.Count && cells[oldIndex] != null) + cells[oldIndex].Remove(identity); + AddToCell(newCell, identity); + } + // Update the stored position and cell + lastIdentityPositions[identity] = (newCell, position); + } + } + else + { + // New object - add it to the grid and track its position + AddToCell(newCell, identity); + lastIdentityPositions[identity] = (newCell, position); + } + } + + // Adds a NetworkIdentity to a specific cell's set of objects + void AddToCell(Cell2D cell, NetworkIdentity identity) + { + int index = GetCellIndex(cell); + if (index < 0 || index >= cells.Count) + return; // Out of bounds, ignore + + // If the cell doesn't exist in the array yet, fetch or create a new set from the pool + if (cells[index] == null) + { + cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet(); + } + cells[index].Add(identity); + } + + // Determines if a new observer can see a given NetworkIdentity + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Check if we have position data for both the object and the observer + if (!lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) identityPos) || + !lastConnectionPositions.TryGetValue(newObserver, out (Cell2D cell, Vector2 worldPos) observerPos)) + return false; // If not, assume no visibility + + // Populate the pre-allocated array with visible cells from the observer's position + grid.GetNeighborCells(observerPos.cell, neighborCells); + + // Check if the object's cell is among the visible ones + for (int i = 0; i < neighborCells.Length; i++) + if (neighborCells[i].Equals(identityPos.cell)) + return true; + + return false; + } + + // Rebuilds the set of observers for a specific NetworkIdentity + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // If the object's position isn't tracked, skip rebuilding + if (!lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) identityPos)) + return; + + // Use the precomputed observer sets to determine visibility + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + // Skip if the connection or its identity is null + if (conn?.identity == null) + continue; + + // Check if this connection can observe the identity + if (connectionObservers.TryGetValue(conn, out HashSet observers) && observers.Contains(identity)) + newObservers.Add(conn); + } + } + + public override void ResetState() + { + lastRebuildTime = 0; + // Clear and return all cell sets to the pool + for (int i = 0; i < cells.Count; i++) + { + if (cells[i] != null) + { + cells[i].Clear(); + cellPool.Push(cells[i]); + cells[i] = null; + } + } + lastIdentityPositions.Clear(); + lastConnectionPositions.Clear(); + connectionObservers.Clear(); + identityKeys.Clear(); + staticObjects.Clear(); + rebuildCounter = 0; + } + + public override void OnDestroyed(NetworkIdentity identity) + { + // If the object was tracked, remove it from its cell and position records + if (lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) pos)) + { + int index = GetCellIndex(pos.cell); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + cells[index].Remove(identity); // Remove from the cell's set + // If the cell's set is now empty, return it to the pool + if (cells[index].Count == 0) + { + cellPool.Push(cells[index]); + cells[index] = null; + } + } + lastIdentityPositions.Remove(identity); // Remove from position tracking + staticObjects.Remove(identity); // Ensure it's removed from static set if present + } + } + + // Computes a unique index for a cell in the sparse array, supporting 9 km bounds + int GetCellIndex(Cell2D cell) + { + int qOffset = cell.q + MAX_Q / 2; // Shift -9 to 9 -> 0 to 18 + int rOffset = cell.r + MAX_R / 2; // Shift -11 to 11 -> 0 to 22 + return qOffset + rOffset * MAX_Q; + } + +#if UNITY_EDITOR + // Draws debug gizmos in the Unity Editor to visualize the 2D grid + void OnDrawGizmos() + { + // Initialize the grid if it hasnt been created yet (e.g., before Awake) + if (grid == null) + grid = new HexGrid2D(visRange); + + // Only draw if theres a local player to base the visualization on + if (NetworkClient.localPlayer != null) + { + Vector3 playerPosition = NetworkClient.localPlayer.transform.position; + + // Convert to grid cell using the full Vector3 for proper plane projection + Vector2 projectedPos = ProjectToGrid(playerPosition); + Cell2D playerCell = grid.WorldToCell(projectedPos); + + // Get all visible cells around the player into the pre-allocated array + grid.GetNeighborCells(playerCell, neighborCells); + + // Set gizmo color for visibility + Gizmos.color = Color.cyan; + + // Draw each visible cell as a 2D hexagon, oriented based on checkMethod + for (int i = 0; i < neighborCells.Length; i++) + { + // Convert cell to world coordinates (2D) + Vector2 worldPos2D = grid.CellToWorld(neighborCells[i]); + + // Convert to 3D position based on checkMethod + Vector3 worldPos = checkMethod == CheckMethod.XZ_FOR_3D + ? new Vector3(worldPos2D.x, 0, worldPos2D.y) // XZ plane, flat + : new Vector3(worldPos2D.x, worldPos2D.y, 0); // XY plane, vertical + + grid.DrawHexGizmo(worldPos, grid.cellRadius, checkMethod); + } + } + } +#endif + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta new file mode 100644 index 0000000..3bcfc72 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 9b8b055f11f85ff428da471a0e625dd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs new file mode 100644 index 0000000..64415da --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs @@ -0,0 +1,336 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Hex Spatial Hash (3D)")] + public class HexSpatialHash3DInterestManagement : InterestManagement + { + [Range(1, 60), Tooltip("Time interval in seconds between observer rebuilds")] + public byte rebuildInterval = 1; + + [Range(1, 60), Tooltip("Time interval in seconds between static object rebuilds")] + public byte staticRebuildInterval = 10; + + [Range(10, 5000), Tooltip("Radius of super hex.\nSet to 10% larger than camera far clip plane.")] + public ushort visRange = 1100; + + [Range(10, 5000), Tooltip("Cell3D height effects all 3 layers")] + public ushort cellHeight = 500; + + [Range(1, 100), Tooltip("Distance an object must move for updating cell positions")] + public ushort minMoveDistance = 1; + + double lastRebuildTime; + + // Counter for batching static object updates + byte rebuildCounter = 0; + + HexGrid3D grid; + + // Sparse array mapping cell indices to sets of NetworkIdentities + readonly List> cells = new List>(); + + // Tracks the last known cell position and world position of each NetworkIdentity for efficient updates + readonly Dictionary lastIdentityPositions = new Dictionary(); + + // Tracks the last known cell position and world position of each player's connection (observer) + readonly Dictionary lastConnectionPositions = new Dictionary(); + + // Pre-allocated array for storing neighbor cells (center + 6 neighbors per layer x 3 layers) + readonly Cell3D[] neighborCells = new Cell3D[21]; + + // Maps each connection to the set of NetworkIdentities it can observe, precomputed for rebuilds + readonly Dictionary> connectionObservers = new Dictionary>(); + + // Reusable list for safe iteration over NetworkIdentities, avoiding ToList() allocations + readonly List identityKeys = new List(); + + // Pool of reusable HashSet instances to reduce allocations + readonly Stack> cellPool = new Stack>(); + + // Set of static NetworkIdentities that don't move, updated less frequently + readonly HashSet staticObjects = new HashSet(); + + // Scene bounds: ±9 km (18 km total) in each dimension + const int MAX_Q = 19; // Covers -9 to 9 (~18 km) + const int MAX_R = 23; // Covers -11 to 11 (~18 km) + const int LAYER_OFFSET = 18; // Offset for -18 to 17 layers + const int MAX_LAYERS = 36; // Total layers for ±9 km (18 km) + const ushort MAX_AREA = 9000; // Maximum area in meters + + void Awake() + { + grid = new HexGrid3D(visRange, cellHeight); + // Initialize cells list with null entries up to max size (±9 km bounds) + int maxSize = MAX_Q * MAX_R * MAX_LAYERS; + for (int i = 0; i < maxSize; i++) + cells.Add(null); + } + + void LateUpdate() + { + if (NetworkTime.time - lastRebuildTime >= rebuildInterval) + { + // Update positions of all active connections (players) in the network + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + if (conn?.identity != null) // Ensure connection and its identity exist + { + Vector3 position = conn.identity.transform.position; + // Only update if the position has changed significantly + if (!lastConnectionPositions.TryGetValue(conn, out (Cell3D cell, Vector3 worldPos) last) || + Vector3.Distance(position, last.worldPos) >= minMoveDistance) + { + Cell3D cell = grid.WorldToCell(position); // Convert world position to grid cell + lastConnectionPositions[conn] = (cell, position); // Store the player's cell and position + } + } + + // Populate the reusable list with current keys for safe iteration + identityKeys.Clear(); + identityKeys.AddRange(lastIdentityPositions.Keys); + + // Update dynamic objects every rebuild, static objects every staticRebuildInterval + bool updateStatic = rebuildCounter >= staticRebuildInterval; + foreach (NetworkIdentity identity in identityKeys) + if (updateStatic || !staticObjects.Contains(identity)) + UpdateIdentityPosition(identity); // Refresh cell position for dynamic or scheduled static objects + + if (updateStatic) + rebuildCounter = 0; // Reset the counter after updating static objects + else + rebuildCounter++; + + // Precompute observer sets for each connection before rebuilding + connectionObservers.Clear(); + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + if (conn?.identity == null || !lastConnectionPositions.TryGetValue(conn, out (Cell3D cell, Vector3 worldPos) connPos)) + continue; + + // Get cells visible from the player's position + grid.GetNeighborCells(connPos.cell, neighborCells); + + // Initialize the observer set for this connection + HashSet observers = new HashSet(); + connectionObservers[conn] = observers; + + // Add all identities in visible cells to the observer set + for (int i = 0; i < neighborCells.Length; i++) + { + int index = GetCellIndex(neighborCells[i]); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + foreach (NetworkIdentity identity in cells[index]) + observers.Add(identity); + } + } + } + + // RebuildAll invokes NetworkServer.RebuildObservers on all spawned objects + base.RebuildAll(); + + // Update the last rebuild time + lastRebuildTime = NetworkTime.time; + } + } + + // Called when a new networked object is spawned on the server + public override void OnSpawned(NetworkIdentity identity) + { + // Register the new object's position in the grid system + UpdateIdentityPosition(identity); + + // Check if the object is statically batched (indicating it won't move) + Renderer[] renderers = identity.gameObject.GetComponentsInChildren(); + if (renderers.Any(r => r.isPartOfStaticBatch)) + staticObjects.Add(identity); + } + + // Updates the grid cell position of a NetworkIdentity when it moves or spawns + void UpdateIdentityPosition(NetworkIdentity identity) + { + // Get the current world position of the object + Vector3 position = identity.transform.position; + + // Convert player position to grid cell coordinates + Cell3D newCell = grid.WorldToCell(position); + + // Check if the object is within ±9 km bounds + if (Mathf.Abs(position.x) > MAX_AREA || Mathf.Abs(position.y) > MAX_AREA || Mathf.Abs(position.z) > MAX_AREA) + return; // Ignore objects outside bounds + + // Check if the object was previously tracked + if (lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) previous)) + { + // Only update if the position has changed significantly or the cell has changed + if (Vector3.Distance(position, previous.worldPos) >= minMoveDistance || !newCell.Equals(previous.cell)) + { + if (!newCell.Equals(previous.cell)) + { + // Object moved to a new cell + // Remove it from the old cell's set and add it to the new cell's set + int oldIndex = GetCellIndex(previous.cell); + if (oldIndex >= 0 && oldIndex < cells.Count && cells[oldIndex] != null) + cells[oldIndex].Remove(identity); + AddToCell(newCell, identity); + } + // Update the stored position and cell + lastIdentityPositions[identity] = (newCell, position); + } + } + else + { + // New object - add it to the grid and track its position + AddToCell(newCell, identity); + lastIdentityPositions[identity] = (newCell, position); + } + } + + // Adds a NetworkIdentity to a specific cell's set of objects + void AddToCell(Cell3D cell, NetworkIdentity identity) + { + int index = GetCellIndex(cell); + if (index < 0 || index >= cells.Count) + return; // Out of bounds, ignore + + // If the cell doesn't exist in the array yet, fetch or create a new set from the pool + if (cells[index] == null) + { + cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet(); + } + cells[index].Add(identity); + } + + // Determines if a new observer can see a given NetworkIdentity + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Check if we have position data for both the object and the observer + if (!lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) identityPos) || + !lastConnectionPositions.TryGetValue(newObserver, out (Cell3D cell, Vector3 worldPos) observerPos)) + return false; // If not, assume no visibility + + // Populate the pre-allocated array with visible cells from the observer's position + grid.GetNeighborCells(observerPos.cell, neighborCells); + + // Check if the object's cell is among the visible ones + for (int i = 0; i < neighborCells.Length; i++) + if (neighborCells[i].Equals(identityPos.cell)) + return true; + + return false; + } + + // Rebuilds the set of observers for a specific NetworkIdentity + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // If the object's position isn't tracked, skip rebuilding + if (!lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) identityPos)) + return; + + // Use the precomputed observer sets to determine visibility + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + // Skip if the connection or its identity is null + if (conn?.identity == null) + continue; + + // Check if this connection can observe the identity + if (connectionObservers.TryGetValue(conn, out HashSet observers) && observers.Contains(identity)) + newObservers.Add(conn); + } + } + + public override void ResetState() + { + lastRebuildTime = 0; + // Clear and return all cell sets to the pool + for (int i = 0; i < cells.Count; i++) + { + if (cells[i] != null) + { + cells[i].Clear(); + cellPool.Push(cells[i]); + cells[i] = null; + } + } + lastIdentityPositions.Clear(); + lastConnectionPositions.Clear(); + connectionObservers.Clear(); + identityKeys.Clear(); + staticObjects.Clear(); + rebuildCounter = 0; + } + + public override void OnDestroyed(NetworkIdentity identity) + { + // If the object was tracked, remove it from its cell and position records + if (lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) pos)) + { + int index = GetCellIndex(pos.cell); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + cells[index].Remove(identity); // Remove from the cell's set + // If the cell's set is now empty, return it to the pool + if (cells[index].Count == 0) + { + cellPool.Push(cells[index]); + cells[index] = null; + } + } + lastIdentityPositions.Remove(identity); // Remove from position tracking + staticObjects.Remove(identity); // Ensure it's removed from static set if present + } + } + + // Computes a unique index for a cell in the sparse array, supporting ±9 km bounds + int GetCellIndex(Cell3D cell) + { + int qOffset = cell.q + MAX_Q / 2; // Shift -9 to 9 -> 0 to 18 + int rOffset = cell.r + MAX_R / 2; // Shift -11 to 11 -> 0 to 22 + int layerOffset = cell.layer + LAYER_OFFSET; // Shift -18 to 17 -> 0 to 35 + return qOffset + rOffset * MAX_Q + layerOffset * MAX_Q * MAX_R; + } + +#if UNITY_EDITOR + + // Draws debug gizmos in the Unity Editor to visualize the grid + void OnDrawGizmos() + { + // Initialize the grid if it hasn't been created yet (e.g., before Awake) + if (grid == null) + grid = new HexGrid3D(visRange, cellHeight); + + // Only draw if there's a local player to base the visualization on + if (NetworkClient.localPlayer != null) + { + Vector3 playerPosition = NetworkClient.localPlayer.transform.position; + + // Convert to grid cell + Cell3D playerCell = grid.WorldToCell(playerPosition); + + // Get all visible cells around the player into the pre-allocated array + grid.GetNeighborCells(playerCell, neighborCells); + + // Set default gizmo color (though overridden per cell) + Gizmos.color = Color.cyan; + + // Draw each visible cell as a hexagonal prism + for (int i = 0; i < neighborCells.Length; i++) + { + // Convert cell to world coordinates + Vector3 worldPos = grid.CellToWorld(neighborCells[i]); + + // Determine the layer relative to the player's cell for color coding + int relativeLayer = neighborCells[i].layer - playerCell.layer; + + // Draw the hexagonal cell with appropriate color based on layer + grid.DrawHexGizmo(worldPos, grid.cellRadius, grid.cellHeight, relativeLayer); + } + } + } + +#endif + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta new file mode 100644 index 0000000..9931906 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 58e492e77a2a1a3488412ceed5c2aa2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs new file mode 100644 index 0000000..1953de7 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs @@ -0,0 +1,146 @@ +// extremely fast spatial hashing interest management based on uMMORPG GridChecker. +// => 30x faster in initial tests +// => scales way higher +// checks on three dimensions (XYZ) which includes the vertical axes. +// this is slower than XY checking for regular spatial hashing. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")] + public class SpatialHashing3DInterestManagement : InterestManagement + { + [Tooltip("The maximum range that objects will be visible at.")] + public int visRange = 30; + + // we use a 9 neighbour grid. + // so we always see in a distance of 2 grids. + // for example, our own grid and then one on top / below / left / right. + // + // this means that grid resolution needs to be distance / 2. + // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // + // on first sight, it seems we need distance / 3 (we see left/us/right). + // but that's not the case. + // resolution would be 10, and we only see 1 cell far, so 10+10=20. + public int resolution => visRange / 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance + + [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] + public float rebuildInterval = 1; + double lastRebuildTime; + + [Header("Debug Settings")] + public bool showSlider; + + // the grid + // begin with a large capacity to avoid resizing & allocations. + Grid3D grid = new Grid3D(1024); + + // project 3d world position to grid position + Vector3Int ProjectToGrid(Vector3 position) => + Vector3Int.RoundToInt(position / resolution); + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // calculate projected positions + Vector3Int projected = ProjectToGrid(identity.transform.position); + Vector3Int 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; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance + } + + 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. + Vector3Int current = ProjectToGrid(identity.transform.position); + grid.GetWithNeighbours(current, newObservers); + } + + [ServerCallback] + public override void ResetState() + { + 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 + Vector3Int 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/SpatialHashing3DInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta new file mode 100644 index 0000000..dfd947a --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 120b4d6121d94e0280cd2ec536b0ea8f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs index eb4c2c5..0cb5e23 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -1,6 +1,8 @@ // extremely fast spatial hashing interest management based on uMMORPG GridChecker. // => 30x faster in initial tests // => scales way higher +// checks on two dimensions only(!), for example: XZ for 3D games or XY for 2D games. +// this is faster than XYZ checking but doesn't check vertical distance. using System.Collections.Generic; using UnityEngine; @@ -12,8 +14,17 @@ 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; + // we use a 9 neighbour grid. + // so we always see in a distance of 2 grids. + // for example, our own grid and then one on top / below / left / right. + // + // this means that grid resolution needs to be distance / 2. + // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // + // on first sight, it seems we need distance / 3 (we see left/us/right). + // but that's not the case. + // resolution would be 10, and we only see 1 cell far, so 10+10=20. + public int resolution => visRange / 2; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] public float rebuildInterval = 1; @@ -27,11 +38,12 @@ public enum CheckMethod [Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")] public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D; - // debugging + [Header("Debug Settings")] public bool showSlider; // the grid - Grid2D grid = new Grid2D(); + // begin with a large capacity to avoid resizing & allocations. + Grid2D grid = new Grid2D(1024); // project 3d world position to grid position Vector2Int ProjectToGrid(Vector3 position) => @@ -62,7 +74,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet _teamId; + set + { + if (Application.IsPlaying(gameObject) && !NetworkServer.active) + throw new InvalidOperationException("teamId can only be set at runtime on active server"); + + if (_teamId == value) + return; + + string oldTeam = _teamId; + _teamId = value; + + //Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement + if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement) + teamInterestManagement.OnTeamChanged(this, oldTeam); + } + } [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 index ca75a7a..71ad16a 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs index 22a8eb0..cb5bdb7 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using UnityEngine; namespace Mirror @@ -6,148 +6,143 @@ 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 Dictionary> teamObjects = + new Dictionary>(); readonly HashSet dirtyTeams = new HashSet(); - public override void OnSpawned(NetworkIdentity identity) + // LateUpdate so that all spawns/despawns/changes are done + [ServerCallback] + void LateUpdate() { - 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)) + // Rebuild all dirty teams + // dirtyTeams will be empty if no teams changed members + // by spawning or destroying or changing teamId in this frame. + foreach (string dirtyTeam in dirtyTeams) { - objects = new HashSet(); - teamObjects.Add(currentTeam, objects); + // rebuild always, even if teamObjects[dirtyTeam] is empty. + // Players might have left the team, but they may still be spawned. + RebuildTeamObservers(dirtyTeam); + + // clean up empty entries in the dict + if (teamObjects[dirtyTeam].Count == 0) + teamObjects.Remove(dirtyTeam); } - objects.Add(identity); + dirtyTeams.Clear(); } - public override void OnDestroyed(NetworkIdentity identity) + [ServerCallback] + void RebuildTeamObservers(string teamId) { - lastObjectTeam.TryGetValue(identity, out string currentTeam); - lastObjectTeam.Remove(identity); - if (currentTeam != string.Empty && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity)) - RebuildTeamObservers(currentTeam); + foreach (NetworkTeam networkTeam in teamObjects[teamId]) + if (networkTeam.netIdentity != null) + NetworkServer.RebuildObservers(networkTeam.netIdentity, false); } - // internal so we can update from tests + // called by NetworkTeam.teamId setter [ServerCallback] - internal void Update() + internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam) { - // 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; + // This object is in a new team so observers in the prior team + // and the new team need to rebuild their respective observers lists. - // string.Empty is never a valid teamId - // Nothing to do if teamId hasn't changed - if (string.IsNullOrWhiteSpace(newTeam) || newTeam == currentTeam) - continue; + // Remove this object from the hashset of the team it just left + // Null / Empty string is never a valid teamId + if (!string.IsNullOrWhiteSpace(oldTeam)) + { + dirtyTeams.Add(oldTeam); + teamObjects[oldTeam].Remove(networkTeam); + } - // Mark new/old Teams as dirty so they get rebuilt - UpdateDirtyTeams(newTeam, currentTeam); + // Null / Empty string is never a valid teamId + if (string.IsNullOrWhiteSpace(networkTeam.teamId)) + return; - // 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); - } + dirtyTeams.Add(networkTeam.teamId); - // rebuild all dirty teams - foreach (string dirtyTeam in dirtyTeams) - RebuildTeamObservers(dirtyTeam); + // Make sure this new team is in the dictionary + if (!teamObjects.ContainsKey(networkTeam.teamId)) + teamObjects[networkTeam.teamId] = new HashSet(); - dirtyTeams.Clear(); + // Add this object to the hashset of the new team + teamObjects[networkTeam.teamId].Add(networkTeam); } - void UpdateDirtyTeams(string newTeam, string currentTeam) + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) { - // string.Empty is never a valid teamId - if (currentTeam != string.Empty) - dirtyTeams.Add(currentTeam); + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) + return; - dirtyTeams.Add(newTeam); - } + string networkTeamId = networkTeam.teamId; - 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); + // Null / Empty string is never a valid teamId...do not add to teamObjects collection + if (string.IsNullOrWhiteSpace(networkTeamId)) + return; - // Set this to the new team this object just entered - lastObjectTeam[netIdentity] = newTeam; + // Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}"); + if (!teamObjects.TryGetValue(networkTeamId, out HashSet objects)) + { + objects = new HashSet(); + teamObjects.Add(networkTeamId, objects); + } - // Make sure this new team is in the dictionary - if (!teamObjects.ContainsKey(newTeam)) - teamObjects.Add(newTeam, new HashSet()); + objects.Add(networkTeam); - // Add this object to the hashset of the new team - teamObjects[newTeam].Add(netIdentity); + // Team ID could have been set in NetworkBehaviour::OnStartServer on this object. + // Since that's after OnCheckObserver is called it would be missed, so force Rebuild here. + // Add the current team to dirtyTeames for LateUpdate to rebuild it. + dirtyTeams.Add(networkTeamId); } - void RebuildTeamObservers(string teamId) + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) { - foreach (NetworkIdentity netIdentity in teamObjects[teamId]) - if (netIdentity != null) - NetworkServer.RebuildObservers(netIdentity, false); + // Don't RebuildSceneObservers here - that will happen in LateUpdate. + // Multiple objects could be destroyed in same frame and we don't + // want to rebuild for each one...let LateUpdate do it once. + // We must add the current team to dirtyTeames for LateUpdate to rebuild it. + if (identity.TryGetComponent(out NetworkTeam currentTeam)) + { + if (!string.IsNullOrWhiteSpace(currentTeam.teamId) && + teamObjects.TryGetValue(currentTeam.teamId, out HashSet objects) && + objects.Remove(currentTeam)) + dirtyTeams.Add(currentTeam.teamId); + } } public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { // Always observed if no NetworkTeam component - if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) + if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) return true; if (identityNetworkTeam.forceShown) return true; - // string.Empty is never a valid teamId + // Null / Empty string 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)) + if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam)) return true; - if (newObserverNetworkTeam.forceShown) - return true; - - // string.Empty is never a valid teamId + // Null / Empty string is never a valid teamId if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId)) return false; - // Observed only if teamId's match + //Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}"); + + // Observed only if teamId's team 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)) + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) { AddAllConnections(newObservers); return; @@ -160,18 +155,18 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet objects)) + // Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged + 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); + foreach (NetworkTeam netTeam in objects) + if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null) + newObservers.Add(netTeam.netIdentity.connectionToClient); } void AddAllConnections(HashSet newObservers) diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta index 6e8748a..2bf3f09 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/LagCompensation.meta b/Assets/Mirror/Components/LagCompensation.meta new file mode 100644 index 0000000..669a5b8 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 00ac1d0527f234939aba22b4d7cbf280 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs new file mode 100644 index 0000000..876eb18 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs @@ -0,0 +1,109 @@ +// Applies HistoryBounds to the physics world by projecting to a trigger Collider. +// This way we can use Physics.Raycast on it. +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/ Lag Compensation/ History Collider")] + public class HistoryCollider : MonoBehaviour + { + [Header("Components")] + [Tooltip("The object's actual collider. We need to know where it is, and how large it is.")] + public Collider actualCollider; + + [Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")] + public BoxCollider boundsCollider; + + [Header("History")] + [Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")] + public int boundsLimit = 8; + + [Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")] + public int boundsPerBucket = 2; + + [Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")] + public float captureInterval = 0.100f; // 100 ms + double lastCaptureTime = 0; + + [Header("Debug")] + public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f); + public Color currentColor = Color.red; + + protected HistoryBounds history = null; + + protected virtual void Awake() + { + history = new HistoryBounds(boundsLimit, boundsPerBucket); + + // ensure colliders were set. + // bounds collider should always be a trigger. + if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set."); + if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set."); + if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject."); + if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger."); + } + + // capturing and projecting onto colliders should use physics update + protected virtual void FixedUpdate() + { + // capture current bounds every interval + if (NetworkTime.localTime >= lastCaptureTime + captureInterval) + { + lastCaptureTime = NetworkTime.localTime; + CaptureBounds(); + } + + // project bounds onto helper collider + ProjectBounds(); + } + + protected virtual void CaptureBounds() + { + // grab current collider bounds + // this is in world space coordinates, and axis aligned + // TODO double check + Bounds bounds = actualCollider.bounds; + + // insert into history + history.Insert(bounds); + } + + protected virtual void ProjectBounds() + { + // grab total collider encapsulating all of history + Bounds total = history.total; + + // don't assign empty bounds, this will throw a Unity warning + if (history.boundsCount == 0) return; + + // scale projection doesn't work yet. + // for now, don't allow scale changes. + if (transform.lossyScale != Vector3.one) + { + Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1)."); + return; + } + + // counter rotate the child collider against the gameobject's rotation. + // we need this to always be axis aligned. + boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation); + + // project world space bounds to collider's local space + boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center); + boundsCollider.size = total.size; // TODO projection? + } + + // TODO runtime drawing for debugging? + protected virtual void OnDrawGizmos() + { + // draw total bounds + Gizmos.color = historyColor; + Gizmos.DrawWireCube(history.total.center, history.total.size); + + // draw current bounds + Gizmos.color = currentColor; + Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size); + } + } +} diff --git a/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs.meta b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs.meta new file mode 100644 index 0000000..e267f0a --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: f5f2158d9776d4b569858f793be4da60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/LagCompensation/HistoryCollider.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/LagCompensation/LagCompensator.cs b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs new file mode 100644 index 0000000..63669c0 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs @@ -0,0 +1,197 @@ +// Add this component to a Player object with collider. +// Automatically keeps a history for lag compensation. +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public struct Capture3D : Capture + { + public double timestamp { get; set; } + public Vector3 position; + public Vector3 size; + + public Capture3D(double timestamp, Vector3 position, Vector3 size) + { + this.timestamp = timestamp; + this.position = position; + this.size = size; + } + + public void DrawGizmo() + { + Gizmos.DrawWireCube(position, size); + } + + public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) => + new Capture3D( + 0, // interpolated snapshot is applied directly. don't need timestamps. + Vector3.LerpUnclamped(from.position, to.position, (float)t), + Vector3.LerpUnclamped(from.size, to.size, (float)t) + ); + + public override string ToString() => $"(time={timestamp} pos={position} size={size})"; + } + + [DisallowMultipleComponent] + [AddComponentMenu("Network/ Lag Compensation/ Lag Compensator")] + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation")] + public class LagCompensator : NetworkBehaviour + { + [Header("Components")] + [Tooltip("The collider to keep a history of.")] + public Collider trackedCollider; // assign this in inspector + + [Header("Settings")] + public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings(); + double lastCaptureTime; + + // lag compensation history of + readonly Queue> history = new Queue>(); + + [Header("Debugging")] + public Color historyColor = Color.white; + + [ServerCallback] + protected virtual void Update() + { + // capture lag compensation snapshots every interval. + // NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet. + if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval) + { + lastCaptureTime = NetworkTime.localTime; + Capture(); + } + } + + [ServerCallback] + protected virtual void Capture() + { + // capture current state + Capture3D capture = new Capture3D( + NetworkTime.localTime, + trackedCollider.bounds.center, + trackedCollider.bounds.size + ); + + // insert into history + LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture); + } + + protected virtual void OnDrawGizmos() + { + // draw history + Gizmos.color = historyColor; + LagCompensation.DrawGizmos(history); + } + + // sampling //////////////////////////////////////////////////////////// + // sample the sub-tick (=interpolated) history of this object for a hit test. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + [ServerCallback] + public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample) + { + // never trust the client: estimate client time instead. + // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking + // the estimation is very good. the error is as low as ~6ms for the demo. + // note that passing 'rtt' is fine: EstimateTime halves it to latency. + double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime); + + // sample the history to get the nearest snapshots around 'timestamp' + if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t)) + { + // interpolate to get a decent estimation at exactly 'timestamp' + sample = Capture3D.Interpolate(resultBefore, resultAfter, t); + return true; + } + else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}"); + + sample = default; + return false; + } + + // convenience tests /////////////////////////////////////////////////// + // there are multiple different ways to check a hit against the sample: + // - raycasting + // - bounds.contains + // - increasing bounds by tolerance and checking contains + // - threshold to bounds.closestpoint + // let's offer a few solutions directly and see which users prefer. + + // bounds check: checks distance to closest point on bounds in history @ -rtt. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + // this is super simple and fast, but not 100% physically accurate since we don't raycast. + [ServerCallback] + public virtual bool BoundsCheck( + NetworkConnectionToClient viewer, + Vector3 hitPoint, + float toleranceDistance, + out float distance, + out Vector3 nearest) + { + // first, sample the history at -rtt of the viewer. + if (Sample(viewer, out Capture3D capture)) + { + // now that we know where the other player was at that time, + // we can see if the hit point was within tolerance of it. + // TODO consider rotations??? + // TODO consider original collider shape?? + Bounds bounds = new Bounds(capture.position, capture.size); + nearest = bounds.ClosestPoint(hitPoint); + distance = Vector3.Distance(nearest, hitPoint); + return distance <= toleranceDistance; + } + nearest = hitPoint; + distance = 0; + return false; + } + + // raycast check: creates a collider the sampled position and raycasts to hitPoint. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + // this is physically accurate (checks against walls etc.), with the cost + // of a runtime instantiation. + // + // originPoint: where the player fired the weapon. + // hitPoint: where the player's local raycast hit. + // tolerance: scale up the sampled collider by % in order to have a bit of a tolerance. + // 0 means no extra tolerance, 0.05 means 5% extra tolerance. + // layerMask: the layer mask to use for the raycast. + [ServerCallback] + public virtual bool RaycastCheck( + NetworkConnectionToClient viewer, + Vector3 originPoint, + Vector3 hitPoint, + float tolerancePercent, + int layerMask, + out RaycastHit hit) + { + // first, sample the history at -rtt of the viewer. + if (Sample(viewer, out Capture3D capture)) + { + // instantiate a real physics collider on demand. + // TODO rotation?? + // TODO different collier types?? + GameObject temp = new GameObject("LagCompensatorTest"); + temp.transform.position = capture.position; + BoxCollider tempCollider = temp.AddComponent(); + tempCollider.size = capture.size * (1 + tolerancePercent); + + // raycast + Vector3 direction = hitPoint - originPoint; + float maxDistance = direction.magnitude * 2; + bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask); + + // cleanup + Destroy(temp); + return result; + } + + hit = default; + return false; + } + } +} diff --git a/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta new file mode 100644 index 0000000..e13b9c0 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a898831dd60c4cdfbfd9a6ea5702ed01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/LagCompensation/LagCompensator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Mirror.Components.asmdef b/Assets/Mirror/Components/Mirror.Components.asmdef index a61c7db..90c360e 100644 --- a/Assets/Mirror/Components/Mirror.Components.asmdef +++ b/Assets/Mirror/Components/Mirror.Components.asmdef @@ -1,14 +1,16 @@ { "name": "Mirror.Components", + "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], - "optionalUnityReferences": [], "includePlatforms": [], "excludePlatforms": [], - "allowUnsafeCode": false, + "allowUnsafeCode": true, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [] + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file diff --git a/Assets/Mirror/Components/Mirror.Components.asmdef.meta b/Assets/Mirror/Components/Mirror.Components.asmdef.meta index 263b6f0..79c4587 100644 --- a/Assets/Mirror/Components/Mirror.Components.asmdef.meta +++ b/Assets/Mirror/Components/Mirror.Components.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Mirror.Components.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkAnimator.cs b/Assets/Mirror/Components/NetworkAnimator.cs index 2360fe3..b573ad7 100644 --- a/Assets/Mirror/Components/NetworkAnimator.cs +++ b/Assets/Mirror/Components/NetworkAnimator.cs @@ -13,8 +13,8 @@ namespace Mirror /// 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. /// + // [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours [AddComponentMenu("Network/Network Animator")] - [RequireComponent(typeof(NetworkIdentity))] [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")] public class NetworkAnimator : NetworkBehaviour { @@ -31,11 +31,12 @@ public class NetworkAnimator : NetworkBehaviour public Animator animator; /// - /// Syncs animator.speed + /// Syncs animator.speed. + /// Default to 1 because Animator.speed defaults to 1. /// [SyncVar(hook = nameof(OnAnimatorSpeedChanged))] - float animatorSpeed; - float previousSpeed; + float animatorSpeed = 1f; + float previousSpeed = 1f; // Note: not an object[] array because otherwise initialization is real annoying int[] lastIntParameters; @@ -67,11 +68,11 @@ bool SendMessagesAllowed return true; } - return (hasAuthority && clientAuthority); + return (isOwned && clientAuthority); } } - void Awake() + void Initialize() { // 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 @@ -87,6 +88,17 @@ void Awake() layerWeight = new float[animator.layerCount]; } + // fix https://github.com/MirrorNetworking/Mirror/issues/2810 + // both Awake and Enable need to initialize arrays. + // in case users call SetActive(false) -> SetActive(true). + void Awake() => Initialize(); + void OnEnable() => Initialize(); + + public virtual void Reset() + { + syncDirection = SyncDirection.ClientToServer; + } + void FixedUpdate() { if (!SendMessagesAllowed) @@ -137,7 +149,7 @@ 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)) + if (isServer || (isOwned && clientAuthority)) return; animator.speed = value; @@ -227,7 +239,7 @@ void SendAnimationParametersMessage(byte[] parameters) void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weight, NetworkReader reader) { - if (hasAuthority && clientAuthority) + if (isOwned && clientAuthority) return; // usually transitions will be triggered by parameters, if not, play anims directly. @@ -245,7 +257,7 @@ void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weigh void HandleAnimParamsMsg(NetworkReader reader) { - if (hasAuthority && clientAuthority) + if (isOwned && clientAuthority) return; ReadParameters(reader); @@ -302,9 +314,18 @@ ulong NextDirtyBits() bool WriteParameters(NetworkWriter writer, bool forceAll = false) { + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize parameterCount to be 100% sure we deserialize correct amount of bytes. + // (255 parameters should be enough for everyone, write it as byte) + byte parameterCount = (byte)parameters.Length; + writer.WriteByte(parameterCount); + ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits(); writer.WriteULong(dirtyBits); - for (int i = 0; i < parameters.Length; i++) + + // iterate on byte count. if it's >256, it won't break + // serialization - just not serialize excess layers. + for (int i = 0; i < parameterCount; i++) { if ((dirtyBits & (1ul << i)) == 0) continue; @@ -331,11 +352,20 @@ bool WriteParameters(NetworkWriter writer, bool forceAll = false) void ReadParameters(NetworkReader reader) { + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize parameterCount to be 100% sure we deserialize correct amount of bytes. + // mismatch shows error to make this super easy to debug. + byte parameterCount = reader.ReadByte(); + if (parameterCount != parameters.Length) + { + Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?", gameObject); + return; + } + 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++) + for (int i = 0; i < parameterCount; i++) { if ((dirtyBits & (1ul << i)) == 0) continue; @@ -362,54 +392,54 @@ void ReadParameters(NetworkReader reader) } } - /// - /// Custom Serialization - /// - /// - /// - /// - public override bool OnSerialize(NetworkWriter writer, bool initialState) + public override void OnSerialize(NetworkWriter writer, bool initialState) { - bool changed = base.OnSerialize(writer, initialState); + base.OnSerialize(writer, initialState); if (initialState) { - for (int i = 0; i < animator.layerCount; i++) + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize layerCount to be 100% sure we deserialize correct amount of bytes. + // (255 layers should be enough for everyone, write it as byte) + byte layerCount = (byte)animator.layerCount; + writer.WriteByte(layerCount); + + // iterate on byte count. if it's >256, it won't break + // serialization - just not serialize excess layers. + for (int i = 0; i < 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); - } + AnimatorStateInfo st = animator.IsInTransition(i) + ? animator.GetNextAnimatorStateInfo(i) + : animator.GetCurrentAnimatorStateInfo(i); + writer.WriteInt(st.fullPathHash); + writer.WriteFloat(st.normalizedTime); writer.WriteFloat(animator.GetLayerWeight(i)); } - WriteParameters(writer, initialState); - return true; + WriteParameters(writer, 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++) + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize layerCount to be 100% sure we deserialize correct amount of bytes. + // mismatch shows error to make this super easy to debug. + byte layerCount = reader.ReadByte(); + if (layerCount != animator.layerCount) + { + Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?", gameObject); + return; + } + + for (int i = 0; i < layerCount; i++) { int stateHash = reader.ReadInt(); float normalizedTime = reader.ReadFloat(); - animator.SetLayerWeight(i, reader.ReadFloat()); + float weight = reader.ReadFloat(); + + animator.SetLayerWeight(i, weight); animator.Play(stateHash, i, normalizedTime); } @@ -437,13 +467,13 @@ public void SetTrigger(int hash) { if (!isClient) { - Debug.LogWarning("Tried to set animation in the server for a client-controlled animator"); + Debug.LogWarning("Tried to set animation in the server for a client-controlled animator", gameObject); return; } - if (!hasAuthority) + if (!isOwned) { - Debug.LogWarning("Only the client with authority can set animations"); + Debug.LogWarning("Only the client with authority can set animations", gameObject); return; } @@ -457,7 +487,7 @@ public void SetTrigger(int hash) { if (!isServer) { - Debug.LogWarning("Tried to set animation in the client for a server-controlled animator"); + Debug.LogWarning("Tried to set animation in the client for a server-controlled animator", gameObject); return; } @@ -476,9 +506,7 @@ public void ResetTrigger(string triggerName) ResetTrigger(Animator.StringToHash(triggerName)); } - /// - /// Causes an animation trigger to be reset for a networked object. - /// + /// Causes an animation trigger to be reset for a networked object. /// Hash id of trigger (from the Animator). public void ResetTrigger(int hash) { @@ -486,13 +514,13 @@ public void ResetTrigger(int hash) { if (!isClient) { - Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator"); + Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator", gameObject); return; } - if (!hasAuthority) + if (!isOwned) { - Debug.LogWarning("Only the client with authority can reset animations"); + Debug.LogWarning("Only the client with authority can reset animations", gameObject); return; } @@ -506,7 +534,7 @@ public void ResetTrigger(int hash) { if (!isServer) { - Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator"); + Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator", gameObject); return; } @@ -558,7 +586,7 @@ void CmdOnAnimationTriggerServerMessage(int hash) // handle and broadcast // host should have already the trigger - bool isHostOwner = isClient && hasAuthority; + bool isHostOwner = isClient && isOwned; if (!isHostOwner) { HandleAnimTriggerMsg(hash); @@ -576,7 +604,7 @@ void CmdOnAnimationResetTriggerServerMessage(int hash) // handle and broadcast // host should have already the trigger - bool isHostOwner = isClient && hasAuthority; + bool isHostOwner = isClient && isOwned; if (!isHostOwner) { HandleAnimResetTriggerMsg(hash); @@ -611,22 +639,22 @@ void RpcOnAnimationParametersClientMessage(byte[] parameters) HandleAnimParamsMsg(networkReader); } - [ClientRpc] + [ClientRpc(includeOwner = false)] void RpcOnAnimationTriggerClientMessage(int hash) { - // host/owner handles this before it is sent - if (isServer || (clientAuthority && hasAuthority)) return; - - HandleAnimTriggerMsg(hash); + // already handled on server in SetTrigger + // or CmdOnAnimationTriggerServerMessage + if (!isServer) + HandleAnimTriggerMsg(hash); } - [ClientRpc] + [ClientRpc(includeOwner = false)] void RpcOnAnimationResetTriggerClientMessage(int hash) { - // host/owner handles this before it is sent - if (isServer || (clientAuthority && hasAuthority)) return; - - HandleAnimResetTriggerMsg(hash); + // already handled on server in ResetTrigger + // or CmdOnAnimationResetTriggerServerMessage + if (!isServer) + HandleAnimResetTriggerMsg(hash); } #endregion diff --git a/Assets/Mirror/Components/NetworkAnimator.cs.meta b/Assets/Mirror/Components/NetworkAnimator.cs.meta index 211ce78..2e48ed4 100644 --- a/Assets/Mirror/Components/NetworkAnimator.cs.meta +++ b/Assets/Mirror/Components/NetworkAnimator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkAnimator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs new file mode 100644 index 0000000..d3c3632 --- /dev/null +++ b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs @@ -0,0 +1,31 @@ +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Diagnostics Debugger")] + public class NetworkDiagnosticsDebugger : MonoBehaviour + { + public bool logInMessages = true; + public bool logOutMessages = true; + void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo) + { + if (logInMessages) + Debug.Log(msgInfo); + } + void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo) + { + if (logOutMessages) + Debug.Log(msgInfo); + } + void OnEnable() + { + NetworkDiagnostics.InMessageEvent += OnInMessage; + NetworkDiagnostics.OutMessageEvent += OnOutMessage; + } + void OnDisable() + { + NetworkDiagnostics.InMessageEvent -= OnInMessage; + NetworkDiagnostics.OutMessageEvent -= OnOutMessage; + } + } +} diff --git a/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta new file mode 100644 index 0000000..04e4335 --- /dev/null +++ b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: bc9f0a0fe4124424b8f9d4927795ee01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkLobbyManager.cs.meta b/Assets/Mirror/Components/NetworkLobbyManager.cs.meta index a32c8c7..5e4ca94 100644 --- a/Assets/Mirror/Components/NetworkLobbyManager.cs.meta +++ b/Assets/Mirror/Components/NetworkLobbyManager.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkLobbyManager.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta b/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta index 7a21eec..23ce3a4 100644 --- a/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta +++ b/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkLobbyPlayer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs b/Assets/Mirror/Components/NetworkPingDisplay.cs index 61e9241..255e62d 100644 --- a/Assets/Mirror/Components/NetworkPingDisplay.cs +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs @@ -13,20 +13,26 @@ public class NetworkPingDisplay : MonoBehaviour { public Color color = Color.white; public int padding = 2; - int width = 150; - int height = 25; + public int width = 150; + public int height = 25; void OnGUI() { // only while client is active if (!NetworkClient.active) return; - // show rtt in bottom right corner, right aligned + // show stats in bottom right corner, right aligned GUI.color = color; Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height); + GUILayout.BeginArea(rect); GUIStyle style = GUI.skin.GetStyle("Label"); style.alignment = TextAnchor.MiddleRight; - GUI.Label(rect, $"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms", style); + GUILayout.BeginHorizontal(style); + GUILayout.Label($"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms"); + GUI.color = NetworkClient.connectionQuality.ColorCode(); + GUILayout.Label($"Q: {new string('-', (int)NetworkClient.connectionQuality)}"); + GUILayout.EndHorizontal(); + GUILayout.EndArea(); GUI.color = Color.white; } } diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs.meta b/Assets/Mirror/Components/NetworkPingDisplay.cs.meta index 221a745..c141614 100644 --- a/Assets/Mirror/Components/NetworkPingDisplay.cs.meta +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkPingDisplay.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody.meta b/Assets/Mirror/Components/NetworkRigidbody.meta new file mode 100644 index 0000000..ce84fee --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 80106690aef541a5b8e2f8fb3d5949ad +timeCreated: 1686733778 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs new file mode 100644 index 0000000..a88341a --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody (Reliable)")] + public class NetworkRigidbodyReliable : NetworkTransformReliable + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody rb; + bool wasKinematic; + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + + // we can't overwrite .target to be a Rigidbody. + // but we can ensure that .target has a Rigidbody, and use it. + if (target.GetComponent() == null) + { + Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + } + } + + // cach Rigidbody and original isKinematic setting + protected override void Awake() + { + // we can't overwrite .target to be a Rigidbody. + // but we can use its Rigidbody component. + rb = target.GetComponent(); + if (rb == null) + { + Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + return; + } + wasKinematic = rb.isKinematic; + base.Awake(); + } + + // reset forced isKinematic flag to original. + // otherwise the overwritten value would remain between sessions forever. + // for example, a game may run as client, set rigidbody.iskinematic=true, + // then run as server, where .iskinematic isn't touched and remains at + // the overwritten=true, even though the user set it to false originally. + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; + + // overwriting Construct() and Apply() to set Rigidbody.MovePosition + // would give more jittery movement. + + // FixedUpdate for physics + void FixedUpdate() + { + // who ever has authority moves the Rigidbody with physics. + // everyone else simply sets it to kinematic. + // so that only the Transform component is synced. + + // host mode + if (isServer && isClient) + { + // in host mode, we own it it if: + // clientAuthority is disabled (hence server / we own it) + // clientAuthority is enabled and we have authority over this object. + bool owned = !clientAuthority || IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // client only + else if (isClient) + { + // on the client, we own it only if clientAuthority is enabled, + // and we have authority over this object. + bool owned = IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // server only + else if (isServer) + { + // on the server, we always own it if clientAuthority is disabled. + bool owned = !clientAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs.meta new file mode 100644 index 0000000..0ebea63 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: cb803efbe62c34d7baece46c9ffebad9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs new file mode 100644 index 0000000..9872ccc --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs @@ -0,0 +1,135 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody 2D (Reliable)")] + public class NetworkRigidbodyReliable2D : NetworkTransformReliable + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody2D rb; + bool wasKinematic; + + protected override void OnValidate() + { + if (Application.isPlaying) return; + + base.OnValidate(); + + // we can't overwrite .target to be a Rigidbody. + // but we can ensure that .target has a Rigidbody, and use it. + if (target.GetComponent() == null) + { + Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); + } + } + + // cach Rigidbody and original isKinematic setting + protected override void Awake() + { + // we can't overwrite .target to be a Rigidbody. + // but we can use its Rigidbody component. + rb = target.GetComponent(); + if (rb == null) + { + Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); + return; + } +#if UNITY_6000_0_OR_NEWER + wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic); +#else + wasKinematic = rb.isKinematic; +#endif + base.Awake(); + } + + // reset forced isKinematic flag to original. + // otherwise the overwritten value would remain between sessions forever. + // for example, a game may run as client, set rigidbody.iskinematic=true, + // then run as server, where .iskinematic isn't touched and remains at + // the overwritten=true, even though the user set it to false originally. +#if UNITY_6000_0_OR_NEWER + public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; + public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; +#else + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; +#endif + + // overwriting Construct() and Apply() to set Rigidbody.MovePosition + // would give more jittery movement. + + // FixedUpdate for physics + void FixedUpdate() + { + // who ever has authority moves the Rigidbody with physics. + // everyone else simply sets it to kinematic. + // so that only the Transform component is synced. + + // host mode + if (isServer && isClient) + { + // in host mode, we own it it if: + // clientAuthority is disabled (hence server / we own it) + // clientAuthority is enabled and we have authority over this object. + bool owned = !clientAuthority || IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. +#if UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // client only + else if (isClient) + { + // on the client, we own it only if clientAuthority is enabled, + // and we have authority over this object. + bool owned = IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. +#if UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // server only + else if (isServer) + { + // on the server, we always own it if clientAuthority is disabled. + bool owned = !clientAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. +#if UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation.eulerAngles.z; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs.meta new file mode 100644 index 0000000..c02ae5d --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs new file mode 100644 index 0000000..e2a34c0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody (Unreliable)")] + public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody rb; + bool wasKinematic; + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + + // we can't overwrite .target to be a Rigidbody. + // but we can ensure that .target has a Rigidbody, and use it. + if (target.GetComponent() == null) + { + Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + } + } + + // cach Rigidbody and original isKinematic setting + protected override void Awake() + { + // we can't overwrite .target to be a Rigidbody. + // but we can use its Rigidbody component. + rb = target.GetComponent(); + if (rb == null) + { + Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + return; + } + wasKinematic = rb.isKinematic; + base.Awake(); + } + + // reset forced isKinematic flag to original. + // otherwise the overwritten value would remain between sessions forever. + // for example, a game may run as client, set rigidbody.iskinematic=true, + // then run as server, where .iskinematic isn't touched and remains at + // the overwritten=true, even though the user set it to false originally. + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; + + // overwriting Construct() and Apply() to set Rigidbody.MovePosition + // would give more jittery movement. + + // FixedUpdate for physics + void FixedUpdate() + { + // who ever has authority moves the Rigidbody with physics. + // everyone else simply sets it to kinematic. + // so that only the Transform component is synced. + + // host mode + if (isServer && isClient) + { + // in host mode, we own it it if: + // clientAuthority is disabled (hence server / we own it) + // clientAuthority is enabled and we have authority over this object. + bool owned = !clientAuthority || IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // client only + else if (isClient) + { + // on the client, we own it only if clientAuthority is enabled, + // and we have authority over this object. + bool owned = IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // server only + else if (isServer) + { + // on the server, we always own it if clientAuthority is disabled. + bool owned = !clientAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs.meta new file mode 100644 index 0000000..646d27c --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3b20dc110904e47f8a154cdcf6433eae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs new file mode 100644 index 0000000..6c021f0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs @@ -0,0 +1,136 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody 2D (Unreliable)")] + public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody2D rb; + bool wasKinematic; + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + + // we can't overwrite .target to be a Rigidbody. + // but we can ensure that .target has a Rigidbody, and use it. + if (target.GetComponent() == null) + { + Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); + } + } + + // cach Rigidbody and original isKinematic setting + protected override void Awake() + { + // we can't overwrite .target to be a Rigidbody. + // but we can use its Rigidbody component. + rb = target.GetComponent(); + if (rb == null) + { + Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); + return; + } + +#if UNITY_6000_0_OR_NEWER + wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic); +#else + wasKinematic = rb.isKinematic; +#endif + base.Awake(); + } + + // reset forced isKinematic flag to original. + // otherwise the overwritten value would remain between sessions forever. + // for example, a game may run as client, set rigidbody.iskinematic=true, + // then run as server, where .iskinematic isn't touched and remains at + // the overwritten=true, even though the user set it to false originally. +#if UNITY_6000_0_OR_NEWER + public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; + public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; +#else + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; +#endif + // overwriting Construct() and Apply() to set Rigidbody.MovePosition + // would give more jittery movement. + + // FixedUpdate for physics + void FixedUpdate() + { + // who ever has authority moves the Rigidbody with physics. + // everyone else simply sets it to kinematic. + // so that only the Transform component is synced. + + // host mode + if (isServer && isClient) + { + // in host mode, we own it it if: + // clientAuthority is disabled (hence server / we own it) + // clientAuthority is enabled and we have authority over this object. + bool owned = !clientAuthority || IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. +#if UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // client only + else if (isClient) + { + // on the client, we own it only if clientAuthority is enabled, + // and we have authority over this object. + bool owned = IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. +#if UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // server only + else if (isServer) + { + // on the server, we always own it if clientAuthority is disabled. + bool owned = !clientAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. +#if UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation.eulerAngles.z; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs.meta new file mode 100644 index 0000000..99239af --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 1c7e12ad9b9ae443c9fdf37e9f5ecd36 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs b/Assets/Mirror/Components/NetworkRoomManager.cs index d432fbb..698b190 100644 --- a/Assets/Mirror/Components/NetworkRoomManager.cs +++ b/Assets/Mirror/Components/NetworkRoomManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -26,7 +25,6 @@ public struct PendingPlayer } [Header("Room Settings")] - [FormerlySerializedAs("m_ShowRoomGUI")] [SerializeField] [Tooltip("This flag controls whether the default UI is shown for the room")] @@ -58,23 +56,22 @@ public struct PendingPlayer /// List of players that are in the Room /// [FormerlySerializedAs("m_PendingPlayers")] - public List pendingPlayers = new List(); + public HashSet pendingPlayers = new HashSet(); [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; + [ReadOnly, 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(); + [ReadOnly, Tooltip("List of Room Player objects")] + public HashSet roomSlots = new HashSet(); public bool allPlayersReady { @@ -102,8 +99,7 @@ public bool allPlayersReady public override void OnValidate() { - // always >= 0 - maxConnections = Mathf.Max(maxConnections, 0); + base.OnValidate(); // always <= maxConnections minPlayers = Mathf.Min(minPlayers, maxConnections); @@ -120,56 +116,13 @@ public override void OnValidate() 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}"); + //Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}"); - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { // cant be ready in room, add to ready list PendingPlayer pending; @@ -193,29 +146,7 @@ void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer) 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; - } + NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, ReplacePlayerOptions.KeepAuthority); } internal void CallOnClientEnterRoom() @@ -238,6 +169,31 @@ internal void CallOnClientExitRoom() } } + /// + /// 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 (!Utils.IsSceneActive(RoomScene)) + return; + + int numberOfReadyPlayers = NetworkServer.connections.Count(conn => + conn.Value != null && + conn.Value.identity != null && + conn.Value.identity.TryGetComponent(out NetworkRoomPlayer nrp) && + nrp.readyToBegin); + + bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers; + if (enoughReadyPlayers) + { + pendingPlayers.Clear(); + allPlayersReady = true; + } + else + allPlayersReady = false; + } + #region server handlers /// @@ -247,15 +203,10 @@ internal void CallOnClientExitRoom() /// Connection from client. public override void OnServerConnect(NetworkConnectionToClient conn) { - if (numPlayers >= maxConnections) - { - conn.Disconnect(); - return; - } - // cannot join game in progress - if (!IsSceneActive(RoomScene)) + if (!Utils.IsSceneActive(RoomScene)) { + Debug.Log($"Not in Room scene...disconnecting {conn}"); conn.Disconnect(); return; } @@ -278,7 +229,7 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn) if (roomPlayer != null) roomSlots.Remove(roomPlayer); - foreach (NetworkIdentity clientOwnedObject in conn.clientOwnedObjects) + foreach (NetworkIdentity clientOwnedObject in conn.owned) { roomPlayer = clientOwnedObject.GetComponent(); if (roomPlayer != null) @@ -294,21 +245,41 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn) player.GetComponent().readyToBegin = false; } - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) RecalculateRoomPlayerIndices(); OnRoomServerDisconnect(conn); base.OnServerDisconnect(conn); -#if UNITY_SERVER - if (numPlayers < 1) + // Restart the server if we're headless and no players are connected. + // This will send server to offline scene, where auto-start will run. + if (Utils.IsHeadless() && 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 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); + } + } + /// /// 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. @@ -319,11 +290,8 @@ public override void OnServerAddPlayer(NetworkConnectionToClient conn) // increment the index before adding the player, so first player starts at 1 clientIndex++; - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { - if (roomSlots.Count == maxConnections) - return; - allPlayersReady = false; //Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}"); @@ -335,7 +303,11 @@ public override void OnServerAddPlayer(NetworkConnectionToClient conn) NetworkServer.AddPlayerForConnection(conn, newRoomGameObject); } else - OnRoomServerAddPlayer(conn); + { + // Late joiners not supported...should've been kicked by OnServerDisconnect + Debug.Log($"Not in Room scene...disconnecting {conn}"); + conn.Disconnect(); + } } [Server] @@ -343,10 +315,9 @@ public void RecalculateRoomPlayerIndices() { if (roomSlots.Count > 0) { - for (int i = 0; i < roomSlots.Count; i++) - { - roomSlots[i].index = i; - } + int i = 0; + foreach (NetworkRoomPlayer player in roomSlots) + player.index = i++; } } @@ -371,7 +342,7 @@ public override void ServerChangeScene(string newSceneName) { // re-add the room object roomPlayer.GetComponent().readyToBegin = false; - NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject); + NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject, ReplacePlayerOptions.KeepAuthority); } } @@ -472,10 +443,7 @@ public override void OnStartClient() /// public override void OnClientConnect() { -#pragma warning disable 618 - // obsolete method calls new method - OnRoomClientConnect(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientConnect(); base.OnClientConnect(); } @@ -485,9 +453,7 @@ public override void OnClientConnect() /// public override void OnClientDisconnect() { -#pragma warning disable 618 - OnRoomClientDisconnect(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientDisconnect(); base.OnClientDisconnect(); } @@ -507,7 +473,7 @@ public override void OnStopClient() /// public override void OnClientSceneChanged() { - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { if (NetworkClient.isConnected) CallOnClientEnterRoom(); @@ -516,10 +482,7 @@ public override void OnClientSceneChanged() CallOnClientExitRoom(); base.OnClientSceneChanged(); -#pragma warning disable 618 - // obsolete method calls new method - OnRoomClientSceneChanged(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientSceneChanged(); } #endregion @@ -612,6 +575,30 @@ public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient c return true; } + /// + /// This is called on server from NetworkRoomPlayer.CmdChangeReadyState when client indicates change in Ready status. + /// + public virtual 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; + } + /// /// 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. @@ -647,19 +634,11 @@ public virtual void OnRoomClientExit() {} /// 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. /// @@ -675,16 +654,6 @@ public virtual void OnRoomStopClient() {} /// 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 @@ -697,7 +666,7 @@ public virtual void OnGUI() if (!showRoomGUI) return; - if (NetworkServer.active && IsSceneActive(GameplayScene)) + if (NetworkServer.active && Utils.IsSceneActive(GameplayScene)) { GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f)); if (GUILayout.Button("Return to Room")) @@ -705,7 +674,7 @@ public virtual void OnGUI() GUILayout.EndArea(); } - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS"); } diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs.meta b/Assets/Mirror/Components/NetworkRoomManager.cs.meta index 76e7d42..744bcdc 100644 --- a/Assets/Mirror/Components/NetworkRoomManager.cs.meta +++ b/Assets/Mirror/Components/NetworkRoomManager.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRoomManager.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs b/Assets/Mirror/Components/NetworkRoomPlayer.cs index d2763d5..6b7cda4 100644 --- a/Assets/Mirror/Components/NetworkRoomPlayer.cs +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs @@ -25,14 +25,14 @@ public class NetworkRoomPlayer : NetworkBehaviour /// 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")] + [ReadOnly, 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.")] + [ReadOnly, Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")] [SyncVar(hook = nameof(IndexChanged))] public int index; @@ -41,7 +41,7 @@ public class NetworkRoomPlayer : NetworkBehaviour /// /// Do not use Start - Override OnStartHost / OnStartClient instead! /// - public void Start() + public virtual void Start() { if (NetworkManager.singleton is NetworkRoomManager room) { @@ -139,7 +139,7 @@ public virtual void OnGUI() if (!room.showRoomGUI) return; - if (!NetworkManager.IsSceneActive(room.RoomScene)) + if (!Utils.IsSceneActive(room.RoomScene)) return; DrawPlayerReadyState(); diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta b/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta index 0299bea..f9d2978 100644 --- a/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRoomPlayer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkStatistics.cs b/Assets/Mirror/Components/NetworkStatistics.cs index a95d4a9..c00bb19 100644 --- a/Assets/Mirror/Components/NetworkStatistics.cs +++ b/Assets/Mirror/Components/NetworkStatistics.cs @@ -4,9 +4,11 @@ namespace Mirror { /// - /// Shows Network messages and bytes sent & received per second. - /// Add this component to the same object as Network Manager. + /// Shows Network messages and bytes sent and 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")] @@ -17,43 +19,43 @@ public class NetworkStatistics : MonoBehaviour // --------------------------------------------------------------------- - // CLIENT + // CLIENT (public fields for other components to grab statistics) // long bytes to support >2GB - int clientIntervalReceivedPackets; - long clientIntervalReceivedBytes; - int clientIntervalSentPackets; - long clientIntervalSentBytes; + [HideInInspector] public int clientIntervalReceivedPackets; + [HideInInspector] public long clientIntervalReceivedBytes; + [HideInInspector] public int clientIntervalSentPackets; + [HideInInspector] public long clientIntervalSentBytes; // results from last interval // long bytes to support >2GB - int clientReceivedPacketsPerSecond; - long clientReceivedBytesPerSecond; - int clientSentPacketsPerSecond; - long clientSentBytesPerSecond; + [HideInInspector] public int clientReceivedPacketsPerSecond; + [HideInInspector] public long clientReceivedBytesPerSecond; + [HideInInspector] public int clientSentPacketsPerSecond; + [HideInInspector] public long clientSentBytesPerSecond; // --------------------------------------------------------------------- - // SERVER + // SERVER (public fields for other components to grab statistics) // capture interval // long bytes to support >2GB - int serverIntervalReceivedPackets; - long serverIntervalReceivedBytes; - int serverIntervalSentPackets; - long serverIntervalSentBytes; + [HideInInspector] public int serverIntervalReceivedPackets; + [HideInInspector] public long serverIntervalReceivedBytes; + [HideInInspector] public int serverIntervalSentPackets; + [HideInInspector] public long serverIntervalSentBytes; // results from last interval // long bytes to support >2GB - int serverReceivedPacketsPerSecond; - long serverReceivedBytesPerSecond; - int serverSentPacketsPerSecond; - long serverSentBytesPerSecond; + [HideInInspector] public int serverReceivedPacketsPerSecond; + [HideInInspector] public long serverReceivedBytesPerSecond; + [HideInInspector] public int serverSentPacketsPerSecond; + [HideInInspector] public long serverSentBytesPerSecond; - // NetworkManager sets Transport.activeTransport in Awake(). + // NetworkManager sets Transport.active in Awake(). // so let's hook into it in Start(). void Start() { // find available transport - Transport transport = Transport.activeTransport; + Transport transport = Transport.active; if (transport != null) { transport.OnClientDataReceived += OnClientReceive; @@ -67,7 +69,7 @@ void Start() void OnDestroy() { // remove transport hooks - Transport transport = Transport.activeTransport; + Transport transport = Transport.active; if (transport != null) { transport.OnClientDataReceived -= OnClientReceive; @@ -145,8 +147,8 @@ void OnGUI() 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)); + // 120 is below NetworkManager HUD in all cases. + GUILayout.BeginArea(new Rect(10, 120, 215, 300)); // show client / server stats if active if (NetworkClient.active) OnClientGUI(); diff --git a/Assets/Mirror/Components/NetworkStatistics.cs.meta b/Assets/Mirror/Components/NetworkStatistics.cs.meta index 0bf4251..ccce31f 100644 --- a/Assets/Mirror/Components/NetworkStatistics.cs.meta +++ b/Assets/Mirror/Components/NetworkStatistics.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkStatistics.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform.meta b/Assets/Mirror/Components/NetworkTransform.meta new file mode 100644 index 0000000..255a0fa --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 36de72d9255741659bcbd1971ed29822 +timeCreated: 1668358590 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs new file mode 100644 index 0000000..0d6c9ed --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs @@ -0,0 +1,547 @@ +// 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 +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public enum CoordinateSpace { Local, World } + + public abstract class NetworkTransformBase : NetworkBehaviour + { + // target transform to sync. can be on a child. + // TODO this field is kind of unnecessary since we now support child NetworkBehaviours + [Header("Target")] + [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] + public Transform target; + + // 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 => isClient && authority; + + // snapshots with initial capacity to avoid early resizing & allocations: see NetworkRigidbodyBenchmark example. + public readonly SortedList clientSnapshots = new SortedList(16); + public readonly SortedList serverSnapshots = new SortedList(16); + + // selective sync ////////////////////////////////////////////////////// + [Header("Selective Sync\nDon't change these at Runtime")] + public bool syncPosition = true; // do not change at runtime! + public bool syncRotation = true; // do not change at runtime! + public bool syncScale = false; // do not change at runtime! rare. off by default. + + [Header("Bandwidth Savings")] + [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] + public bool onlySyncOnChange = true; + [Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")] + public bool compressRotation = true; + + // interpolation is on by default, but can be disabled to jump to + // the destination immediately. some projects need this. + [Header("Interpolation")] + [Tooltip("Set to false to have a snap-like effect on position movement.")] + public bool interpolatePosition = true; + [Tooltip("Set to false to have a snap-like effect on rotations.")] + public bool interpolateRotation = true; + [Tooltip("Set to false to remove scale smoothing. Example use-case: Instant flipping of sprites that use -X and +X for direction.")] + public bool interpolateScale = true; + + // CoordinateSpace /////////////////////////////////////////////////////////// + [Header("Coordinate Space")] + [Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")] + public CoordinateSpace coordinateSpace = CoordinateSpace.Local; + + // convert syncInterval to sendIntervalMultiplier. + // in the future this can be moved into core to support tick aligned Sync, + public uint sendIntervalMultiplier + { + get + { + if (syncInterval > 0) + { + // if syncInterval is > 0, calculate how many multiples of NetworkManager.sendRate it is + // + // for example: + // NetworkServer.sendInterval is 1/60 = 0.16 + // NetworkTransform.syncInterval is 0.5 (500ms). + // 0.5 / 0.16 = 3.125 + // in other words: 3.125 x sendInterval + // + // note that NetworkServer.sendInterval is usually set on start. + // to make this work in Edit mode, make sure that NetworkManager + // OnValidate sets NetworkServer.sendInterval immediately. + float multiples = syncInterval / NetworkServer.sendInterval; + + // syncInterval is always supposed to sync at a minimum of 1 x sendInterval. + // that's what we do for every other NetworkBehaviour since + // we only sync in Broadcast() which is called @ sendInterval. + return multiples > 1 ? (uint)Mathf.RoundToInt(multiples) : 1; + } + + // if syncInterval is 0, use NetworkManager.sendRate (x1) + return 1; + } + } + + [Header("Timeline Offset")] + [Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")] + public bool timelineOffset = true; + + // Ninja's Notes on offset & mulitplier: + // + // In a no multiplier scenario: + // 1. Snapshots are sent every frame (frame being 1 NM send interval). + // 2. Time Interpolation is set to be 'behind' by 2 frames times. + // In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter. + // + // In a multiplier scenario: + // 1. Snapshots are sent every 10 frames. + // 2. Time Interpolation remains 'behind by 2 frames'. + // When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2. + // Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate. + // + protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1); + protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0; + + // velocity for convenience (animators etc.) + // this isn't technically NetworkTransforms job, but it's needed by so many projects that we just provide it anyway. + public Vector3 velocity { get; private set; } + public Vector3 angularVelocity { get; private set; } + + // debugging /////////////////////////////////////////////////////////// + [Header("Debug")] + public bool showGizmos; + public bool showOverlay; + public Color overlayColor = new Color(0, 0, 0, 0.5f); + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + + // configure in awake + Configure(); + } + + // initialization ////////////////////////////////////////////////////// + // forcec configuration of some settings + protected virtual void Configure() + { + // set target to self if none yet + if (target == null) target = transform; + + // Unity doesn't support setting world scale. + // OnValidate force disables syncScale in world mode. + if (coordinateSpace == CoordinateSpace.World) syncScale = false; + } + + // make sure to call this when inheriting too! + protected virtual void Awake() + { + // sometimes OnValidate() doesn't run before launching a project. + // need to guarantee configuration runs. + Configure(); + } + + // snapshot functions ////////////////////////////////////////////////// + // get local/world position + protected virtual Vector3 GetPosition() => + coordinateSpace == CoordinateSpace.Local ? target.localPosition : target.position; + + // get local/world rotation + protected virtual Quaternion GetRotation() => + coordinateSpace == CoordinateSpace.Local ? target.localRotation : target.rotation; + + // get local/world scale + protected virtual Vector3 GetScale() => + coordinateSpace == CoordinateSpace.Local ? target.localScale : target.lossyScale; + + // set local/world position + protected virtual void SetPosition(Vector3 position) + { + if (coordinateSpace == CoordinateSpace.Local) + target.localPosition = position; + else + target.position = position; + } + + // set local/world rotation + protected virtual void SetRotation(Quaternion rotation) + { + if (coordinateSpace == CoordinateSpace.Local) + target.localRotation = rotation; + else + target.rotation = rotation; + } + + // set local/world position + protected virtual void SetScale(Vector3 scale) + { + if (coordinateSpace == CoordinateSpace.Local) + target.localScale = scale; + // Unity doesn't support setting world scale. + // OnValidate disables syncScale in world mode. + // else + // target.lossyScale = scale; // TODO + } + + // construct a snapshot of the current state + // => internal for testing + protected virtual TransformSnapshot Construct() + { + // NetworkTime.localTime for double precision until Unity has it too + return new TransformSnapshot( + // our local time is what the other end uses as remote time + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + 0, // the other end fills out local time itself + GetPosition(), + GetRotation(), + GetScale() + ); + } + + protected void AddSnapshot(SortedList snapshots, double timeStamp, Vector3? position, Quaternion? rotation, Vector3? scale) + { + // 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 = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition(); + if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation(); + if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale(); + + // insert transform snapshot + SnapshotInterpolation.InsertIfNotExists( + snapshots, + NetworkClient.snapshotSettings.bufferLimit, + new TransformSnapshot( + timeStamp, // arrival remote timestamp. NOT remote time. + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + position.Value, + rotation.Value, + scale.Value + ) + ); + } + + // 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 Apply(TransformSnapshot interpolated, TransformSnapshot endGoal) + { + // 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. + + // calculate the velocity and angular velocity for the object + // these can be used to drive animations or other behaviours + if (!isOwned && Time.deltaTime > 0) + { + velocity = (transform.localPosition - interpolated.position) / Time.deltaTime; + angularVelocity = (transform.localRotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime; + } + + // interpolate parts + if (syncPosition) SetPosition(interpolatePosition ? interpolated.position : endGoal.position); + if (syncRotation) SetRotation(interpolateRotation ? interpolated.rotation : endGoal.rotation); + if (syncScale) SetScale(interpolateScale ? interpolated.scale : endGoal.scale); + } + + // 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 (syncDirection != SyncDirection.ClientToServer) 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 target.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 (syncDirection != SyncDirection.ClientToServer) 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 target.position=pos + RpcTeleport(destination, rotation); + } + + // 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); + } + + // teleport on server, broadcast to clients. + [Server] + public void ServerTeleport(Vector3 destination, Quaternion rotation) + { + OnTeleport(destination, rotation); + RpcTeleport(destination, rotation); + } + + [ClientRpc] + void RpcResetState() + { + ResetState(); + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination) + { + // set the new position. + // interpolation will automatically continue. + target.position = destination; + + // reset interpolation to immediately jump to the new position. + // do not call Reset() here, this would cause delta compression to + // get out of sync for NetworkTransformReliable because NTReliable's + // 'override Reset()' resets lastDe/SerializedPosition: + // https://github.com/MirrorNetworking/Mirror/issues/3588 + // because client's next OnSerialize() will delta compress, + // but server's last delta will have been reset, causing offsets. + // + // instead, simply clear snapshots. + ResetState(); + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination as first entry? + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) + { + // set the new position. + // interpolation will automatically continue. + target.position = destination; + target.rotation = rotation; + + // reset interpolation to immediately jump to the new position. + // do not call Reset() here, this would cause delta compression to + // get out of sync for NetworkTransformReliable because NTReliable's + // 'override Reset()' resets lastDe/SerializedPosition: + // https://github.com/MirrorNetworking/Mirror/issues/3588 + // because client's next OnSerialize() will delta compress, + // but server's last delta will have been reset, causing offsets. + // + // instead, simply clear snapshots. + ResetState(); + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination as first entry? + } + + public virtual void ResetState() + { + // disabled objects aren't updated anymore. + // so let's clear the buffers. + serverSnapshots.Clear(); + clientSnapshots.Clear(); + + // Prevent resistance from CharacterController + // or non-knematic Rigidbodies when teleporting. + Physics.SyncTransforms(); + } + + public virtual void Reset() + { + ResetState(); + // default to ClientToServer so this works immediately for users + syncDirection = SyncDirection.ClientToServer; + } + + protected virtual void OnEnable() + { + ResetState(); + + if (NetworkServer.active) + NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged; + } + + protected virtual void OnDisable() + { + ResetState(); + + if (NetworkServer.active) + NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged; + } + + [ServerCallback] + void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity identity, bool authorityState) + { + if (identity != netIdentity) return; + + // If server gets authority or syncdirection is server to client, + // we don't reset buffers. + // This is because if syncdirection is S to C, we will never have + // snapshot issues since there is only ever 1 source. + + if (syncDirection == SyncDirection.ClientToServer) + { + ResetState(); + RpcResetState(); + } + } + + // OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // debug /////////////////////////////////////////////////////////////// + protected virtual void OnGUI() + { + if (!showOverlay) return; + if (!Camera.main) 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(target.position); + + // enough alpha, in front of camera and in screen? + if (point.z >= 0 && Utils.IsPointInScreen(point)) + { + 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:{serverSnapshots.Count}"); + GUILayout.Label($"Client Buffer:{clientSnapshots.Count}"); + + 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; + + // calculate threshold for 'old enough' snapshots + double threshold = NetworkTime.localTime - NetworkClient.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 + TransformSnapshot entry = buffer.Values[i]; + bool oldEnough = entry.localTime <= threshold; + Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; + Gizmos.DrawWireCube(entry.position, Vector3.one); + } + + // extra: lines between start<->position<->goal + Gizmos.color = Color.green; + Gizmos.DrawLine(buffer.Values[0].position, target.position); + Gizmos.color = Color.white; + Gizmos.DrawLine(target.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(serverSnapshots); + if (isClient) DrawGizmos(clientSnapshots); + } +#endif + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs.meta new file mode 100644 index 0000000..bc6aac2 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7c44135fde488424eaf28566206ce473 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs new file mode 100644 index 0000000..a231b88 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs @@ -0,0 +1,717 @@ +// Quake NetworkTransform based on 2022 NetworkTransformUnreliable. +// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ +// Quake: https://www.jfedor.org/quake3/ +// +// Base class for NetworkTransform and NetworkTransformChild. +// => simple unreliable sync without any interpolation for now. +// => which means we don't need teleport detection either +// +// several functions are virtual in case someone needs to modify a part. +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform Hybrid")] + public class NetworkTransformHybrid : NetworkBehaviourHybrid + { + public bool useFixedUpdate; + TransformSnapshot? pendingSnapshot; + + // target transform to sync. can be on a child. + [Header("Target")] + [Tooltip("The Transform component to sync. May be on this GameObject, or on a child.")] + public Transform target; + + [Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")] + public int bufferSizeLimit = 64; + internal SortedList clientSnapshots = new SortedList(); + internal SortedList serverSnapshots = new SortedList(); + + // CUSTOM CHANGE: bring back sendRate. this will probably be ported to Mirror. + // TODO but use built in syncInterval instead of the extra field here! + [Header("Synchronization")] + [Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")] + public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333' + public float sendInterval => 1f / sendRate; + // END CUSTOM CHANGE + + // delta compression needs to remember 'last' to compress against. + // this is from reliable full state serializations, not from last + // unreliable delta since that isn't guaranteed to be delivered. + Vector3 lastSerializedBaselinePosition = Vector3.zero; + Quaternion lastSerializedBaselineRotation = Quaternion.identity; + Vector3 lastSerializedBaselineScale = Vector3.one; + + // save last deserialized baseline to delta decompress against + Vector3 lastDeserializedBaselinePosition = Vector3.zero; // unused, but keep for delta + Quaternion lastDeserializedBaselineRotation = Quaternion.identity; // unused, but keep for delta + Vector3 lastDeserializedBaselineScale = Vector3.one; // unused, but keep for delta + + // sensitivity is for changed-detection, + // this is != precision, which is for quantization and delta compression. + [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; + + // selective sync ////////////////////////////////////////////////////// + [Header("Selective Sync & interpolation")] + public bool syncPosition = true; + public bool syncRotation = true; + public bool syncScale = false; + + // velocity for convenience (animators etc.) + // this isn't technically NetworkTransforms job, but it's needed by so many projects that we just provide it anyway. + public Vector3 velocity { get; private set; } + public Vector3 angularVelocity { get; private set; } + + // debugging /////////////////////////////////////////////////////////// + [Header("Debug")] + public bool debugDraw; + public bool showGizmos; + public bool showOverlay; + public Color overlayColor = new Color(0, 0, 0, 0.5f); + + // initialization ////////////////////////////////////////////////////// + // make sure to call this when inheriting too! + protected virtual void Awake() {} + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + Reset(); + } + + void Reset() + { + // set target to self if none yet + if (target == null) target = transform; + + // we use sendRate for convenience. + // but project it to syncInterval for NetworkTransformHybrid to work properly. + syncInterval = sendInterval; + + // default to ClientToServer so this works immediately for users + syncDirection = SyncDirection.ClientToServer; + } + + // 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(TransformSnapshot 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. + + // calculate the velocity and angular velocity for the object + // these can be used to drive animations or other behaviours + if (!isOwned && Time.deltaTime > 0) + { + velocity = (transform.localPosition - interpolated.position) / Time.deltaTime; + angularVelocity = (transform.localRotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime; + } + + if (syncPosition) target.localPosition = interpolated.position; + if (syncRotation) target.localRotation = interpolated.rotation; + if (syncScale) target.localScale = interpolated.scale; + } + + // store state after baseline sync + protected override void StoreState() + { + target.GetLocalPositionAndRotation(out lastSerializedBaselinePosition, out lastSerializedBaselineRotation); + lastSerializedBaselineScale = target.localScale; + } + + // check if position / rotation / scale changed since last _full reliable_ sync. + // squared comparisons for performance + protected override bool StateChanged() + { + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + Vector3 scale = target.localScale; + + if (syncPosition) + { + float positionDelta = Vector3.Distance(position, lastSerializedBaselinePosition); + if (positionDelta >= positionSensitivity) + { + return true; + } + } + + if (syncRotation) + { + float rotationDelta = Quaternion.Angle(lastSerializedBaselineRotation, rotation); + if (rotationDelta >= rotationSensitivity) + { + return true; + } + } + + if (syncScale) + { + float scaleDelta = Vector3.Distance(scale, lastSerializedBaselineScale); + if (scaleDelta >= scaleSensitivity) + { + return true; + } + } + + return false; + } + + // serialization /////////////////////////////////////////////////////// + // called on server and on client, depending on SyncDirection + protected override void OnSerializeBaseline(NetworkWriter writer) + { + // perf: get position/rotation directly. TransformSnapshot is too expensive. + // TransformSnapshot snapshot = ConstructSnapshot(); + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + Vector3 scale = target.localScale; + + if (syncPosition) writer.WriteVector3(position); + if (syncRotation) writer.WriteQuaternion(rotation); + if (syncScale) writer.WriteVector3(scale); + } + + // called on server and on client, depending on SyncDirection + protected override void OnDeserializeBaseline(NetworkReader reader, byte baselineTick) + { + // deserialize + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + if (syncPosition) + { + position = reader.ReadVector3(); + lastDeserializedBaselinePosition = position.Value; + } + if (syncRotation) + { + rotation = reader.ReadQuaternion(); + lastDeserializedBaselineRotation = rotation.Value; + } + if (syncScale) + { + scale = reader.ReadVector3(); + lastDeserializedBaselineScale = scale.Value; + } + + // debug draw: baseline = yellow + if (debugDraw && position.HasValue) Debug.DrawLine(position.Value, position.Value + Vector3.up, Color.yellow, 10f); + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + { + if (isServer) + { + OnClientToServerDeltaSync(position, rotation, scale); + } + else if (isClient) + { + OnServerToClientDeltaSync(position, rotation, scale); + } + } + } + + // called on server and on client, depending on SyncDirection + protected override void OnSerializeDelta(NetworkWriter writer) + { + // perf: get position/rotation directly. TransformSnapshot is too expensive. + // TransformSnapshot snapshot = ConstructSnapshot(); + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + Vector3 scale = target.localScale; + + if (syncPosition) writer.WriteVector3(position); + if (syncRotation) writer.WriteQuaternion(rotation); + if (syncScale) writer.WriteVector3(scale); + } + + // called on server and on client, depending on SyncDirection + protected override void OnDeserializeDelta(NetworkReader reader, byte baselineTick) + { + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + if (syncPosition) position = reader.ReadVector3(); + if (syncRotation) rotation = reader.ReadQuaternion(); + if (syncScale) scale = reader.ReadVector3(); + + // debug draw: delta = white + if (debugDraw && position.HasValue) Debug.DrawLine(position.Value, position.Value + Vector3.up, Color.white, 10f); + + if (isServer) + { + OnClientToServerDeltaSync(position, rotation, scale); + } + else if (isClient) + { + OnServerToClientDeltaSync(position, rotation, scale); + } + } + + // processing ////////////////////////////////////////////////////////// + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerDeltaSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever-growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; + + // only player owned objects (with a connection) can send to + // server. we can get the timestamp from the connection. + double timestamp = connectionToClient.remoteTimeStamp; + + // insert transform snapshot + SnapshotInterpolation.InsertIfNotExists( + serverSnapshots, + bufferSizeLimit, + new TransformSnapshot( + timestamp, // arrival remote timestamp. NOT remote time. + NetworkTime.localTime, // Unity 2019 doesn't have Time.timeAsDouble yet + position.HasValue ? position.Value : Vector3.zero, + rotation.HasValue ? rotation.Value : Quaternion.identity, + scale.HasValue ? scale.Value : Vector3.one + )); + } + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientDeltaSync(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; + + // Debug.Log($"[{name}] Client: received delta for baseline #{baselineTick}"); + + // 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; + + // 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 (!syncPosition) position = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].position : target.localPosition; + // if (!syncRotation) rotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : target.localRotation; + // if (!syncScale) scale = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].scale : target.localScale; + + // insert snapshot + SnapshotInterpolation.InsertIfNotExists( + clientSnapshots, + bufferSizeLimit, + new TransformSnapshot( + timestamp, // arrival remote timestamp. NOT remote time. + NetworkTime.localTime, // Unity 2019 doesn't have Time.timeAsDouble yet + position.HasValue ? position.Value : Vector3.zero, + rotation.HasValue ? rotation.Value : Quaternion.identity, + scale.HasValue ? scale.Value : Vector3.one + )); + } + + // update server /////////////////////////////////////////////////////// + void UpdateServerInterpolation() + { + // 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 (syncDirection == SyncDirection.ClientToServer && !isOwned) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + // CUSTOM CHANGE: allow for custom sendRate+sendInterval again. + // for example, if the object is moving @ 1 Hz, always put it back by 1s. + // that's how we still get smooth movement even with a global timeline. + connectionToClient.remoteTimeline - sendInterval, + // END CUSTOM CHANGE + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + if (useFixedUpdate) + pendingSnapshot = computed; + else + ApplySnapshot(computed); + } + } + } + + // update client /////////////////////////////////////////////////////// + void UpdateClientInterpolation() + { + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + // CUSTOM CHANGE: allow for custom sendRate+sendInterval again. + // for example, if the object is moving @ 1 Hz, always put it back by 1s. + // that's how we still get smooth movement even with a global timeline. + NetworkTime.time - sendInterval, // == NetworkClient.localTimeline from snapshot interpolation + // END CUSTOM CHANGE + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + if (useFixedUpdate) + pendingSnapshot = computed; + else + ApplySnapshot(computed); + } + } + + // Update() without LateUpdate() split: otherwise perf. is cut in half! + protected override void Update() + { + base.Update(); // NetworkBehaviourHybrid + + if (isServer) + { + // interpolate remote clients + UpdateServerInterpolation(); + } + // 'else if' because host mode shouldn't update both. + else if (isClient) + { + // interpolate remote client (and local player if no authority) + if (!IsClientWithAuthority) UpdateClientInterpolation(); + } + } + + void FixedUpdate() + { + if (!useFixedUpdate) return; + + if (pendingSnapshot.HasValue) + { + ApplySnapshot(pendingSnapshot.Value); + pendingSnapshot = null; + } + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination) + { + // reset any in-progress interpolation & buffers + ResetState(); + + // set the new position. + // interpolation will automatically continue. + target.position = destination; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination 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 + ResetState(); + + // set the new position. + // interpolation will automatically continue. + target.position = destination; + target.rotation = rotation; + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination 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); + } + + // 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 (syncDirection != SyncDirection.ClientToServer) 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 target.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 (syncDirection != SyncDirection.ClientToServer) 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 target.position=pos + RpcTeleport(destination, rotation); + } + + [Server] + public void ServerTeleport(Vector3 destination, Quaternion rotation) + { + OnTeleport(destination, rotation); + RpcTeleport(destination, rotation); + } + + public override void ResetState() + { + base.ResetState(); // NetworkBehaviourHybrid + + // disabled objects aren't updated anymore so let's clear the buffers. + serverSnapshots.Clear(); + clientSnapshots.Clear(); + + // reset baseline + lastSerializedBaselinePosition = Vector3.zero; + lastSerializedBaselineRotation = Quaternion.identity; + lastSerializedBaselineScale = Vector3.one; + + lastDeserializedBaselinePosition = Vector3.zero; + lastDeserializedBaselineRotation = Quaternion.identity; + lastDeserializedBaselineScale = Vector3.one; + + // Prevent resistance from CharacterController + // or non-knematic Rigidbodies when teleporting. + Physics.SyncTransforms(); + + // Debug.Log($"[{name}] ResetState to baselineTick=0"); + } + + protected virtual void OnDisable() => ResetState(); + protected virtual void OnEnable() => ResetState(); + + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // OnSerialize(initial) is called every time when a player starts observing us. + // note this is _not_ called just once on spawn. + + base.OnSerialize(writer, initialState); // NetworkBehaviourHybrid + + // 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) + { + // spawn message is used as first baseline. + // perf: get position/rotation directly. TransformSnapshot is too expensive. + // TransformSnapshot snapshot = ConstructSnapshot(); + target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); + Vector3 scale = target.localScale; + + if (syncPosition) writer.WriteVector3(position); + if (syncRotation) writer.WriteQuaternion(rotation); + if (syncScale) writer.WriteVector3(scale); + } + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + base.OnDeserialize(reader, initialState); // NetworkBehaviourHybrid + + // 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) + { + // save last deserialized baseline tick number to compare deltas against + Vector3 position = Vector3.zero; + Quaternion rotation = Quaternion.identity; + Vector3 scale = Vector3.one; + + if (syncPosition) + { + position = reader.ReadVector3(); + lastDeserializedBaselinePosition = position; + } + if (syncRotation) + { + rotation = reader.ReadQuaternion(); + lastDeserializedBaselineRotation = rotation; + } + if (syncScale) + { + scale = reader.ReadVector3(); + lastDeserializedBaselineScale = scale; + } + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnServerToClientDeltaSync(position, rotation, scale); + } + } + // CUSTOM CHANGE /////////////////////////////////////////////////////////// + // Don't run OnGUI or draw gizmos in debug builds. + // OnGUI allocates even if it does nothing. avoid in release. + //#if UNITY_EDITOR || DEVELOPMENT_BUILD +#if UNITY_EDITOR + // debug /////////////////////////////////////////////////////////////// + // END CUSTOM CHANGE /////////////////////////////////////////////////////// + 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(target.position); + + // enough alpha, in front of camera and in screen? + if (point.z >= 0 && Utils.IsPointInScreen(point)) + { + 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:{serverSnapshots.Count}"); + GUILayout.Label($"Client Buffer:{clientSnapshots.Count}"); + + 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; + + // calculate threshold for 'old enough' snapshots + double threshold = NetworkTime.localTime - NetworkClient.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 + TransformSnapshot entry = buffer.Values[i]; + bool oldEnough = entry.localTime <= 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, target.position); + Gizmos.color = Color.white; + Gizmos.DrawLine(target.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(serverSnapshots); + if (isClient) DrawGizmos(clientSnapshots); + } +#endif + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs.meta new file mode 100644 index 0000000..077cb28 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8f63ea2e505fd484193fb31c5c55ca73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs new file mode 100644 index 0000000..44a48d0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs @@ -0,0 +1,448 @@ +// NetworkTransform V3 (reliable) by mischa (2022-10) +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Reliable)")] + public class NetworkTransformReliable : NetworkTransformBase + { + uint sendIntervalCounter = 0; + double lastSendIntervalTime = double.MinValue; + TransformSnapshot? pendingSnapshot; + + [Header("Additional Settings")] + [Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")] + public float onlySyncOnChangeCorrectionMultiplier = 2; + public bool useFixedUpdate; + + [Header("Rotation")] + [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] + public float rotationSensitivity = 0.01f; + + // delta compression is capable of detecting byte-level changes. + // if we scale float position to bytes, + // then small movements will only change one byte. + // this gives optimal bandwidth. + // benchmark with 0.01 precision: 130 KB/s => 60 KB/s + // benchmark with 0.1 precision: 130 KB/s => 30 KB/s + [Header("Precision")] + [Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")] + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float positionPrecision = 0.01f; // 1 cm + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float scalePrecision = 0.01f; // 1 cm + + // delta compression needs to remember 'last' to compress against + protected Vector3Long lastSerializedPosition = Vector3Long.zero; + protected Vector3Long lastDeserializedPosition = Vector3Long.zero; + + protected Vector3Long lastSerializedScale = Vector3Long.zero; + protected Vector3Long lastDeserializedScale = Vector3Long.zero; + + // Used to store last sent snapshots + protected TransformSnapshot last; + + // update ////////////////////////////////////////////////////////////// + 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(); + } + + void FixedUpdate() + { + if (!useFixedUpdate) return; + + if (pendingSnapshot.HasValue && !IsClientWithAuthority) + { + // Apply via base method, but in FixedUpdate + Apply(pendingSnapshot.Value, pendingSnapshot.Value); + pendingSnapshot = null; + } + } + + void LateUpdate() + { + // set dirty to trigger OnSerialize. either always, or only if changed. + // It has to be checked in LateUpdate() for onlySyncOnChange to avoid + // the possibility of Update() running first before the object's movement + // script's Update(), which then causes NT to send every alternate frame + // instead. + if (isServer || (IsClientWithAuthority && NetworkClient.ready)) + { + if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct()))) + SetDirty(); + + CheckLastSendTime(); + } + } + + protected virtual void UpdateServer() + { + // 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. + // -> connectionToClient is briefly null after scene changes: + // https://github.com/MirrorNetworking/Mirror/issues/3329 + if (syncDirection == SyncDirection.ClientToServer && + connectionToClient != null && + !isOwned) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + } + } + } + + protected virtual void UpdateClient() + { + if (useFixedUpdate) + { + if (!IsClientWithAuthority && clientSnapshots.Count > 0) + { + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, + out TransformSnapshot from, + out TransformSnapshot to, + out double t + ); + pendingSnapshot = TransformSnapshot.Interpolate(from, to, t); + } + } + else + { + // client authority, and local player (= allowed to move myself)? + if (!IsClientWithAuthority) + { + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + } + } + } + } + + protected virtual void CheckLastSendTime() + { + // timeAsDouble not available in older Unity versions. + if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime)) + { + if (sendIntervalCounter == sendIntervalMultiplier) + sendIntervalCounter = 0; + sendIntervalCounter++; + } + } + + // check if position / rotation / scale changed since last sync + protected virtual bool Changed(TransformSnapshot current) => + // position is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.position, current.position, positionPrecision) || + // rotation isn't quantized / delta compressed. + // check with sensitivity. + Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity || + // scale is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.scale, current.scale, scalePrecision); + + // helper function to compare quantized representations of a Vector3 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision) + { + Compression.ScaleToLong(u, precision, out Vector3Long uQuantized); + Compression.ScaleToLong(v, precision, out Vector3Long vQuantized); + return uQuantized != vQuantized; + } + + // NT may be used on client/server/host to Owner/Observers with + // ServerToClient or ClientToServer. + // however, OnSerialize should always delta against last. + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // get current snapshot for broadcasting. + TransformSnapshot snapshot = Construct(); + + // ClientToServer optimization: + // for interpolated client owned identities, + // always broadcast the latest known snapshot so other clients can + // interpolate immediately instead of catching up too + + // TODO dirty mask? [compression is very good w/o it already] + // each vector's component is delta compressed. + // an unchanged component would still require 1 byte. + // let's use a dirty bit mask to filter those out as well. + + // initial + if (initialState) + { + // If there is a last serialized snapshot, we use it. + // This prevents the new client getting a snapshot that is different + // from what the older clients last got. If this happens, and on the next + // regular serialisation the delta compression will get wrong values. + // Notes: + // 1. Interestingly only the older clients have it wrong, because at the end + // of this function, last = snapshot which is the initial state's snapshot + // 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate + // snapshot constructed would have been the same as the last anyway. + if (last.remoteTime > 0) snapshot = last; + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + else + writer.WriteQuaternion(snapshot.rotation); + } + if (syncScale) writer.WriteVector3(snapshot.scale); + } + // delta + else + { + // int before = writer.Position; + + if (syncPosition) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedPosition, quantized); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + else + writer.WriteQuaternion(snapshot.rotation); + } + if (syncScale) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedScale, quantized); + } + } + + // save serialized as 'last' for next delta compression + if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); + if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); + + // set 'last' + last = snapshot; + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + // initial + if (initialState) + { + if (syncPosition) position = reader.ReadVector3(); + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + else + rotation = reader.ReadQuaternion(); + } + if (syncScale) scale = reader.ReadVector3(); + } + // delta + else + { + // varint -> delta -> quantize + if (syncPosition) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition); + position = Compression.ScaleToFloat(quantized, positionPrecision); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + else + rotation = reader.ReadQuaternion(); + } + if (syncScale) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale); + scale = Compression.ScaleToFloat(quantized, scalePrecision); + } + } + + // handle depending on server / client / host. + // server has priority for host mode. + if (isServer) OnClientToServerSync(position, rotation, scale); + else if (isClient) OnServerToClientSync(position, rotation, scale); + + // save deserialized as 'last' for next delta compression + if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition); + if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale); + } + + // sync //////////////////////////////////////////////////////////////// + + // 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 (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; + + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier)) + { + RewriteHistory( + serverSnapshots, + connectionToClient.remoteTimeStamp, + NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline. + NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet + GetPosition(), + GetRotation(), + GetScale()); + } + + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); + } + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // don't apply for local player with authority + if (IsClientWithAuthority) return; + + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier)) + { + RewriteHistory( + clientSnapshots, + NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline. + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + NetworkClient.sendInterval * sendIntervalMultiplier, + GetPosition(), + GetRotation(), + GetScale()); + } + + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); + } + + // only sync on change ///////////////////////////////////////////////// + // snap interp. needs a continous flow of packets. + // 'only sync on change' interrupts it while not changed. + // once it restarts, snap interp. will interp from the last old position. + // this will cause very noticeable stutter for the first move each time. + // the fix is quite simple. + + // 1. detect if the remaining snapshot is too old from a past move. + static bool NeedsCorrection( + SortedList snapshots, + double remoteTimestamp, + double bufferTime, + double toleranceMultiplier) => + snapshots.Count == 1 && + remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier; + + // 2. insert a fake snapshot at current position, + // exactly one 'sendInterval' behind the newly received one. + static void RewriteHistory( + SortedList snapshots, + // timestamp of packet arrival, not interpolated remote time! + double remoteTimeStamp, + double localTime, + double sendInterval, + Vector3 position, + Quaternion rotation, + Vector3 scale) + { + // clear the previous snapshot + snapshots.Clear(); + + // insert a fake one at where we used to be, + // 'sendInterval' behind the new one. + SnapshotInterpolation.InsertIfNotExists( + snapshots, + NetworkClient.snapshotSettings.bufferLimit, + new TransformSnapshot( + remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time. + localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet + position, + rotation, + scale + ) + ); + } + + // reset state for next session. + // do not ever call this during a session (i.e. after teleport). + // calling this will break delta compression. + public override void ResetState() + { + base.ResetState(); + + // reset delta + lastSerializedPosition = Vector3Long.zero; + lastDeserializedPosition = Vector3Long.zero; + + lastSerializedScale = Vector3Long.zero; + lastDeserializedScale = Vector3Long.zero; + + // reset 'last' for delta too + last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero); + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs.meta new file mode 100644 index 0000000..c8be862 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8ff3ba0becae47b8b9381191598957c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs new file mode 100644 index 0000000..adac721 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -0,0 +1,462 @@ +// NetworkTransform V2 by mischa (2021-07) +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Unreliable)")] + public class NetworkTransformUnreliable : NetworkTransformBase + { + uint sendIntervalCounter = 0; + double lastSendIntervalTime = double.MinValue; + TransformSnapshot? pendingSnapshot; + + [Header("Additional Settings")] + // Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover. + [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results.")] + public float bufferResetMultiplier = 3; + public bool useFixedUpdate; + + [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; + + // Used to store last sent snapshots + protected TransformSnapshot lastSnapshot; + protected Changed cachedChangedComparison; + protected bool hasSentUnchangedPosition; + + // update ////////////////////////////////////////////////////////////// + // Update applies interpolation + void Update() + { + if (isServer) UpdateServerInterpolation(); + // for all other clients (and for local player if !authority), + // we need to apply snapshots from the buffer. + // 'else if' because host mode shouldn't interpolate client + else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation(); + } + + void FixedUpdate() + { + if (!useFixedUpdate) return; + + if (pendingSnapshot.HasValue) + { + Apply(pendingSnapshot.Value, pendingSnapshot.Value); + pendingSnapshot = null; + } + } + + // LateUpdate broadcasts. + // movement scripts may change positions in Update. + // use LateUpdate to ensure changes are detected in the same frame. + // otherwise this may run before user update, delaying detection until next frame. + // this could cause visible jitter. + void LateUpdate() + { + // if server then always sync to others. + if (isServer) UpdateServerBroadcast(); + // client authority, and local player (= allowed to move myself)? + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient && IsClientWithAuthority) UpdateClientBroadcast(); + } + + protected virtual void CheckLastSendTime() + { + // We check interval every frame, and then send if interval is reached. + // So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent, + // thus we reset the counter here. + // This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame, + // because intervalCounter is always = 1 in the previous version. + + // Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571 + + if (sendIntervalCounter >= sendIntervalMultiplier) + sendIntervalCounter = 0; + + // timeAsDouble not available in older Unity versions. + if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime)) + sendIntervalCounter++; + } + + void UpdateServerBroadcast() + { + // 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. + // TODO send same time that NetworkServer sends time snapshot? + CheckLastSendTime(); + + if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation! + (syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority)) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + TransformSnapshot snapshot = Construct(); + + cachedChangedComparison = CompareChangedSnapshots(snapshot); + + if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; } + + SyncData syncData = new SyncData(cachedChangedComparison, snapshot); + + RpcServerToClientSync(syncData); + + if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + UpdateLastSentSnapshot(cachedChangedComparison, snapshot); + } + } + } + + void UpdateServerInterpolation() + { + // 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. + // -> connectionToClient is briefly null after scene changes: + // https://github.com/MirrorNetworking/Mirror/issues/3329 + if (syncDirection == SyncDirection.ClientToServer && + connectionToClient != null && + !isOwned) + { + if (serverSnapshots.Count == 0) return; + + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); + } + } + + void UpdateClientBroadcast() + { + // 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. + CheckLastSendTime(); + if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation! + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + TransformSnapshot snapshot = Construct(); + + cachedChangedComparison = CompareChangedSnapshots(snapshot); + + if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; } + + SyncData syncData = new SyncData(cachedChangedComparison, snapshot); + + CmdClientToServerSync(syncData); + + if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + UpdateLastSentSnapshot(cachedChangedComparison, snapshot); + } + } + } + + void UpdateClientInterpolation() + { + // only while we have snapshots + if (clientSnapshots.Count == 0) return; + + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); + } + + public override void 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(GetPosition()); + if (syncRotation) writer.WriteQuaternion(GetRotation()); + if (syncScale) writer.WriteVector3(GetScale()); + } + } + + 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) SetPosition(reader.ReadVector3()); + if (syncRotation) SetRotation(reader.ReadQuaternion()); + if (syncScale) SetScale(reader.ReadVector3()); + } + } + + protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot) + { + if (change == Changed.None || change == Changed.CompressRot) return; + + if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x; + if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y; + if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z; + + if (compressRotation) + { + if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation; + } + else + { + Vector3 newRotation; + newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x; + newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y; + newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z; + + lastSnapshot.rotation = Quaternion.Euler(newRotation); + } + + if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale; + } + + // Returns true if position, rotation AND scale are unchanged, within given sensitivity range. + // Note the sensitivity comparison are different for pos, rot and scale. + protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot) + { + Changed change = Changed.None; + + if (syncPosition) + { + bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity; + if (positionChanged) + { + if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX; + if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY; + if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ; + } + } + + if (syncRotation) + { + if (compressRotation) + { + bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; + if (rotationChanged) + { + // Here we set all Rot enum flags, to tell us if there was a change in rotation + // when using compression. If no change, we don't write the compressed Quat. + change |= Changed.CompressRot; + change |= Changed.Rot; + } + else + { + change |= Changed.CompressRot; + } + } + else + { + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX; + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY; + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ; + } + } + + if (syncScale) + { + if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale; + } + + return change; + } + + [Command(channel = Channels.Unreliable)] + void CmdClientToServerSync(SyncData syncData) + { + OnClientToServerSync(syncData); + //For client authority, immediately pass on the client snapshot to all other + //clients instead of waiting for server to send its snapshots. + if (syncDirection == SyncDirection.ClientToServer) + RpcServerToClientSync(syncData); + } + + protected virtual void OnClientToServerSync(SyncData syncData) + { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; + + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) 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) + { + double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval; + + if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + ResetState(); + } + + UpdateSyncData(ref syncData, serverSnapshots); + + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale); + } + + [ClientRpc(channel = Channels.Unreliable)] + void RpcServerToClientSync(SyncData syncData) => + OnServerToClientSync(syncData); + + protected virtual void OnServerToClientSync(SyncData syncData) + { + // 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; + + // 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) + { + double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval; + + if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + ResetState(); + } + + UpdateSyncData(ref syncData, clientSnapshots); + + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale); + } + + protected virtual void UpdateSyncData(ref SyncData syncData, SortedList snapshots) + { + if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot) + { + syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition(); + syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation(); + syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale(); + } + else + { + // Just going to update these without checking if syncposition or not, + // because if not syncing position, NT will not apply any position data + // to the target during Apply(). + + syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x); + syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y); + syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z); + + // If compressRot is true, we already have the Quat in syncdata. + if ((syncData.changedDataByte & Changed.CompressRot) == 0) + { + syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x); + syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ; + syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z); + + syncData.quatRotation = Quaternion.Euler(syncData.vecRotation); + } + else + { + syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation()); + } + + syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale()); + } + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs.meta new file mode 100644 index 0000000..e5e968f --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a553cb17010b2403e8523b558bffbc14 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs new file mode 100644 index 0000000..01b863c --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs @@ -0,0 +1,68 @@ +// 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 TransformSnapshot : 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 remoteTime { get; set; } + + // the local timestamp (when we received it) + // used to know if the first two snapshots are old enough to start. + public double localTime { get; set; } + + public Vector3 position; + public Quaternion rotation; + public Vector3 scale; + + public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale) + { + this.remoteTime = remoteTime; + this.localTime = localTime; + this.position = position; + this.rotation = rotation; + this.scale = scale; + } + + public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot 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 TransformSnapshot( + // 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) + ); + } + + public override string ToString() => + $"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})"; + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs.meta b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs.meta new file mode 100644 index 0000000..b8e9173 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: d3dae77b43dc4e1dbb2012924b2da79c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs new file mode 100644 index 0000000..9b6d51c --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs @@ -0,0 +1,156 @@ +using UnityEngine; +using System; +using Mirror; + +namespace Mirror +{ + [Serializable] + public struct SyncData + { + public Changed changedDataByte; + public Vector3 position; + public Quaternion quatRotation; + public Vector3 vecRotation; + public Vector3 scale; + + public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale) + { + this.changedDataByte = _dataChangedByte; + this.position = _position; + this.quatRotation = _rotation; + this.vecRotation = quatRotation.eulerAngles; + this.scale = _scale; + } + + public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot) + { + this.changedDataByte = _dataChangedByte; + this.position = _snapshot.position; + this.quatRotation = _snapshot.rotation; + this.vecRotation = quatRotation.eulerAngles; + this.scale = _snapshot.scale; + } + + public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale) + { + this.changedDataByte = _dataChangedByte; + this.position = _position; + this.vecRotation = _vecRotation; + this.quatRotation = Quaternion.Euler(vecRotation); + this.scale = _scale; + } + } + + [Flags] + public enum Changed : byte + { + None = 0, + PosX = 1 << 0, + PosY = 1 << 1, + PosZ = 1 << 2, + CompressRot = 1 << 3, + RotX = 1 << 4, + RotY = 1 << 5, + RotZ = 1 << 6, + Scale = 1 << 7, + + Pos = PosX | PosY | PosZ, + Rot = RotX | RotY | RotZ + } + + + public static class SyncDataReaderWriter + { + public static void WriteSyncData(this NetworkWriter writer, SyncData syncData) + { + writer.WriteByte((byte)syncData.changedDataByte); + + // Write position + if ((syncData.changedDataByte & Changed.PosX) > 0) + { + writer.WriteFloat(syncData.position.x); + } + + if ((syncData.changedDataByte & Changed.PosY) > 0) + { + writer.WriteFloat(syncData.position.y); + } + + if ((syncData.changedDataByte & Changed.PosZ) > 0) + { + writer.WriteFloat(syncData.position.z); + } + + // Write rotation + if ((syncData.changedDataByte & Changed.CompressRot) > 0) + { + if((syncData.changedDataByte & Changed.Rot) > 0) + { + writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation)); + } + } + else + { + if ((syncData.changedDataByte & Changed.RotX) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.x); + } + + if ((syncData.changedDataByte & Changed.RotY) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.y); + } + + if ((syncData.changedDataByte & Changed.RotZ) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.z); + } + } + + // Write scale + if ((syncData.changedDataByte & Changed.Scale) > 0) + { + writer.WriteVector3(syncData.scale); + } + } + + public static SyncData ReadSyncData(this NetworkReader reader) + { + Changed changedData = (Changed)reader.ReadByte(); + + // If we have nothing to read here, let's say because posX is unchanged, then we can write anything + // for now, but in the NT, we will need to check changedData again, to put the right values of the axis + // back. We don't have it here. + + Vector3 position = + new Vector3( + (changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0 + ); + + Vector3 vecRotation = new Vector3(); + Quaternion quatRotation = new Quaternion(); + + if ((changedData & Changed.CompressRot) > 0) + { + quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion(); + } + else + { + vecRotation = + new Vector3( + (changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0 + ); + } + + Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3(); + + SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale); + + return _syncData; + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta new file mode 100644 index 0000000..2f2d647 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a1c0832ca88e749ff96fe04cebb617ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform2k.meta b/Assets/Mirror/Components/NetworkTransform2k.meta deleted file mode 100644 index fe99bf0..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index b7b8e81..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ʻ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 deleted file mode 100644 index a569990..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 54e77a7..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs +++ /dev/null @@ -1,776 +0,0 @@ -// 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 deleted file mode 100644 index ab649d9..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 8032506..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index ae36756..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index efd91c0..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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 deleted file mode 100644 index f43458f..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/Components/PredictedRigidbody.meta b/Assets/Mirror/Components/PredictedRigidbody.meta new file mode 100644 index 0000000..a513fe0 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 09cc6745984c453a8cfb4cf4244d2570 +timeCreated: 1693576410 diff --git a/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat new file mode 100644 index 0000000..ff29a73 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat @@ -0,0 +1,85 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: LocalGhostMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - _ALPHAPREMULTIPLY_ON + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 3000 + stringTagMap: + RenderType: Transparent + disabledShaderPasses: [] + m_LockedProperties: + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 10 + - _GlossMapScale: 1 + - _Glossiness: 0.92 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 3 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 0 + m_Colors: + - _Color: {r: 1, g: 0, b: 0.067070484, a: 0.15686275} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta new file mode 100644 index 0000000..4a9b6eb --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 411a48b4a197d4924bec3e3809bc9320 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs new file mode 100644 index 0000000..02abda3 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -0,0 +1,1021 @@ +// PredictedRigidbody which stores & indidvidually rewinds history per Rigidbody. +// +// This brings significant performance savings because: +// - if a scene has 1000 objects +// - and a player interacts with say 3 objects at a time +// - Physics.Simulate() would resimulate 1000 objects +// - where as this component only resimulates the 3 changed objects +// +// The downside is that history rewinding is done manually via Vector math, +// instead of real physics. It's not 100% correct - but it sure is fast! +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public enum PredictionMode { Smooth, Fast } + + // [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it. + public class PredictedRigidbody : NetworkBehaviour + { + Transform tf; // this component is performance critical. cache .transform getter! + + // Prediction sometimes moves the Rigidbody to a ghost object. + // .predictedRigidbody is always kept up to date to wherever the RB is. + // other components should use this when accessing Rigidbody. + public Rigidbody predictedRigidbody; + Transform predictedRigidbodyTransform; // predictedRigidbody.transform for performance (Get/SetPositionAndRotation) + + Vector3 lastPosition; + + // motion smoothing happen on-demand, because it requires moving physics components to another GameObject. + // this only starts at a given velocity and ends when stopped moving. + // to avoid constant on/off/on effects, it also stays on for a minimum time. + [Header("Motion Smoothing")] + [Tooltip("Prediction supports two different modes: Smooth and Fast:\n\nSmooth: Physics are separated from the GameObject & applied in the background. Rendering smoothly follows the physics for perfectly smooth interpolation results. Much softer, can be even too soft where sharp collisions won't look as sharp (i.e. Billiard balls avoid the wall before even hitting it).\n\nFast: Physics remain on the GameObject and corrections are applied hard. Much faster since we don't need to update a separate GameObject, a bit harsher, more precise.")] + public PredictionMode mode = PredictionMode.Smooth; + [Tooltip("Smoothing via Ghost-following only happens on demand, while moving with a minimum velocity.")] + public float motionSmoothingVelocityThreshold = 0.1f; + float motionSmoothingVelocityThresholdSqr; // ² cached in Awake + public float motionSmoothingAngularVelocityThreshold = 5.0f; // Billiards demo: 0.1 is way too small, takes forever for IsMoving()==false + float motionSmoothingAngularVelocityThresholdSqr; // ² cached in Awake + public float motionSmoothingTimeTolerance = 0.5f; + double motionSmoothingLastMovedTime; + + // client keeps state history for correction & reconciliation. + // this needs to be a SortedList because we need to be able to insert inbetween. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! + [Header("State History")] + public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough + readonly SortedList stateHistory = new SortedList(); + public float recordInterval = 0.050f; + + [Tooltip("(Optional) performance optimization where FixedUpdate.RecordState() only inserts state into history if the state actually changed.\nThis is generally a good idea.")] + public bool onlyRecordChanges = true; + + [Tooltip("(Optional) performance optimization where received state is compared to the LAST recorded state first, before sampling the whole history.\n\nThis can save significant traversal overhead for idle objects with a tiny chance of missing corrections for objects which revisisted the same position in the recent history twice.")] + public bool compareLastFirst = true; + + [Header("Reconciliation")] + [Tooltip("Correction threshold in meters. For example, 0.1 means that if the client is off by more than 10cm, it gets corrected.")] + public double positionCorrectionThreshold = 0.10; + double positionCorrectionThresholdSqr; // ² cached in Awake + [Tooltip("Correction threshold in degrees. For example, 5 means that if the client is off by more than 5 degrees, it gets corrected.")] + public double rotationCorrectionThreshold = 5; + + [Tooltip("Applying server corrections one frame ahead gives much better results. We don't know why yet, so this is an option for now.")] + public bool oneFrameAhead = true; + + [Header("Smoothing")] + [Tooltip("Snap to the server state directly when velocity is < threshold. This is useful to reduce jitter/fighting effects before coming to rest.\nNote this applies position, rotation and velocity(!) so it's still smooth.")] + public float snapThreshold = 2; // 0.5 has too much fighting-at-rest, 2 seems ideal. + + [Header("Visual Interpolation")] + [Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")] + public bool showGhost = true; + [Tooltip("Physics components are moved onto a ghost object beyond this threshold. Main object visually interpolates to it.")] + public float ghostVelocityThreshold = 0.1f; + + [Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")] + public Material localGhostMaterial; + public Material remoteGhostMaterial; + + [Tooltip("Performance optimization: only create/destroy ghosts every n-th frame is enough.")] + public int checkGhostsEveryNthFrame = 4; + + [Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")] + public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least + public float rotationInterpolationSpeed = 10; + + [Tooltip("Teleport if we are further than 'multiplier x collider size' behind.")] + public float teleportDistanceMultiplier = 10; + + [Header("Bandwidth")] + [Tooltip("Reduce sends while velocity==0. Client's objects may slightly move due to gravity/physics, so we still want to send corrections occasionally even if an object is idle on the server the whole time.")] + public bool reduceSendsWhileIdle = true; + + // Rigidbody & Collider are moved out into a separate object. + // this way the visual object can smoothly follow. + protected GameObject physicsCopy; + // protected Transform physicsCopyTransform; // caching to avoid GetComponent + // protected Rigidbody physicsCopyRigidbody => rb; // caching to avoid GetComponent + // protected Collider physicsCopyCollider; // caching to avoid GetComponent + float smoothFollowThreshold; // caching to avoid calculation in LateUpdate + float smoothFollowThresholdSqr; // caching to avoid calculation in LateUpdate + + // we also create one extra ghost for the exact known server state. + protected GameObject remoteCopy; + + // joints + Vector3 initialPosition; + Quaternion initialRotation; + // Vector3 initialScale; // don't change scale for now. causes issues with parenting. + + Color originalColor; + + protected virtual void Awake() + { + tf = transform; + predictedRigidbody = GetComponent(); + if (predictedRigidbody == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component."); + predictedRigidbodyTransform = predictedRigidbody.transform; + + // in fast mode, we need to force enable Rigidbody.interpolation. + // otherwise there's not going to be any smoothing whatsoever. + if (mode == PredictionMode.Fast) + { + predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate; + } + + // cache some threshold to avoid calculating them in LateUpdate + float colliderSize = GetComponentInChildren().bounds.size.magnitude; + smoothFollowThreshold = colliderSize * teleportDistanceMultiplier; + smoothFollowThresholdSqr = smoothFollowThreshold * smoothFollowThreshold; + + // cache initial position/rotation/scale to be used when moving physics components (configurable joints' range of motion) + initialPosition = tf.position; + initialRotation = tf.rotation; + // initialScale = tf.localScale; + + // cache ² computations + motionSmoothingVelocityThresholdSqr = motionSmoothingVelocityThreshold * motionSmoothingVelocityThreshold; + motionSmoothingAngularVelocityThresholdSqr = motionSmoothingAngularVelocityThreshold * motionSmoothingAngularVelocityThreshold; + positionCorrectionThresholdSqr = positionCorrectionThreshold * positionCorrectionThreshold; + } + + protected virtual void CopyRenderersAsGhost(GameObject destination, Material material) + { + // find the MeshRenderer component, which sometimes is on a child. + MeshRenderer originalMeshRenderer = GetComponentInChildren(true); + MeshFilter originalMeshFilter = GetComponentInChildren(true); + if (originalMeshRenderer != null && originalMeshFilter != null) + { + MeshFilter meshFilter = destination.AddComponent(); + meshFilter.mesh = originalMeshFilter.mesh; + + MeshRenderer meshRenderer = destination.AddComponent(); + meshRenderer.material = originalMeshRenderer.material; + + // renderers often have multiple materials. copy all. + if (originalMeshRenderer.materials != null) + { + Material[] materials = new Material[originalMeshRenderer.materials.Length]; + for (int i = 0; i < materials.Length; ++i) + { + materials[i] = material; + } + meshRenderer.materials = materials; // need to reassign to see it in effect + } + } + // if we didn't find a renderer, show a warning + else Debug.LogWarning($"PredictedRigidbody: {name} found no renderer to copy onto the visual object. If you are using a custom setup, please overwrite PredictedRigidbody.CreateVisualCopy()."); + } + + // instantiate a physics-only copy of the gameobject to apply corrections. + // this way the main visual object can smoothly follow. + // it's best to separate the physics instead of separating the renderers. + // some projects have complex rendering / animation setups which we can't touch. + // besides, Rigidbody+Collider are two components, where as renders may be many. + protected virtual void CreateGhosts() + { + // skip if host mode or already separated + if (isServer || physicsCopy != null) return; + + // Debug.Log($"Separating Physics for {name}"); // logging this allocates too much + + // create an empty GameObject with the same name + _Physical + // it's important to copy world position/rotation/scale, not local! + // because the original object may be a child of another. + // + // for example: + // parent (scale=1.5) + // child (scale=0.5) + // + // if we copy localScale then the copy has scale=0.5, where as the + // original would have a global scale of ~1.0. + physicsCopy = new GameObject($"{name}_Physical"); + + // assign the same Layer for the physics copy. + // games may use a custom physics collision matrix, layer matters. + physicsCopy.layer = gameObject.layer; + + // add the PredictedRigidbodyPhysical component + PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent(); + physicsGhostRigidbody.target = tf; + + // when moving (Configurable)Joints, their range of motion is + // relative to the initial position. if we move them after the + // GameObject rotated, the range of motion is wrong. + // the easiest solution is to move to initial position, + // then move physics components, then move back. + // => remember previous + Vector3 position = tf.position; + Quaternion rotation = tf.rotation; + // Vector3 scale = tf.localScale; // don't change scale for now. causes issues with parenting. + // => reset to initial + physicsGhostRigidbody.transform.position = tf.position = initialPosition; + physicsGhostRigidbody.transform.rotation = tf.rotation = initialRotation; + physicsGhostRigidbody.transform.localScale = tf.lossyScale;// world scale! // = initialScale; // don't change scale for now. causes issues with parenting. + // => move physics components + PredictionUtils.MovePhysicsComponents(gameObject, physicsCopy); + // => reset previous + physicsGhostRigidbody.transform.position = tf.position = position; + physicsGhostRigidbody.transform.rotation = tf.rotation = rotation; + //physicsGhostRigidbody.transform.localScale = tf.lossyScale; // world scale! //= scale; // don't change scale for now. causes issues with parenting. + + // show ghost by copying all renderers / materials with ghost material applied + if (showGhost) + { + // one for the locally predicted rigidbody + CopyRenderersAsGhost(physicsCopy, localGhostMaterial); + + // one for the latest remote state for comparison + // it's important to copy world position/rotation/scale, not local! + // because the original object may be a child of another. + // + // for example: + // parent (scale=1.5) + // child (scale=0.5) + // + // if we copy localScale then the copy has scale=0.5, where as the + // original would have a global scale of ~1.0. + remoteCopy = new GameObject($"{name}_Remote"); + remoteCopy.transform.position = tf.position; // world position! + remoteCopy.transform.rotation = tf.rotation; // world rotation! + remoteCopy.transform.localScale = tf.lossyScale; // world scale! + CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial); + } + + // assign our Rigidbody reference to the ghost + predictedRigidbody = physicsCopy.GetComponent(); + predictedRigidbodyTransform = predictedRigidbody.transform; + } + + protected virtual void DestroyGhosts() + { + // move the copy's Rigidbody back onto self. + // important for scene objects which may be reused for AOI spawn/despawn. + // otherwise next time they wouldn't have a collider anymore. + if (physicsCopy != null) + { + // when moving (Configurable)Joints, their range of motion is + // relative to the initial position. if we move them after the + // GameObject rotated, the range of motion is wrong. + // the easiest solution is to move to initial position, + // then move physics components, then move back. + // => remember previous + Vector3 position = tf.position; + Quaternion rotation = tf.rotation; + Vector3 scale = tf.localScale; + // => reset to initial + physicsCopy.transform.position = tf.position = initialPosition; + physicsCopy.transform.rotation = tf.rotation = initialRotation; + physicsCopy.transform.localScale = tf.lossyScale;// = initialScale; + // => move physics components + PredictionUtils.MovePhysicsComponents(physicsCopy, gameObject); + // => reset previous + tf.position = position; + tf.rotation = rotation; + tf.localScale = scale; + + // when moving components back, we need to undo the joints initial-delta rotation that we added. + Destroy(physicsCopy); + + // reassign our Rigidbody reference + predictedRigidbody = GetComponent(); + predictedRigidbodyTransform = predictedRigidbody.transform; + } + + // simply destroy the remote copy + if (remoteCopy != null) Destroy(remoteCopy); + } + + // this shows in profiler LateUpdates! need to make this as fast as possible! + protected virtual void SmoothFollowPhysicsCopy() + { + // hard follow: + // predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); + // tf.SetPositionAndRotation(physicsPosition, physicsRotation); + + // ORIGINAL VERSION: CLEAN AND SIMPLE + /* + // if we are further than N colliders sizes behind, then teleport + float colliderSize = physicsCopyCollider.bounds.size.magnitude; + float threshold = colliderSize * teleportDistanceMultiplier; + float distance = Vector3.Distance(tf.position, physicsCopyRigidbody.position); + if (distance > threshold) + { + tf.position = physicsCopyRigidbody.position; + tf.rotation = physicsCopyRigidbody.rotation; + Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}"); + return; + } + + // smoothly interpolate to the target position. + // speed relative to how far away we are + float positionStep = distance * positionInterpolationSpeed; + tf.position = Vector3.MoveTowards(tf.position, physicsCopyRigidbody.position, positionStep * Time.deltaTime); + + // smoothly interpolate to the target rotation. + // Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. + tf.rotation = Quaternion.Slerp(tf.rotation, physicsCopyRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime); + */ + + // FAST VERSION: this shows in profiler a lot, so cache EVERYTHING! + tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than tf.position + tf.rotation + predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); // faster than Rigidbody .position and .rotation + float deltaTime = Time.deltaTime; + + // slow and simple version: + // float distance = Vector3.Distance(currentPosition, physicsPosition); + // if (distance > smoothFollowThreshold) + // faster version + Vector3 delta = physicsPosition - currentPosition; + float sqrDistance = Vector3.SqrMagnitude(delta); + float distance = Mathf.Sqrt(sqrDistance); + if (sqrDistance > smoothFollowThresholdSqr) + { + tf.SetPositionAndRotation(physicsPosition, physicsRotation); // faster than .position and .rotation manually + Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {smoothFollowThreshold:F2}"); + return; + } + + // smoothly interpolate to the target position. + // speed relative to how far away we are. + // => speed increases by distance² because the further away, the + // sooner we need to catch the fuck up + // float positionStep = (distance * distance) * interpolationSpeed; + float positionStep = distance * positionInterpolationSpeed; + + Vector3 newPosition = MoveTowardsCustom(currentPosition, physicsPosition, delta, sqrDistance, distance, positionStep * deltaTime); + + // smoothly interpolate to the target rotation. + // Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. + // Quaternions always need to be normalized in order to be a valid rotation after operations + Quaternion newRotation = Quaternion.Slerp(currentRotation, physicsRotation, rotationInterpolationSpeed * deltaTime).normalized; + + // assign position and rotation together. faster than accessing manually. + tf.SetPositionAndRotation(newPosition, newRotation); + } + + // simple and slow version with MoveTowards, which recalculates delta and delta.sqrMagnitude: + // Vector3 newPosition = Vector3.MoveTowards(currentPosition, physicsPosition, positionStep * deltaTime); + // faster version copied from MoveTowards: + // this increases Prediction Benchmark Client's FPS from 615 -> 640. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector3 MoveTowardsCustom( + Vector3 current, + Vector3 target, + Vector3 _delta, // pass this in since we already calculated it + float _sqrDistance, // pass this in since we already calculated it + float _distance, // pass this in since we already calculated it + float maxDistanceDelta) + { + if (_sqrDistance == 0.0 || maxDistanceDelta >= 0.0 && _sqrDistance <= maxDistanceDelta * maxDistanceDelta) + return target; + + float distFactor = maxDistanceDelta / _distance; // unlike Vector3.MoveTowards, we only calculate this once + return new Vector3( + // current.x + (_delta.x / _distance) * maxDistanceDelta, + // current.y + (_delta.y / _distance) * maxDistanceDelta, + // current.z + (_delta.z / _distance) * maxDistanceDelta); + current.x + _delta.x * distFactor, + current.y + _delta.y * distFactor, + current.z + _delta.z * distFactor); + } + + // destroy visual copy only in OnStopClient(). + // OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed. + public override void OnStopClient() + { + DestroyGhosts(); + } + + void UpdateServer() + { + // bandwidth optimization while idle. + if (reduceSendsWhileIdle) + { + // while moving, always sync every frame for immediate corrections. + // while idle, only sync once per second. + // + // we still need to sync occasionally because objects on client + // may still slide or move slightly due to gravity, physics etc. + // and those still need to get corrected if not moving on server. + // + // TODO + // next round of optimizations: if client received nothing for 1s, + // force correct to last received state. then server doesn't need + // to send once per second anymore. + syncInterval = IsMoving() ? 0 : 1; + } + + // always set dirty to always serialize in next sync interval. + SetDirty(); + } + + // movement detection is virtual, in case projects want to use other methods. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual bool IsMoving() => + // straight forward implementation + // predictedRigidbody.velocity.magnitude >= motionSmoothingVelocityThreshold || + // predictedRigidbody.angularVelocity.magnitude >= motionSmoothingAngularVelocityThreshold; + // faster implementation with cached ² +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity.sqrMagnitude >= motionSmoothingVelocityThresholdSqr || +#else + predictedRigidbody.velocity.sqrMagnitude >= motionSmoothingVelocityThresholdSqr || +#endif + predictedRigidbody.angularVelocity.sqrMagnitude >= motionSmoothingAngularVelocityThresholdSqr; + // TODO maybe merge the IsMoving() checks & callbacks with UpdateState(). + void UpdateGhosting() + { + // perf: enough to check ghosts every few frames. + // PredictionBenchmark: only checking every 4th frame: 585 => 600 FPS + if (Time.frameCount % checkGhostsEveryNthFrame != 0) return; + + // client only uses ghosts on demand while interacting. + // this way 1000 GameObjects don't need +1000 Ghost GameObjects all the time! + + // no ghost at the moment + if (physicsCopy == null) + { + // faster than velocity threshold? then create the ghosts. + // with 10% buffer zone so we don't flip flop all the time. + if (IsMoving()) + { + CreateGhosts(); + OnBeginPrediction(); + } + } + // ghosting at the moment + else + { + // always set last moved time while moving. + // this way we can avoid on/off/oneffects when stopping. + if (IsMoving()) + { + motionSmoothingLastMovedTime = NetworkTime.time; + } + // slower than velocity threshold? then destroy the ghosts. + // with a minimum time since starting to move, to avoid on/off/on effects. + else + { + if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) + { + DestroyGhosts(); + OnEndPrediction(); + physicsCopy = null; // TESTING + } + } + } + } + + // when using Fast mode, we don't create any ghosts. + // but we still want to check IsMoving() in order to support the same + // user callbacks. + bool lastMoving = false; + void UpdateState() + { + // perf: enough to check ghosts every few frames. + // PredictionBenchmark: only checking every 4th frame: 770 => 800 FPS + if (Time.frameCount % checkGhostsEveryNthFrame != 0) return; + + bool moving = IsMoving(); + + // started moving? + if (moving && !lastMoving) + { + OnBeginPrediction(); + lastMoving = true; + } + // stopped moving? + else if (!moving && lastMoving) + { + // ensure a minimum time since starting to move, to avoid on/off/on effects. + if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) + { + OnEndPrediction(); + lastMoving = false; + } + } + } + + void Update() + { + if (isServer) UpdateServer(); + if (isClientOnly) + { + if (mode == PredictionMode.Smooth) + UpdateGhosting(); + else if (mode == PredictionMode.Fast) + UpdateState(); + } + } + + void LateUpdate() + { + // only follow on client-only, not in server or host mode + if (isClientOnly && mode == PredictionMode.Smooth && physicsCopy) SmoothFollowPhysicsCopy(); + } + + void FixedUpdate() + { + // on clients (not host) we record the current state every FixedUpdate. + // this is cheap, and allows us to keep a dense history. + if (!isClientOnly) return; + + // OPTIMIZATION: RecordState() is expensive because it inserts into a SortedList. + // only record if state actually changed! + // risks not having up to date states when correcting, + // but it doesn't matter since we'll always compare with the 'newest' anyway. + // + // we check in here instead of in RecordState() because RecordState() should definitely record if we call it! + if (onlyRecordChanges) + { + // TODO maybe don't reuse the correction thresholds? + tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); + // clean & simple: + // if (Vector3.Distance(lastRecorded.position, position) < positionCorrectionThreshold && + // Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold) + // faster: + if ((lastRecorded.position - position).sqrMagnitude < positionCorrectionThresholdSqr && + Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold) + { + // Debug.Log($"FixedUpdate for {name}: taking optimized early return instead of recording state."); + return; + } + } + + RecordState(); + } + + // manually store last recorded so we can easily check against this + // without traversing the SortedList. + RigidbodyState lastRecorded; + double lastRecordTime; + void RecordState() + { + // performance optimization: only call NetworkTime.time getter once + double networkTime = NetworkTime.time; + + // instead of recording every fixedupdate, let's record in an interval. + // we don't want to record every tiny move and correct too hard. + if (networkTime < lastRecordTime + recordInterval) return; + lastRecordTime = networkTime; + + // NetworkTime.time is always behind by bufferTime. + // prediction aims to be on the exact same server time (immediately). + // use predictedTime to record state, otherwise we would record in the past. + double predictedTime = NetworkTime.predictedTime; + + // FixedUpdate may run twice in the same frame / NetworkTime.time. + // for now, simply don't record if already recorded there. + // previously we checked ContainsKey which is O(logN) for SortedList + // if (stateHistory.ContainsKey(predictedTime)) + // return; + // instead, simply store the last recorded time and don't insert if same. + if (predictedTime == lastRecorded.timestamp) return; + + // keep state history within limit + if (stateHistory.Count >= stateHistoryLimit) + stateHistory.RemoveAt(0); + + // grab current position/rotation/velocity only once. + // this is performance critical, avoid calling .transform multiple times. + tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than accessing .position + .rotation manually +#if UNITY_6000_0_OR_NEWER + Vector3 currentVelocity = predictedRigidbody.linearVelocity; +#else + Vector3 currentVelocity = predictedRigidbody.velocity; +#endif + Vector3 currentAngularVelocity = predictedRigidbody.angularVelocity; + + // calculate delta to previous state (if any) + Vector3 positionDelta = Vector3.zero; + Vector3 velocityDelta = Vector3.zero; + Vector3 angularVelocityDelta = Vector3.zero; + Quaternion rotationDelta = Quaternion.identity; + int stateHistoryCount = stateHistory.Count; // perf: only grab .Count once + if (stateHistoryCount > 0) + { + RigidbodyState last = stateHistory.Values[stateHistoryCount - 1]; + positionDelta = currentPosition - last.position; + velocityDelta = currentVelocity - last.velocity; + // Quaternions always need to be normalized in order to be valid rotations after operations + rotationDelta = (currentRotation * Quaternion.Inverse(last.rotation)).normalized; + angularVelocityDelta = currentAngularVelocity - last.angularVelocity; + + // debug draw the recorded state + // Debug.DrawLine(last.position, currentPosition, Color.red, lineTime); + } + + // create state to insert + RigidbodyState state = new RigidbodyState( + predictedTime, + positionDelta, + currentPosition, + rotationDelta, + currentRotation, + velocityDelta, + currentVelocity, + angularVelocityDelta, + currentAngularVelocity + ); + + // add state to history + stateHistory.Add(predictedTime, state); + + // manually remember last inserted state for faster .Last comparisons + lastRecorded = state; + } + + // optional user callbacks, in case people need to know about events. + protected virtual void OnSnappedIntoPlace() {} + protected virtual void OnBeforeApplyState() {} + protected virtual void OnCorrected() {} + protected virtual void OnBeginPrediction() {} // when the Rigidbody moved above threshold and we created a ghost + protected virtual void OnEndPrediction() {} // when the Rigidbody came to rest and we destroyed the ghost + + void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity) + { + // fix rigidbodies seemingly dancing in place instead of coming to rest. + // hard snap to the position below a threshold velocity. + // this is fine because the visual object still smoothly interpolates to it. + // => consider both velocity and angular velocity (in case of Rigidbodies only rotating with joints etc.) +#if UNITY_6000_0_OR_NEWER + if (predictedRigidbody.linearVelocity.magnitude <= snapThreshold && + predictedRigidbody.angularVelocity.magnitude <= snapThreshold) +#else + if (predictedRigidbody.velocity.magnitude <= snapThreshold && + predictedRigidbody.angularVelocity.magnitude <= snapThreshold) +#endif + { + // Debug.Log($"Prediction: snapped {name} into place because velocity {predictedRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); + + // apply server state immediately. + // important to apply velocity as well, instead of Vector3.zero. + // in case an object is still slightly moving, we don't want it + // to stop and start moving again on client - slide as well here. + predictedRigidbody.position = position; + predictedRigidbody.rotation = rotation; + // projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error + if (!predictedRigidbody.isKinematic) + { +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity = velocity; +#else + predictedRigidbody.velocity = velocity; +#endif + predictedRigidbody.angularVelocity = angularVelocity; + } + + // clear history and insert the exact state we just applied. + // this makes future corrections more accurate. + stateHistory.Clear(); + stateHistory.Add(timestamp, new RigidbodyState( + timestamp, + Vector3.zero, + position, + Quaternion.identity, + rotation, + Vector3.zero, + velocity, + Vector3.zero, + angularVelocity + )); + + // user callback + OnSnappedIntoPlace(); + return; + } + + // we have a callback for snapping into place (above). + // we also need one for corrections without snapping into place. + // call it before applying pos/rot/vel in case we need to set kinematic etc. + OnBeforeApplyState(); + + // apply the state to the Rigidbody + if (mode == PredictionMode.Smooth) + { + // Smooth mode separates Physics from Renderering. + // Rendering smoothly follows Physics in SmoothFollowPhysicsCopy(). + // this allows us to be able to hard teleport to the correction. + // which gives most accurate results since the Rigidbody can't + // be stopped by another object when trying to correct. + predictedRigidbody.position = position; + predictedRigidbody.rotation = rotation; + } + else if (mode == PredictionMode.Fast) + { + // Fast mode doesn't separate physics from rendering. + // The only smoothing we get is from Rigidbody.MovePosition. + predictedRigidbody.MovePosition(position); + predictedRigidbody.MoveRotation(rotation); + } + + // there's only one way to set velocity. + // (projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error) + if (!predictedRigidbody.isKinematic) + { +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity = velocity; +#else + predictedRigidbody.velocity = velocity; +#endif + predictedRigidbody.angularVelocity = angularVelocity; + } + } + + // process a received server state. + // compares it against our history and applies corrections if needed. + void OnReceivedState(double timestamp, RigidbodyState state)//, bool sleeping) + { + // always update remote state ghost + if (remoteCopy != null) + { + Transform remoteCopyTransform = remoteCopy.transform; + remoteCopyTransform.SetPositionAndRotation(state.position, state.rotation); // faster than .position + .rotation setters + remoteCopyTransform.localScale = tf.lossyScale; // world scale! see CreateGhosts comment. + } + + + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // color code remote sleeping objects to debug objects coming to rest + // if (showRemoteSleeping) + // { + // rend.material.color = sleeping ? Color.gray : originalColor; + // } + + // performance: get Rigidbody position & rotation only once, + // and together via its transform + predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); + + // OPTIONAL performance optimization when comparing idle objects. + // even idle objects will have a history of ~32 entries. + // sampling & traversing through them is unnecessarily costly. + // instead, compare directly against the current rigidbody position! + // => this is technically not 100% correct if an object runs in + // circles where it may revisit the same position twice. + // => but practically, objects that didn't move will have their + // whole history look like the last inserted state. + // => comparing against that is free and gives us a significant + // performance saving vs. a tiny chance of incorrect results due + // to objects running in circles. + // => the RecordState() call below is expensive too, so we want to + // do this before even recording the latest state. the only way + // to do this (in case last recorded state is too old), is to + // compare against live rigidbody.position without any recording. + // this is as fast as it gets for skipping idle objects. + // + // if this ever causes issues, feel free to disable it. + float positionToStateDistanceSqr = Vector3.SqrMagnitude(state.position - physicsPosition); + if (compareLastFirst && + // Vector3.Distance(state.position, physicsPosition) < positionCorrectionThreshold && // slow comparison + positionToStateDistanceSqr < positionCorrectionThresholdSqr && // fast comparison + Quaternion.Angle(state.rotation, physicsRotation) < rotationCorrectionThreshold) + { + // Debug.Log($"OnReceivedState for {name}: taking optimized early return!"); + return; + } + + // we only capture state every 'interval' milliseconds. + // so the newest entry in 'history' may be up to 'interval' behind 'now'. + // if there's no latency, we may receive a server state for 'now'. + // sampling would fail, if we haven't recorded anything in a while. + // to solve this, always record the current state when receiving a server state. + RecordState(); + + // correction requires at least 2 existing states for 'before' and 'after'. + // if we don't have two yet, drop this state and try again next time once we recorded more. + if (stateHistory.Count < 2) return; + + RigidbodyState oldest = stateHistory.Values[0]; + RigidbodyState newest = stateHistory.Values[stateHistory.Count - 1]; + + // edge case: is the state older than the oldest state in history? + // this can happen if the client gets so far behind the server + // that it doesn't have a recored history to sample from. + // in that case, we should hard correct the client. + // otherwise it could be out of sync as long as it's too far behind. + if (state.timestamp < oldest.timestamp) + { + // when starting, client may only have 2-3 states in history. + // it's expected that server states would be behind those 2-3. + // only show a warning if it's behind the full history limit! + if (stateHistory.Count >= stateHistoryLimit) + Debug.LogWarning($"Hard correcting client object {name} because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind."); + + // force apply the state + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); + return; + } + + // edge case: is it newer than the newest state in history? + // this can happen if client's predictedTime predicts too far ahead of the server. + // in that case, log a warning for now but still apply the correction. + // otherwise it could be out of sync as long as it's too far ahead. + // + // for example, when running prediction on the same machine with near zero latency. + // when applying corrections here, this looks just fine on the local machine. + if (newest.timestamp < state.timestamp) + { + // the correction is for a state in the future. + // we clamp it to 'now'. + // but only correct if off by threshold. + // TODO maybe we should interpolate this back to 'now'? + // if (Vector3.Distance(state.position, physicsPosition) >= positionCorrectionThreshold) // slow comparison + if (positionToStateDistanceSqr >= positionCorrectionThresholdSqr) // fast comparison + { + // this can happen a lot when latency is ~0. logging all the time allocates too much and is too slow. + // double ahead = state.timestamp - newest.timestamp; + // Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter."); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); + } + return; + } + + // find the two closest client states between timestamp + if (!Prediction.Sample(stateHistory, timestamp, out RigidbodyState before, out RigidbodyState after, out int afterIndex, out double t)) + { + // something went very wrong. sampling should've worked. + // hard correct to recover the error. + Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history."); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); + return; + } + + // interpolate between them to get the best approximation + RigidbodyState interpolated = RigidbodyState.Interpolate(before, after, (float)t); + + // calculate the difference between where we were and where we should be + // TODO only position for now. consider rotation etc. too later + // float positionToInterpolatedDistance = Vector3.Distance(state.position, interpolated.position); // slow comparison + float positionToInterpolatedDistanceSqr = Vector3.SqrMagnitude(state.position - interpolated.position); // fast comparison + float rotationToInterpolatedDistance = Quaternion.Angle(state.rotation, interpolated.rotation); + // Debug.Log($"Sampled history of size={stateHistory.Count} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3} / {correctionThreshold:F3}"); + + // too far off? then correct it + if (positionToInterpolatedDistanceSqr >= positionCorrectionThresholdSqr || // fast comparison + //positionToInterpolatedDistance >= positionCorrectionThreshold || // slow comparison + rotationToInterpolatedDistance >= rotationCorrectionThreshold) + { + // Debug.Log($"CORRECTION NEEDED FOR {name} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3}"); + + // show the received correction position + velocity for debugging. + // helps to compare with the interpolated/applied correction locally. + //Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime); + + // insert the correction and correct the history on top of it. + // returns the final recomputed state after rewinding. + RigidbodyState recomputed = Prediction.CorrectHistory(stateHistory, stateHistoryLimit, state, before, after, afterIndex); + + // log, draw & apply the final position. + // always do this here, not when iterating above, in case we aren't iterating. + // for example, on same machine with near zero latency. + // int correctedAmount = stateHistory.Count - afterIndex; + // Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}"); + //Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime); + ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity, recomputed.angularVelocity); + + // user callback + OnCorrected(); + } + } + + // send state to clients every sendInterval. + // reliable for now. + // TODO we should use the one from FixedUpdate + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // Time.time was at the beginning of this frame. + // NetworkLateUpdate->Broadcast->OnSerialize is at the end of the frame. + // as result, client should use this to correct the _next_ frame. + // otherwise we see noticeable resets that seem off by one frame. + // + // to solve this, we can send the current deltaTime. + // server is technically supposed to be at a fixed frame rate, but this can vary. + // sending server's current deltaTime is the safest option. + // client then applies it on top of remoteTimestamp. + + + // FAST VERSION: this shows in profiler a lot, so cache EVERYTHING! + tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); // faster than tf.position + tf.rotation. server's rigidbody is on the same transform. + + // simple but slow write: + // writer.WriteFloat(Time.deltaTime); + // writer.WriteVector3(position); + // writer.WriteQuaternion(rotation); + // writer.WriteVector3(predictedRigidbody.velocity); + // writer.WriteVector3(predictedRigidbody.angularVelocity); + + // performance optimization: write a whole struct at once via blittable: + PredictedSyncData data = new PredictedSyncData( + Time.deltaTime, + position, + rotation, +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity, +#else + predictedRigidbody.velocity, +#endif + predictedRigidbody.angularVelocity);//, + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // predictedRigidbody.IsSleeping()); + writer.WritePredictedSyncData(data); + } + + // read the server's state, compare with client state & correct if necessary. + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + // deserialize data + // we want to know the time on the server when this was sent, which is remoteTimestamp. + double timestamp = NetworkClient.connection.remoteTimeStamp; + + // simple but slow read: + // double serverDeltaTime = reader.ReadFloat(); + // Vector3 position = reader.ReadVector3(); + // Quaternion rotation = reader.ReadQuaternion(); + // Vector3 velocity = reader.ReadVector3(); + // Vector3 angularVelocity = reader.ReadVector3(); + + // performance optimization: read a whole struct at once via blittable: + PredictedSyncData data = reader.ReadPredictedSyncData(); + double serverDeltaTime = data.deltaTime; + Vector3 position = data.position; + Quaternion rotation = data.rotation; + Vector3 velocity = data.velocity; + Vector3 angularVelocity = data.angularVelocity; + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // bool sleeping = data.sleeping != 0; + + // server sends state at the end of the frame. + // parse and apply the server's delta time to our timestamp. + // otherwise we see noticeable resets that seem off by one frame. + timestamp += serverDeltaTime; + + // however, adding yet one more frame delay gives much(!) better results. + // we don't know why yet, so keep this as an option for now. + // possibly because client captures at the beginning of the frame, + // with physics happening at the end of the frame? + if (oneFrameAhead) timestamp += serverDeltaTime; + + // process received state + OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, Quaternion.identity, rotation, Vector3.zero, velocity, Vector3.zero, angularVelocity));//, sleeping); + } + + protected override void OnValidate() + { + base.OnValidate(); + + // force syncDirection to be ServerToClient + syncDirection = SyncDirection.ServerToClient; + + // state should be synced immediately for now. + // later when we have prediction fully dialed in, + // then we can maybe relax this a bit. + syncInterval = 0; + } + + // helper function for Physics tests to check if a Rigidbody belongs to + // a PredictedRigidbody component (either on it, or on its ghost). + public static bool IsPredicted(Rigidbody rb, out PredictedRigidbody predictedRigidbody) + { + // by default, Rigidbody is on the PredictedRigidbody GameObject + if (rb.TryGetComponent(out predictedRigidbody)) + return true; + + // it might be on a ghost while interacting + if (rb.TryGetComponent(out PredictedRigidbodyPhysicsGhost ghost)) + { + predictedRigidbody = ghost.target.GetComponent(); + return true; + } + + // otherwise the Rigidbody does not belong to any PredictedRigidbody. + predictedRigidbody = null; + return false; + } + + // helper function for Physics tests to check if a Collider (which may be in children) belongs to + // a PredictedRigidbody component (either on it, or on its ghost). + public static bool IsPredicted(Collider co, out PredictedRigidbody predictedRigidbody) + { + // by default, Collider is on the PredictedRigidbody GameObject or it's children. + predictedRigidbody = co.GetComponentInParent(); + if (predictedRigidbody != null) + return true; + + // it might be on a ghost while interacting + PredictedRigidbodyPhysicsGhost ghost = co.GetComponentInParent(); + if (ghost != null && ghost.target != null && ghost.target.TryGetComponent(out predictedRigidbody)) + return true; + + // otherwise the Rigidbody does not belong to any PredictedRigidbody. + predictedRigidbody = null; + return false; + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta new file mode 100644 index 0000000..1a3ed44 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta @@ -0,0 +1,22 @@ +fileFormatVersion: 2 +guid: d38927cdc6024b9682b5fe9778b9ef99 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, + type: 2} + - remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776, + type: 2} + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs new file mode 100644 index 0000000..f28d49f --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs @@ -0,0 +1,15 @@ +// Prediction moves out the Rigidbody & Collider into a separate object. +// this component simply points back to the owner component. +// in case Raycasts hit it and need to know the owner, etc. +using UnityEngine; + +namespace Mirror +{ + public class PredictedRigidbodyPhysicsGhost : MonoBehaviour + { + // this is performance critical, so store target's .Transform instead of + // PredictedRigidbody, this way we don't need to call the .transform getter. + [Tooltip("The predicted rigidbody owner.")] + public Transform target; + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta new file mode 100644 index 0000000..19f13c2 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 25593abc9bf0d44878a4ad6018204061 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs new file mode 100644 index 0000000..636f397 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs @@ -0,0 +1 @@ +// removed 2024-02-09 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta new file mode 100644 index 0000000..22d9cf0 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 62e7e9424c7e48d69b6a3517796142a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs new file mode 100644 index 0000000..fa652fd --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs @@ -0,0 +1,54 @@ +// this struct exists only for OnDe/Serialize performance. +// instead of WriteVector3+Quaternion+Vector3+Vector3, +// we read & write the whole struct as blittable once. +// +// struct packing can cause odd results with blittable on different platforms, +// so this is usually not recommended! +// +// in this case however, we need to squeeze everything we can out of prediction +// to support low even devices / VR. +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Mirror +{ + // struct packing + + [StructLayout(LayoutKind.Sequential)] // explicitly force sequential + public struct PredictedSyncData + { + public float deltaTime; // 4 bytes (word aligned) + public Vector3 position; // 12 bytes (word aligned) + public Quaternion rotation; // 16 bytes (word aligned) + public Vector3 velocity; // 12 bytes (word aligned) + public Vector3 angularVelocity; // 12 bytes (word aligned) + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // public byte sleeping; // 1 byte: bool isn't blittable + + // constructor for convenience + public PredictedSyncData(float deltaTime, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity)//, bool sleeping) + { + this.deltaTime = deltaTime; + this.position = position; + this.rotation = rotation; + this.velocity = velocity; + this.angularVelocity = angularVelocity; + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // this.sleeping = sleeping ? (byte)1 : (byte)0; + } + } + + // NetworkReader/Writer extensions to write this struct + public static class PredictedSyncDataReadWrite + { + public static void WritePredictedSyncData(this NetworkWriter writer, PredictedSyncData data) + { + writer.WriteBlittable(data); + } + + public static PredictedSyncData ReadPredictedSyncData(this NetworkReader reader) + { + return reader.ReadBlittable(); + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta new file mode 100644 index 0000000..8a7fe10 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: f595f112a39e4634b670d56991b23823 +timeCreated: 1710387026 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs new file mode 100644 index 0000000..82945c9 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs @@ -0,0 +1,430 @@ +// standalone utility functions for PredictedRigidbody component. +using System; +using UnityEngine; + +namespace Mirror +{ + public static class PredictionUtils + { + // rigidbody /////////////////////////////////////////////////////////// + // move a Rigidbody + settings from one GameObject to another. + public static void MoveRigidbody(GameObject source, GameObject destination, bool destroySource = true) + { + // create a new Rigidbody component on destination. + // note that adding a Joint automatically adds a Rigidbody. + // so first check if one was added yet. + Rigidbody original = source.GetComponent(); + if (original == null) throw new Exception($"Prediction: attempted to move {source}'s Rigidbody to the predicted copy, but there was no component."); + Rigidbody rigidbodyCopy; + if (!destination.TryGetComponent(out rigidbodyCopy)) + rigidbodyCopy = destination.AddComponent(); + + // copy all properties + rigidbodyCopy.mass = original.mass; +#if UNITY_6000_0_OR_NEWER + rigidbodyCopy.linearDamping = original.linearDamping; + rigidbodyCopy.angularDamping = original.angularDamping; +#else + rigidbodyCopy.drag = original.drag; + rigidbodyCopy.angularDrag = original.angularDrag; +#endif + rigidbodyCopy.useGravity = original.useGravity; + rigidbodyCopy.isKinematic = original.isKinematic; + rigidbodyCopy.interpolation = original.interpolation; + rigidbodyCopy.collisionDetectionMode = original.collisionDetectionMode; + // fix: need to set freezeRotation before constraints: + // https://github.com/MirrorNetworking/Mirror/pull/3946 + rigidbodyCopy.freezeRotation = original.freezeRotation; + rigidbodyCopy.constraints = original.constraints; + rigidbodyCopy.sleepThreshold = original.sleepThreshold; + + // moving (Configurable)Joints messes up their range of motion unless + // we reset to initial position first (we do this in PredictedRigibody.cs). + // so here we don't set the Rigidbody's physics position at all. + // rigidbodyCopy.position = original.position; + // rigidbodyCopy.rotation = original.rotation; + + // projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error + if (!original.isKinematic) + { +#if UNITY_6000_0_OR_NEWER + rigidbodyCopy.linearVelocity = original.linearVelocity; +#else + rigidbodyCopy.velocity = original.velocity; +#endif + rigidbodyCopy.angularVelocity = original.angularVelocity; + } + + // destroy original + if (destroySource) GameObject.Destroy(original); + } + + // helper function: if a collider is on a child, copy that child first. + // this way child's relative position/rotation/scale are preserved. + public static GameObject CopyRelativeTransform(GameObject source, Transform sourceChild, GameObject destination) + { + // is this on the source root? then we want to put it on the destination root. + if (sourceChild == source.transform) return destination; + + // is this on a child? then create the same child with the same transform on destination. + // note this is technically only correct for the immediate child since + // .localPosition is relative to parent, but this is good enough. + GameObject child = new GameObject(sourceChild.name); + child.transform.SetParent(destination.transform, true); + child.transform.localPosition = sourceChild.localPosition; + child.transform.localRotation = sourceChild.localRotation; + child.transform.localScale = sourceChild.localScale; + + // assign the same Layer for the physics copy. + // games may use a custom physics collision matrix, layer matters. + child.layer = sourceChild.gameObject.layer; + + return child; + } + + // colliders /////////////////////////////////////////////////////////// + // move all BoxColliders + settings from one GameObject to another. + public static void MoveBoxColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + BoxCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (BoxCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + BoxCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.size = sourceCollider.size; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all SphereColliders + settings from one GameObject to another. + public static void MoveSphereColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + SphereCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (SphereCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + SphereCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all CapsuleColliders + settings from one GameObject to another. + public static void MoveCapsuleColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + CapsuleCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (CapsuleCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + CapsuleCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + colliderCopy.height = sourceCollider.height; + colliderCopy.direction = sourceCollider.direction; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all MeshColliders + settings from one GameObject to another. + public static void MoveMeshColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + MeshCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (MeshCollider sourceCollider in sourceColliders) + { + // when Models have Mesh->Read/Write disabled, it means that Unity + // uploads the mesh directly to the GPU and erases it on the CPU. + // on some platforms this makes moving a MeshCollider in builds impossible: + // + // "CollisionMeshData couldn't be created because the mesh has been marked as non-accessible." + // + // on other platforms, this works fine. + // let's show an explicit log message so in case collisions don't + // work at runtime, it's obvious why it happens and how to fix it. + if (!sourceCollider.sharedMesh.isReadable) + { + Debug.Log($"[Prediction]: MeshCollider on {sourceCollider.name} isn't readable, which may indicate that the Mesh only exists on the GPU. If {sourceCollider.name} is missing collisions, then please select the model in the Project Area, and enable Mesh->Read/Write so it's also available on the CPU!"); + // don't early return. keep trying, it may work. + } + + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + MeshCollider colliderCopy = target.AddComponent(); + colliderCopy.sharedMesh = sourceCollider.sharedMesh; + colliderCopy.convex = sourceCollider.convex; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all Colliders + settings from one GameObject to another. + public static void MoveAllColliders(GameObject source, GameObject destination, bool destroySource = true) + { + MoveBoxColliders(source, destination, destroySource); + MoveSphereColliders(source, destination, destroySource); + MoveCapsuleColliders(source, destination, destroySource); + MoveMeshColliders(source, destination, destroySource); + } + + // joints ////////////////////////////////////////////////////////////// + // move all CharacterJoints + settings from one GameObject to another. + public static void MoveCharacterJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + CharacterJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (CharacterJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + CharacterJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.enableProjection = sourceJoint.enableProjection; + jointCopy.highTwistLimit = sourceJoint.highTwistLimit; + jointCopy.lowTwistLimit = sourceJoint.lowTwistLimit; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.projectionAngle = sourceJoint.projectionAngle; + jointCopy.projectionDistance = sourceJoint.projectionDistance; + jointCopy.swing1Limit = sourceJoint.swing1Limit; + jointCopy.swing2Limit = sourceJoint.swing2Limit; + jointCopy.swingAxis = sourceJoint.swingAxis; + jointCopy.swingLimitSpring = sourceJoint.swingLimitSpring; + jointCopy.twistLimitSpring = sourceJoint.twistLimitSpring; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all ConfigurableJoints + settings from one GameObject to another. + public static void MoveConfigurableJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + ConfigurableJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (ConfigurableJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + ConfigurableJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.angularXLimitSpring = sourceJoint.angularXLimitSpring; + jointCopy.angularXDrive = sourceJoint.angularXDrive; + jointCopy.angularXMotion = sourceJoint.angularXMotion; + jointCopy.angularYLimit = sourceJoint.angularYLimit; + jointCopy.angularYMotion = sourceJoint.angularYMotion; + jointCopy.angularYZDrive = sourceJoint.angularYZDrive; + jointCopy.angularYZLimitSpring = sourceJoint.angularYZLimitSpring; + jointCopy.angularZLimit = sourceJoint.angularZLimit; + jointCopy.angularZMotion = sourceJoint.angularZMotion; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.configuredInWorldSpace = sourceJoint.configuredInWorldSpace; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.highAngularXLimit = sourceJoint.highAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs + jointCopy.linearLimitSpring = sourceJoint.linearLimitSpring; + jointCopy.linearLimit = sourceJoint.linearLimit; + jointCopy.lowAngularXLimit = sourceJoint.lowAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs + jointCopy.massScale = sourceJoint.massScale; + jointCopy.projectionAngle = sourceJoint.projectionAngle; + jointCopy.projectionDistance = sourceJoint.projectionDistance; + jointCopy.projectionMode = sourceJoint.projectionMode; + jointCopy.rotationDriveMode = sourceJoint.rotationDriveMode; + jointCopy.secondaryAxis = sourceJoint.secondaryAxis; + jointCopy.slerpDrive = sourceJoint.slerpDrive; + jointCopy.swapBodies = sourceJoint.swapBodies; + jointCopy.targetAngularVelocity = sourceJoint.targetAngularVelocity; + jointCopy.targetPosition = sourceJoint.targetPosition; + jointCopy.targetRotation = sourceJoint.targetRotation; + jointCopy.targetVelocity = sourceJoint.targetVelocity; + jointCopy.xDrive = sourceJoint.xDrive; + jointCopy.xMotion = sourceJoint.xMotion; + jointCopy.yDrive = sourceJoint.yDrive; + jointCopy.yMotion = sourceJoint.yMotion; + jointCopy.zDrive = sourceJoint.zDrive; + jointCopy.zMotion = sourceJoint.zMotion; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all FixedJoints + settings from one GameObject to another. + public static void MoveFixedJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + FixedJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (FixedJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + FixedJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.massScale = sourceJoint.massScale; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all HingeJoints + settings from one GameObject to another. + public static void MoveHingeJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + HingeJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (HingeJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + HingeJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.limits = sourceJoint.limits; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.motor = sourceJoint.motor; + jointCopy.spring = sourceJoint.spring; + jointCopy.useLimits = sourceJoint.useLimits; + jointCopy.useMotor = sourceJoint.useMotor; + jointCopy.useSpring = sourceJoint.useSpring; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif +#if UNITY_2022_3_OR_NEWER + jointCopy.extendedLimits = sourceJoint.extendedLimits; + jointCopy.useAcceleration = sourceJoint.useAcceleration; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all SpringJoints + settings from one GameObject to another. + public static void MoveSpringJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + SpringJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (SpringJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + SpringJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.damper = sourceJoint.damper; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.maxDistance = sourceJoint.maxDistance; + jointCopy.minDistance = sourceJoint.minDistance; + jointCopy.spring = sourceJoint.spring; + jointCopy.tolerance = sourceJoint.tolerance; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all Joints + settings from one GameObject to another. + public static void MoveAllJoints(GameObject source, GameObject destination, bool destroySource = true) + { + MoveCharacterJoints(source, destination, destroySource); + MoveConfigurableJoints(source, destination, destroySource); + MoveFixedJoints(source, destination, destroySource); + MoveHingeJoints(source, destination, destroySource); + MoveSpringJoints(source, destination, destroySource); + } + + // all ///////////////////////////////////////////////////////////////// + // move all physics components from one GameObject to another. + public static void MovePhysicsComponents(GameObject source, GameObject destination, bool destroySource = true) + { + // need to move joints first, otherwise we might see: + // 'can't move Rigidbody because a Joint depends on it' + MoveAllJoints(source, destination, destroySource); + MoveAllColliders(source, destination, destroySource); + MoveRigidbody(source, destination, destroySource); + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta new file mode 100644 index 0000000..0c3ac22 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 17cfe1beb3f94a69b94bf60afc37ef7a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat new file mode 100644 index 0000000..d652a50 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat @@ -0,0 +1,85 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RemoteGhostMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - _ALPHAPREMULTIPLY_ON + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 3000 + stringTagMap: + RenderType: Transparent + disabledShaderPasses: [] + m_LockedProperties: + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 10 + - _GlossMapScale: 1 + - _Glossiness: 0.92 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 3 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 0 + m_Colors: + - _Color: {r: 0.09849727, g: 1, b: 0, a: 0.15686275} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta new file mode 100644 index 0000000..ec597ae --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 04f0b2088c857414393bab3b80356776 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs new file mode 100644 index 0000000..c30ce4a --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs @@ -0,0 +1,60 @@ +// PredictedRigidbody stores a history of its rigidbody states. +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + // inline everything because this is performance critical! + public struct RigidbodyState : PredictedState + { + public double timestamp { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } + + // we want to store position delta (last + delta = current), and current. + // this way we can apply deltas on top of corrected positions to get the corrected final position. + public Vector3 positionDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this position + public Vector3 position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public Quaternion rotationDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this rotation + public Quaternion rotation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public Vector3 velocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity + public Vector3 velocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public Vector3 angularVelocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity + public Vector3 angularVelocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public RigidbodyState( + double timestamp, + Vector3 positionDelta, + Vector3 position, + Quaternion rotationDelta, + Quaternion rotation, + Vector3 velocityDelta, + Vector3 velocity, + Vector3 angularVelocityDelta, + Vector3 angularVelocity) + { + this.timestamp = timestamp; + this.positionDelta = positionDelta; + this.position = position; + this.rotationDelta = rotationDelta; + this.rotation = rotation; + this.velocityDelta = velocityDelta; + this.velocity = velocity; + this.angularVelocityDelta = angularVelocityDelta; + this.angularVelocity = angularVelocity; + } + + public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t) + { + return new RigidbodyState + { + position = Vector3.Lerp(a.position, b.position, t), + // Quaternions always need to be normalized in order to be a valid rotation after operations + rotation = Quaternion.Slerp(a.rotation, b.rotation, t).normalized, + velocity = Vector3.Lerp(a.velocity, b.velocity, t), + angularVelocity = Vector3.Lerp(a.angularVelocity, b.angularVelocity, t) + }; + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs.meta new file mode 100644 index 0000000..593ce27 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ed0e1c0c874c4c9db6be2d5885bb7bee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling.meta b/Assets/Mirror/Components/Profiling.meta new file mode 100644 index 0000000..043fbfe --- /dev/null +++ b/Assets/Mirror/Components/Profiling.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4b799e47369048fcb1604e5b3e7d71cf +timeCreated: 1724245584 diff --git a/Assets/Mirror/Components/Profiling/BaseUIGraph.cs b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs new file mode 100644 index 0000000..94e84e9 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs @@ -0,0 +1,217 @@ +using System; +using UnityEngine; +using UnityEngine.Serialization; +using UnityEngine.UI; +namespace Mirror +{ + public enum GraphAggregationMode + { + Sum, + Average, + PerSecond, + Min, + Max + } + + public abstract class BaseUIGraph : MonoBehaviour + { + static readonly int MaxValue = Shader.PropertyToID("_MaxValue"); + static readonly int GraphData = Shader.PropertyToID("_GraphData"); + static readonly int CategoryCount = Shader.PropertyToID("_CategoryCount"); + static readonly int Colors = Shader.PropertyToID("_CategoryColors"); + static readonly int Width = Shader.PropertyToID("_Width"); + static readonly int DataStart = Shader.PropertyToID("_DataStart"); + + public Material Material; + public Graphic Renderer; + [Range(1, 64)] + public int Points = 64; + public float SecondsPerPoint = 1; + public Color[] CategoryColors = new[] { Color.cyan }; + public bool IsStacked; + + public Text[] LegendTexts; + [Header("Diagnostics")] + [ReadOnly, SerializeField] + Material runtimeMaterial; + + float[] graphData; + // graphData is a circular buffer, this is the offset to get the 0-index + int graphDataStartIndex; + // Is graphData dirty and needs to be set to the material + bool isGraphDataDirty; + // currently aggregating data to be added to the graph soon + float[] aggregatingData; + GraphAggregationMode[] aggregatingModes; + // Counts for avg aggregation mode + int[] aggregatingDataCounts; + // How much time has elapsed since the last aggregation finished + float aggregatingTime; + + int DataLastIndex => (graphDataStartIndex - 1 + Points) % Points; + + void Awake() + { + Renderer.material = runtimeMaterial = Instantiate(Material); + graphData = new float[Points * CategoryColors.Length]; + aggregatingData = new float[CategoryColors.Length]; + aggregatingDataCounts = new int[CategoryColors.Length]; + aggregatingModes = new GraphAggregationMode[CategoryColors.Length]; + isGraphDataDirty = true; + } + + protected virtual void OnValidate() + { + if (Renderer == null) + Renderer = GetComponent(); + } + + protected virtual void Update() + { + for (int i = 0; i < CategoryColors.Length; i++) + { + CollectData(i, out float value, out GraphAggregationMode mode); + // we probably don't need negative values, so lets skip supporting it + if (value < 0) + { + Debug.LogWarning("Graphing negative values is not supported."); + value = 0; + } + + if (mode != aggregatingModes[i]) + { + aggregatingModes[i] = mode; + ResetCurrent(i); + } + + switch (mode) + { + case GraphAggregationMode.Average: + case GraphAggregationMode.Sum: + case GraphAggregationMode.PerSecond: + aggregatingData[i] += value; + aggregatingDataCounts[i]++; + break; + case GraphAggregationMode.Min: + if (aggregatingData[i] > value) + aggregatingData[i] = value; + break; + case GraphAggregationMode.Max: + if (value > aggregatingData[i]) + aggregatingData[i] = value; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + aggregatingTime += Time.deltaTime; + if (aggregatingTime > SecondsPerPoint) + { + graphDataStartIndex = (graphDataStartIndex + 1) % Points; + ClearDataAt(DataLastIndex); + for (int i = 0; i < CategoryColors.Length; i++) + { + float value = aggregatingData[i]; + switch (aggregatingModes[i]) + { + case GraphAggregationMode.Sum: + case GraphAggregationMode.Min: + case GraphAggregationMode.Max: + // do nothing! + break; + case GraphAggregationMode.Average: + value /= aggregatingDataCounts[i]; + break; + case GraphAggregationMode.PerSecond: + value /= aggregatingTime; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + SetCurrentGraphData(i, value); + ResetCurrent(i); + } + + aggregatingTime = 0; + } + } + + void ResetCurrent(int i) + { + switch (aggregatingModes[i]) + { + case GraphAggregationMode.Min: + aggregatingData[i] = float.MaxValue; + break; + default: + aggregatingData[i] = 0; + break; + } + + aggregatingDataCounts[i] = 0; + } + + protected virtual string FormatValue(float value) => $"{value:N1}"; + + protected abstract void CollectData(int category, out float value, out GraphAggregationMode mode); + + void SetCurrentGraphData(int c, float value) + { + graphData[DataLastIndex * CategoryColors.Length + c] = value; + isGraphDataDirty = true; + } + + void ClearDataAt(int i) + { + for (int c = 0; c < CategoryColors.Length; c++) + graphData[i * CategoryColors.Length + c] = 0; + + isGraphDataDirty = true; + } + + public void LateUpdate() + { + if (isGraphDataDirty) + { + runtimeMaterial.SetInt(Width, Points); + runtimeMaterial.SetInt(DataStart, graphDataStartIndex); + float max = 1; + if (IsStacked) + for (int x = 0; x < Points; x++) + { + float total = 0; + for (int c = 0; c < CategoryColors.Length; c++) + total += graphData[x * CategoryColors.Length + c]; + + if (total > max) + max = total; + } + else + for (int i = 0; i < graphData.Length; i++) + { + float v = graphData[i]; + if (v > max) + max = v; + } + + max = AdjustMaxValue(max); + for (int i = 0; i < LegendTexts.Length; i++) + { + Text legendText = LegendTexts[i]; + float pct = (float)i / (LegendTexts.Length - 1); + legendText.text = FormatValue(max * pct); + } + + runtimeMaterial.SetFloat(MaxValue, max); + runtimeMaterial.SetFloatArray(GraphData, graphData); + runtimeMaterial.SetInt(CategoryCount, CategoryColors.Length); + runtimeMaterial.SetColorArray(Colors, CategoryColors); + isGraphDataDirty = false; + } + } + + protected virtual float AdjustMaxValue(float max) => Mathf.Ceil(max); + } +} diff --git a/Assets/Mirror/Components/Profiling/BaseUIGraph.cs.meta b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs.meta new file mode 100644 index 0000000..51dab56 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: 0f7dbe0fe96842f3b3ec76d05d2bb24a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + - Renderer: {instanceID: 0} + - runtimeMaterial: {instanceID: 0} + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/BaseUIGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs new file mode 100644 index 0000000..d0f1a86 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs @@ -0,0 +1,40 @@ +using System; +using UnityEngine; +namespace Mirror +{ + public class FpsMinMaxAvgGraph : BaseUIGraph + { + protected override void CollectData(int category, out float value, out GraphAggregationMode mode) + { + value = 1 / Time.deltaTime; + switch (category) + { + case 0: + mode = GraphAggregationMode.Average; + break; + case 1: + mode = GraphAggregationMode.Min; + break; + case 2: + mode = GraphAggregationMode.Max; + break; + default: + throw new ArgumentOutOfRangeException($"{category} is not valid."); + } + } + + protected override void OnValidate() + { + base.OnValidate(); + if (CategoryColors.Length != 3) + CategoryColors = new[] + { + Color.cyan, // avg + Color.red, // min + Color.green // max + }; + + IsStacked = false; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs.meta b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs.meta new file mode 100644 index 0000000..25af560 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 73bc3b929ec94537a8dbd67eb9d0c2c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/LineGraph.mat b/Assets/Mirror/Components/Profiling/LineGraph.mat new file mode 100644 index 0000000..e42a0ce --- /dev/null +++ b/Assets/Mirror/Components/Profiling/LineGraph.mat @@ -0,0 +1,89 @@ +%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: LineGraph + m_Shader: {fileID: 4800000, guid: e9fd6820072746bbaa3a83a449c31709, type: 3} + 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 + - _CategoryCount: 0 + - _ColorMask: 15 + - _Cutoff: 0.5 + - _DataStart: 0 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _LineWidth: 1 + - _MaxValue: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _Stencil: 0 + - _StencilComp: 8 + - _StencilOp: 0 + - _StencilReadMask: 255 + - _StencilWriteMask: 255 + - _UVSec: 0 + - _UseUIAlphaClip: 0 + - _Width: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/Assets/Mirror/Components/Profiling/LineGraph.mat.meta b/Assets/Mirror/Components/Profiling/LineGraph.mat.meta new file mode 100644 index 0000000..10ff58a --- /dev/null +++ b/Assets/Mirror/Components/Profiling/LineGraph.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 5f77111e39fad6240bbf2a93d735b648 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/LineGraph.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs new file mode 100644 index 0000000..ac2b1fd --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs @@ -0,0 +1,85 @@ +using System; +using UnityEngine; +namespace Mirror +{ + public class NetworkBandwidthGraph : BaseUIGraph + { + int dataIn; + int dataOut; + + void Start() + { + // Ordering, Awake happens before NetworkDiagnostics reset + NetworkDiagnostics.InMessageEvent += OnReceive; + NetworkDiagnostics.OutMessageEvent += OnSend; + } + + void OnEnable() + { + // If we've been inactive, clear counter + dataIn = 0; + dataOut = 0; + } + + void OnDestroy() + { + NetworkDiagnostics.InMessageEvent -= OnReceive; + NetworkDiagnostics.OutMessageEvent -= OnSend; + } + + void OnSend(NetworkDiagnostics.MessageInfo obj) => dataOut += obj.bytes; + + void OnReceive(NetworkDiagnostics.MessageInfo obj) => dataIn += obj.bytes; + + protected override void CollectData(int category, out float value, out GraphAggregationMode mode) + { + mode = GraphAggregationMode.PerSecond; + switch (category) + { + case 0: + value = dataIn; + dataIn = 0; + break; + case 1: + value = dataOut; + dataOut = 0; + break; + default: + throw new ArgumentOutOfRangeException($"{category} is not valid."); + } + } + + static readonly string[] Units = new[] { "B/s", "KiB/s", "MiB/s" }; + const float UnitScale = 1024; + + protected override string FormatValue(float value) + { + string selectedUnit = null; + for (int i = 0; i < Units.Length; i++) + { + string unit = Units[i]; + selectedUnit = unit; + if (i > 0) + value /= UnitScale; + + if (value < UnitScale) + break; + } + + return $"{value:N0} {selectedUnit}"; + } + + protected override void OnValidate() + { + base.OnValidate(); + if (CategoryColors.Length != 2) + CategoryColors = new[] + { + Color.red, // min + Color.green // max + }; + + IsStacked = false; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs.meta b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs.meta new file mode 100644 index 0000000..adcf9b7 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e1ae19b97f0e4a5eb8cf5158d97506f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader new file mode 100644 index 0000000..c491953 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader @@ -0,0 +1,178 @@ +Shader "Mirror/NetworkGraphLines" +{ + Properties + { + [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} + _Color ("Tint", Color) = (1,1,1,1) + _Width ("Width", Int) = 0 + _LineWidth ("Line Width", Float) = 0.005 + _CategoryCount ("CategoryCount", Int) = 0 + _MaxValue ("MaxValue", Float) = 1 + _DataStart ("DataStart", Int) = 0 + + _StencilComp ("Stencil Comparison", Float) = 8 + _Stencil ("Stencil ID", Float) = 0 + _StencilOp ("Stencil Operation", Float) = 0 + _StencilWriteMask ("Stencil Write Mask", Float) = 255 + _StencilReadMask ("Stencil Read Mask", Float) = 255 + + _ColorMask ("Color Mask", Float) = 15 + + [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 + } + + SubShader + { + Tags + { + "Queue"="Transparent" + "IgnoreProjector"="True" + "RenderType"="Transparent" + "PreviewType"="Plane" + "CanUseSpriteAtlas"="True" + } + + Stencil + { + Ref [_Stencil] + Comp [_StencilComp] + Pass [_StencilOp] + ReadMask [_StencilReadMask] + WriteMask [_StencilWriteMask] + } + + Cull Off + Lighting Off + ZWrite Off + ZTest [unity_GUIZTestMode] + Blend SrcAlpha OneMinusSrcAlpha + ColorMask [_ColorMask] + + Pass + { + Name "Default" + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 2.0 + + #include "UnityCG.cginc" + #include "UnityUI.cginc" + + #pragma multi_compile_local _ UNITY_UI_CLIP_RECT + #pragma multi_compile_local _ UNITY_UI_ALPHACLIP + + struct appdata_t + { + float4 vertex : POSITION; + float4 color : COLOR; + float2 texcoord : TEXCOORD0; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + fixed4 color : COLOR; + float2 texcoord : TEXCOORD0; + float4 worldPosition : TEXCOORD1; + UNITY_VERTEX_OUTPUT_STEREO + }; + + sampler2D _MainTex; // we dont use this, but unitys ui library expects the shader to have a texture + fixed4 _Color; + fixed4 _TextureSampleAdd; + float4 _ClipRect; + float4 _MainTex_ST; + + uint _Width; + half _LineWidth; + uint _CategoryCount; + uint _MaxValue; + uint _DataStart; + half _GraphData[64 /* max. 128 points */ * 8 /* max 8 categories */]; + half4 _CategoryColors[8 /* max 8 categories */]; + + v2f vert(appdata_t v) + { + v2f OUT; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); + OUT.worldPosition = v.vertex; + OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); + + OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); + + OUT.color = v.color * _Color; + return OUT; + } + + // Helper function to calculate the shortest distance from a point (p) to a line segment (from a to b) + float distanceToLineSegment(float2 p, float2 a, float2 b) + { + float2 ab = b - a; + float2 ap = p - a; + float t = saturate(dot(ap, ab) / dot(ab, ab)); + // Clamp t between 0 and 1 to ensure we stay within the segment + float2 closestPoint = a + t * ab; // Find the closest point on the line segment + return length(p - closestPoint); // Return the distance from p to the closest point on the line + } + + fixed4 frag(v2f IN) : SV_Target + { + uint wCur = (uint)(IN.texcoord.x * _Width); + uint wMin = wCur == 0 ? 0 : wCur - 1; + uint wMax = wCur == _Width - 1 ? wCur : wCur + 1; + float2 screenSize = _ScreenParams.xy; + // this scaling only works if the object is flat and not rotated - but thats fine + float2 pixelScale = float2(1 / ddx(IN.texcoord.x), 1 / ddy(IN.texcoord.y)); + float2 screenSpaceUV = IN.texcoord * pixelScale; + half4 color = half4(0, 0, 0, 0); + // Loop through the graph's points + bool colored = false; + for (uint wNonOffset = wMin; wNonOffset < wMax && !colored; wNonOffset++) + { + uint w = (wNonOffset + _DataStart) % _Width; + // previous entry, unless it's the start, then we clamp to start + uint nextW = (w + 1) % _Width; + + float texPosCurrentX = float(wNonOffset) / _Width; + float texPosPrevX = texPosCurrentX + 1.0f / _Width; + + + for (uint c = 0; c < _CategoryCount; c++) + { + float categoryValueCurrent = _GraphData[w * _CategoryCount + c] / _MaxValue; + float categoryValueNext = _GraphData[nextW * _CategoryCount + c] / _MaxValue; + + float2 pointCurrent = float2(texPosCurrentX, categoryValueCurrent); + float2 pointNext = float2(texPosPrevX, categoryValueNext); + + float distance = distanceToLineSegment(screenSpaceUV, pointCurrent * pixelScale, + pointNext * pixelScale); + + if (distance < _LineWidth) + { + color = _CategoryColors[c]; + colored = true; + break; + } + } + } + + color *= IN.color; + + #ifdef UNITY_UI_CLIP_RECT + color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); + #endif + + #ifdef UNITY_UI_ALPHACLIP + clip (color.a - 0.001); + #endif + + return color; + } + ENDCG + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader.meta b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader.meta new file mode 100644 index 0000000..0fcbede --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: e9fd6820072746bbaa3a83a449c31709 +timeCreated: 1725809505 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkGraphLines.shader + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader new file mode 100644 index 0000000..e666d41 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader @@ -0,0 +1,138 @@ +Shader "Mirror/NetworkGraphStacked" +{ + Properties + { + [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} + _Color ("Tint", Color) = (1,1,1,1) + _Width ("Width", Int) = 0 + _CategoryCount ("CategoryCount", Int) = 0 + _MaxValue ("MaxValue", Float) = 1 + _DataStart ("DataStart", Int) = 0 + + _StencilComp ("Stencil Comparison", Float) = 8 + _Stencil ("Stencil ID", Float) = 0 + _StencilOp ("Stencil Operation", Float) = 0 + _StencilWriteMask ("Stencil Write Mask", Float) = 255 + _StencilReadMask ("Stencil Read Mask", Float) = 255 + + _ColorMask ("Color Mask", Float) = 15 + + [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 + } + + SubShader + { + Tags + { + "Queue"="Transparent" + "IgnoreProjector"="True" + "RenderType"="Transparent" + "PreviewType"="Plane" + "CanUseSpriteAtlas"="True" + } + + Stencil + { + Ref [_Stencil] + Comp [_StencilComp] + Pass [_StencilOp] + ReadMask [_StencilReadMask] + WriteMask [_StencilWriteMask] + } + + Cull Off + Lighting Off + ZWrite Off + ZTest [unity_GUIZTestMode] + Blend SrcAlpha OneMinusSrcAlpha + ColorMask [_ColorMask] + + Pass + { + Name "Default" + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 2.0 + + #include "UnityCG.cginc" + #include "UnityUI.cginc" + + #pragma multi_compile_local _ UNITY_UI_CLIP_RECT + #pragma multi_compile_local _ UNITY_UI_ALPHACLIP + + struct appdata_t + { + float4 vertex : POSITION; + float4 color : COLOR; + float2 texcoord : TEXCOORD0; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + fixed4 color : COLOR; + float2 texcoord : TEXCOORD0; + float4 worldPosition : TEXCOORD1; + UNITY_VERTEX_OUTPUT_STEREO + }; + + sampler2D _MainTex; // we dont use this, but unitys ui library expects the shader to have a texture + fixed4 _Color; + fixed4 _TextureSampleAdd; + float4 _ClipRect; + float4 _MainTex_ST; + + uint _Width; + uint _CategoryCount; + uint _MaxValue; + uint _DataStart; + half _GraphData[64 /* max. 64 points */ * 8 /* max 8 categories */]; + half4 _CategoryColors[8 /* max 8 categories */]; + + v2f vert(appdata_t v) + { + v2f OUT; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); + OUT.worldPosition = v.vertex; + OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); + + OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); + + OUT.color = v.color * _Color; + return OUT; + } + + fixed4 frag(v2f IN) : SV_Target + { + uint w = ((uint)(IN.texcoord.x * _Width) + _DataStart) % _Width; + half4 color = half4(0, 0, 0, 0); + float totalValue = 0; + for (uint c = 0; c < _CategoryCount; c++) + { + float categoryValue = _GraphData[w * _CategoryCount + c] / _MaxValue; + totalValue += categoryValue; + if (totalValue >= IN.texcoord.y) + { + color = _CategoryColors[c]; + break; + } + } + color *= IN.color; + + #ifdef UNITY_UI_CLIP_RECT + color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); + #endif + + #ifdef UNITY_UI_ALPHACLIP + clip (color.a - 0.001); + #endif + + return color; + } + ENDCG + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader.meta b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader.meta new file mode 100644 index 0000000..2559cf6 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: b5b24284f35f4992bcd4cc43919267d7 +timeCreated: 1724246251 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs new file mode 100644 index 0000000..bcc1639 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs @@ -0,0 +1,34 @@ +using System; +using UnityEngine; +namespace Mirror +{ + public class NetworkPingGraph : BaseUIGraph + { + protected override void CollectData(int category, out float value, out GraphAggregationMode mode) + { + mode = GraphAggregationMode.Average; + switch (category) + { + case 0: + value = (float)NetworkTime.rtt * 1000f; + break; + case 1: + value = (float)NetworkTime.rttVariance * 1000f; + break; + default: + throw new ArgumentOutOfRangeException($"{category} is not valid."); + } + } + + protected override string FormatValue(float value) => $"{value:N0}ms"; + + protected override void OnValidate() + { + base.OnValidate(); + if (CategoryColors.Length != 2) + CategoryColors = new[] { Color.cyan, Color.yellow }; + + IsStacked = false; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs.meta b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs.meta new file mode 100644 index 0000000..4ec56aa --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 14a68afedbbf4568b0decc5c3fe6dfd9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkPingGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs new file mode 100644 index 0000000..fe78918 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Mirror.RemoteCalls; +using UnityEngine; + +namespace Mirror +{ + public class NetworkRuntimeProfiler : MonoBehaviour + { + [Serializable] + public class Sorter : IComparer + { + public SortBy Order; + + public int Compare(Stat a, Stat b) + { + if (a == null) + return 1; + + if (b == null) + return -1; + + // Compare B to A for desc order + switch (Order) + { + case SortBy.RecentBytes: + return b.RecentBytes.CompareTo(a.RecentBytes); + case SortBy.RecentCount: + return b.RecentCount.CompareTo(a.RecentCount); + case SortBy.TotalBytes: + return b.TotalBytes.CompareTo(a.TotalBytes); + case SortBy.TotalCount: + return b.TotalCount.CompareTo(a.TotalCount); + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public enum SortBy + { + RecentBytes, + RecentCount, + TotalBytes, + TotalCount + } + + public class Stat + { + public string Name; + public long TotalCount; + public long TotalBytes; + + public long RecentCount; + public long RecentBytes; + + public void ResetRecent() + { + RecentCount = 0; + RecentBytes = 0; + } + + public void Add(int count, int bytes) + { + TotalBytes += bytes; + TotalCount += count; + + RecentBytes += bytes; + RecentCount += count; + } + } + + class MessageStats + { + public readonly Dictionary MessageByType = new Dictionary(); + public readonly Dictionary RpcByHash = new Dictionary(); + + public void Record(NetworkDiagnostics.MessageInfo info) + { + Type type = info.message.GetType(); + if (!MessageByType.TryGetValue(type, out Stat stat)) + { + stat = new Stat + { + Name = type.ToString(), + TotalCount = 0, + TotalBytes = 0, + RecentCount = 0, + RecentBytes = 0 + }; + + MessageByType[type] = stat; + } + + stat.Add(info.count, info.bytes * info.count); + + if (info.message is CommandMessage cmd) + RecordRpc(cmd.functionHash, info); + else if (info.message is RpcMessage rpc) + RecordRpc(rpc.functionHash, info); + } + + void RecordRpc(ushort hash, NetworkDiagnostics.MessageInfo info) + { + if (!RpcByHash.TryGetValue(hash, out Stat stat)) + { + string name = "n/a"; + RemoteCallDelegate rpcDelegate = RemoteProcedureCalls.GetDelegate(hash); + if (rpcDelegate != null) + name = $"{rpcDelegate.Method.DeclaringType}.{rpcDelegate.GetMethodName().Replace(RemoteProcedureCalls.InvokeRpcPrefix, "")}"; + + stat = new Stat + { + Name = name, + TotalCount = 0, + TotalBytes = 0, + RecentCount = 0, + RecentBytes = 0 + }; + + RpcByHash[hash] = stat; + } + + stat.Add(info.count, info.bytes * info.count); + } + + public void ResetRecent() + { + foreach (Stat stat in MessageByType.Values) + stat.ResetRecent(); + + foreach (Stat stat in RpcByHash.Values) + stat.ResetRecent(); + } + } + + [Tooltip("How many seconds to accumulate 'recent' stats for, this is also the output interval")] + public float RecentDuration = 5; + public Sorter Sort = new Sorter(); + + public enum OutputType + { + UnityLog, + StdOut, + File + } + + public OutputType Output; + [Tooltip("If Output is set to 'File', where to the path of that file")] + public string OutputFilePath = "network-stats.log"; + + readonly MessageStats inStats = new MessageStats(); + readonly MessageStats outStats = new MessageStats(); + readonly StringBuilder printBuilder = new StringBuilder(); + float elapsedSinceReset; + + void Start() + { + // Ordering, Awake happens before NetworkDiagnostics reset + NetworkDiagnostics.InMessageEvent += HandleMessageIn; + NetworkDiagnostics.OutMessageEvent += HandleMessageOut; + } + + void OnDestroy() + { + NetworkDiagnostics.InMessageEvent -= HandleMessageIn; + NetworkDiagnostics.OutMessageEvent -= HandleMessageOut; + } + + void HandleMessageOut(NetworkDiagnostics.MessageInfo info) => outStats.Record(info); + + void HandleMessageIn(NetworkDiagnostics.MessageInfo info) => inStats.Record(info); + + void LateUpdate() + { + elapsedSinceReset += Time.deltaTime; + if (elapsedSinceReset > RecentDuration) + { + elapsedSinceReset = 0; + Print(); + inStats.ResetRecent(); + outStats.ResetRecent(); + } + } + + void Print() + { + printBuilder.Clear(); + printBuilder.AppendLine($"Stats for {DateTime.Now} ({RecentDuration:N1}s interval)"); + int nameMaxLength = "OUT Message".Length; + + foreach (Stat stat in inStats.MessageByType.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + foreach (Stat stat in outStats.MessageByType.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + foreach (Stat stat in inStats.RpcByHash.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + foreach (Stat stat in outStats.RpcByHash.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + string recentBytes = "Recent Bytes"; + string recentCount = "Recent Count"; + string totalBytes = "Total Bytes"; + string totalCount = "Total Count"; + int maxBytesLength = FormatBytes(999999).Length; + int maxCountLength = FormatCount(999999).Length; + + int recentBytesPad = Mathf.Max(recentBytes.Length, maxBytesLength); + int recentCountPad = Mathf.Max(recentCount.Length, maxCountLength); + int totalBytesPad = Mathf.Max(totalBytes.Length, maxBytesLength); + int totalCountPad = Mathf.Max(totalCount.Length, maxCountLength); + string header = $"| {"IN Message".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + string sep = "".PadLeft(header.Length, '-'); + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + + foreach (Stat stat in inStats.MessageByType.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + header = $"| {"IN RPCs".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + foreach (Stat stat in inStats.RpcByHash.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + header = $"| {"OUT Message".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + foreach (Stat stat in outStats.MessageByType.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + header = $"| {"OUT RPCs".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + + foreach (Stat stat in outStats.RpcByHash.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + printBuilder.AppendLine(sep); + + switch (Output) + { + case OutputType.UnityLog: + Debug.Log(printBuilder.ToString()); + break; + case OutputType.StdOut: + Console.Write(printBuilder); + break; + case OutputType.File: + File.AppendAllText(OutputFilePath, printBuilder.ToString()); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + static string FormatBytes(long bytes) + { + const double KiB = 1024; + const double MiB = KiB * 1024; + const double GiB = MiB * 1024; + const double TiB = GiB * 1024; + + if (bytes < KiB) + return $"{bytes:N0} B"; + + if (bytes < MiB) + return $"{bytes / KiB:N2} KiB"; + + if (bytes < GiB) + return $"{bytes / MiB:N2} MiB"; + + if (bytes < TiB) + return $"{bytes / GiB:N2} GiB"; + + return $"{bytes / TiB:N2} TiB"; + } + + string FormatCount(long count) + { + const double K = 1000; + const double M = K * 1000; + const double G = M * 1000; + const double T = G * 1000; + + if (count < K) + return $"{count:N0}"; + + if (count < M) + return $"{count / K:N2} K"; + + if (count < G) + return $"{count / M:N2} M"; + + if (count < T) + return $"{count / G:N2} G"; + + return $"{count / T:N2} T"; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs.meta b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs.meta new file mode 100644 index 0000000..6fc7aed --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ef8db82aeb77400bb9e80850e39065a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs.meta b/Assets/Mirror/Components/Profiling/Prefabs.meta new file mode 100644 index 0000000..e81bce5 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 083c6613a11cad746bb252bc7748947f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab new file mode 100644 index 0000000..5aff497 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab @@ -0,0 +1,1776 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &119935549420283780 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8824290184142807954} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8824290184142807954 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 119935549420283780} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 8397968435809926716} + - {fileID: 8383045724353990353} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &215824790723025810 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8473536528693599174} + - component: {fileID: 8359308210431883230} + m_Layer: 5 + m_Name: LegendOUT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8473536528693599174 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5970650343794535896} + - {fileID: 2927000160895229417} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8359308210431883230 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + m_CullTransparentMesh: 1 +--- !u!1 &230788547622420441 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968396119680197966} + - component: {fileID: 5129519199212594243} + - component: {fileID: 5123833194418803548} + m_Layer: 5 + m_Name: GraphContent + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &968396119680197966 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 842792608162333385} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 10, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5129519199212594243 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_CullTransparentMesh: 1 +--- !u!114 &5123833194418803548 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &969091221012082297 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8397968435809926716} + - component: {fileID: 5852067364008015930} + - component: {fileID: 4136376136871219296} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8397968435809926716 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8824290184142807954} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5852067364008015930 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_CullTransparentMesh: 1 +--- !u!114 &4136376136871219296 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1694800138959048866 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5970650343794535896} + - component: {fileID: 3299375740833370906} + - component: {fileID: 6202027950733238712} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5970650343794535896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8473536528693599174} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3299375740833370906 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_CullTransparentMesh: 1 +--- !u!114 &6202027950733238712 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1711370235648153955 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5455569689841791731} + - component: {fileID: 8443939090520853786} + - component: {fileID: 1177453892628721408} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5455569689841791731 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5885248672530866080} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8443939090520853786 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_CullTransparentMesh: 1 +--- !u!114 &1177453892628721408 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &2002525035342291879 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1750250400045347475} + - component: {fileID: 8835486359842310823} + m_Layer: 5 + m_Name: LegendIN + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1750250400045347475 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5157207313487273501} + - {fileID: 3683314132040949185} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8835486359842310823 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + m_CullTransparentMesh: 1 +--- !u!1 &3357588547163078786 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2288491264626255212} + m_Layer: 5 + m_Name: LegendsY + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2288491264626255212 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3357588547163078786} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5885248672530866080} + - {fileID: 5389867435348264296} + - {fileID: 8455135462272940247} + - {fileID: 6309157798075429021} + - {fileID: 8824290184142807954} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 10, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3534129496521135982 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 842792608162333385} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &842792608162333385 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3534129496521135982} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 968396119680197966} + - {fileID: 2288491264626255212} + - {fileID: 2581675890115619580} + - {fileID: 5071673759804795070} + m_Father: {fileID: 2138752905301465689} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3670180597793129839 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2138752905301465689} + - component: {fileID: 8024388048541391361} + m_Layer: 5 + m_Name: BandwidthGraph + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2138752905301465689 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 3363869356760374277} + - {fileID: 842792608162333385} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &8024388048541391361 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e1ae19b97f0e4a5eb8cf5158d97506f5, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 5123833194418803548} + Points: 64 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 1, g: 0, b: 0, a: 1} + - {r: 0, g: 1, b: 0, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 142216866270345907} + - {fileID: 4330748233085529081} + - {fileID: 2248143582364066648} + - {fileID: 97403263534861610} + - {fileID: 4349487955187298716} + runtimeMaterial: {fileID: 0} +--- !u!1 &3678091663373353095 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5732919540871248940} + - component: {fileID: 6719434420897279190} + - component: {fileID: 142216866270345907} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5732919540871248940 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5885248672530866080} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6719434420897279190 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_CullTransparentMesh: 1 +--- !u!114 &142216866270345907 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3714570775115803475 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8383045724353990353} + - component: {fileID: 1355767179239727018} + - component: {fileID: 4349487955187298716} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8383045724353990353 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8824290184142807954} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1355767179239727018 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_CullTransparentMesh: 1 +--- !u!114 &4349487955187298716 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &4375453557142464801 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2581675890115619580} + - component: {fileID: 5374880666536229423} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2581675890115619580 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1750250400045347475} + - {fileID: 8473536528693599174} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &5374880666536229423 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &4917723141099053105 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3363869356760374277} + - component: {fileID: 4476095696555559496} + - component: {fileID: 5567867155054281268} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3363869356760374277 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2138752905301465689} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4476095696555559496 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_CullTransparentMesh: 1 +--- !u!114 &5567867155054281268 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5274949478677654638 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5885248672530866080} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5885248672530866080 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5274949478677654638} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5455569689841791731} + - {fileID: 5732919540871248940} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5352407663590911692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 9183933987449881395} + - component: {fileID: 7256530256701424070} + - component: {fileID: 4330748233085529081} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &9183933987449881395 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5389867435348264296} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7256530256701424070 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_CullTransparentMesh: 1 +--- !u!114 &4330748233085529081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &5693887164122654533 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6309157798075429021} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6309157798075429021 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5693887164122654533} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 7988638580994470440} + - {fileID: 3076841666317101811} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5810998373083543261 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5071673759804795070} + - component: {fileID: 1370951599340238612} + - component: {fileID: 2160324143502055429} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5071673759804795070 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 842792608162333385} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &1370951599340238612 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_CullTransparentMesh: 1 +--- !u!114 &2160324143502055429 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: BANDWIDTH +--- !u!1 &6326536691934325288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6145669649686337282} + - component: {fileID: 968747920458182069} + - component: {fileID: 4680362740251307526} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6145669649686337282 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8455135462272940247} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &968747920458182069 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_CullTransparentMesh: 1 +--- !u!114 &4680362740251307526 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6354099053443266201 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3127105718844619361} + - component: {fileID: 6920956740542454426} + - component: {fileID: 2248143582364066648} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3127105718844619361 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8455135462272940247} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6920956740542454426 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_CullTransparentMesh: 1 +--- !u!114 &2248143582364066648 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6498744302665369373 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2927000160895229417} + - component: {fileID: 3795603901461861956} + - component: {fileID: 3504239642613663755} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2927000160895229417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8473536528693599174} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3795603901461861956 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_CullTransparentMesh: 1 +--- !u!114 &3504239642613663755 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: out +--- !u!1 &6501576574154348994 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8455135462272940247} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8455135462272940247 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6501576574154348994} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 6145669649686337282} + - {fileID: 3127105718844619361} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6868069573715158019 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5389867435348264296} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5389867435348264296 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6868069573715158019} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 8254041970903625228} + - {fileID: 9183933987449881395} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &7002258827437049681 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5157207313487273501} + - component: {fileID: 5222763877749258818} + - component: {fileID: 1845491664005460411} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5157207313487273501 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1750250400045347475} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5222763877749258818 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_CullTransparentMesh: 1 +--- !u!114 &1845491664005460411 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7189887428595669623 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7988638580994470440} + - component: {fileID: 7267528306069058291} + - component: {fileID: 4777559063183442036} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7988638580994470440 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6309157798075429021} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7267528306069058291 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_CullTransparentMesh: 1 +--- !u!114 &4777559063183442036 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7203666293600016388 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3683314132040949185} + - component: {fileID: 263296843630000285} + - component: {fileID: 5681678021283145729} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3683314132040949185 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1750250400045347475} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &263296843630000285 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_CullTransparentMesh: 1 +--- !u!114 &5681678021283145729 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: in +--- !u!1 &8334833703202091165 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8254041970903625228} + - component: {fileID: 9068658072675687366} + - component: {fileID: 3509666831540494135} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8254041970903625228 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5389867435348264296} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &9068658072675687366 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_CullTransparentMesh: 1 +--- !u!114 &3509666831540494135 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &8757401605356351412 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3076841666317101811} + - component: {fileID: 3104114958221384706} + - component: {fileID: 97403263534861610} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3076841666317101811 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6309157798075429021} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &3104114958221384706 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_CullTransparentMesh: 1 +--- !u!114 &97403263534861610 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab.meta new file mode 100644 index 0000000..440e669 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 3c7a97355c25a2b4da731b53876f8a8b +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab new file mode 100644 index 0000000..b29376f --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab @@ -0,0 +1,1976 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &24878003039380082 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1738718049027005141} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1738718049027005141 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 24878003039380082} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546206307382570} + - {fileID: 343546206776216637} + - {fileID: 343546206294766220} + - {fileID: 343546206649983697} + m_Father: {fileID: 343546206339761112} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546204850636262 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546204850636257} + - component: {fileID: 343546204850636259} + - component: {fileID: 343546204850636256} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546204850636257 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204850636262} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546206877774020} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546204850636259 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204850636262} + m_CullTransparentMesh: 1 +--- !u!114 &343546204850636256 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204850636262} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546204890306331 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546204890306330} + - component: {fileID: 343546204890306340} + - component: {fileID: 343546204890306341} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546204890306330 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204890306331} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546206506823101} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546204890306340 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204890306331} + m_CullTransparentMesh: 1 +--- !u!114 &343546204890306341 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204890306331} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: max +--- !u!1 &343546205136049349 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205136049348} + - component: {fileID: 343546205136049350} + - component: {fileID: 343546205136049351} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205136049348 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205136049349} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205806955365} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546205136049350 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205136049349} + m_CullTransparentMesh: 1 +--- !u!114 &343546205136049351 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205136049349} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546205212282750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205212282745} + - component: {fileID: 343546205212282747} + m_Layer: 5 + m_Name: LegendAVG + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205212282745 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205212282750} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546205253700114} + - {fileID: 343546206801970585} + m_Father: {fileID: 343546206294766220} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205212282747 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205212282750} + m_CullTransparentMesh: 1 +--- !u!1 &343546205253700115 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205253700114} + - component: {fileID: 343546205253700124} + - component: {fileID: 343546205253700125} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205253700114 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205253700115} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205212282745} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205253700124 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205253700115} + m_CullTransparentMesh: 1 +--- !u!114 &343546205253700125 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205253700115} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205281663119 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205281663118} + - component: {fileID: 343546205281663113} + m_Layer: 5 + m_Name: LegendMIN + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205281663118 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205281663119} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546205496339525} + - {fileID: 343546206359818246} + m_Father: {fileID: 343546206294766220} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205281663113 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205281663119} + m_CullTransparentMesh: 1 +--- !u!1 &343546205394542240 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205394542243} + - component: {fileID: 343546205394542253} + - component: {fileID: 343546205394542242} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205394542243 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205394542240} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205508279401} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546205394542253 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205394542240} + m_CullTransparentMesh: 1 +--- !u!114 &343546205394542242 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205394542240} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546205496339642 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205496339525} + - component: {fileID: 343546205496339527} + - component: {fileID: 343546205496339524} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205496339525 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205496339642} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205281663118} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205496339527 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205496339642} + m_CullTransparentMesh: 1 +--- !u!114 &343546205496339524 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205496339642} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205500504725 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205500504724} + - component: {fileID: 343546205500504726} + - component: {fileID: 343546205500504727} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205500504724 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205500504725} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546206877774020} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205500504726 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205500504725} + m_CullTransparentMesh: 1 +--- !u!114 &343546205500504727 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205500504725} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205508279406 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205508279401} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205508279401 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205508279406} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546206740227476} + - {fileID: 343546205394542243} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205523578647 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205523578646} + - component: {fileID: 343546205523578640} + - component: {fileID: 343546205523578641} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205523578646 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205523578647} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205634725308} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205523578640 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205523578647} + m_CullTransparentMesh: 1 +--- !u!114 &343546205523578641 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205523578647} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205634725309 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205634725308} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205634725308 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205634725309} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546205523578646} + - {fileID: 343546206440079829} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205685920600 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205685920603} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205685920603 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205685920600} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546206488145202} + - {fileID: 343546206326199842} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205806955354 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205806955365} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205806955365 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205806955354} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546206737158417} + - {fileID: 343546205136049348} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205859589119 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205859589118} + - component: {fileID: 343546205859589112} + - component: {fileID: 343546205859589113} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205859589118 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205859589119} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546206339761112} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205859589112 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205859589119} + m_CullTransparentMesh: 1 +--- !u!114 &343546205859589113 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205859589119} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206294766221 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206294766220} + - component: {fileID: 343546206294766223} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206294766220 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206294766221} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546205212282745} + - {fileID: 343546205281663118} + - {fileID: 343546206506823101} + m_Father: {fileID: 1738718049027005141} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &343546206294766223 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206294766221} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &343546206307382571 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206307382570} + - component: {fileID: 343546206307382580} + - component: {fileID: 343546206307382581} + m_Layer: 5 + m_Name: GraphContent + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206307382570 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206307382571} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1738718049027005141} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206307382580 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206307382571} + m_CullTransparentMesh: 1 +--- !u!114 &343546206307382581 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206307382571} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206326199843 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206326199842} + - component: {fileID: 343546206326199852} + - component: {fileID: 343546206326199853} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206326199842 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206326199843} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205685920603} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546206326199852 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206326199843} + m_CullTransparentMesh: 1 +--- !u!114 &343546206326199853 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206326199843} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546206339761113 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206339761112} + - component: {fileID: 4901013733809370033} + m_Layer: 5 + m_Name: FPSMinMaxAvg + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206339761112 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206339761113} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546205859589118} + - {fileID: 1738718049027005141} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &4901013733809370033 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206339761113} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 73bc3b929ec94537a8dbd67eb9d0c2c6, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 343546206307382581} + Points: 16 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 0, g: 1, b: 1, a: 1} + - {r: 1, g: 0, b: 0, a: 1} + - {r: 0, g: 1, b: 0, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 343546205394542242} + - {fileID: 343546204850636256} + - {fileID: 343546206440079828} + - {fileID: 343546206326199853} + - {fileID: 343546205136049351} + runtimeMaterial: {fileID: 0} +--- !u!1 &343546206359818247 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206359818246} + - component: {fileID: 343546206359818240} + - component: {fileID: 343546206359818241} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206359818246 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206359818247} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205281663118} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206359818240 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206359818247} + m_CullTransparentMesh: 1 +--- !u!114 &343546206359818241 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206359818247} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: min +--- !u!1 &343546206440079818 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206440079829} + - component: {fileID: 343546206440079831} + - component: {fileID: 343546206440079828} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206440079829 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206440079818} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205634725308} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546206440079831 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206440079818} + m_CullTransparentMesh: 1 +--- !u!114 &343546206440079828 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206440079818} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546206488145203 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206488145202} + - component: {fileID: 343546206488145212} + - component: {fileID: 343546206488145213} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206488145202 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206488145203} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205685920603} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206488145212 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206488145203} + m_CullTransparentMesh: 1 +--- !u!114 &343546206488145213 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206488145203} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206506823090 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206506823101} + - component: {fileID: 343546206506823100} + m_Layer: 5 + m_Name: LegendMAX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206506823101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206506823090} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546206516923994} + - {fileID: 343546204890306330} + m_Father: {fileID: 343546206294766220} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206506823100 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206506823090} + m_CullTransparentMesh: 1 +--- !u!1 &343546206516923995 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206516923994} + - component: {fileID: 343546206516924004} + - component: {fileID: 343546206516924005} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206516923994 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206516923995} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546206506823101} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206516924004 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206516923995} + m_CullTransparentMesh: 1 +--- !u!114 &343546206516924005 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206516923995} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206649983702 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206649983697} + - component: {fileID: 343546206649983699} + - component: {fileID: 343546206649983696} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206649983697 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206649983702} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1738718049027005141} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &343546206649983699 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206649983702} + m_CullTransparentMesh: 1 +--- !u!114 &343546206649983696 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206649983702} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: FPS +--- !u!1 &343546206737158422 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206737158417} + - component: {fileID: 343546206737158419} + - component: {fileID: 343546206737158416} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206737158417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206737158422} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205806955365} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206737158419 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206737158422} + m_CullTransparentMesh: 1 +--- !u!114 &343546206737158416 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206737158422} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206740227477 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206740227476} + - component: {fileID: 343546206740227478} + - component: {fileID: 343546206740227479} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206740227476 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206740227477} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205508279401} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206740227478 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206740227477} + m_CullTransparentMesh: 1 +--- !u!114 &343546206740227479 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206740227477} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206776216626 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206776216637} + m_Layer: 5 + m_Name: LegendsY + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206776216637 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206776216626} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546205508279401} + - {fileID: 343546206877774020} + - {fileID: 343546205634725308} + - {fileID: 343546205685920603} + - {fileID: 343546205806955365} + m_Father: {fileID: 1738718049027005141} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546206801970590 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206801970585} + - component: {fileID: 343546206801970587} + - component: {fileID: 343546206801970584} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206801970585 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206801970590} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 343546205212282745} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206801970587 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206801970590} + m_CullTransparentMesh: 1 +--- !u!114 &343546206801970584 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206801970590} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: avg +--- !u!1 &343546206877774021 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206877774020} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206877774020 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206877774021} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 343546205500504724} + - {fileID: 343546204850636257} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab.meta new file mode 100644 index 0000000..4e2d104 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 9bdc42bca9b7109428d00fe33bdb5102 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab new file mode 100644 index 0000000..b90a863 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab @@ -0,0 +1,765 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &2777084101886578567 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3864348419064627986} + - component: {fileID: 4699359553083341963} + m_Layer: 5 + m_Name: Graphs + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &3864348419064627986 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2777084101886578567} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 4509006437822490170} + - {fileID: 522813179161291686} + - {fileID: 7802229218722708586} + m_Father: {fileID: 5551289487596275721} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &4699359553083341963 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2777084101886578567} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 10 + m_Right: 0 + m_Top: 10 + m_Bottom: 0 + m_ChildAlignment: 2 + m_Spacing: 0 + m_ChildForceExpandWidth: 0 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 0 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &4512081604627528395 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5551289487596275721} + - component: {fileID: 5641452096830013034} + - component: {fileID: 4961392508020504993} + - component: {fileID: 1349218098020237989} + - component: {fileID: 8415974033864006777} + m_Layer: 5 + m_Name: GraphCanvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5551289487596275721 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 3864348419064627986} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!223 &5641452096830013034 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!114 &4961392508020504993 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1280, y: 720} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 1 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!114 &1349218098020237989 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &8415974033864006777 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a27b133d890c41828d3b01ffa12fe440, type: 3} + m_Name: + m_EditorClassIdentifier: + Key: 291 + ToToggle: {fileID: 2777084101886578567} +--- !u!1001 &4204022305993221602 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 3864348419064627986} + m_Modifications: + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 86.666664 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 43.333332 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 86.666664 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 150 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 1668.6208 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -85 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761113, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_Name + value: FPSMinMaxAvg + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 86.666664 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 216.66666 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 9bdc42bca9b7109428d00fe33bdb5102, type: 3} +--- !u!224 &4509006437822490170 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + m_PrefabInstance: {fileID: 4204022305993221602} + m_PrefabAsset: {fileID: 0} +--- !u!1001 &7469102913200609509 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 3864348419064627986} + m_Modifications: + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 195 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 2997867828362567431, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_Name + value: PingGraph + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 65 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_RootOrder + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.y + value: 150 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 1968.6208 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -85 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: ed3b4e27086dcc64b8c6605011a321e2, type: 3} +--- !u!224 &522813179161291686 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + m_PrefabInstance: {fileID: 7469102913200609509} + m_PrefabAsset: {fileID: 0} +--- !u!1001 &8208221870570858035 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 3864348419064627986} + m_Modifications: + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 65 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 1986485545668258777, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: Points + value: 32 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_RootOrder + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.y + value: 150 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 2268.6208 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -85 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3670180597793129839, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_Name + value: NetworkGraph + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 195 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 3c7a97355c25a2b4da731b53876f8a8b, type: 3} +--- !u!224 &7802229218722708586 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + m_PrefabInstance: {fileID: 8208221870570858035} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab.meta new file mode 100644 index 0000000..4e4dab5 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: e87e1847def3c1f41b19b7df4f0920b3 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab new file mode 100644 index 0000000..bb77911 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab @@ -0,0 +1,2888 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &119935549420283780 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8824290184142807954} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8824290184142807954 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 119935549420283780} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 8397968435809926716} + - {fileID: 8383045724353990353} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &215824790723025810 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8473536528693599174} + - component: {fileID: 8359308210431883230} + m_Layer: 5 + m_Name: LegendOUT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8473536528693599174 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5970650343794535896} + - {fileID: 2927000160895229417} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8359308210431883230 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + m_CullTransparentMesh: 1 +--- !u!1 &230788547622420441 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968396119680197966} + - component: {fileID: 5129519199212594243} + - component: {fileID: 5123833194418803548} + m_Layer: 5 + m_Name: GraphContent - Bandwidth + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &968396119680197966 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 842792608162333385} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10.1066, y: 0} + m_SizeDelta: {x: -60.2131, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5129519199212594243 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_CullTransparentMesh: 1 +--- !u!114 &5123833194418803548 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &969091221012082297 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8397968435809926716} + - component: {fileID: 5852067364008015930} + - component: {fileID: 4136376136871219296} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8397968435809926716 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8824290184142807954} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5852067364008015930 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_CullTransparentMesh: 1 +--- !u!114 &4136376136871219296 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1144042303701158085 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2770498519712146759} + - component: {fileID: 1909324844259712510} + - component: {fileID: 6801190371405574669} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2770498519712146759 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1144042303701158085} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6118054815374899758} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1909324844259712510 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1144042303701158085} + m_CullTransparentMesh: 1 +--- !u!114 &6801190371405574669 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1144042303701158085} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: jitter +--- !u!1 &1194010084318017500 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3962977762644453858} + m_Layer: 5 + m_Name: LegendsY - Ping + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3962977762644453858 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1194010084318017500} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 8467240887132563218} + - {fileID: 2900620391526308637} + - {fileID: 1423636040061542291} + - {fileID: 3701477534332925528} + - {fileID: 5515203252368553261} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 109.89, y: 0} + m_SizeDelta: {x: -199.79, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1649716185402538073 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7375623566472823448} + - component: {fileID: 7280101622563951250} + - component: {fileID: 1772663036056914474} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7375623566472823448 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649716185402538073} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6118054815374899758} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7280101622563951250 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649716185402538073} + m_CullTransparentMesh: 1 +--- !u!114 &1772663036056914474 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649716185402538073} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1694800138959048866 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5970650343794535896} + - component: {fileID: 3299375740833370906} + - component: {fileID: 6202027950733238712} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5970650343794535896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8473536528693599174} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3299375740833370906 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_CullTransparentMesh: 1 +--- !u!114 &6202027950733238712 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1711370235648153955 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5455569689841791731} + - component: {fileID: 8443939090520853786} + - component: {fileID: 1177453892628721408} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5455569689841791731 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5885248672530866080} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8443939090520853786 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_CullTransparentMesh: 1 +--- !u!114 &1177453892628721408 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &2002525035342291879 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1750250400045347475} + - component: {fileID: 8835486359842310823} + m_Layer: 5 + m_Name: LegendIN + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1750250400045347475 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5157207313487273501} + - {fileID: 3683314132040949185} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8835486359842310823 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + m_CullTransparentMesh: 1 +--- !u!1 &3357588547163078786 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2288491264626255212} + m_Layer: 5 + m_Name: LegendsY - Bandwidth + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2288491264626255212 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3357588547163078786} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5885248672530866080} + - {fileID: 5389867435348264296} + - {fileID: 8455135462272940247} + - {fileID: 6309157798075429021} + - {fileID: 8824290184142807954} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10.107, y: 0} + m_SizeDelta: {x: -60.213, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3534129496521135982 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 842792608162333385} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &842792608162333385 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3534129496521135982} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 968396119680197966} + - {fileID: 5267541382525022770} + - {fileID: 2288491264626255212} + - {fileID: 3962977762644453858} + - {fileID: 2581675890115619580} + - {fileID: 5071673759804795070} + m_Father: {fileID: 2138752905301465689} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3569519790910280589 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1259420167673541393} + - component: {fileID: 4579425422591491119} + - component: {fileID: 8580294978504068442} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1259420167673541393 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3569519790910280589} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1423636040061542291} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &4579425422591491119 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3569519790910280589} + m_CullTransparentMesh: 1 +--- !u!114 &8580294978504068442 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3569519790910280589} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3670180597793129839 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2138752905301465689} + - component: {fileID: 8024388048541391361} + - component: {fileID: 6891156770396053448} + m_Layer: 5 + m_Name: NetworkGraph + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2138752905301465689 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 3363869356760374277} + - {fileID: 842792608162333385} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &8024388048541391361 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e1ae19b97f0e4a5eb8cf5158d97506f5, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 5123833194418803548} + Points: 64 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 1, g: 0, b: 0, a: 1} + - {r: 0, g: 1, b: 0, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 142216866270345907} + - {fileID: 4330748233085529081} + - {fileID: 2248143582364066648} + - {fileID: 97403263534861610} + - {fileID: 4349487955187298716} + runtimeMaterial: {fileID: 0} +--- !u!114 &6891156770396053448 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 14a68afedbbf4568b0decc5c3fe6dfd9, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 3880664398471042142} + Points: 64 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 0, g: 1, b: 1, a: 1} + - {r: 1, g: 0.92156863, b: 0.015686275, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 2117081375211614847} + - {fileID: 7864316414649457688} + - {fileID: 8580294978504068442} + - {fileID: 1639235332378233126} + - {fileID: 7668278254153150358} + runtimeMaterial: {fileID: 0} +--- !u!1 &3678091663373353095 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5732919540871248940} + - component: {fileID: 6719434420897279190} + - component: {fileID: 142216866270345907} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5732919540871248940 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5885248672530866080} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6719434420897279190 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_CullTransparentMesh: 1 +--- !u!114 &142216866270345907 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3714570775115803475 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8383045724353990353} + - component: {fileID: 1355767179239727018} + - component: {fileID: 4349487955187298716} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8383045724353990353 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8824290184142807954} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1355767179239727018 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_CullTransparentMesh: 1 +--- !u!114 &4349487955187298716 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &4375453557142464801 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2581675890115619580} + - component: {fileID: 5374880666536229423} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2581675890115619580 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1750250400045347475} + - {fileID: 8473536528693599174} + - {fileID: 4635932635135080026} + - {fileID: 6118054815374899758} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &5374880666536229423 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &4490397563866728061 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8036502514150168521} + - component: {fileID: 3374747859601888581} + - component: {fileID: 460765458322436772} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8036502514150168521 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4490397563866728061} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4635932635135080026} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3374747859601888581 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4490397563866728061} + m_CullTransparentMesh: 1 +--- !u!114 &460765458322436772 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4490397563866728061} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &4492650180689071546 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7595841946320721050} + - component: {fileID: 186511036850520057} + - component: {fileID: 7292537739385968087} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7595841946320721050 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4492650180689071546} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4635932635135080026} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &186511036850520057 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4492650180689071546} + m_CullTransparentMesh: 1 +--- !u!114 &7292537739385968087 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4492650180689071546} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: ping +--- !u!1 &4671836082000350783 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1224777268200255961} + - component: {fileID: 6711771988030203581} + - component: {fileID: 7864316414649457688} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1224777268200255961 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4671836082000350783} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2900620391526308637} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6711771988030203581 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4671836082000350783} + m_CullTransparentMesh: 1 +--- !u!114 &7864316414649457688 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4671836082000350783} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &4723476562664894706 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3701477534332925528} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3701477534332925528 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4723476562664894706} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 6198365725618080772} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &4917723141099053105 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3363869356760374277} + - component: {fileID: 4476095696555559496} + - component: {fileID: 5567867155054281268} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3363869356760374277 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2138752905301465689} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4476095696555559496 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_CullTransparentMesh: 1 +--- !u!114 &5567867155054281268 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5075371807985313428 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4635932635135080026} + - component: {fileID: 7147312742844013158} + m_Layer: 5 + m_Name: LegendPING + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4635932635135080026 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5075371807985313428} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 8036502514150168521} + - {fileID: 7595841946320721050} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7147312742844013158 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5075371807985313428} + m_CullTransparentMesh: 1 +--- !u!1 &5274949478677654638 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5885248672530866080} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5885248672530866080 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5274949478677654638} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5455569689841791731} + - {fileID: 5732919540871248940} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5352407663590911692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 9183933987449881395} + - component: {fileID: 7256530256701424070} + - component: {fileID: 4330748233085529081} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &9183933987449881395 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5389867435348264296} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7256530256701424070 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_CullTransparentMesh: 1 +--- !u!114 &4330748233085529081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &5597103007742484545 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1423636040061542291} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1423636040061542291 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5597103007742484545} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1259420167673541393} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5628951584669016692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5267541382525022770} + - component: {fileID: 3449685404681157396} + - component: {fileID: 3880664398471042142} + m_Layer: 5 + m_Name: GraphContent - Ping + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5267541382525022770 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5628951584669016692} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 842792608162333385} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10.1066, y: 0} + m_SizeDelta: {x: -60.2131, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3449685404681157396 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5628951584669016692} + m_CullTransparentMesh: 1 +--- !u!114 &3880664398471042142 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5628951584669016692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5693887164122654533 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6309157798075429021} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6309157798075429021 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5693887164122654533} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 7988638580994470440} + - {fileID: 3076841666317101811} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5810998373083543261 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5071673759804795070} + - component: {fileID: 1370951599340238612} + - component: {fileID: 2160324143502055429} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5071673759804795070 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 842792608162333385} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &1370951599340238612 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_CullTransparentMesh: 1 +--- !u!114 &2160324143502055429 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: netgraph +--- !u!1 &5949791456184720458 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 808931863454774844} + - component: {fileID: 3262084368540049565} + - component: {fileID: 7668278254153150358} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &808931863454774844 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5949791456184720458} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5515203252368553261} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &3262084368540049565 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5949791456184720458} + m_CullTransparentMesh: 1 +--- !u!114 &7668278254153150358 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5949791456184720458} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6299708506135008504 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6198365725618080772} + - component: {fileID: 4666306959683799360} + - component: {fileID: 1639235332378233126} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6198365725618080772 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6299708506135008504} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 3701477534332925528} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &4666306959683799360 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6299708506135008504} + m_CullTransparentMesh: 1 +--- !u!114 &1639235332378233126 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6299708506135008504} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6326536691934325288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6145669649686337282} + - component: {fileID: 968747920458182069} + - component: {fileID: 4680362740251307526} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6145669649686337282 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8455135462272940247} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &968747920458182069 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_CullTransparentMesh: 1 +--- !u!114 &4680362740251307526 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6354099053443266201 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3127105718844619361} + - component: {fileID: 6920956740542454426} + - component: {fileID: 2248143582364066648} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3127105718844619361 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8455135462272940247} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6920956740542454426 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_CullTransparentMesh: 1 +--- !u!114 &2248143582364066648 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6473252433622726265 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5515203252368553261} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5515203252368553261 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6473252433622726265} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 808931863454774844} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6498744302665369373 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2927000160895229417} + - component: {fileID: 3795603901461861956} + - component: {fileID: 3504239642613663755} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2927000160895229417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8473536528693599174} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3795603901461861956 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_CullTransparentMesh: 1 +--- !u!114 &3504239642613663755 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: out +--- !u!1 &6501576574154348994 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8455135462272940247} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8455135462272940247 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6501576574154348994} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 6145669649686337282} + - {fileID: 3127105718844619361} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6697282991844538288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8467240887132563218} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8467240887132563218 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6697282991844538288} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 8045971123728951344} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6868069573715158019 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5389867435348264296} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5389867435348264296 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6868069573715158019} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 8254041970903625228} + - {fileID: 9183933987449881395} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &7002258827437049681 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5157207313487273501} + - component: {fileID: 5222763877749258818} + - component: {fileID: 1845491664005460411} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5157207313487273501 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1750250400045347475} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5222763877749258818 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_CullTransparentMesh: 1 +--- !u!114 &1845491664005460411 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7189887428595669623 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7988638580994470440} + - component: {fileID: 7267528306069058291} + - component: {fileID: 4777559063183442036} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7988638580994470440 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6309157798075429021} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7267528306069058291 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_CullTransparentMesh: 1 +--- !u!114 &4777559063183442036 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7203666293600016388 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3683314132040949185} + - component: {fileID: 263296843630000285} + - component: {fileID: 5681678021283145729} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3683314132040949185 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 1750250400045347475} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &263296843630000285 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_CullTransparentMesh: 1 +--- !u!114 &5681678021283145729 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: in +--- !u!1 &8077720310600692407 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6118054815374899758} + - component: {fileID: 3202099948270756252} + m_Layer: 5 + m_Name: LegendJITTER + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6118054815374899758 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8077720310600692407} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 7375623566472823448} + - {fileID: 2770498519712146759} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3202099948270756252 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8077720310600692407} + m_CullTransparentMesh: 1 +--- !u!1 &8334833703202091165 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8254041970903625228} + - component: {fileID: 9068658072675687366} + - component: {fileID: 3509666831540494135} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8254041970903625228 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5389867435348264296} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &9068658072675687366 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_CullTransparentMesh: 1 +--- !u!114 &3509666831540494135 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &8757401605356351412 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3076841666317101811} + - component: {fileID: 3104114958221384706} + - component: {fileID: 97403263534861610} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3076841666317101811 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6309157798075429021} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &3104114958221384706 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_CullTransparentMesh: 1 +--- !u!114 &97403263534861610 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &8983883851149759884 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8045971123728951344} + - component: {fileID: 1946944942956829893} + - component: {fileID: 2117081375211614847} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8045971123728951344 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8983883851149759884} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 8467240887132563218} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1946944942956829893 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8983883851149759884} + m_CullTransparentMesh: 1 +--- !u!114 &2117081375211614847 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8983883851149759884} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &9092185120150146117 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2900620391526308637} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2900620391526308637 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9092185120150146117} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1224777268200255961} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab.meta new file mode 100644 index 0000000..1e2c86c --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: a4ee3092d7c7f4a38a70b11014175ebd +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab new file mode 100644 index 0000000..c35ae4d --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab @@ -0,0 +1,1776 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &512166646911724149 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5569695547238921462} + - component: {fileID: 7180649172285861411} + - component: {fileID: 3698306165924257989} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5569695547238921462 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 512166646911724149} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4620809971601425299} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7180649172285861411 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 512166646911724149} + m_CullTransparentMesh: 1 +--- !u!114 &3698306165924257989 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 512166646911724149} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &693974290553253915 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2454008239292490697} + - component: {fileID: 6928098193469425262} + - component: {fileID: 9151821596118048258} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2454008239292490697 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 693974290553253915} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 7987015295734336241} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6928098193469425262 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 693974290553253915} + m_CullTransparentMesh: 1 +--- !u!114 &9151821596118048258 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 693974290553253915} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &724734797682004671 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 216369457880424682} + - component: {fileID: 6446260028879253612} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &216369457880424682 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724734797682004671} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 6899184289734897349} + - {fileID: 942441613928011978} + m_Father: {fileID: 5294687292649652416} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &6446260028879253612 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724734797682004671} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &1093758345687749999 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7987015295734336241} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7987015295734336241 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1093758345687749999} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 911697406546784338} + - {fileID: 2454008239292490697} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1549744656742816016 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5294687292649652416} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5294687292649652416 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1549744656742816016} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1768608949435380201} + - {fileID: 1469817193488350550} + - {fileID: 216369457880424682} + - {fileID: 6975151376875653975} + m_Father: {fileID: 6982534731248530243} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1723664113960925886 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6658247662285819325} + - component: {fileID: 1965012378351765188} + - component: {fileID: 4336023028329030949} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6658247662285819325 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1723664113960925886} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 7590176880698012387} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1965012378351765188 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1723664113960925886} + m_CullTransparentMesh: 1 +--- !u!114 &4336023028329030949 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1723664113960925886} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1751972084552208902 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7870100887797750244} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7870100887797750244 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1751972084552208902} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 7889663160516621925} + - {fileID: 3705598884744263366} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1934697115913144479 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2203074428927526136} + - component: {fileID: 2023856609668669680} + - component: {fileID: 2206720168024543330} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2203074428927526136 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1934697115913144479} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 942441613928011978} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2023856609668669680 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1934697115913144479} + m_CullTransparentMesh: 1 +--- !u!114 &2206720168024543330 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1934697115913144479} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0.9215687, b: 0.015686275, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &2810967667379106609 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1217973610716945855} + - component: {fileID: 1127519584872800040} + - component: {fileID: 362765526694760680} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1217973610716945855 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2810967667379106609} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 7590176880698012387} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1127519584872800040 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2810967667379106609} + m_CullTransparentMesh: 1 +--- !u!114 &362765526694760680 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2810967667379106609} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &2969139731012847270 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6899184289734897349} + - component: {fileID: 5775259578045717352} + m_Layer: 5 + m_Name: LegendPing + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6899184289734897349 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2969139731012847270} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 908890067499602259} + - {fileID: 8083971487120669529} + m_Father: {fileID: 216369457880424682} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5775259578045717352 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2969139731012847270} + m_CullTransparentMesh: 1 +--- !u!1 &2997867828362567431 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6982534731248530243} + - component: {fileID: 5865401507776993270} + m_Layer: 5 + m_Name: PingGraph + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6982534731248530243 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2997867828362567431} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 1056662130267328870} + - {fileID: 5294687292649652416} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &5865401507776993270 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2997867828362567431} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 14a68afedbbf4568b0decc5c3fe6dfd9, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 5857741690380071345} + Points: 32 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 0, g: 1, b: 1, a: 1} + - {r: 1, g: 0.92156863, b: 0.015686275, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 3160579670834603183} + - {fileID: 9151821596118048258} + - {fileID: 2754244171796987324} + - {fileID: 362765526694760680} + - {fileID: 8382168573226188986} + runtimeMaterial: {fileID: 0} +--- !u!1 &3105901817953974653 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4652558859609211497} + - component: {fileID: 1400924857942257598} + - component: {fileID: 3160579670834603183} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4652558859609211497 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3105901817953974653} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2684192023408130911} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1400924857942257598 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3105901817953974653} + m_CullTransparentMesh: 1 +--- !u!114 &3160579670834603183 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3105901817953974653} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3205747331435468870 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 942441613928011978} + - component: {fileID: 6327993025711354549} + m_Layer: 5 + m_Name: LegendJitter + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &942441613928011978 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3205747331435468870} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 2203074428927526136} + - {fileID: 5372474103490645384} + m_Father: {fileID: 216369457880424682} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6327993025711354549 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3205747331435468870} + m_CullTransparentMesh: 1 +--- !u!1 &3640500073563500983 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1056662130267328870} + - component: {fileID: 8437258848603187620} + - component: {fileID: 4311061136952486782} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1056662130267328870 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3640500073563500983} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6982534731248530243} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8437258848603187620 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3640500073563500983} + m_CullTransparentMesh: 1 +--- !u!114 &4311061136952486782 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3640500073563500983} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &3668972322997488969 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4620809971601425299} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4620809971601425299 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3668972322997488969} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 5569695547238921462} + - {fileID: 4579137679805290761} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3867562142981469492 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2684192023408130911} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2684192023408130911 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3867562142981469492} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 2990412341526050050} + - {fileID: 4652558859609211497} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &4001027587005364764 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1469817193488350550} + m_Layer: 5 + m_Name: LegendsY + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1469817193488350550 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4001027587005364764} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 2684192023408130911} + - {fileID: 7987015295734336241} + - {fileID: 7870100887797750244} + - {fileID: 7590176880698012387} + - {fileID: 4620809971601425299} + m_Father: {fileID: 5294687292649652416} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &4037112855574815760 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5372474103490645384} + - component: {fileID: 767451476397572077} + - component: {fileID: 419353126541061517} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5372474103490645384 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4037112855574815760} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 942441613928011978} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &767451476397572077 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4037112855574815760} + m_CullTransparentMesh: 1 +--- !u!114 &419353126541061517 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4037112855574815760} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Jitter +--- !u!1 &5326465489219711421 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7889663160516621925} + - component: {fileID: 8263199366839964538} + - component: {fileID: 4067121774068232129} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7889663160516621925 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5326465489219711421} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 7870100887797750244} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8263199366839964538 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5326465489219711421} + m_CullTransparentMesh: 1 +--- !u!114 &4067121774068232129 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5326465489219711421} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5506933360427255673 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 911697406546784338} + - component: {fileID: 6011329830179878330} + - component: {fileID: 2951732881719804678} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &911697406546784338 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5506933360427255673} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 7987015295734336241} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6011329830179878330 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5506933360427255673} + m_CullTransparentMesh: 1 +--- !u!114 &2951732881719804678 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5506933360427255673} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5788823770659353615 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1768608949435380201} + - component: {fileID: 8205076076307683200} + - component: {fileID: 5857741690380071345} + m_Layer: 5 + m_Name: GraphContent + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1768608949435380201 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5788823770659353615} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5294687292649652416} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8205076076307683200 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5788823770659353615} + m_CullTransparentMesh: 1 +--- !u!114 &5857741690380071345 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5788823770659353615} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6335670163093277870 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2990412341526050050} + - component: {fileID: 8490552629736366217} + - component: {fileID: 7762443815874078549} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2990412341526050050 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6335670163093277870} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 2684192023408130911} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8490552629736366217 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6335670163093277870} + m_CullTransparentMesh: 1 +--- !u!114 &7762443815874078549 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6335670163093277870} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6425321695431688699 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7590176880698012387} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7590176880698012387 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6425321695431688699} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 6658247662285819325} + - {fileID: 1217973610716945855} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6673247198283208466 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6975151376875653975} + - component: {fileID: 1474497204505854206} + - component: {fileID: 3859801130584468475} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6975151376875653975 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6673247198283208466} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 5294687292649652416} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &1474497204505854206 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6673247198283208466} + m_CullTransparentMesh: 1 +--- !u!114 &3859801130584468475 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6673247198283208466} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: PING +--- !u!1 &6858241430425100238 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4579137679805290761} + - component: {fileID: 7750660140099561029} + - component: {fileID: 8382168573226188986} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4579137679805290761 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6858241430425100238} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4620809971601425299} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7750660140099561029 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6858241430425100238} + m_CullTransparentMesh: 1 +--- !u!114 &8382168573226188986 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6858241430425100238} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &7921722240077587436 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3705598884744263366} + - component: {fileID: 7268291769466718766} + - component: {fileID: 2754244171796987324} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3705598884744263366 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7921722240077587436} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 7870100887797750244} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7268291769466718766 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7921722240077587436} + m_CullTransparentMesh: 1 +--- !u!114 &2754244171796987324 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7921722240077587436} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &8434725759024132407 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8083971487120669529} + - component: {fileID: 2625528277965073533} + - component: {fileID: 1427747543481707907} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8083971487120669529 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8434725759024132407} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6899184289734897349} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2625528277965073533 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8434725759024132407} + m_CullTransparentMesh: 1 +--- !u!114 &1427747543481707907 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8434725759024132407} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Ping +--- !u!1 &8835429319627613495 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 908890067499602259} + - component: {fileID: 8735215443978414631} + - component: {fileID: 2493539334624522905} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &908890067499602259 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8835429319627613495} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 6899184289734897349} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8735215443978414631 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8835429319627613495} + m_CullTransparentMesh: 1 +--- !u!114 &2493539334624522905 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8835429319627613495} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab.meta new file mode 100644 index 0000000..57c4881 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: ed3b4e27086dcc64b8c6605011a321e2 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/StackedGraph.mat b/Assets/Mirror/Components/Profiling/StackedGraph.mat new file mode 100644 index 0000000..a4c4b44 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/StackedGraph.mat @@ -0,0 +1,88 @@ +%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: StackedGraph + m_Shader: {fileID: 4800000, guid: b5b24284f35f4992bcd4cc43919267d7, type: 3} + 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 + - _CategoryCount: 0 + - _ColorMask: 15 + - _Cutoff: 0.5 + - _DataStart: 0 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _MaxValue: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _Stencil: 0 + - _StencilComp: 8 + - _StencilOp: 0 + - _StencilReadMask: 255 + - _StencilWriteMask: 255 + - _UVSec: 0 + - _UseUIAlphaClip: 0 + - _Width: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/Assets/Mirror/Components/Profiling/StackedGraph.mat.meta b/Assets/Mirror/Components/Profiling/StackedGraph.mat.meta new file mode 100644 index 0000000..6c9422c --- /dev/null +++ b/Assets/Mirror/Components/Profiling/StackedGraph.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 14fba9d19cfe7f346bfb595965558722 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/StackedGraph.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/ToggleHotkey.cs b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs new file mode 100644 index 0000000..130e452 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs @@ -0,0 +1,15 @@ +using UnityEngine; +namespace Mirror +{ + public class ToggleHotkey : MonoBehaviour + { + public KeyCode Key = KeyCode.F10; + public GameObject ToToggle; + + void Update() + { + if (Input.GetKeyDown(Key)) + ToToggle.SetActive(!ToToggle.activeSelf); + } + } +} diff --git a/Assets/Mirror/Components/Profiling/ToggleHotkey.cs.meta b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs.meta new file mode 100644 index 0000000..cdcf3aa --- /dev/null +++ b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a27b133d890c41828d3b01ffa12fe440 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/ToggleHotkey.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/RemoteStatistics.cs b/Assets/Mirror/Components/RemoteStatistics.cs new file mode 100644 index 0000000..5b3ede9 --- /dev/null +++ b/Assets/Mirror/Components/RemoteStatistics.cs @@ -0,0 +1,441 @@ +// remote statistics panel from Mirror II to show connections, load, etc. +// server syncs statistics to clients if authenticated. +// +// attach this to a player. +// requires NetworkStatistics component on the Network object. +// +// Unity's OnGUI is the easiest to use solution at the moment. +// * playfab is super complex to set up +// * http servers would be nice, but still need to open ports, live refresh, etc +// +// for safety reasons, let's keep this read-only. +// at least until there's safe authentication. +using System; +using System.IO; +using UnityEngine; + +namespace Mirror +{ + // server -> client + struct Stats + { + // general + public int connections; + public double uptime; + public int configuredTickRate; + public int actualTickRate; + + // traffic + public long sentBytesPerSecond; + public long receiveBytesPerSecond; + + // cpu + public float serverTickInterval; + public double fullUpdateAvg; + public double serverEarlyAvg; + public double serverLateAvg; + public double transportEarlyAvg; + public double transportLateAvg; + + // C# boilerplate + public Stats( + // general + int connections, + double uptime, + int configuredTickRate, + int actualTickRate, + // traffic + long sentBytesPerSecond, + long receiveBytesPerSecond, + // cpu + float serverTickInterval, + double fullUpdateAvg, + double serverEarlyAvg, + double serverLateAvg, + double transportEarlyAvg, + double transportLateAvg + ) + { + // general + this.connections = connections; + this.uptime = uptime; + this.configuredTickRate = configuredTickRate; + this.actualTickRate = actualTickRate; + + // traffic + this.sentBytesPerSecond = sentBytesPerSecond; + this.receiveBytesPerSecond = receiveBytesPerSecond; + + // cpu + this.serverTickInterval = serverTickInterval; + this.fullUpdateAvg = fullUpdateAvg; + this.serverEarlyAvg = serverEarlyAvg; + this.serverLateAvg = serverLateAvg; + this.transportEarlyAvg = transportEarlyAvg; + this.transportLateAvg = transportLateAvg; + } + } + + // [RequireComponent(typeof(NetworkStatistics))] <- needs to be on Network GO, not on NI + public class RemoteStatistics : NetworkBehaviour + { + // components ("fake statics" for similar API) + protected NetworkStatistics NetworkStatistics; + + // broadcast to client. + // stats are quite huge, let's only send every few seconds via TargetRpc. + // instead of sending multiple times per second via NB.OnSerialize. + [Tooltip("Send stats every 'interval' seconds to client.")] + public float sendInterval = 1; + double lastSendTime; + + [Header("GUI")] + public bool showGui; + public KeyCode hotKey = KeyCode.BackQuote; + Rect windowRect = new Rect(0, 0, 400, 400); + + // password can't be stored in code or in Unity project. + // it would be available in clients otherwise. + // this is not perfectly secure. that's why RemoteStatistics is read-only. + [Header("Authentication")] + public string passwordFile = "remote_statistics.txt"; + protected bool serverAuthenticated; // client needs to authenticate + protected bool clientAuthenticated; // show GUI until authenticated + protected string serverPassword = null; // null means not found, auth impossible + protected string clientPassword = ""; // for GUI + + // statistics synced to client + Stats stats; + + void LoadPassword() + { + // TODO only load once, not for all players? + // let's avoid static state for now. + + // load the password + string path = Path.GetFullPath(passwordFile); + if (File.Exists(path)) + { + // don't spam the server logs for every player's loaded file + // Debug.Log($"RemoteStatistics: loading password file: {path}"); + try + { + serverPassword = File.ReadAllText(path); + } + catch (Exception exception) + { + Debug.LogWarning($"RemoteStatistics: failed to read password file: {exception}"); + } + } + else + { + Debug.LogWarning($"RemoteStatistics: password file has not been created. Authentication will be impossible. Please save the password in: {path}"); + } + } + + protected override void OnValidate() + { + base.OnValidate(); + syncMode = SyncMode.Owner; + } + + // make sure to call base function when overwriting! + // public so it can also be called from tests (and be overwritten by users) + public override void OnStartServer() + { + NetworkStatistics = NetworkManager.singleton.GetComponent(); + if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!"); + + // server needs to load the password + LoadPassword(); + } + + public override void OnStartLocalPlayer() + { + // center the window initially + windowRect.x = Screen.width / 2 - windowRect.width / 2; + windowRect.y = Screen.height / 2 - windowRect.height / 2; + } + + [TargetRpc] + void TargetRpcSync(Stats v) + { + // store stats and flag as authenticated + clientAuthenticated = true; + stats = v; + } + + [Command] + public void CmdAuthenticate(string v) + { + // was a valid password loaded on the server, + // and did the client send the correct one? + if (!string.IsNullOrWhiteSpace(serverPassword) && + serverPassword.Equals(v)) + { + serverAuthenticated = true; + Debug.Log($"RemoteStatistics: connectionId {connectionToClient.connectionId} authenticated with player {name}"); + } + } + + void UpdateServer() + { + // only sync if client has authenticated on the server + if (!serverAuthenticated) return; + + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= lastSendTime + sendInterval) + { + lastSendTime = NetworkTime.localTime; + + // target rpc to owner client + TargetRpcSync(new Stats( + // general + NetworkServer.connections.Count, + NetworkTime.time, + NetworkServer.tickRate, + NetworkServer.actualTickRate, + + // traffic + NetworkStatistics.serverSentBytesPerSecond, + NetworkStatistics.serverReceivedBytesPerSecond, + + // cpu + NetworkServer.tickInterval, + NetworkServer.fullUpdateDuration.average, + NetworkServer.earlyUpdateDuration.average, + NetworkServer.lateUpdateDuration.average, + 0, // TODO ServerTransport.earlyUpdateDuration.average, + 0 // TODO ServerTransport.lateUpdateDuration.average + )); + } + } + + void UpdateClient() + { + if (Input.GetKeyDown(hotKey)) + showGui = !showGui; + } + + void Update() + { + if (isServer) UpdateServer(); + if (isLocalPlayer) UpdateClient(); + } + + void OnGUI() + { + if (!isLocalPlayer) return; + if (!showGui) return; + + windowRect = GUILayout.Window(0, windowRect, OnWindow, "Remote Statistics"); + windowRect = Utils.KeepInScreen(windowRect); + } + + // Text: value + void GUILayout_TextAndValue(string text, string value) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(text); + GUILayout.FlexibleSpace(); + GUILayout.Label(value); + GUILayout.EndHorizontal(); + } + + // fake a progress bar via horizontal scroll bar with ratio as width + void GUILayout_ProgressBar(double ratio, int width) + { + // clamp ratio, otherwise >1 would make it extremely large + ratio = Mathd.Clamp01(ratio); + GUILayout.HorizontalScrollbar(0, (float)ratio, 0, 1, GUILayout.Width(width)); + } + + // need to specify progress bar & caption width, + // otherwise differently sized captions would always misalign the + // progress bars. + void GUILayout_TextAndProgressBar(string text, double ratio, int progressbarWidth, string caption, int captionWidth, Color captionColor) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(text); + GUILayout.FlexibleSpace(); + GUILayout_ProgressBar(ratio, progressbarWidth); + + // coloring the caption is enough. otherwise it's too much. + GUI.color = captionColor; + GUILayout.Label(caption, GUILayout.Width(captionWidth)); + GUI.color = Color.white; + + GUILayout.EndHorizontal(); + } + + void GUI_Authenticate() + { + GUILayout.BeginVertical("Box"); // start general + GUILayout.Label("Authentication"); + + // warning if insecure connection + // if (ClientTransport.IsEncrypted()) + // { + // GUILayout.Label("Connection is encrypted!"); + // } + // else + // { + GUILayout.Label("Connection is not encrypted. Use with care!"); + // } + + // input + clientPassword = GUILayout.PasswordField(clientPassword, '*'); + + // button + GUI.enabled = !string.IsNullOrWhiteSpace(clientPassword); + if (GUILayout.Button("Authenticate")) + { + CmdAuthenticate(clientPassword); + } + GUI.enabled = true; + + GUILayout.EndVertical(); // end general + } + + void GUI_General( + int connections, + double uptime, + int configuredTickRate, + int actualTickRate) + { + GUILayout.BeginVertical("Box"); // start general + GUILayout.Label("General"); + + // connections + GUILayout_TextAndValue("Connections:", $"{connections}"); + + // uptime + GUILayout_TextAndValue("Uptime:", $"{Utils.PrettySeconds(uptime)}"); // TODO + + // tick rate + // might be lower under heavy load. + // might be higher in editor if targetFrameRate can't be set. + GUI.color = actualTickRate < configuredTickRate ? Color.red : Color.green; + GUILayout_TextAndValue("Tick Rate:", $"{actualTickRate} Hz / {configuredTickRate} Hz"); + GUI.color = Color.white; + + GUILayout.EndVertical(); // end general + } + + void GUI_Traffic( + long serverSentBytesPerSecond, + long serverReceivedBytesPerSecond) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("Network"); + + GUILayout_TextAndValue("Outgoing:", $"{Utils.PrettyBytes(serverSentBytesPerSecond) }/s"); + GUILayout_TextAndValue("Incoming:", $"{Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s"); + + GUILayout.EndVertical(); + } + + void GUI_Cpu( + float serverTickInterval, + double fullUpdateAvg, + double serverEarlyAvg, + double serverLateAvg, + double transportEarlyAvg, + double transportLateAvg) + { + const int barWidth = 120; + const int captionWidth = 90; + + GUILayout.BeginVertical("Box"); + GUILayout.Label("CPU"); + + // unity update + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + double fullRatio = fullUpdateAvg / serverTickInterval; + GUILayout_TextAndProgressBar( + "World Update Avg:", + fullRatio, + barWidth, $"{fullUpdateAvg * 1000:F1} ms", + captionWidth, + fullRatio <= 0.9 ? Color.green : Color.red); + + // server update + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + double serverRatio = (serverEarlyAvg + serverLateAvg) / serverTickInterval; + GUILayout_TextAndProgressBar( + "Server Update Avg:", + serverRatio, + barWidth, $"{serverEarlyAvg * 1000:F1} + {serverLateAvg * 1000:F1} ms", + captionWidth, + serverRatio <= 0.9 ? Color.green : Color.red); + + // transport: early + late update milliseconds. + // for threaded transport, this is the thread's update time. + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + // double transportRatio = (transportEarlyAvg + transportLateAvg) / serverTickInterval; + // GUILayout_TextAndProgressBar( + // "Transport Avg:", + // transportRatio, + // barWidth, + // $"{transportEarlyAvg * 1000:F1} + {transportLateAvg * 1000:F1} ms", + // captionWidth, + // transportRatio <= 0.9 ? Color.green : Color.red); + + GUILayout.EndVertical(); + } + + void GUI_Notice() + { + // for security reasons, let's keep this read-only for now. + + // single line keeps input & visuals simple + // GUILayout.BeginVertical("Box"); + // GUILayout.Label("Global Notice"); + // notice = GUILayout.TextField(notice); + // if (GUILayout.Button("Send")) + // { + // // TODO + // } + // GUILayout.EndVertical(); + } + + void OnWindow(int windowID) + { + if (!clientAuthenticated) + { + GUI_Authenticate(); + } + else + { + GUI_General( + stats.connections, + stats.uptime, + stats.configuredTickRate, + stats.actualTickRate + ); + + GUI_Traffic( + stats.sentBytesPerSecond, + stats.receiveBytesPerSecond + ); + + GUI_Cpu( + stats.serverTickInterval, + stats.fullUpdateAvg, + stats.serverEarlyAvg, + stats.serverLateAvg, + stats.transportEarlyAvg, + stats.transportLateAvg + ); + + GUI_Notice(); + } + + // dragable window in any case + GUI.DragWindow(new Rect(0, 0, 10000, 10000)); + } + } +} diff --git a/Assets/Mirror/Components/RemoteStatistics.cs.meta b/Assets/Mirror/Components/RemoteStatistics.cs.meta new file mode 100644 index 0000000..8754194 --- /dev/null +++ b/Assets/Mirror/Components/RemoteStatistics.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ba360e4ff6b44fc6898f56322b90c6c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/RemoteStatistics.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime.meta b/Assets/Mirror/Core.meta similarity index 100% rename from Assets/Mirror/Runtime.meta rename to Assets/Mirror/Core.meta diff --git a/Assets/Mirror/Core/AssemblyInfo.cs b/Assets/Mirror/Core/AssemblyInfo.cs new file mode 100644 index 0000000..a9c6442 --- /dev/null +++ b/Assets/Mirror/Core/AssemblyInfo.cs @@ -0,0 +1,13 @@ +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")] +[assembly: InternalsVisibleTo("Mirror.Components")] diff --git a/Assets/Mirror/Core/AssemblyInfo.cs.meta b/Assets/Mirror/Core/AssemblyInfo.cs.meta new file mode 100644 index 0000000..879e6d0 --- /dev/null +++ b/Assets/Mirror/Core/AssemblyInfo.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e28d5f410e25b42e6a76a2ffc10e4675 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Attributes.cs b/Assets/Mirror/Core/Attributes.cs new file mode 100644 index 0000000..53114b4 --- /dev/null +++ b/Assets/Mirror/Core/Attributes.cs @@ -0,0 +1,101 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + /// + /// SyncVars are used to automatically synchronize a variable between the server and all clients. The direction of synchronization depends on the Sync Direction property, ServerToClient by default. + /// + /// When Sync Direction is equal to ServerToClient, the value should be changed on the server side and synchronized to all clients. + /// Otherwise, the value should be changed on the client side and synchronized to server and other clients. + /// + /// Hook parameter allows you to define a method to be invoked when gets an value update. Notice that the hook method will not be called on the change side. + /// + [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; + } + + /// + /// Only an active server will run this method. + /// Prints a warning if a client or in-active server tries to execute this method. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ServerAttribute : Attribute {} + + /// + /// Only an active server will run this method. + /// No warning is thrown. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ServerCallbackAttribute : Attribute {} + + /// + /// Only an active client will run this method. + /// Prints a warning if the server or in-active client tries to execute this method. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ClientAttribute : Attribute {} + + /// + /// Only an active client will run 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 {} + + /// + /// Used to make a field readonly in the inspector + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class ReadOnlyAttribute : PropertyAttribute {} + + /// + /// When defining multiple Readers/Writers for the same type, indicate which one Weaver must use. + /// + [AttributeUsage(AttributeTargets.Method)] + public class WeaverPriorityAttribute : Attribute {} +} diff --git a/Assets/Mirror/Core/Attributes.cs.meta b/Assets/Mirror/Core/Attributes.cs.meta new file mode 100644 index 0000000..09a8b6f --- /dev/null +++ b/Assets/Mirror/Core/Attributes.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c04c722ee2ffd49c8a56ab33667b10b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Attributes.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Batching.meta b/Assets/Mirror/Core/Batching.meta similarity index 100% rename from Assets/Mirror/Runtime/Batching.meta rename to Assets/Mirror/Core/Batching.meta diff --git a/Assets/Mirror/Core/Batching/Batcher.cs b/Assets/Mirror/Core/Batching/Batcher.cs new file mode 100644 index 0000000..6f7cad9 --- /dev/null +++ b/Assets/Mirror/Core/Batching/Batcher.cs @@ -0,0 +1,206 @@ +// 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. each batch has one. + public const int TimestampSize = sizeof(double); + + // Message header size. each message has one. + public static int MessageHeaderSize(int messageSize) => + Compression.VarUIntSize((ulong)messageSize); + + // maximum overhead for a single message. + // useful for the outside to calculate max message sizes. + public static int MaxMessageOverhead(int messageSize) => + TimestampSize + MessageHeaderSize(messageSize); + + // 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. + readonly Queue batches = new Queue(); + + // current batch in progress. + // we also store the timestamp to ensure we don't add a message from another frame, + // as this would introduce subtle jitter! + // + // for example: + // - a batch is started at t=1, another message is added at t=2 and then it's flushed + // - NetworkTransform uses remoteTimestamp which is t=1 + // - snapshot interpolation would off by one (or multiple) frames! + NetworkWriterPooled batch; + double batchTimestamp; + + 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) + { + // safety: message timestamp is only written once. + // make sure all messages in this batch are from the same timestamp. + // otherwise it could silently introduce jitter. + // + // this happened before: + // - NetworkEarlyUpdate @ t=1 processes transport messages + // - a handler replies by sending a message + // - a new batch is started @ t=1, timestamp is encoded + // - NetworkLateUpdate @ t=2 decides it's time to broadcast + // - NetworkTransform sends @ t=2 + // - we add to the above batch which already encoded t=1 + // - Client receives the batch which timestamp t=1 + // - NetworkTransform uses remoteTime for interpolation + // remoteTime is the batch timestamp which is t=1 + // - the NetworkTransform message is actually t=2 + // => smooth interpolation would be impossible! + // NT thinks the position was @ t=1 but actually it was @ t=2 ! + // + // the solution: if timestamp changed, enqueue the existing batch + if (batch != null && batchTimestamp != timeStamp) + { + batches.Enqueue(batch); + batch = null; + batchTimestamp = 0; + } + + // predict the needed size, which is varint(size) + content + int headerSize = Compression.VarUIntSize((ulong)message.Count); + int neededSize = headerSize + message.Count; + + // 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 + neededSize > threshold) + { + batches.Enqueue(batch); + batch = null; + batchTimestamp = 0; + } + + // 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); + + // remember the encoded timestamp, see safety check below. + batchTimestamp = 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. + // + // include size prefix as varint! + // -> fixes NetworkMessage serialization mismatch corrupting the + // next message in a batch. + // -> a _lot_ of time was wasted debugging corrupt batches. + // no easy way to figure out which NetworkMessage has a mismatch. + // -> this is worth everyone's sanity. + // -> varint means we prefix with 1 byte most of the time. + // -> the same issue in NetworkIdentity was why Mirror started! + Compression.CompressVarUInt(batch, (ulong)message.Count); + 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; + } + + // return all batches to the pool for cleanup + public void Clear() + { + // return batch in progress + if (batch != null) + { + NetworkWriterPool.Return(batch); + batch = null; + batchTimestamp = 0; + } + + // return all queued batches + foreach (NetworkWriterPooled queued in batches) + NetworkWriterPool.Return(queued); + + batches.Clear(); + } + } +} diff --git a/Assets/Mirror/Core/Batching/Batcher.cs.meta b/Assets/Mirror/Core/Batching/Batcher.cs.meta new file mode 100644 index 0000000..2449b26 --- /dev/null +++ b/Assets/Mirror/Core/Batching/Batcher.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 0afaaa611a2142d48a07bdd03b68b2b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Batching/Batcher.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Batching/Unbatcher.cs b/Assets/Mirror/Core/Batching/Unbatcher.cs new file mode 100644 index 0000000..6b2c405 --- /dev/null +++ b/Assets/Mirror/Core/Batching/Unbatcher.cs @@ -0,0 +1,129 @@ +// 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. + readonly Queue batches = new Queue(); + + public int BatchesCount => batches.Count; + + // NetworkReader is only created once, + // then pointed to the first batch. + readonly 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.TimestampSize) + 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) + // message ArraySegment is only valid until the next call. + // timestamp is the REMOTE time when the batch was created remotely. + public bool GetNextMessage(out ArraySegment message, out double remoteTimeStamp) + { + message = default; + remoteTimeStamp = 0; + + // 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) + return false; + + // was our reader pointed to anything yet? + if (reader.Capacity == 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 return false; + } + + // use the current batch's remote timestamp + // AFTER potentially moving to the next batch ABOVE! + remoteTimeStamp = readerRemoteTimeStamp; + + // enough data to read the size prefix? + if (reader.Remaining == 0) + return false; + + // read the size prefix as varint + // see Batcher.AddMessage comments for explanation. + int size = (int)Compression.DecompressVarUInt(reader); + + // validate size prefix, in case attackers send malicious data + if (reader.Remaining < size) + return false; + + // return the message of size + message = reader.ReadBytesSegment(size); + return true; + } + } +} diff --git a/Assets/Mirror/Core/Batching/Unbatcher.cs.meta b/Assets/Mirror/Core/Batching/Unbatcher.cs.meta new file mode 100644 index 0000000..a2d8dfa --- /dev/null +++ b/Assets/Mirror/Core/Batching/Unbatcher.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 328562d71e1c45c58581b958845aa7a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Batching/Unbatcher.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/ConnectionQuality.cs b/Assets/Mirror/Core/ConnectionQuality.cs new file mode 100644 index 0000000..9989f87 --- /dev/null +++ b/Assets/Mirror/Core/ConnectionQuality.cs @@ -0,0 +1,74 @@ +// standalone, Unity-independent connection-quality algorithm & enum. +// don't need to use this directly, it's built into Mirror's NetworkClient. +using UnityEngine; + +namespace Mirror +{ + public enum ConnectionQuality : byte + { + ESTIMATING, // still estimating + POOR, // unplayable + FAIR, // very noticeable latency, not very enjoyable anymore + GOOD, // very playable for everyone but high level competitors + EXCELLENT // ideal experience for high level competitors + } + + public enum ConnectionQualityMethod : byte + { + Simple, // simple estimation based on rtt and jitter + Pragmatic // based on snapshot interpolation adjustment + } + + // provide different heuristics for users to choose from. + // simple heuristics to get started. + // this will be iterated on over time based on user feedback. + public static class ConnectionQualityHeuristics + { + // convenience extension to color code Connection Quality + public static Color ColorCode(this ConnectionQuality quality) + { + switch (quality) + { + case ConnectionQuality.POOR: return Color.red; + case ConnectionQuality.FAIR: return new Color(1.0f, 0.647f, 0.0f); + case ConnectionQuality.GOOD: return Color.yellow; + case ConnectionQuality.EXCELLENT: return Color.green; + default: return Color.gray; // ESTIMATING + } + } + + // straight forward estimation + // rtt: average round trip time in seconds. + // jitter: average latency variance. + public static ConnectionQuality Simple(double rtt, double jitter) + { + if (rtt <= 0.100 && jitter <= 0.10) return ConnectionQuality.EXCELLENT; + if (rtt <= 0.200 && jitter <= 0.20) return ConnectionQuality.GOOD; + if (rtt <= 0.400 && jitter <= 0.50) return ConnectionQuality.FAIR; + return ConnectionQuality.POOR; + } + + // snapshot interpolation based estimation. + // snap. interp. adjusts buffer time based on connection quality. + // based on this, we can measure how far away we are from the ideal. + // the returned quality will always directly correlate with gameplay. + // => requires SnapshotInterpolation dynamicAdjustment to be enabled! + public static ConnectionQuality Pragmatic(double targetBufferTime, double currentBufferTime) + { + // buffer time is set by the game developer. + // estimating in multiples is a great way to be game independent. + // for example, a fast paced shooter and a slow paced RTS will both + // have poor connection if the multiplier is >10. + double multiplier = currentBufferTime / targetBufferTime; + + // empirically measured with Tanks demo + LatencySimulation. + // it's not obvious to estimate on paper. + if (multiplier <= 1.15) return ConnectionQuality.EXCELLENT; + if (multiplier <= 1.25) return ConnectionQuality.GOOD; + if (multiplier <= 1.50) return ConnectionQuality.FAIR; + + // anything else is poor + return ConnectionQuality.POOR; + } + } +} diff --git a/Assets/Mirror/Core/ConnectionQuality.cs.meta b/Assets/Mirror/Core/ConnectionQuality.cs.meta new file mode 100644 index 0000000..50eddb3 --- /dev/null +++ b/Assets/Mirror/Core/ConnectionQuality.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ff663b880e33e4606b545c8b497041c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/ConnectionQuality.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/HostMode.cs b/Assets/Mirror/Core/HostMode.cs new file mode 100644 index 0000000..3054b3c --- /dev/null +++ b/Assets/Mirror/Core/HostMode.cs @@ -0,0 +1,44 @@ +// host mode related helper functions. +// usually they set up both server & client. +// it's cleaner to keep them in one place, instead of only in server / client. +using System; + +namespace Mirror +{ + public static class HostMode + { + // keep the local connections setup in one function. + // makes host setup easier to follow. + internal static void SetupConnections() + { + // create local connections pair, both are connected + Utils.CreateLocalConnections( + out LocalConnectionToClient connectionToClient, + out LocalConnectionToServer connectionToServer); + + // set client connection + NetworkClient.connection = connectionToServer; + + // set server connection + NetworkServer.SetLocalConnection(connectionToClient); + } + + // call OnConnected on server & client. + // public because NetworkClient.ConnectLocalServer was public before too. + public static void InvokeOnConnected() + { + // 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)NetworkClient.connection).QueueConnectedEvent(); + } + } +} diff --git a/Assets/Mirror/Core/HostMode.cs.meta b/Assets/Mirror/Core/HostMode.cs.meta new file mode 100644 index 0000000..6e10676 --- /dev/null +++ b/Assets/Mirror/Core/HostMode.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: d27175a08d5341fc97645b49ee533d5a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/HostMode.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/InterestManagement.cs b/Assets/Mirror/Core/InterestManagement.cs new file mode 100644 index 0000000..88ead0f --- /dev/null +++ b/Assets/Mirror/Core/InterestManagement.cs @@ -0,0 +1,146 @@ +// 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 : InterestManagementBase + { + // allocate newObservers helper HashSet + readonly HashSet newObservers = + new HashSet(); + + // 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); + } + } + + public override void Rebuild(NetworkIdentity identity, bool initialize) + { + // clear newObservers hashset before using it + newObservers.Clear(); + + // not force hidden? + if (identity.visibility != Visibility.ForceHidden) + { + 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 {gameObject} {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(NetworkServer.localConnection)) + { + SetHostVisibility(identity, false); + } + } + } + } +} diff --git a/Assets/Mirror/Core/InterestManagement.cs.meta b/Assets/Mirror/Core/InterestManagement.cs.meta new file mode 100644 index 0000000..a13677b --- /dev/null +++ b/Assets/Mirror/Core/InterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 41d809934003479f97e992eebb7ed6af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/InterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/InterestManagementBase.cs b/Assets/Mirror/Core/InterestManagementBase.cs new file mode 100644 index 0000000..1a3cc1b --- /dev/null +++ b/Assets/Mirror/Core/InterestManagementBase.cs @@ -0,0 +1,103 @@ +// interest management component for custom solutions like +// distance based, spatial hashing, raycast based, etc. +// low level base class allows for low level spatial hashing etc., which is 3-5x faster. +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public abstract class InterestManagementBase : MonoBehaviour + { + // initialize NetworkServer/Client .aoi. + // previously we did this in Awake(), but that's called for disabled + // components too. if we do it OnEnable(), then it's not set for + // disabled components. + protected virtual void OnEnable() + { + // do not check if == null or error if already set. + // users may enabled/disable components randomly, + // causing this to be called multiple times. + NetworkServer.aoi = this; + NetworkClient.aoi = this; + } + + [ServerCallback] + public virtual void ResetState() {} + + // 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); + + + // 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; + + // reason to also set lights/audio/terrain/etc.: + // Let's say players were holding a flashlight or magic wand with a particle effect. Without this, + // host client would see the light / particles for all players in all subscenes because we don't + // hide lights and particles. Host client would hear ALL audio sources in all subscenes too. We + // hide the renderers, which covers basic objects and UI, but we don't hide anything else that may + // be a child of a networked object. Same idea for cars with lights and sounds in other subscenes + // that host client shouldn't see or hear...host client wouldn't see the car itself, but sees the + // lights moving around and hears all of their engines / horns / etc. + foreach (Light light in identity.GetComponentsInChildren()) + light.enabled = visible; + + foreach (AudioSource audio in identity.GetComponentsInChildren()) + audio.enabled = visible; + + foreach (Terrain terrain in identity.GetComponentsInChildren()) + { + terrain.drawHeightmap = visible; + terrain.drawTreesAndFoliage = visible; + } + + foreach (ParticleSystem particle in identity.GetComponentsInChildren()) + { + ParticleSystem.EmissionModule emission = particle.emission; + emission.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) {} + + public abstract void Rebuild(NetworkIdentity identity, bool initialize); + + /// Adds the specified connection to the observers of identity + protected void AddObserver(NetworkConnectionToClient connection, NetworkIdentity identity) + { + connection.AddToObserving(identity); + identity.observers.Add(connection.connectionId, connection); + } + + /// Removes the specified connection from the observers of identity + protected void RemoveObserver(NetworkConnectionToClient connection, NetworkIdentity identity) + { + connection.RemoveFromObserving(identity, false); + identity.observers.Remove(connection.connectionId); + } + } +} diff --git a/Assets/Mirror/Core/InterestManagementBase.cs.meta b/Assets/Mirror/Core/InterestManagementBase.cs.meta new file mode 100644 index 0000000..257afc4 --- /dev/null +++ b/Assets/Mirror/Core/InterestManagementBase.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 18bd2ffe65a444f3b13d59bdac7f2228 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/InterestManagementBase.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation.meta b/Assets/Mirror/Core/LagCompensation.meta new file mode 100644 index 0000000..3c2b3f9 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d2656015ded44e83a24f4c4776bafd40 +timeCreated: 1687920405 diff --git a/Assets/Mirror/Core/LagCompensation/Capture.cs b/Assets/Mirror/Core/LagCompensation/Capture.cs new file mode 100644 index 0000000..e4fdabe --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/Capture.cs @@ -0,0 +1,13 @@ +namespace Mirror +{ + public interface Capture + { + // server timestamp at time of capture. + double timestamp { get; set; } + + // optional gizmo drawing for visual debugging. + // history is only known on the server, which usually doesn't render. + // showing Gizmos in the Editor is enough. + void DrawGizmo(); + } +} diff --git a/Assets/Mirror/Core/LagCompensation/Capture.cs.meta b/Assets/Mirror/Core/LagCompensation/Capture.cs.meta new file mode 100644 index 0000000..a9cc6cc --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/Capture.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 347e831952e943a49095cadd39a5aeb2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/Capture.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs new file mode 100644 index 0000000..29ebc2e --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs @@ -0,0 +1,139 @@ +// HistoryBounds keeps a bounding box of all the object's bounds in the past N seconds. +// useful to decide which objects to rollback, instead of rolling back all of them. +// https://www.youtube.com/watch?v=zrIY0eIyqmI (37:00) +// standalone C# implementation to be engine (and language) agnostic. + +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // FakeByte: gather bounds in smaller buckets. + // for example, bucket(t0,t1,t2), bucket(t3,t4,t5), ... + // instead of removing old bounds t0, t1, ... + // we remove a whole bucket every 3 times: bucket(t0,t1,t2) + // and when building total bounds, we encapsulate a few larger buckets + // instead of many smaller bounds. + // + // => a bucket is encapsulate(bounds0, bounds1, bounds2) so we don't + // need a custom struct, simply reuse bounds but remember that each + // entry includes N timestamps. + // + // => note that simply reducing capture interval is _not_ the same. + // we want to capture in detail in case players run in zig-zag. + // but still grow larger buckets internally. + public class HistoryBounds + { + // mischa: use MinMaxBounds to avoid Unity Bounds.Encapsulate conversions. + readonly int boundsPerBucket; + readonly Queue fullBuckets; + + // full bucket limit. older ones will be removed. + readonly int bucketLimit; + + // bucket in progress, contains 0..boundsPerBucket bounds encapsulated. + MinMaxBounds? currentBucket; + int currentBucketSize; + + // amount of total bounds, including bounds in full buckets + current + public int boundsCount { get; private set; } + + // total bounds encapsulating all of the bounds history. + // totalMinMax is used for internal calculations. + // public total is used for Unity representation. + MinMaxBounds totalMinMax; + public Bounds total + { + get + { + Bounds bounds = new Bounds(); + bounds.SetMinMax(totalMinMax.min, totalMinMax.max); + return bounds; + } + } + + public HistoryBounds(int boundsLimit, int boundsPerBucket) + { + // bucketLimit via '/' cuts off remainder. + // that's what we want, since we always have a 'currentBucket'. + this.boundsPerBucket = boundsPerBucket; + this.bucketLimit = (boundsLimit / boundsPerBucket); + + // initialize queue with maximum capacity to avoid runtime resizing + // capacity +1 because it makes the code easier if we insert first, and then remove. + fullBuckets = new Queue(bucketLimit + 1); + } + + // insert new bounds into history. calculates new total bounds. + // Queue.Dequeue() always has the oldest bounds. + public void Insert(Bounds bounds) + { + // convert to MinMax representation for faster .Encapsulate() + MinMaxBounds minmax = new MinMaxBounds + { + min = bounds.min, + max = bounds.max + }; + + // initialize 'total' if not initialized yet. + // we don't want to call (0,0).Encapsulate(bounds). + if (boundsCount == 0) + { + totalMinMax = minmax; + } + + // add to current bucket: + // either initialize new one, or encapsulate into existing one + if (currentBucket == null) + { + currentBucket = minmax; + } + else + { + currentBucket.Value.Encapsulate(minmax); + } + + // current bucket has one more bounds. + // total bounds increased as well. + currentBucketSize += 1; + boundsCount += 1; + + // always encapsulate into total immediately. + // this is free. + totalMinMax.Encapsulate(minmax); + + // current bucket full? + if (currentBucketSize == boundsPerBucket) + { + // move it to full buckets + fullBuckets.Enqueue(currentBucket.Value); + currentBucket = null; + currentBucketSize = 0; + + // full bucket capacity reached? + if (fullBuckets.Count > bucketLimit) + { + // remove oldest bucket + fullBuckets.Dequeue(); + boundsCount -= boundsPerBucket; + + // recompute total bounds + // instead of iterating N buckets, we iterate N / boundsPerBucket buckets. + // TODO technically we could reuse 'currentBucket' before clearing instead of encapsulating again + totalMinMax = minmax; + foreach (MinMaxBounds bucket in fullBuckets) + totalMinMax.Encapsulate(bucket); + } + } + } + + public void Reset() + { + fullBuckets.Clear(); + currentBucket = null; + currentBucketSize = 0; + boundsCount = 0; + totalMinMax = new MinMaxBounds(); + } + } +} diff --git a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta new file mode 100644 index 0000000..290df33 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ca9ea58b98a34f73801b162cd5de724e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/HistoryBounds.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensation.cs b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs new file mode 100644 index 0000000..ae37a3f --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs @@ -0,0 +1,144 @@ +// standalone lag compensation algorithm +// based on the Valve Networking Model: +// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking +using System.Collections.Generic; + +namespace Mirror +{ + public static class LagCompensation + { + // history is of . + // Queue allows for fast 'remove first' and 'append last'. + // + // make sure to always insert in order. + // inserting out of order like [1,2,4,3] would cause issues. + // can't safeguard this because Queue doesn't have .Last access. + public static void Insert( + Queue> history, + int historyLimit, + double timestamp, + T capture) + where T : Capture + { + // make space according to history limit. + // do this before inserting, to avoid resizing past capacity. + if (history.Count >= historyLimit) + history.Dequeue(); + + // insert + history.Enqueue(new KeyValuePair(timestamp, capture)); + } + + // get the two snapshots closest to a given timestamp. + // those can be used to interpolate the exact snapshot at that time. + // if timestamp is newer than the newest history entry, then we extrapolate. + // 't' will be between 1 and 2, before is second last, after is last. + // callers should Lerp(before, after, t=1.5) to extrapolate the hit. + // see comments below for extrapolation. + public static bool Sample( + Queue> history, + double timestamp, // current server time + double interval, // capture interval + out T before, + out T after, + out double t) // interpolation factor + where T : Capture + { + before = default; + after = default; + t = 0; + + // can't sample an empty history + // interpolation needs at least one entry. + // extrapolation needs at least two entries. + // can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0. + if(history.Count < 2) { + return false; + } + + // older than oldest + if (timestamp < history.Peek().Key) { + return false; + } + + // iterate through the history + // TODO faster version: guess start index by how many 'intervals' we are behind. + // search around that area. + // should be O(1) most of the time, unless sampling was off. + KeyValuePair prev = new KeyValuePair(); + KeyValuePair prevPrev = new KeyValuePair(); + foreach(KeyValuePair entry in history) { + // exact match? + if (timestamp == entry.Key) { + before = entry.Value; + after = entry.Value; + t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp); + return true; + } + + // did we check beyond timestamp? then return the previous two. + if (entry.Key > timestamp) { + before = prev.Value; + after = entry.Value; + t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp); + return true; + } + + // remember the last two for extrapolation. + // Queue doesn't have access to .Last. + prevPrev = prev; + prev = entry; + } + + // newer than newest: extrapolate up to one interval. + // let's say we capture every 100 ms: + // 100, 200, 300, 400 + // and the server is at 499 + // if a client sends CmdFire at time 480, then there's no history entry. + // => adding the current entry every time would be too expensive. + // worst case we would capture at 401, 402, 403, 404, ... 100 times + // => not extrapolating isn't great. low latency clients would be + // punished by missing their targets since no entry at 'time' was found. + // => extrapolation is the best solution. make sure this works as + // expected and within limits. + if (prev.Key < timestamp && timestamp <= prev.Key + interval) { + // return the last two valid snapshots. + // can't just return (after, after) because we can't extrapolate + // if their distance is 0. + before = prevPrev.Value; + after = prev.Value; + + // InverseLerp will give [after, after+interval]. + // but we return [before, after, t]. + // so add +1 for the distance from before->after + t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp); + return true; + } + + return false; + } + + // never trust the client. + // we estimate when a message was sent. + // don't trust the client to tell us the time. + // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking + // Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation + // => lag compensation demo estimation is off by only ~6ms + public static double EstimateTime(double serverTime, double rtt, double bufferTime) + { + // packet latency is one trip from client to server, so rtt / 2 + // client view interpolation is the snapshot interpolation buffer time + double latency = rtt / 2; + return serverTime - latency - bufferTime; + } + + // convenience function to draw all history gizmos. + // this should be called from OnDrawGizmos. + public static void DrawGizmos(Queue> history) + where T : Capture + { + foreach (KeyValuePair entry in history) + entry.Value.DrawGizmo(); + } + } +} diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta new file mode 100644 index 0000000..c697593 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ad53cc7d12144d0ba3a8b0a4515e5d17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/LagCompensation.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs new file mode 100644 index 0000000..a7ec944 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs @@ -0,0 +1,19 @@ +// snapshot interpolation settings struct. +// can easily be exposed in Unity inspectors. +using System; +using UnityEngine; + +namespace Mirror +{ + // class so we can define defaults easily + [Serializable] + public class LagCompensationSettings + { + [Header("Buffering")] + [Tooltip("Keep this many past snapshots in the buffer. The larger this is, the further we can rewind into the past.\nMaximum rewind time := historyAmount * captureInterval")] + public int historyLimit = 6; + + [Tooltip("Capture state every 'captureInterval' seconds. Larger values will space out the captures more, which gives a longer history but with possible gaps inbetween.\nSmaller values will have fewer gaps, with shorter history.")] + public float captureInterval = 0.100f; // 100 ms + } +} diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs.meta b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs.meta new file mode 100644 index 0000000..d57904c --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fa80bec245f94bf8a28ec78777992a1c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs new file mode 100644 index 0000000..b1b4874 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs @@ -0,0 +1,73 @@ +// Unity's Bounds struct is represented as (center, extents). +// HistoryBounds make heavy use of .Encapsulate(), which has to convert +// Unity's (center, extents) to (min, max) every time, and then convert back. +// +// It's faster to use a (min, max) representation directly instead. +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public struct MinMaxBounds: IEquatable + { + public Vector3 min; + public Vector3 max; + + // encapsulate a single point + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Encapsulate(Vector3 point) + { + min = Vector3.Min(this.min, point); + max = Vector3.Max(this.max, point); + } + + // encapsulate another bounds + public void Encapsulate(MinMaxBounds bounds) + { + Encapsulate(bounds.min); + Encapsulate(bounds.max); + } + + // convenience comparison with Unity's bounds, for unit tests etc. + public static bool operator ==(MinMaxBounds lhs, Bounds rhs) => + lhs.min == rhs.min && + lhs.max == rhs.max; + + public static bool operator !=(MinMaxBounds lhs, Bounds rhs) => + !(lhs == rhs); + + public override bool Equals(object obj) => + obj is MinMaxBounds other && + min == other.min && + max == other.max; + + public bool Equals(MinMaxBounds other) => + min.Equals(other.min) && max.Equals(other.max); + + public bool Equals(Bounds other) => + min.Equals(other.min) && max.Equals(other.max); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + public override int GetHashCode() => HashCode.Combine(min, max); +#else + public override int GetHashCode() + { + // return HashCode.Combine(min, max); without using .Combine for older Unity versions + unchecked + { + int hash = 17; + hash = hash * 23 + min.GetHashCode(); + hash = hash * 23 + max.GetHashCode(); + return hash; + } + } +#endif + + // tostring + public override string ToString() => $"({min}, {max})"; + } +} diff --git a/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta new file mode 100644 index 0000000..04b5ed9 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4372b1e1a1cc4c669cc7bf0925f59d29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LocalConnectionToClient.cs b/Assets/Mirror/Core/LocalConnectionToClient.cs new file mode 100644 index 0000000..3ceba29 --- /dev/null +++ b/Assets/Mirror/Core/LocalConnectionToClient.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; + +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; + + // packet queue + internal readonly Queue queue = new Queue(); + + public LocalConnectionToClient() : base(LocalConnectionId) {} + + internal override void Send(ArraySegment segment, int channelId = Channels.Reliable) + { + // instead of invoking it directly, we enqueue and process next update. + // this way we can simulate a similar call flow as with remote clients. + // the closer we get to simulating host as remote, the better! + // both directions do this, so [Command] and [Rpc] behave the same way. + + //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; + + // don't ping host client in host mode + protected override void UpdatePing() {} + + internal override void Update() + { + base.Update(); + + // 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)) + { + NetworkServer.OnTransportData(connectionId, batchWriter.ToArraySegment(), Channels.Reliable); + } + } + + NetworkWriterPool.Return(writer); + } + } + + 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/Core/LocalConnectionToClient.cs.meta b/Assets/Mirror/Core/LocalConnectionToClient.cs.meta new file mode 100644 index 0000000..e910e0d --- /dev/null +++ b/Assets/Mirror/Core/LocalConnectionToClient.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a88758df7db2043d6a9d926e0b6d4191 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LocalConnectionToClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs b/Assets/Mirror/Core/LocalConnectionToServer.cs similarity index 82% rename from Assets/Mirror/Runtime/LocalConnectionToServer.cs rename to Assets/Mirror/Core/LocalConnectionToServer.cs index 378ffdb..15669c7 100644 --- a/Assets/Mirror/Runtime/LocalConnectionToServer.cs +++ b/Assets/Mirror/Core/LocalConnectionToServer.cs @@ -13,8 +13,6 @@ public class LocalConnectionToServer : NetworkConnectionToServer // 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; @@ -30,22 +28,15 @@ internal override void Send(ArraySegment segment, int channelId = Channels return; } - // OnTransportData assumes batching. - // so let's make a batch with proper timestamp prefix. - Batcher batcher = GetBatchForChannelId(channelId); - batcher.AddMessage(segment, NetworkTime.localTime); + // instead of invoking it directly, we enqueue and process next update. + // this way we can simulate a similar call flow as with remote clients. + // the closer we get to simulating host as remote, the better! + // both directions do this, so [Command] and [Rpc] behave the same way. - // 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."); - } + //Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}"); + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.WriteBytes(segment.Array, segment.Offset, segment.Count); + connectionToClient.queue.Enqueue(writer); } internal override void Update() diff --git a/Assets/Mirror/Core/LocalConnectionToServer.cs.meta b/Assets/Mirror/Core/LocalConnectionToServer.cs.meta new file mode 100644 index 0000000..4a1ee42 --- /dev/null +++ b/Assets/Mirror/Core/LocalConnectionToServer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: cdfff390c3504158a269e8b8662e2a40 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LocalConnectionToServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Messages.cs b/Assets/Mirror/Core/Messages.cs new file mode 100644 index 0000000..899b0ea --- /dev/null +++ b/Assets/Mirror/Core/Messages.cs @@ -0,0 +1,186 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + // need to send time every sendInterval. + // batching automatically includes remoteTimestamp. + // all we need to do is ensure that an empty message is sent. + // and react to it. + // => we don't want to insert a snapshot on every batch. + // => do it exactly every sendInterval on every TimeSnapshotMessage. + public struct TimeSnapshotMessage : NetworkMessage {} + + 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 ushort 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 ushort functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + [Flags] public enum SpawnFlags : byte + { + None = 0, + isOwner = 1 << 0, + isLocalPlayer = 1 << 1 + } + + public struct SpawnMessage : NetworkMessage + { + // netId of new or existing object + public uint netId; + // isOwner and isLocalPlayer are merged into one byte via bitwise op + public SpawnFlags spawnFlags; + public ulong sceneId; + // If sceneId != 0 then it is used instead of assetId + public uint 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; + + // Backwards compatibility after implementing spawnFlags + public bool isOwner + { + get => spawnFlags.HasFlag(SpawnFlags.isOwner); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isOwner + : spawnFlags & ~SpawnFlags.isOwner; + } + + // Backwards compatibility after implementing spawnFlags + public bool isLocalPlayer + { + get => spawnFlags.HasFlag(SpawnFlags.isLocalPlayer); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isLocalPlayer + : spawnFlags & ~SpawnFlags.isLocalPlayer; + } + } + + public struct ChangeOwnerMessage : NetworkMessage + { + public uint netId; + // isOwner and isLocalPlayer are merged into one byte via bitwise op + public SpawnFlags spawnFlags; + + // Backwards compatibility after implementing spawnFlags + public bool isOwner + { + get => spawnFlags.HasFlag(SpawnFlags.isOwner); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isOwner + : spawnFlags & ~SpawnFlags.isOwner; + } + + // Backwards compatibility after implementing spawnFlags + public bool isLocalPlayer + { + get => spawnFlags.HasFlag(SpawnFlags.isLocalPlayer); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isLocalPlayer + : spawnFlags & ~SpawnFlags.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; + } + + // whoever wants to measure rtt, sends this to the other end. + public struct NetworkPingMessage : NetworkMessage + { + // local time is used to calculate round trip time, + // and to calculate the predicted time offset. + public double localTime; + + // predicted time is sent to compare the final error, for debugging only + public double predictedTimeAdjusted; + + public NetworkPingMessage(double localTime, double predictedTimeAdjusted) + { + this.localTime = localTime; + this.predictedTimeAdjusted = predictedTimeAdjusted; + } + } + + // the other end responds with this message. + // we can use this to calculate rtt. + public struct NetworkPongMessage : NetworkMessage + { + // local time is used to calculate round trip time. + public double localTime; + + // predicted error is used to adjust the predicted timeline. + public double predictionErrorUnadjusted; + public double predictionErrorAdjusted; // for debug purposes + + public NetworkPongMessage(double localTime, double predictionErrorUnadjusted, double predictionErrorAdjusted) + { + this.localTime = localTime; + this.predictionErrorUnadjusted = predictionErrorUnadjusted; + this.predictionErrorAdjusted = predictionErrorAdjusted; + } + } +} diff --git a/Assets/Mirror/Core/Messages.cs.meta b/Assets/Mirror/Core/Messages.cs.meta new file mode 100644 index 0000000..3411dc8 --- /dev/null +++ b/Assets/Mirror/Core/Messages.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 938f6f28a6c5b48a0bbd7782342d763b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Messages.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Mirror.asmdef b/Assets/Mirror/Core/Mirror.asmdef new file mode 100644 index 0000000..2fa8d95 --- /dev/null +++ b/Assets/Mirror/Core/Mirror.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Mirror", + "rootNamespace": "", + "references": [ + "GUID:325984b52e4128546bc7558552f8b1d2" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Core/Mirror.asmdef.meta b/Assets/Mirror/Core/Mirror.asmdef.meta new file mode 100644 index 0000000..69f6cee --- /dev/null +++ b/Assets/Mirror/Core/Mirror.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 30817c1a0e6d646d99c048fc403f5979 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Mirror.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs b/Assets/Mirror/Core/NetworkAuthenticator.cs similarity index 93% rename from Assets/Mirror/Runtime/NetworkAuthenticator.cs rename to Assets/Mirror/Core/NetworkAuthenticator.cs index 9f99b50..aa1e7f7 100644 --- a/Assets/Mirror/Runtime/NetworkAuthenticator.cs +++ b/Assets/Mirror/Core/NetworkAuthenticator.cs @@ -25,7 +25,7 @@ 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 + /// Called on server from OnServerConnectInternal when a client needs to authenticate public virtual void OnServerAuthenticate(NetworkConnectionToClient conn) {} protected void ServerAccept(NetworkConnectionToClient conn) @@ -44,7 +44,7 @@ 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 + /// Called on client from OnClientConnectInternal when a client needs to authenticate public virtual void OnClientAuthenticate() {} protected void ClientAccept() diff --git a/Assets/Mirror/Core/NetworkAuthenticator.cs.meta b/Assets/Mirror/Core/NetworkAuthenticator.cs.meta new file mode 100644 index 0000000..be59e4a --- /dev/null +++ b/Assets/Mirror/Core/NetworkAuthenticator.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 407fc95d4a8257f448799f26cdde0c2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs new file mode 100644 index 0000000..f887f47 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -0,0 +1,1386 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + // SyncMode decides if a component is synced to all observers, or only owner + public enum SyncMode { Observers, Owner } + + // SyncDirection decides if a component is synced from: + // * server to all clients + // * owner client, to server, to all other clients + // + // naming: 'ClientToServer' etc. instead of 'ClientAuthority', because + // that wouldn't be accurate. server's OnDeserialize can still validate + // client data before applying. it's really about direction, not authority. + public enum SyncDirection { ServerToClient, ClientToServer } + + /// Base class for networked components. + // [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours + [AddComponentMenu("")] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")] + public abstract class NetworkBehaviour : MonoBehaviour + { + /// Sync direction for OnSerialize. ServerToClient by default. ClientToServer for client authority. + [Tooltip("Server Authority calls OnSerialize on the server and syncs it to clients.\n\nClient Authority calls OnSerialize on the owning client, syncs it to server, which then broadcasts it to all other clients.\n\nUse server authority for cheat safety.")] + [HideInInspector] public SyncDirection syncDirection = SyncDirection.ServerToClient; + + /// 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. + // + // NetworkServer & NetworkClient broadcast() are behind a sendInterval timer now. + // it makes sense to keep every component's syncInterval setting at '0' by default. + // otherwise, the overlapping timers could introduce unexpected latency. + // careful: default of '0.1' may + [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; + 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; + + /// isOwned is true on the client if this NetworkIdentity is one of the .owned entities of our connection on the server. + // for example: main player & pets are owned. monsters & npcs aren't. + public bool isOwned => netIdentity.isOwned; + + /// authority is true if we are allowed to modify this component's state. On server, it's true if SyncDirection is ServerToClient. On client, it's true if SyncDirection is ClientToServer and(!) if this object is owned by the client. + // on the client: if Client->Server SyncDirection and owned + // on the server: if Server->Client SyncDirection + // on the host: if Server->Client SyncDirection (= server owns it), or if Client->Server and owned (=host client owns it) + // in host mode: always true because either server or client always has authority, and host is both. + // + // for example, NetworkTransform: + // client may modify position if ClientAuthority mode and owned + // server may modify position only if server authority + // + // note that in original Mirror, hasAuthority only meant 'isOwned'. + // there was no syncDirection to check. + // + // also note that this is a per-NetworkBehaviour flag. + // another component may not be client authoritative, etc. + public bool authority + { + get + { + // host mode needs to be checked explicitly + if (isClient && isServer) return syncDirection == SyncDirection.ServerToClient || isOwned; + + // client-only + if (isClient) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // server-only + return syncDirection == SyncDirection.ServerToClient; + } + } + + /// 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 byte 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 since NB child classes read this field in the weaver generated SerializeSyncVars method + protected ulong syncVarDirtyBits; + // 64 bit mask, tracking up to 64 sync collections. + // 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; + + protected virtual void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + // we now allow child NetworkBehaviours. + // we can not [RequireComponent(typeof(NetworkIdentity))] anymore. + // instead, we need to ensure a NetworkIdentity is somewhere in the + // parents. + // only run this in Editor. don't add more runtime overhead. + + // GetComponentInParent(includeInactive) is needed because Prefabs are not + // considered active, so this check requires to scan inactive. +#if UNITY_2021_3_OR_NEWER // 2021 has GetComponentInParent(bool includeInactive = false) + if (GetComponent() == null && + GetComponentInParent(true) == null) + { + Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or its parents.", this); + } +#elif UNITY_2020_3_OR_NEWER // 2020 only has GetComponentsInParent(bool includeInactive = false), we can use this too + NetworkIdentity[] parentsIds = GetComponentsInParent(true); + int parentIdsCount = parentsIds != null ? parentsIds.Length : 0; + if (GetComponent() == null && parentIdsCount == 0) + { + Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or its parents.", this); + } +#endif + } + + // USED BY WEAVER to set syncvars in host mode without deadlocking + protected bool GetSyncVarHookGuard(ulong dirtyBit) => + (syncVarHookGuard & dirtyBit) != 0UL; + + // 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; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void SetSyncObjectDirtyBit(ulong dirtyBit) + { + syncObjectDirtyBits |= dirtyBit; + } + + /// Set as dirty so that it's synced to clients again. + // these are masks, not bit numbers, ie. 110011b not '2' for 2nd bit. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetSyncVarDirtyBit(ulong dirtyBit) + { + syncVarDirtyBits |= dirtyBit; + } + + /// Set as dirty to trigger OnSerialize & send. Dirty bits are cleared after the send. + // previously one had to use SetSyncVarDirtyBit(1), which is confusing. + // simply reuse SetSyncVarDirtyBit for now. + // instead of adding another field. + // syncVarDirtyBits does trigger OnSerialize as well. + // + // it's important to set _all_ bits as dirty. + // for example, server needs to broadcast ClientToServer components. + // if we only set the first bit, only that SyncVar would be broadcast. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetDirty() => SetSyncVarDirtyBit(ulong.MaxValue); + + // true if syncInterval elapsed and any SyncVar or SyncObject is dirty + // OR both bitmasks. != 0 if either was dirty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty() => + // check bits first. this is basically free. + (syncVarDirtyBits | syncObjectDirtyBits) != 0UL && + // only check time if bits were dirty. this is more expensive. + NetworkTime.localTime - lastSyncTime >= syncInterval; + + /// Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally 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 = () => SetSyncObjectDirtyBit(nthBit); + + // who is allowed to modify SyncList/SyncSet/etc.: + // on client: only if owned ClientToserver + // on server: only if ServerToClient. + // but also for initial state when spawning. + // need to set a lambda because 'isClient' isn't available in + // InitSyncObject yet, which is called from the constructor. + syncObject.IsWritable = () => + { + // carefully check each mode separately to ensure correct results. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3342 + + // normally we would check isServer / isClient here. + // users may add to SyncLists before the object was spawned. + // isServer / isClient would still be false. + // so we need to check NetworkServer/Client.active here instead. + + // host mode: any ServerToClient and any local client owned + if (NetworkServer.active && NetworkClient.active) + return syncDirection == SyncDirection.ServerToClient || isOwned; + + // server only: any ServerToClient + if (NetworkServer.active) + return syncDirection == SyncDirection.ServerToClient; + + // client only: only ClientToServer and owned + if (NetworkClient.active) + { + // spawned: only ClientToServer and owned + if (netId != 0) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // not spawned (character selection previews, etc.): always allow + // fixes https://github.com/MirrorNetworking/Mirror/issues/3343 + return true; + } + + // undefined behaviour should throw to make it very obvious + throw new Exception("InitSyncObject: IsWritable: neither NetworkServer nor NetworkClient are active."); + }; + + // when do we record changes: + // on client: only if owned ClientToServer + // on server: only if 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 = () => + { + // carefully check each mode separately to ensure correct results. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3342 + + // host mode: only if observed + if (isServer && isClient) return netIdentity.observers.Count > 0; + + // server only: only if observed + if (isServer) return netIdentity.observers.Count > 0; + + // client only: only ClientToServer and owned + if (isClient) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // users may add to SyncLists before the object was spawned. + // isServer / isClient would still be false. + // in that case, allow modifying but don't record changes yet. + return false; + }; + } + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + protected void SendCommandInternal(string functionFullName, int functionHashCode, 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 {functionFullName} called on {name} without an active client.", gameObject); + 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($"Command {functionFullName} called on {name} while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady.", gameObject); + return; + } + + // local players can always send commands, regardless of authority, + // other objects must have authority. + if (!(!requiresAuthority || isLocalPlayer || isOwned)) + { + Debug.LogWarning($"Command {functionFullName} called on {name} without authority.", gameObject); + 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($"Command {functionFullName} called on {name} with no client running.", gameObject); + return; + } + + if (netId == 0) + { + Debug.LogWarning($"Command {functionFullName} called on {name} with netId=0. Maybe it wasn't spawned yet?", gameObject); + return; + } + + // construct the message + CommandMessage message = new CommandMessage + { + netId = netId, + componentIndex = ComponentIndex, + // type+func so Inventory.RpcUse != Equipment.RpcUse + functionHash = (ushort)functionHashCode, + // 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 + // This bypasses the null check in NetworkClient.Send but we have + // a null check above with a detailed error log. + NetworkClient.connection.Send(message, channelId); + } + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + protected void SendRPCInternal(string functionFullName, int functionHashCode, NetworkWriter writer, int channelId, bool includeOwner) + { + // this was in Weaver before + if (!NetworkServer.active) + { + Debug.LogError($"RPC Function {functionFullName} called without an active server.", gameObject); + 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}", gameObject); + return; + } + + // construct the message + RpcMessage message = new RpcMessage + { + netId = netId, + componentIndex = ComponentIndex, + // type+func so Inventory.RpcUse != Equipment.RpcUse + functionHash = (ushort)functionHashCode, + // segment to avoid reader allocations + payload = writer.ToArraySegment() + }; + + // serialize it to each ready observer's connection's rpc buffer. + // send them all at once, instead of sending one message per rpc. + // NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId); + + // safety check used to be in SendToReadyObservers. keep it for now. + if (netIdentity.observers == null || netIdentity.observers.Count == 0) + return; + + // serialize the message only once + using (NetworkWriterPooled serialized = NetworkWriterPool.Get()) + { + serialized.Write(message); + + // send to every observer. + // batching buffers this automatically. + foreach (NetworkConnectionToClient conn in netIdentity.observers.Values) + { + bool isOwner = conn == netIdentity.connectionToClient; + if ((!isOwner || includeOwner) && conn.isReady) + { + conn.Send(message, channelId); + } + } + } + } + + // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions + protected void SendTargetRPCInternal(NetworkConnection conn, string functionFullName, int functionHashCode, NetworkWriter writer, int channelId) + { + if (!NetworkServer.active) + { + Debug.LogError($"TargetRPC {functionFullName} was called on {name} when server not active.", gameObject); + return; + } + + if (!isServer) + { + Debug.LogWarning($"TargetRpc {functionFullName} called on {name} but that object has not been spawned or has been unspawned.", gameObject); + 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} can't be sent because it was given a null connection. Make sure {name} is owned by a connection, or if you pass a connection manually then make sure it's not null. For example, TargetRpcs can be called on Player/Pet which are owned by a connection. However, they can not be called on Monsters/Npcs which don't have an owner connection.", gameObject); + return; + } + + // TODO change conn type to NetworkConnectionToClient to begin with. + if (!(conn is NetworkConnectionToClient connToClient)) + { + Debug.LogError($"TargetRPC {functionFullName} called on {name} requires a NetworkConnectionToClient but was given {conn.GetType().Name}", gameObject); + return; + } + + // construct the message + RpcMessage message = new RpcMessage + { + netId = netId, + componentIndex = ComponentIndex, + // type+func so Inventory.RpcUse != Equipment.RpcUse + functionHash = (ushort)functionHashCode, + // segment to avoid reader allocations + payload = writer.ToArraySegment() + }; + + // send it to the connection. + // batching buffers this automatically. + 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.activeHost && !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.activeHost && !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.activeHost && !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.activeHost && !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.activeHost && !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) + { + if (newGameObject.TryGetComponent(out NetworkIdentity identity)) + { + 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) + { + if (newGameObject.TryGetComponent(out NetworkIdentity identity)) + { + 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 neither, fallback to original field + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447 + if (isServer || !isClient) + { + 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()); + // } + // } + 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); + // } + // } + 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); + // } + // } + 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); + // } + // } + 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 neither, fallback to original field + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447 + if (isServer || !isClient) + { + 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; + byte 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; + byte 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] NetworkBehaviours. + // -> 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 neither, fallback to original field + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447 + if (isServer || !isClient) + { + 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; + } + + // ensure componentIndex is in range. + // show explicit errors if something went wrong, instead of IndexOutOfRangeException. + // removing components at runtime isn't allowed, yet this happened in a project so we need to check for it. + if (syncNetBehaviour.componentIndex >= identity.NetworkBehaviours.Length) + { + Debug.LogError($"[SyncVar] {typeof(T)} on {name}'s {GetType()}: can't access {identity.name} NetworkBehaviour[{syncNetBehaviour.componentIndex}] because it only has {identity.NetworkBehaviours.Length} components.\nWas a NetworkBeahviour accidentally destroyed at runtime?"); + return null; + } + + behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T; + return behaviourField; + } + + 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 void OnSerialize(NetworkWriter writer, bool initialState) + { + SerializeSyncObjects(writer, initialState); + SerializeSyncVars(writer, initialState); + } + + /// Override to do custom deserialization (instead of SyncVars/SyncLists). Use OnSerialize too. + public virtual void OnDeserialize(NetworkReader reader, bool initialState) + { + DeserializeSyncObjects(reader, initialState); + DeserializeSyncVars(reader, initialState); + } + + void SerializeSyncObjects(NetworkWriter writer, bool initialState) + { + // if initialState: write all SyncObjects (SyncList/Set/etc) + // otherwise write dirtyBits+dirty SyncVars + if (initialState) + SerializeObjectsAll(writer); + else + SerializeObjectsDelta(writer); + } + + void DeserializeSyncObjects(NetworkReader reader, bool initialState) + { + if (initialState) + { + DeserializeObjectsAll(reader); + } + else + { + DeserializeObjectsDelta(reader); + } + } + + // USED BY WEAVER + protected virtual void SerializeSyncVars(NetworkWriter writer, bool initialState) + { + // 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 void SerializeObjectsAll(NetworkWriter writer) + { + for (int i = 0; i < syncObjects.Count; i++) + { + SyncObject syncObject = syncObjects[i]; + syncObject.OnSerializeAll(writer); + } + } + + public void SerializeObjectsDelta(NetworkWriter writer) + { + // 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); + } + } + } + + 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); + } + } + } + + // safely serialize each component in a way that one reading too much or + // too few bytes will show obvious, easy to resolve error messages. + // + // prevents the original UNET bug which started Mirror: + // https://github.com/vis2k/Mirror/issues/2617 + // where one component would read too much, and then all following reads + // on other entities would be mismatched, causing the weirdest errors. + // + // reads <> for 100% safety. + internal void Serialize(NetworkWriter writer, bool initialState) + { + // reserve length header to ensure the correct amount will be read. + // originally we used a 4 byte header (too bandwidth heavy). + // instead, let's "& 0xFF" the size. + // + // this is cleaner than barriers at the end of payload, because: + // - ensures the correct safety is read _before_ payload. + // - it's quite hard to break the check. + // a component would need to read/write the intented amount + // multiplied by 255 in order to miss the check. + // with barriers, reading 1 byte too much may still succeed if the + // next component's first byte matches the expected barrier. + // - we can still attempt to correct the invalid position via the + // safety length byte (we know that one is correct). + // + // it's just overall cleaner, and still low on bandwidth. + + // write placeholder length byte + // (jumping back later is WAY faster than allocating a temporary + // writer for the payload, then writing payload.size, payload) + int headerPosition = writer.Position; + writer.WriteByte(0); + int contentPosition = writer.Position; + + // write payload + try + { + // note this may not write anything if no syncIntervals elapsed + 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={GetType()} sceneId={netIdentity.sceneId:X}\n\n{e}"); + } + int endPosition = writer.Position; + + // fill in length hash as the last byte of the 4 byte length + writer.Position = headerPosition; + int size = endPosition - contentPosition; + byte safety = (byte)(size & 0xFF); + writer.WriteByte(safety); + writer.Position = endPosition; + + //Debug.Log($"OnSerializeSafely written for object {name} component:{GetType()} sceneId:{sceneId:X} header:{headerPosition} content:{contentPosition} end:{endPosition} contentSize:{endPosition - contentPosition}"); + } + + // correct the read size with the 1 byte length hash (by mischa). + // -> the component most likely read a few too many/few bytes. + // -> we know the correct last byte of the expected size (=the safety). + // -> attempt to reconstruct the size via safety byte. + // it will be correct unless someone wrote way way too much, + // as in > 255 bytes worth too much. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int ErrorCorrection(int size, byte safety) + { + // clear the last byte which most likely contains the error + uint cleared = (uint)size & 0xFFFFFF00; + + // insert the safety which we know to be correct + return (int)(cleared | safety); + } + + // returns false in case of errors. + // server needs to know in order to disconnect on error. + internal bool Deserialize(NetworkReader reader, bool initialState) + { + // detect errors, but attempt to correct before returning + bool result = true; + + // read 1 byte length hash safety & capture beginning for size check + byte safety = reader.ReadByte(); + int chunkStart = reader.Position; + + // 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: {name} component:{GetType()} sceneId:{sceneId:X} length:{contentSize}"); + 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={GetType()} netId={netId}. Possible Reasons:\n" + + $" * Do {GetType()}'s OnSerialize and OnDeserialize calls write the same amount of data? \n" + + $" * Was there an exception in {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}"); + result = false; + } + + // compare bytes read with length hash + int size = reader.Position - chunkStart; + byte sizeHash = (byte)(size & 0xFF); + if (sizeHash != safety) + { + // warn the user. + Debug.LogWarning($"{name} (netId={netId}): {GetType()} OnDeserialize size mismatch. It read {size} bytes, which caused a size hash mismatch of {sizeHash:X2} vs. {safety:X2}. Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases."); + + // attempt to fix the position, so the following components + // don't all fail. this is very likely to work, unless the user + // read more than 255 bytes too many / too few. + // + // see test: SerializationSizeMismatch. + int correctedSize = ErrorCorrection(size, safety); + reader.Position = chunkStart + correctedSize; + result = false; + } + + return result; + } + + 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() {} + + // Weaver injects this into inheriting classes to return true. + // allows runtime & tests to check if a type was weaved. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool Weaved() => false; + } +} diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs.meta b/Assets/Mirror/Core/NetworkBehaviour.cs.meta new file mode 100644 index 0000000..3c6e6f5 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviour.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 655ee8cba98594f70880da5cc4dc442d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkBehaviour.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkBehaviourHybrid.cs b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs new file mode 100644 index 0000000..988c5cc --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs @@ -0,0 +1,483 @@ +// base class for "Hybrid" sync components. +// inspired by the Quake networking model, but made to scale. +// https://www.jfedor.org/quake3/ +using System; +using UnityEngine; + +namespace Mirror +{ + public abstract class NetworkBehaviourHybrid : NetworkBehaviour + { + // 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 => isClient && authority; + + [Tooltip("Occasionally send a full reliable state to delta compress against. This only applies to Components with SyncMethod=Unreliable.")] + public int baselineRate = 1; + public float baselineInterval => baselineRate < int.MaxValue ? 1f / baselineRate : 0; // for 1 Hz, that's 1000ms + protected double lastBaselineTime; + protected double lastDeltaTime; + + // delta compression needs to remember 'last' to compress against. + byte lastSerializedBaselineTick = 0; + byte lastDeserializedBaselineTick = 0; + + [Tooltip("Enable to send all unreliable messages twice. Only useful for extremely fast-paced games since it doubles bandwidth costs.")] + public bool unreliableRedundancy = false; + + [Tooltip("When sending a reliable baseline, should we also send an unreliable delta or rely on the reliable baseline to arrive in a similar time?")] + public bool baselineIsDelta = true; + + // change detection: we need to do this carefully in order to get it right. + // + // DONT just check changes in UpdateBaseline(). this would introduce MrG's grid issue: + // server start in A1, reliable baseline sent to client + // server moves to A2, unreliabe delta sent to client + // server moves to A1, nothing is sent to client becuase last baseline position == position + // => client wouldn't know we moved back to A1 + // + // INSTEAD: every update() check for changes since baseline: + // UpdateDelta() keeps sending only if changed since _baseline_ + // UpdateBaseline() resends if there was any change in the period since last baseline. + // => this avoids the A1->A2->A1 grid issue above + bool changedSinceBaseline = false; + + [Header("Debug")] + public bool debugLog = false; + + public virtual void ResetState() + { + lastSerializedBaselineTick = 0; + lastDeserializedBaselineTick = 0; + changedSinceBaseline = false; + } + + // user callbacks ////////////////////////////////////////////////////// + protected abstract void OnSerializeBaseline(NetworkWriter writer); + protected abstract void OnDeserializeBaseline(NetworkReader reader, byte baselineTick); + + protected abstract void OnSerializeDelta(NetworkWriter writer); + protected abstract void OnDeserializeDelta(NetworkReader reader, byte baselineTick); + + // implementations must store the current baseline state when requested: + // - implementations can use this to compress deltas against + // - implementations can use this to detect changes since baseline + // this is called whenever a baseline was sent. + protected abstract void StoreState(); + + // implementations may compare current state to the last stored state. + // this way we only need to send another reliable baseline if changed since last. + // this is called every syncInterval, not every baseline sync interval. + // (see comments where this is called). + protected abstract bool StateChanged(); + + // user callback in case drops due to baseline mismatch need to be logged/visualized/debugged. + protected virtual void OnDrop(byte lastBaselineTick, byte baselineTick, NetworkReader reader) {} + + // rpcs / cmds ///////////////////////////////////////////////////////// + // reliable baseline. + // include owner in case of server authority. + [ClientRpc(channel = Channels.Reliable)] + void RpcServerToClientBaseline(ArraySegment data) + { + // baseline is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + // host mode: baseline Rpc is also sent through host's local connection and applied. + // applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter. + // in other words: never apply the rpcs in host mode. + if (isServer) return; + + using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) + { + // deserialize + // save last deserialized baseline tick number to compare deltas against + lastDeserializedBaselineTick = reader.ReadByte(); + OnDeserializeBaseline(reader, lastDeserializedBaselineTick); + } + } + + // unreliable delta. + // include owner in case of server authority. + [ClientRpc(channel = Channels.Unreliable)] + void RpcServerToClientDelta(ArraySegment data) + { + // delta is broadcast to all clients. + // ignore if this object is owned by this client. + if (IsClientWithAuthority) return; + + // host mode: baseline Rpc is also sent through host's local connection and applied. + // applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter. + // in other words: never apply the rpcs in host mode. + if (isServer) return; + + // deserialize + using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) + { + // deserialize + byte baselineTick = reader.ReadByte(); + + // ensure this delta is for our last known baseline. + // we should never apply a delta on top of a wrong baseline. + if (baselineTick != lastDeserializedBaselineTick) + { + OnDrop(lastDeserializedBaselineTick, baselineTick, reader); + + // this can happen if unreliable arrives before reliable etc. + // no need to log this except when debugging. + if (debugLog) Debug.Log($"[{name}] Client: received delta for wrong baseline #{baselineTick}. Last was {lastDeserializedBaselineTick}. Ignoring."); + return; + } + + OnDeserializeDelta(reader, baselineTick); + } + } + + [Command(channel = Channels.Reliable)] // reliable baseline + void CmdClientToServerBaseline(ArraySegment data) + { + // deserialize + using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) + { + // deserialize + lastDeserializedBaselineTick = reader.ReadByte(); + OnDeserializeBaseline(reader, lastDeserializedBaselineTick); + } + } + + [Command(channel = Channels.Unreliable)] // unreliable delta + void CmdClientToServerDelta(ArraySegment data) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) + { + // deserialize + byte baselineTick = reader.ReadByte(); + + // ensure this delta is for our last known baseline. + // we should never apply a delta on top of a wrong baseline. + if (baselineTick != lastDeserializedBaselineTick) + { + OnDrop(lastDeserializedBaselineTick, baselineTick, reader); + + // this can happen if unreliable arrives before reliable etc. + // no need to log this except when debugging. + if (debugLog) Debug.Log($"[{name}] Server: received delta for wrong baseline #{baselineTick} from: {connectionToClient}. Last was {lastDeserializedBaselineTick}. Ignoring."); + return; + } + + OnDeserializeDelta(reader, baselineTick); + } + } + + // update server /////////////////////////////////////////////////////// + protected virtual void UpdateServerBaseline(double localTime) + { + // send a reliable baseline every 1 Hz + if (localTime < lastBaselineTime + baselineInterval) return; + + // only sync if changed since last reliable baseline + if (!changedSinceBaseline) return; + + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // serialize + writer.WriteByte(frameCount); + OnSerializeBaseline(writer); + + // send (no need for redundancy since baseline is reliable) + RpcServerToClientBaseline(writer); + } + + // save the last baseline's tick number. + // included in baseline to identify which one it was on client + // included in deltas to ensure they are on top of the correct baseline + lastSerializedBaselineTick = frameCount; + lastBaselineTime = NetworkTime.localTime; + + // perf. & bandwidth optimization: + // send a delta right after baseline to avoid potential head of + // line blocking, or skip the delta whenever we sent reliable? + // for example: + // 1 Hz baseline + // 10 Hz delta + // => 11 Hz total if we still send delta after reliable + // => 10 Hz total if we skip delta after reliable + // in that case, skip next delta by simply resetting last delta sync's time. + if (baselineIsDelta) lastDeltaTime = localTime; + + // request to store last baseline state (i.e. position) for change detection. + StoreState(); + + // baseline was just sent after a change. reset change detection. + changedSinceBaseline = false; + + if (debugLog) Debug.Log($"[{name}] Server: sent baseline #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}"); + } + + protected virtual void UpdateServerDelta(double localTime) + { + // 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. + // TODO send same time that NetworkServer sends time snapshot? + + if (localTime < lastDeltaTime + syncInterval) return; + + // look for changes every unreliable sendInterval! + // every reliable interval isn't enough, this would cause MrG's grid issue: + // server start in A1, reliable baseline sent to clients + // server moves to A2, unreliabe delta sent to clients + // server moves back to A1, nothing is sent to clients because last baseline position == position + // => clients wouldn't know we moved back to A1 + // every update works, but it's unnecessary overhead since sends only happen every sendInterval + // every unreliable sendInterval is the perfect place to look for changes. + if (StateChanged()) changedSinceBaseline = true; + + // only sync on change: + // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. + if (!changedSinceBaseline) return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // serialize + writer.WriteByte(lastSerializedBaselineTick); + OnSerializeDelta(writer); + + // send (with optional redundancy to make up for message drops) + RpcServerToClientDelta(writer); + if (unreliableRedundancy) + RpcServerToClientDelta(writer); + } + + lastDeltaTime = localTime; + + if (debugLog) Debug.Log($"[{name}] Server: sent delta for #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}"); + } + + protected virtual void UpdateServerSync() + { + // server broadcasts all objects all the time. + // -> not just ServerToClient: ClientToServer need to be broadcast to others too + + // perf: only grab NetworkTime.localTime property once. + double localTime = NetworkTime.localTime; + + // broadcast + UpdateServerBaseline(localTime); + UpdateServerDelta(localTime); + } + + // update client /////////////////////////////////////////////////////// + protected virtual void UpdateClientBaseline(double localTime) + { + // send a reliable baseline every 1 Hz + if (localTime < lastBaselineTime + baselineInterval) return; + + // only sync if changed since last reliable baseline + if (!changedSinceBaseline) return; + + // save bandwidth by only transmitting what is needed. + // -> ArraySegment with random data is slower since byte[] copying + // -> Vector3? and Quaternion? nullables takes more bandwidth + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // serialize + writer.WriteByte(frameCount); + OnSerializeBaseline(writer); + + // send (no need for redundancy since baseline is reliable) + CmdClientToServerBaseline(writer); + } + + // save the last baseline's tick number. + // included in baseline to identify which one it was on client + // included in deltas to ensure they are on top of the correct baseline + lastSerializedBaselineTick = frameCount; + lastBaselineTime = NetworkTime.localTime; + + // perf. & bandwidth optimization: + // send a delta right after baseline to avoid potential head of + // line blocking, or skip the delta whenever we sent reliable? + // for example: + // 1 Hz baseline + // 10 Hz delta + // => 11 Hz total if we still send delta after reliable + // => 10 Hz total if we skip delta after reliable + // in that case, skip next delta by simply resetting last delta sync's time. + if (baselineIsDelta) lastDeltaTime = localTime; + + // request to store last baseline state (i.e. position) for change detection. + // IMPORTANT + // OnSerialize(initial) is called for the spawn payload whenever + // someone starts observing this object. we always must make + // this the new baseline, otherwise this happens: + // - server broadcasts baseline @ t=1 + // - server broadcasts delta for baseline @ t=1 + // - ... time passes ... + // - new observer -> OnSerialize sends current position @ t=2 + // - server broadcasts delta for baseline @ t=1 + // => client's baseline is t=2 but receives delta for t=1 _!_ + StoreState(); + + // baseline was just sent after a change. reset change detection. + changedSinceBaseline = false; + + if (debugLog) Debug.Log($"[{name}] Client: sent baseline #{lastSerializedBaselineTick} at time: {localTime}"); + } + + protected virtual void UpdateClientDelta(double localTime) + { + // 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 (localTime < lastDeltaTime + syncInterval) return; + + // look for changes every unreliable sendInterval! + // every reliable interval isn't enough, this would cause MrG's grid issue: + // client start in A1, reliable baseline sent to server and other clients + // client moves to A2, unreliabe delta sent to server and other clients + // client moves back to A1, nothing is sent to server because last baseline position == position + // => server / other clients wouldn't know we moved back to A1 + // every update works, but it's unnecessary overhead since sends only happen every sendInterval + // every unreliable sendInterval is the perfect place to look for changes. + if (StateChanged()) changedSinceBaseline = true; + + // only sync on change: + // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. + if (!changedSinceBaseline) return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // serialize + writer.WriteByte(lastSerializedBaselineTick); + OnSerializeDelta(writer); + + // send (with optional redundancy to make up for message drops) + CmdClientToServerDelta(writer); + if (unreliableRedundancy) + CmdClientToServerDelta(writer); + } + + lastDeltaTime = localTime; + + if (debugLog) Debug.Log($"[{name}] Client: sent delta for #{lastSerializedBaselineTick} at time: {localTime}"); + } + + protected virtual void UpdateClientSync() + { + // client authority, and local player (= allowed to move myself)? + if (IsClientWithAuthority) + { + // https://github.com/vis2k/Mirror/pull/2992/ + if (!NetworkClient.ready) return; + + // perf: only grab NetworkTime.localTime property once. + double localTime = NetworkTime.localTime; + + UpdateClientBaseline(localTime); + UpdateClientDelta(localTime); + } + } + + // Update() without LateUpdate() split: otherwise perf. is cut in half! + protected virtual void Update() + { + // if server then always sync to others. + if (isServer) UpdateServerSync(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClientSync(); + } + + // OnSerialize(initial) is called every time when a player starts observing us. + // note this is _not_ called just once on spawn. + // call this from inheriting classes immediately in OnSerialize(). + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + if (initialState) + { + // always include the tick for deltas to compare against. + byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! + writer.WriteByte(frameCount); + + // IMPORTANT + // OnSerialize(initial) is called for the spawn payload whenever + // someone starts observing this object. we always must make + // this the new baseline, otherwise this happens: + // - server broadcasts baseline @ t=1 + // - server broadcasts delta for baseline @ t=1 + // - ... time passes ... + // - new observer -> OnSerialize sends current position @ t=2 + // - server broadcasts delta for baseline @ t=1 + // => client's baseline is t=2 but receives delta for t=1 _!_ + lastSerializedBaselineTick = (byte)Time.frameCount; + lastBaselineTime = NetworkTime.localTime; + + // request to store last baseline state (i.e. position) for change detection. + StoreState(); + } + } + + // call this from inheriting classes immediately in OnDeserialize(). + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + if (initialState) + { + // save last deserialized baseline tick number to compare deltas against + lastDeserializedBaselineTick = reader.ReadByte(); + } + } + } +} diff --git a/Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta new file mode 100644 index 0000000..bfa737d --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 657535a722c74173bdaa18a4394ce016 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkBehaviourHybrid.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs new file mode 100644 index 0000000..e9ed726 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs @@ -0,0 +1,33 @@ +using System; + +namespace Mirror +{ + // 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}]"; + } + } +} diff --git a/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta new file mode 100644 index 0000000..0a925a7 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: b04fe7518657486089dfaf811db0b3ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkBehaviourSyncVar.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs new file mode 100644 index 0000000..d9cce5a --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -0,0 +1,1885 @@ +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 partial class NetworkClient + { + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + // + // additionally, server & client need to use the same send interval. + // otherwise it's too easy to accidentally cause interpolation issues if + // a component sends with client.interval but interpolates with + // server.interval, etc. + public static int sendRate => NetworkServer.sendRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; + + // For security, it is recommended to disconnect a player if a networked + // action triggers an exception\nThis could prevent components being + // accessed in an undefined state, which may be an attack vector for + // exploits. + // + // However, some games may want to allow exceptions in order to not + // interrupt the player's experience. + public static bool exceptionsDisconnect = true; // security by default + + // 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 NetworkConnectionToServer 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; + + /// active is true while a client is connecting/connected either as standalone or as host client. + // (= while the network is active) + public static bool active => connectState == ConnectState.Connecting || + connectState == ConnectState.Connected; + + /// active is true while the client is connected in host mode. + // naming consistent with NetworkServer.activeHost. + public static bool activeHost => connection is LocalConnectionToServer; + + /// 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; + + // 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; + public static Action OnTransportExceptionEvent; + + /// Registered spawnable prefabs by assetId. + public static readonly Dictionary prefabs = + new Dictionary(); + + // custom spawn / unspawn handlers by assetId. + // 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(); + + internal static Unbatcher unbatcher = new Unbatcher(); + + // interest management component (optional) + // only needed for SetHostVisibility + public static InterestManagementBase aoi; + + // scene loading + public static bool isLoadingScene; + + // connection quality + // this is set by a virtual function in NetworkManager, + // which allows users to overwrite it with their own estimations. + public static ConnectionQuality connectionQuality = ConnectionQuality.ESTIMATING; + public static ConnectionQuality lastConnectionQuality = ConnectionQuality.ESTIMATING; + public static ConnectionQualityMethod connectionQualityMethod = ConnectionQualityMethod.Simple; + public static float connectionQualityInterval = 3; + static double lastConnectionQualityUpdate; + + /// + /// Invoked when connection quality changes. + /// First argument is the old quality, second argument is the new quality. + /// + public static event Action onConnectionQualityChanged; + + // initialization ////////////////////////////////////////////////////// + static void AddTransportHandlers() + { + // community Transports may forget to call OnDisconnected. + // which could cause handlers to be added twice with +=. + // ensure we always clear the old ones first. + // fixes: https://github.com/vis2k/Mirror/issues/3152 + RemoveTransportHandlers(); + + // += so that other systems can also hook into it (i.e. statistics) + Transport.active.OnClientConnected += OnTransportConnected; + Transport.active.OnClientDataReceived += OnTransportData; + Transport.active.OnClientDisconnected += OnTransportDisconnected; + Transport.active.OnClientError += OnTransportError; + Transport.active.OnClientTransportException += OnTransportException; + } + + static void RemoveTransportHandlers() + { + // -= so that other systems can also hook into it (i.e. statistics) + Transport.active.OnClientConnected -= OnTransportConnected; + Transport.active.OnClientDataReceived -= OnTransportData; + Transport.active.OnClientDisconnected -= OnTransportDisconnected; + Transport.active.OnClientError -= OnTransportError; + Transport.active.OnClientTransportException -= OnTransportException; + } + + // connect ///////////////////////////////////////////////////////////// + // initialize is called before every connect + static void Initialize(bool hostMode) + { + // safety: ensure Weaving succeded. + // if it silently failed, we would get lots of 'writer not found' + // and other random errors at runtime instead. this is cleaner. + if (!WeaverFuse.Weaved()) + { + // if it failed, throw an exception to early exit all Connect calls. + throw new Exception("NetworkClient won't start because Weaving failed or didn't run."); + } + + // Debug.Log($"Client Connect: {address}"); + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.active' first"); + + // reset unbatcher in case any batches from last session remain. + // need to do this in Initialize() so it runs for the host as well. + // fixes host mode scene transition receiving data from previous scene. + // credits: BigBoxVR + unbatcher = new Unbatcher(); + + // reset time interpolation on every new connect. + // ensures last sessions' state is cleared before starting again. + InitTimeInterpolation(); + + RegisterMessageHandlers(hostMode); + Transport.active.enabled = true; + } + + /// Connect client to a NetworkServer by address. + public static void Connect(string address) + { + Initialize(false); + + AddTransportHandlers(); + connectState = ConnectState.Connecting; + Transport.active.ClientConnect(address); + connection = new NetworkConnectionToServer(); + } + + /// Connect client to a NetworkServer by Uri. + public static void Connect(Uri uri) + { + Initialize(false); + + AddTransportHandlers(); + connectState = ConnectState.Connecting; + Transport.active.ClientConnect(uri); + connection = new NetworkConnectionToServer(); + } + + // TODO why are there two connect host methods? + // called from NetworkManager.FinishStartHost() + public static void ConnectHost() + { + Initialize(true); + connectState = ConnectState.Connected; + HostMode.SetupConnections(); + } + + // 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(); + + // the handler may want to send messages to the client + // thus we should set the connected state before calling the handler + connectState = ConnectState.Connected; + // ping right away after connecting so client gets new time asap + NetworkTime.SendPing(); + OnConnectedEvent?.Invoke(); + } + else Debug.LogError("Skipped Connect message handling because connection is null."); + } + + // helper function + static bool UnpackAndInvoke(NetworkReader reader, int channelId) + { + if (NetworkMessages.UnpackId(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)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: failed to add batch, disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkClient: failed to add batch."); + + 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 ArraySegment message, out double remoteTimestamp)) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message)) + { + // enough to read at least header size? + if (reader.Remaining >= NetworkMessages.IdSize) + { + // 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. + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: failed to unpack and invoke message. Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkClient: failed to unpack and invoke message."); + + return; + } + } + // otherwise disconnect + else + { + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: received Message was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning("NetworkClient: received Message was too short (messages should start with message id)"); + 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 + // + // previously OnDisconnected was only invoked if connection != null. + // however, if DNS resolve fails in Transport.Connect(), + // OnDisconnected would never be called because 'connection' is only + // created after the Transport.Connect() call. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3365 + OnDisconnectedEvent?.Invoke(); + + connectState = ConnectState.Disconnected; + ready = false; + snapshots.Clear(); + localTimeline = 0; + + // 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?.Cleanup(); + connection = null; + + // transport handlers are only added when connecting. + // so only remove when actually disconnecting. + RemoveTransportHandlers(); + } + + // transport errors are forwarded to high level + static void OnTransportError(TransportError error, string reason) + { + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Client Transport Error: {error}: {reason}. This is fine."); + OnErrorEvent?.Invoke(error, reason); + } + + static void OnTransportException(Exception exception) + { + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Client Transport Exception: {exception}. This is fine."); + OnTransportExceptionEvent?.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 //////////////////////////////////////////////////// + internal static void RegisterMessageHandlers(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) + { + // host mode doesn't need destroy messages (see NetworkServer::UnSpawnInternal) + RegisterHandler(_ => { }); + 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(NetworkTime.OnClientPing, false); + RegisterHandler(OnSpawn); + RegisterHandler(OnObjectSpawnStarted); + RegisterHandler(OnObjectSpawnFinished); + RegisterHandler(OnEntityStateMessage); + } + + // These handlers are the same for host and remote clients + RegisterHandler(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through + RegisterHandler(OnChangeOwner); + RegisterHandler(OnRPCMessage); + } + + /// 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 = NetworkMessageId.Id; + 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."); + } + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + // 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] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); + } + + /// Register a handler for a 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 = NetworkMessageId.Id; + 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."); + } + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + // 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, int channelId) => handler(value, channelId); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); + } + + /// 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 + { + // 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. + ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + void HandlerWrapped(NetworkConnection _, T value) => handler(value); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); + } + + /// Replace a handler for a particular message type. Should require authentication by default. This version passes channelId to the handler. + // 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 + { + // 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. + ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); + } + + /// Unregister a message handler of type T. + public static bool UnregisterHandler() + where T : struct, NetworkMessage + { + // use int to minimize collisions + ushort msgType = NetworkMessageId.Id; + return handlers.Remove(msgType); + } + + // spawnable prefabs /////////////////////////////////////////////////// + /// Find the registered prefab for this asset id. + // Useful for debuggers + public static bool GetPrefab(uint assetId, out GameObject prefab) + { + prefab = null; + return assetId != 0 && + prefabs.TryGetValue(assetId, out prefab) && + prefab != null; + } + + /// Validates Prefab then adds it to prefabs dictionary. + static void RegisterPrefabIdentity(NetworkIdentity prefab) + { + if (prefab.assetId == 0) + { + 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; + } + + // disallow child NetworkIdentities. + // TODO likely not necessary anymore due to the new check in + // NetworkIdentity.OnValidate. + 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, uint newAssetId) + { + if (prefab == null) + { + Debug.LogError("Could not register prefab because it was null"); + return; + } + + if (newAssetId == 0) + { + Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty"); + return; + } + + if (!prefab.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + if (identity.assetId != 0 && 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; + } + + if (!prefab.TryGetComponent(out NetworkIdentity identity)) + { + 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, uint 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; + } + + if (!prefab.TryGetComponent(out NetworkIdentity identity)) + { + 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; + } + + if (identity.assetId == 0) + { + 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 {identity.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, uint newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + if (newAssetId == 0) + { + 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; + } + + if (!prefab.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + if (identity.assetId != 0 && 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; + uint 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; + } + + if (!prefab.TryGetComponent(out NetworkIdentity identity)) + { + 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; + } + + uint assetId = identity.assetId; + + if (assetId == 0) + { + 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; + } + + if (!prefab.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogError($"Could not unregister '{prefab.name}' since it contains no NetworkIdentity component"); + return; + } + + uint 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(uint 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(uint 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 == 0) + { + Debug.LogError("Can not Register SpawnHandler for empty assetId"); + 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(uint 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(uint 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("NetworkClient can't AddPlayer before being ready. Please call NetworkClient.Ready() first. Clients are considered ready after joining the game world."); + } + + /// 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) + { + // add to spawned first because DeserializeClient may need it for SyncVars + spawned[message.netId] = identity; + + if (message.assetId != 0) + 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; + + // configure flags + // the below DeserializeClient call invokes SyncVarHooks. + // flags always need to be initialized before that. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3259 + identity.netId = message.netId; + identity.isOwned = message.isOwner; + + if (identity.isOwned) + connection?.owned.Add(identity); + + if (message.isLocalPlayer) + InternalAddPlayer(identity); + + // configure isClient/isLocalPlayer flags. + // => after InternalAddPlayer. can't initialize .isLocalPlayer + // before InternalAddPlayer sets .localPlayer + // => before DeserializeClient, otherwise SyncVar hooks wouldn't + // have isClient/isLocalPlayer set yet. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3259 + InitializeIdentityFlags(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.DeserializeClient(payloadReader, true); + } + } + + // 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) + { + InvokeIdentityCallbacks(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 == 0 && 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 identity); + return identity; + } + + 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; + } + + if (!obj.TryGetComponent(out NetworkIdentity identity)) + { + 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; + } + + /// 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 + // need to check netId to make sure object is not spawned + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3541 + // PrepareToSpawnSceneObjects may be called multiple times in case + // the ObjectSpawnStarted message is received multiple times. + if (Utils.IsSceneObject(identity) && + identity.netId == 0) + { + if (spawnableObjects.TryGetValue(identity.sceneId, out NetworkIdentity existingIdentity)) + { + string msg = $"NetworkClient: Duplicate sceneId {identity.sceneId} detected on {identity.gameObject.name} and {existingIdentity.gameObject.name}\n" + + $"This can happen if a networked object is persisted in DontDestroyOnLoad through loading / changing to the scene where it originated,\n" + + $"otherwise you may need to open and re-save the {identity.gameObject.scene} to reset scene id's."; + Debug.LogWarning(msg, identity.gameObject); + } + else + { + spawnableObjects.Add(identity.sceneId, identity); + } + } + } + } + + internal static void OnObjectSpawnStarted(ObjectSpawnStartedMessage _) + { + // Debug.Log("SpawnStarted"); + PrepareToSpawnSceneObjects(); + pendingSpawns.Clear(); + isSpawnFinished = false; + } + + static readonly Dictionary pendingSpawns = new Dictionary(); + + internal static void OnObjectSpawnFinished(ObjectSpawnFinishedMessage _) + { + // 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)) + { + // NetworkIdentities should always be removed from .spawned when + // they are destroyed. for safety, let's double check here. + if (identity != null) + { + // We may have deferred ApplySpawnPayload in OnSpawn + // to avoid cross-reference race conditions with SyncVars. + // Apply payload before invoking OnStartClient etc. callbacks + // so that all data is there when they are invoked. + // Note that Interest Management may not have updated spawned + // dictionary yet, so not all identities may be in pendingSpawns. + // Generally that's user error in their code, so we don't throw + // a warning here, but keep the warning code for debugging if needed. + if (pendingSpawns.TryGetValue(identity, out SpawnMessage message)) + ApplySpawnPayload(identity, message); + //else + // Debug.LogWarning($"Expected pendingSpawns to contain {identity}: {identity.netId} but didn't"); + + BootstrapIdentity(identity); + } + else Debug.LogWarning("Found null entry in NetworkClient.spawned. This is unexpected. Was the NetworkIdentity not destroyed properly?"); + } + + pendingSpawns.Clear(); + isSpawnFinished = true; + } + + // host mode callbacks ///////////////////////////////////////////////// + static void OnHostClientObjectHide(ObjectHideMessage message) + { + //Debug.Log($"ClientScene::OnLocalObjectObjHide netId:{message.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && + identity != null) + { + if (aoi != null) + aoi.SetHostVisibility(identity, 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 identity) && identity != null) + { + spawned[message.netId] = identity; + if (message.isOwner) connection.owned.Add(identity); + + // now do the actual 'spawning' on host mode + if (message.isLocalPlayer) + InternalAddPlayer(identity); + + // set visibility before invoking OnStartClient etc. callbacks + if (aoi != null) + aoi.SetHostVisibility(identity, true); + + identity.isOwned = message.isOwner; + BootstrapIdentity(identity); + } + } + + // configure flags & invoke callbacks + static void BootstrapIdentity(NetworkIdentity identity) + { + InitializeIdentityFlags(identity); + InvokeIdentityCallbacks(identity); + } + + // set up NetworkIdentity flags on the client. + // needs to be separate from invoking callbacks. + // cleaner, and some places need to set flags first. + static void InitializeIdentityFlags(NetworkIdentity identity) + { + // initialize flags before invoking callbacks. + // this way isClient/isLocalPlayer is correct during callbacks. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3362 + identity.isClient = true; + identity.isLocalPlayer = localPlayer == identity; + + // .connectionToServer is only available for local players. + // set it here, before invoking any callbacks. + // this way it's available in _all_ callbacks. + if (identity.isLocalPlayer) + identity.connectionToServer = connection; + } + + // invoke NetworkIdentity callbacks on the client. + // needs to be separate from configuring flags. + // cleaner, and some places need to set flags first. + static void InvokeIdentityCallbacks(NetworkIdentity identity) + { + // invoke OnStartClient + identity.OnStartClient(); + + // invoke OnStartAuthority + identity.NotifyAuthority(); + + // invoke OnStartLocalPlayer + if (identity.isLocalPlayer) + identity.OnStartLocalPlayer(); + } + + // client-only mode callbacks ////////////////////////////////////////// + static void OnEntityStateMessage(EntityStateMessage message) + { + // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + identity.DeserializeClient(reader, false); + } + else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: 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:{message.functionHash} netId:{message.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity)) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + identity.HandleRemoteCall(message.componentIndex, message.functionHash, RemoteCallType.ClientRpc, reader); + } + // Rpcs often can't be applied if interest management unspawned them + } + + 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)) + { + if (isSpawnFinished) + { + ApplySpawnPayload(identity, message); + } + else + { + // Defer ApplySpawnPayload until OnObjectSpawnFinished + // add to spawned because later when we ApplySpawnPayload + // there may be SyncVars that cross-reference other objects + + // When deferring ApplySpawnPayload via pendingSpawns until OnObjectSpawnFinished, + // simply copying the SpawnMessage struct isn't sufficient. The payload is an + // ArraySegment referencing the original buffer received from the server, + // managed by the client's NetworkReaderPooled. This buffer may be recycled or + // reused after OnSpawn but before ApplySpawnPayload, leading to corruption + // (e.g., EndOfStreamException in NetworkReader.ReadBlittable when reading past + // available bytes, as seen with 20+ objects in Benchmark). Deep copying payload + // ensures the data remains intact and independent of the reader's pooled buffer + // lifecycle, preventing corruption during deferred application. + byte[] payloadCopy = new byte[message.payload.Count]; + if (message.payload.Count > 0) + Array.Copy(message.payload.Array, message.payload.Offset, payloadCopy, 0, message.payload.Count); + SpawnMessage messageCopy = new SpawnMessage + { + netId = message.netId, + spawnFlags = message.spawnFlags, // Preserves isOwner and isLocalPlayer via flags + sceneId = message.sceneId, + assetId = message.assetId, + position = message.position, + rotation = message.rotation, + scale = message.scale, + payload = new ArraySegment(payloadCopy) + }; + spawned[message.netId] = identity; + pendingSpawns[identity] = messageCopy; + } + } + } + + 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.isOwned = message.isOwner; + + // Add / Remove to client's connectionToServer.owned hashset. + if (identity.isOwned) + connection?.owned.Add(identity); + else + connection?.owned.Remove(identity); + + // Call OnStartAuthority / OnStopAuthority + 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.connectionToServer = connection; + identity.OnStartLocalPlayer(); + } + // 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; + // TODO set .connectionToServer to null for old local player? + // since we set it in the above 'if' case too. + } + } + + // 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.active != null) + Transport.active.ClientEarlyUpdate(); + + // time snapshot interpolation + UpdateTimeInterpolation(); + } + + // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate + // (we add this to the UnityEngine in NetworkLoop) + internal static void NetworkLateUpdate() + { + // broadcast ClientToServer components while active + if (active) + { + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + // + // Unity 2019 doesn't have Time.timeAsDouble yet + bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + if (!Application.isPlaying || sendIntervalElapsed) + { + Broadcast(); + } + + UpdateConnectionQuality(); + } + + // Connection Quality ////////////////////////////////////////////////// + // uses 'pragmatic' version based on snapshot interpolation by default. + void UpdateConnectionQuality() + { + // only recalculate every few seconds + // we don't want to fire Good->Bad->Good->Bad dozens of times per second. + if (connectionQualityInterval > 0 && NetworkTime.time > lastConnectionQualityUpdate + connectionQualityInterval) + { + lastConnectionQualityUpdate = NetworkTime.time; + + switch (connectionQualityMethod) + { + case ConnectionQualityMethod.Simple: + connectionQuality = ConnectionQualityHeuristics.Simple(NetworkTime.rtt, NetworkTime.rttVariance); + break; + case ConnectionQualityMethod.Pragmatic: + connectionQuality = ConnectionQualityHeuristics.Pragmatic(initialBufferTime, bufferTime); + break; + } + + if (lastConnectionQuality != connectionQuality) + { + // Invoke the event before assigning the new value so + // the event handler can compare old and new values. + onConnectionQualityChanged?.Invoke(lastConnectionQuality, connectionQuality); + lastConnectionQuality = connectionQuality; + } + } + } + + // update connections to flush out messages _after_ broadcast + // 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.active != null) + Transport.active.ClientLateUpdate(); + } + + // broadcast /////////////////////////////////////////////////////////// + // make sure Broadcast() is only called every sendInterval. + // calling it every update() would require too much bandwidth. + static void Broadcast() + { + // joined the world yet? + if (!connection.isReady) return; + + // nothing to do in host mode. server already knows the state. + if (NetworkServer.active) return; + + // send time snapshot every sendInterval. + Send(new TimeSnapshotMessage(), Channels.Unreliable); + + // broadcast client state to server + BroadcastToServer(); + } + + // NetworkServer has BroadcastToConnection. + // NetworkClient has BroadcastToServer. + static void BroadcastToServer() + { + // for each entity that the client owns + foreach (NetworkIdentity identity in connection.owned) + { + // make sure it's not null or destroyed. + // (which can happen if someone uses + // GameObject.Destroy instead of + // NetworkServer.Destroy) + if (identity != null) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // get serialization for this entity viewed by this connection + // (if anything was serialized this time) + identity.SerializeClient(writer); + if (writer.Position > 0) + { + // send state update message + EntityStateMessage message = new EntityStateMessage + { + netId = identity.netId, + payload = writer.ToArraySegment() + }; + Send(message); + } + } + } + // spawned list should have no null entries because we + // always call Remove in OnObjectDestroy everywhere. + // if it does have null then we missed something. + else Debug.LogWarning($"Found 'null' entry in owned list for client. This is unexpected behaviour."); + } + } + + // destroy ///////////////////////////////////////////////////////////// + /// 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.ResetState(); + } + // 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.ResetState(); + identity.gameObject.SetActive(false); + } + // spawned objects are destroyed + else + { + GameObject.Destroy(identity.gameObject); + } + } + } + } + } + spawned.Clear(); + connection?.owned.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."); + } + } + + static void DestroyObject(uint netId) + { + // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}"); + if (spawned.TryGetValue(netId, out NetworkIdentity identity) && identity != null) + { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + + identity.OnStopClient(); + + // custom unspawn handler for this prefab? (for prefab pools etc.) + if (InvokeUnSpawnHandler(identity.assetId, identity.gameObject)) + { + // reset object after user's handler + identity.ResetState(); + } + // otherwise fall back to default Destroy + else if (identity.sceneId == 0) + { + // don't call reset before destroy so that values are still set in OnDestroy + GameObject.Destroy(identity.gameObject); + } + // scene object.. disable it in scene instead of destroying + else + { + identity.gameObject.SetActive(false); + spawnableObjects[identity.sceneId] = identity; + // reset for scene objects + identity.ResetState(); + } + + // remove from dictionary no matter how it is unspawned + connection.owned.Remove(identity); // if any + spawned.Remove(netId); + } + //else Debug.LogWarning($"Did not find target for destroy message for {netId}"); + } + + // shutdown //////////////////////////////////////////////////////////// + /// Shutdown the client. + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void Shutdown() + { + //Debug.Log("Shutting down client."); + + // objects need to be destroyed before spawners are cleared + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3334 + DestroyAllClientObjects(); + + // calls prefabs.Clear(); + // calls spawnHandlers.Clear(); + // calls unspawnHandlers.Clear(); + ClearSpawners(); + + spawned.Clear(); + connection?.owned.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.active != null) + Transport.active.ClientDisconnect(); + + // reset statics + connectState = ConnectState.None; + connection = null; + localPlayer = null; + ready = false; + isSpawnFinished = false; + isLoadingScene = false; + lastSendTime = 0; + + 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; + OnTransportExceptionEvent = null; + } + + // GUI ///////////////////////////////////////////////////////////////// + // called from NetworkManager to display timeline interpolation status. + // useful to indicate catchup / slowdown / dynamic adjustment etc. + public static void OnGUI() + { + // only if in world + if (!ready) return; + + GUILayout.BeginArea(new Rect(10, 5, 1020, 50)); + + GUILayout.BeginHorizontal("Box"); + GUILayout.Label("Snapshot Interp.:"); + // color while catching up / slowing down + if (localTimescale > 1) GUI.color = Color.green; // green traffic light = go fast + else if (localTimescale < 1) GUI.color = Color.red; // red traffic light = go slow + else GUI.color = Color.white; + GUILayout.Box($"timeline: {localTimeline:F2}"); + GUILayout.Box($"buffer: {snapshots.Count}"); + GUILayout.Box($"DriftEMA: {NetworkClient.driftEma.Value:F2}"); + GUILayout.Box($"DelTimeEMA: {NetworkClient.deliveryTimeEma.Value:F2}"); + GUILayout.Box($"timescale: {localTimescale:F2}"); + GUILayout.Box($"BTM: {NetworkClient.bufferTimeMultiplier:F2}"); // current dynamically adjusted multiplier + GUILayout.Box($"RTT: {NetworkTime.rtt * 1000:F0}ms"); + GUILayout.Box($"PredErrUNADJ: {NetworkTime.predictionErrorUnadjusted * 1000:F0}ms"); + GUILayout.Box($"PredErrADJ: {NetworkTime.predictionErrorAdjusted * 1000:F0}ms"); + GUILayout.EndHorizontal(); + + GUILayout.EndArea(); + } + } +} diff --git a/Assets/Mirror/Core/NetworkClient.cs.meta b/Assets/Mirror/Core/NetworkClient.cs.meta new file mode 100644 index 0000000..33080e4 --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: abe6be14204d94224a3e7cd99dd2ea73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs new file mode 100644 index 0000000..2578034 --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public static partial class NetworkClient + { + // snapshot interpolation settings ///////////////////////////////////// + // TODO expose the settings to the user later. + // via NetMan or NetworkClientConfig or NetworkClient as component etc. + public static SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings(); + + // snapshot interpolation runtime data ///////////////////////////////// + // buffer time is dynamically adjusted. + // store the current multiplier here, without touching the original in settings. + // this way we can easily reset to or compare with original where needed. + public static double bufferTimeMultiplier; + + // original buffer time based on the settings + // dynamically adjusted buffer time based on dynamically adjusted multiplier + public static double initialBufferTime => NetworkServer.sendInterval * snapshotSettings.bufferTimeMultiplier; + public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + + // + public static SortedList snapshots = new SortedList(); + + // for smooth interpolation, we need to interpolate along server time. + // any other time (arrival on client, client local time, etc.) is not + // going to give smooth results. + // in other words, this is the remote server's time, but adjusted. + // + // internal for use from NetworkTime. + // double for long running servers, see NetworkTime comments. + internal static double localTimeline; + + // catchup / slowdown adjustments are applied to timescale, + // to be adjusted in every update instead of when receiving messages. + internal static double localTimescale = 1; + + // catchup ///////////////////////////////////////////////////////////// + // we use EMA to average the last second worth of snapshot time diffs. + // manually averaging the last second worth of values with a for loop + // would be the same, but a moving average is faster because we only + // ever add one value. + static ExponentialMovingAverage driftEma; + + // dynamic buffer time adjustment ////////////////////////////////////// + // DEPRECATED 2024-10-08 + [Obsolete("NeworkClient.dynamicAdjustment was moved to NetworkClient.snapshotSettings.dynamicAdjustment")] + public static bool dynamicAdjustment => snapshotSettings.dynamicAdjustment; + // DEPRECATED 2024-10-08 + [Obsolete("NeworkClient.dynamicAdjustmentTolerance was moved to NetworkClient.snapshotSettings.dynamicAdjustmentTolerance")] + public static float dynamicAdjustmentTolerance => snapshotSettings.dynamicAdjustmentTolerance; + // DEPRECATED 2024-10-08 + [Obsolete("NeworkClient.dynamicAdjustment was moved to NetworkClient.snapshotSettings.dynamicAdjustment")] + public static int deliveryTimeEmaDuration => snapshotSettings.deliveryTimeEmaDuration; + + static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + + // OnValidate: see NetworkClient.cs + // add snapshot & initialize client interpolation time if needed + + // initialization called from Awake + static void InitTimeInterpolation() + { + // reset timeline, localTimescale & snapshots from last session (if any) + bufferTimeMultiplier = snapshotSettings.bufferTimeMultiplier; + localTimeline = 0; + localTimescale = 1; + snapshots.Clear(); + + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.deliveryTimeEmaDuration); + } + + // server sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. + + // Unity 2019 doesn't have Time.timeAsDouble yet + OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); + } + + // see comments at the top of this file + public static void OnTimeSnapshot(TimeSnapshot snap) + { + // Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}"); + + // (optional) dynamic adjustment + if (snapshotSettings.dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + snapshotSettings.dynamicAdjustmentTolerance + ); + } + + // insert into the buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + snapshotSettings.bufferLimit, + snap, + ref localTimeline, + ref localTimescale, + NetworkServer.sendInterval, + bufferTime, + snapshotSettings.catchupSpeed, + snapshotSettings.slowdownSpeed, + ref driftEma, + snapshotSettings.catchupNegativeThreshold, + snapshotSettings.catchupPositiveThreshold, + ref deliveryTimeEma); + + // Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}"); + } + + // call this from early update, so the timeline is safe to use in update + static void UpdateTimeInterpolation() + { + // only while we have snapshots. + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + // NetworkTime uses unscaled time and ignores Time.timeScale. + // fixes Time.timeScale getting server & client time out of sync: + // https://github.com/MirrorNetworking/Mirror/issues/3409 + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale); + + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } + } +} diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta new file mode 100644 index 0000000..4e836ff --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ad039071a9cc487b9f7831d28bbe8e83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkConnection.cs b/Assets/Mirror/Core/NetworkConnection.cs new file mode 100644 index 0000000..1268ca2 --- /dev/null +++ b/Assets/Mirror/Core/NetworkConnection.cs @@ -0,0 +1,207 @@ +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; + + /// 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; + + /// 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. + // .owned is now valid both on server and on client. + // 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 readonly HashSet owned = new HashSet(); + + // 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; + } + + // 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.active.GetBatchThreshold(channelId); + + // create batcher + batch = new Batcher(threshold); + batches[channelId] = batch; + } + return batch; + } + + // Send stage one: NetworkMessage + /// Send a NetworkMessage to this connection over the given channel. + public void Send(T message, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message + NetworkMessages.Pack(message, writer); + + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkConnection.Send: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + + // send allocation free + 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. + // => make sure to validate message size before calling Send! + [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 + 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 ((int key, Batcher batcher) in batches) // Unity 2020 doesn't support deconstruct yet + foreach (KeyValuePair kvp in batches) + { + // make and send as many batches as necessary from the stored + // messages. + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // make a batch with our local time (double precision) + while (kvp.Value.GetBatch(writer)) + { + // message size is validated in Send, with test coverage. + // we can send directly without checking again. + ArraySegment segment = writer.ToArraySegment(); + + // 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(); + + // cleanup is called before the connection is removed. + // return any batches' pooled writers before the connection disappears. + // otherwise if a connection disappears before flushing, writers would + // never be returned to the pool. + public virtual void Cleanup() + { + foreach (Batcher batcher in batches.Values) + { + batcher.Clear(); + } + } + } +} diff --git a/Assets/Mirror/Core/NetworkConnection.cs.meta b/Assets/Mirror/Core/NetworkConnection.cs.meta new file mode 100644 index 0000000..9516827 --- /dev/null +++ b/Assets/Mirror/Core/NetworkConnection.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 11ea41db366624109af1f0834bcdde2f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkConnection.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkConnectionToClient.cs b/Assets/Mirror/Core/NetworkConnectionToClient.cs new file mode 100644 index 0000000..d80749c --- /dev/null +++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public class NetworkConnectionToClient : NetworkConnection + { + // rpcs are collected in a buffer, and then flushed out together. + // this way we don't need one NetworkMessage per rpc. + // => prepares for LocalWorldState as well. + // ensure max size when adding! + readonly NetworkWriter reliableRpcs = new NetworkWriter(); + readonly NetworkWriter unreliableRpcs = new NetworkWriter(); + + public virtual string address { get; private set; } + + /// 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; + + /// NetworkIdentities that this connection can see + // TODO move to server's NetworkConnectionToClient? + public readonly HashSet observing = new HashSet(); + + // unbatcher + public Unbatcher unbatcher = new Unbatcher(); + + // server runs a time snapshot interpolation for each client's local time. + // this is necessary for client auth movement to still be smooth on the + // server for host mode. + // TODO move them along server's timeline in the future. + // perhaps with an offset. + // for now, keep compatibility by manually constructing a timeline. + ExponentialMovingAverage driftEma; + ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + public double remoteTimeline; + public double remoteTimescale; + double bufferTimeMultiplier = 2; + double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + + // + readonly SortedList snapshots = new SortedList(); + + // Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients. + public int snapshotBufferSizeLimit = 64; + + // ping for rtt (round trip time) + // useful for statistics, lag compensation, etc. + double lastPingTime = 0; + internal ExponentialMovingAverage _rtt = new ExponentialMovingAverage(NetworkTime.PingWindowSize); + + /// Round trip time (in seconds) that it takes a message to go server->client->server. + public double rtt => _rtt.Value; + + internal NetworkConnectionToClient() : base() { } + + public NetworkConnectionToClient(int networkConnectionId, string clientAddress = "localhost") : base() + { + connectionId = networkConnectionId; + address = clientAddress; + + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.deliveryTimeEmaDuration); + + // buffer limit should be at least multiplier to have enough in there + snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.snapshotSettings.bufferTimeMultiplier, snapshotBufferSizeLimit); + } + + public override string ToString() => $"connection({connectionId})"; + + public void OnTimeSnapshot(TimeSnapshot snapshot) + { + // protect against ever growing buffer size attacks + if (snapshots.Count >= snapshotBufferSizeLimit) return; + + // (optional) dynamic adjustment + if (NetworkClient.snapshotSettings.dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + NetworkClient.snapshotSettings.dynamicAdjustmentTolerance + ); + // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} "); + } + + // insert into the server buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + NetworkClient.snapshotSettings.bufferLimit, + snapshot, + ref remoteTimeline, + ref remoteTimescale, + NetworkServer.sendInterval, + bufferTime, + NetworkClient.snapshotSettings.catchupSpeed, + NetworkClient.snapshotSettings.slowdownSpeed, + ref driftEma, + NetworkClient.snapshotSettings.catchupNegativeThreshold, + NetworkClient.snapshotSettings.catchupPositiveThreshold, + ref deliveryTimeEma + ); + } + + public void UpdateTimeInterpolation() + { + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale); + + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } + + // Send stage three: hand off to transport + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => + Transport.active.ServerSend(connectionId, segment, channelId); + + protected virtual void UpdatePing() + { + // localTime (double) instead of Time.time for accuracy over days + if (NetworkTime.localTime >= lastPingTime + NetworkTime.PingInterval) + { + // TODO it would be safer for the server to store the last N + // messages' timestamp and only send a message number. + // This way client's can't just modify the timestamp. + // predictedTime parameter is 0 because the server doesn't predict. + NetworkPingMessage pingMessage = new NetworkPingMessage(NetworkTime.localTime, 0); + Send(pingMessage, Channels.Unreliable); + lastPingTime = NetworkTime.localTime; + } + } + + internal override void Update() + { + UpdatePing(); + base.Update(); + } + + /// 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; + reliableRpcs.Position = 0; + unreliableRpcs.Position = 0; + Transport.active.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) + { + owned.Add(obj); + } + + internal void RemoveOwnedObject(NetworkIdentity obj) + { + owned.Remove(obj); + } + + internal void DestroyOwnedObjects() + { + // create a copy because the list might be modified when destroying + HashSet tmp = new HashSet(owned); + foreach (NetworkIdentity netIdentity in tmp) + { + if (netIdentity != null) + { + // disown scene objects, destroy instantiated objects. + if (netIdentity.sceneId != 0) + NetworkServer.RemovePlayerForConnection(this, RemovePlayerOptions.KeepActive); + else + NetworkServer.Destroy(netIdentity.gameObject); + } + } + + // clear the hashset because we destroyed them all + owned.Clear(); + } + } +} diff --git a/Assets/Mirror/Core/NetworkConnectionToClient.cs.meta b/Assets/Mirror/Core/NetworkConnectionToClient.cs.meta new file mode 100644 index 0000000..d00cd5f --- /dev/null +++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: bb2195f8b29d24f0680a57fde2e9fd09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkConnectionToClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs b/Assets/Mirror/Core/NetworkConnectionToServer.cs similarity index 81% rename from Assets/Mirror/Runtime/NetworkConnectionToServer.cs rename to Assets/Mirror/Core/NetworkConnectionToServer.cs index a1ebc5f..496813d 100644 --- a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs +++ b/Assets/Mirror/Core/NetworkConnectionToServer.cs @@ -5,12 +5,10 @@ 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); + Transport.active.ClientSend(segment, channelId); /// Disconnects this connection. public override void Disconnect() @@ -20,7 +18,7 @@ public override void Disconnect() // TODO remove redundant state. have one source of truth for .ready! isReady = false; NetworkClient.ready = false; - Transport.activeTransport.ClientDisconnect(); + Transport.active.ClientDisconnect(); } } } diff --git a/Assets/Mirror/Core/NetworkConnectionToServer.cs.meta b/Assets/Mirror/Core/NetworkConnectionToServer.cs.meta new file mode 100644 index 0000000..6f95398 --- /dev/null +++ b/Assets/Mirror/Core/NetworkConnectionToServer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 761977cbf38a34ded9dd89de45445675 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkConnectionToServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkDiagnostics.cs b/Assets/Mirror/Core/NetworkDiagnostics.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkDiagnostics.cs rename to Assets/Mirror/Core/NetworkDiagnostics.cs diff --git a/Assets/Mirror/Core/NetworkDiagnostics.cs.meta b/Assets/Mirror/Core/NetworkDiagnostics.cs.meta new file mode 100644 index 0000000..efefe99 --- /dev/null +++ b/Assets/Mirror/Core/NetworkDiagnostics.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c3754b39e5f8740fd93f3337b2c4274e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkDiagnostics.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs new file mode 100644 index 0000000..af60ef3 --- /dev/null +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -0,0 +1,1411 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Mirror.RemoteCalls; +using UnityEngine; +using UnityEngine.Serialization; + +#if UNITY_EDITOR +using UnityEditor; + +#if UNITY_2021_2_OR_NEWER +using UnityEditor.SceneManagement; +#else +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; + + public void ResetWriters() + { + ownerWriter.Position = 0; + observersWriter.Position = 0; + } + } + + /// 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; + + /// isOwned is true on the client if this NetworkIdentity is one of the .owned entities of our connection on the server. + // for example: main player & pets are owned. monsters & npcs aren't. + public bool isOwned { get; internal set; } + + // internal so NetworkManager can reset it from StopClient. + internal bool clientStarted; + + /// The set of network connections (players) that can see this object. + public readonly Dictionary observers = + new Dictionary(); + + /// 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; + + // assetId used to spawn prefabs across the network. + // originally a Guid, but a 4 byte uint is sufficient + // (as suggested by james) + // + // it's also easier to work with for serialization etc. + // serialized and visible in inspector for easier debugging + [SerializeField, HideInInspector] uint _assetId; + + // 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 + // + // 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 uint assetId + { + get + { +#if UNITY_EDITOR + // old UNET comment: + // This is important because sometimes OnValidate does not run + // (like when adding NetworkIdentity to prefab with no child links) + if (_assetId == 0) + SetupIDs(); +#endif + return _assetId; + } + // assetId is set internally when creating or duplicating a prefab + internal set + { + // should never be empty + if (value == 0) + { + Debug.LogError($"Can not set AssetId to empty guid on NetworkIdentity '{name}', old assetId '{_assetId}'"); + return; + } + + // always set it otherwise. + // for new prefabs, it will set from 0 to N. + // for duplicated prefabs, it will set from N to M. + // either way, it's always set to a valid GUID. + _assetId = value; + // Debug.Log($"Setting AssetId on NetworkIdentity '{name}', new assetId '{value:X4}'"); + } + } + + /// 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; + + // get all NetworkBehaviour components + public NetworkBehaviour[] NetworkBehaviours { get; private set; } + + // to save bandwidth, we send one 64 bit dirty mask + // instead of 1 byte index per dirty component. + // which means we can't allow > 64 components (it's enough). + const int MaxNetworkBehaviours = 64; + + // 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. + [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.")] + [FormerlySerializedAs("visible")] + public Visibility visibility = 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() + }; + + // Keep track of all sceneIds to detect scene duplicates + static readonly Dictionary sceneIds = + new Dictionary(); + + // Helper function to handle Command/Rpc + internal void HandleRemoteCall(byte componentIndex, ushort 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}]."); + } + } + + // 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(); + } + + // 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; + } + + /// Gets the NetworkIdentity from the sceneIds dictionary with the corresponding id + public static NetworkIdentity GetSceneIdentity(ulong id) => sceneIds[id]; + + 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 NetworkBehaviour components, including children. + // Some users need NetworkTransform on child bones, etc. + // => Deterministic: https://forum.unity.com/threads/getcomponentsinchildren.4582/#post-33983 + // => Never null. GetComponents returns [] if none found. + // => Include inactive. We need all child components. + NetworkBehaviours = GetComponentsInChildren(true); + ValidateComponents(); + + // initialize each one + for (int i = 0; i < NetworkBehaviours.Length; ++i) + { + NetworkBehaviour component = NetworkBehaviours[i]; + component.netIdentity = this; + component.ComponentIndex = (byte)i; + } + } + + void ValidateComponents() + { + 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); + } + else if (NetworkBehaviours.Length > MaxNetworkBehaviours) + { + Debug.LogError($"NetworkIdentity {name} has too many NetworkBehaviour components: only {MaxNetworkBehaviours} NetworkBehaviour components are allowed in order to save bandwidth.", this); + } + } + + // 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 + DisallowChildNetworkIdentities(); + SetupIDs(); +#endif + } + + // expose our AssetId Guid to uint mapping code in case projects need to map Guids to uint as well. + // this way their projects won't break if we change our mapping algorithm. + // needs to be available at runtime / builds, don't wrap in #if UNITY_EDITOR + public static uint AssetGuidToUint(Guid guid) => (uint)guid.GetHashCode(); // deterministic + +#if UNITY_EDITOR + // child NetworkIdentities are not supported. + // Disallow them and show an error for the user to fix. + // This needs to work for Prefabs & Scene objects, so the previous check + // in NetworkClient.RegisterPrefab is not enough. + void DisallowChildNetworkIdentities() + { +#if UNITY_2020_3_OR_NEWER + NetworkIdentity[] identities = GetComponentsInChildren(true); +#else + NetworkIdentity[] identities = GetComponentsInChildren(); +#endif + if (identities.Length > 1) + { + // always log the next child component so it's easy to fix. + // if there are multiple, then after removing it'll log the next. + Debug.LogError($"'{name}' has another NetworkIdentity component on '{identities[1].name}'. There should only be one NetworkIdentity, and it must be on the root object. Please remove the other one.", this); + } + } + + void AssignAssetID(string path) + { + // only set if not empty. fixes https://github.com/vis2k/Mirror/issues/2765 + if (!string.IsNullOrWhiteSpace(path)) + { + // if we generate the assetId then we MUST be sure to set dirty + // in order to save the prefab object properly. otherwise it + // would be regenerated every time we reopen the prefab. + // -> Undo.RecordObject is the new EditorUtility.SetDirty! + // -> we need to call it before changing. + // + // to verify this, duplicate a prefab and double click to open it. + // add a log message if "_assetId != before_". + // without RecordObject, it'll log every time because it's not saved. + Undo.RecordObject(this, "Assigned AssetId"); + + // uint before = _assetId; + Guid guid = new Guid(AssetDatabase.AssetPathToGUID(path)); + assetId = AssetGuidToUint(guid); + // if (_assetId != before) Debug.Log($"Assigned assetId={assetId} to {name}"); + } + } + + 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) + { + _assetId = 0; + } + // 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; + } + + if (isClient) + { + // ServerChangeScene doesn't send destroy messages. + // some identities may persist in DDOL. + // some are destroyed by scene change. + // if an identity is still in .owned remove it. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3308 + if (NetworkClient.connection != null) + NetworkClient.connection.owned.Remove(this); + + // if an identity is still in .spawned, remove it too. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3324 + // + // however, verify that spawned[netId] is this NetworkIdentity + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3785 + // - server: netId=42 walks out of and back into AOI range in same frame + // - client frame 1: + // on_destroymsg(42) -> NetworkClient.DestroyObject -> GameObject.Destroy(42) // next frame + // on_spawnmsg(42) -> NetworkClient.SpawnPrefab -> Instantiate(42) -> spawned[42]=new_identity + // - client frame 2: + // Unity destroys the old 42 + // NetworkIdentity.OnDestroy removes .spawned[42] which is new_identity not old_identity + // new_identity becomes orphaned + // + // solution: only remove if spawned[netId] is this NetworkIdentity or null + if (NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity entry)) + { + if (entry == this || entry == null) + NetworkClient.spawned.Remove(netId); + } + } + + // workaround for cyclid NI<->NB reference causing memory leaks + // after Destroy. [Credits: BigBoxVR/R.S.] + // TODO report this to Unity! + this.NetworkBehaviours = null; + } + + internal void OnStartServer() + { + 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); + } + } + } + + internal void OnStartClient() + { + if (clientStarted) return; + + clientStarted = 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() + { + // In case this object was destroyed already don't call + // OnStopClient if OnStartClient hasn't been called. + if (!clientStarted) return; + + 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); + } + } + } + + internal static NetworkIdentity previousLocalPlayer = null; + internal void OnStartLocalPlayer() + { + // ensure OnStartLocalPlayer is only called once. + // Room demo would call it multiple times: + // - once from ApplySpawnPayload + // - once from OnObjectSpawnFinished + // + // to reproduce: + // - open room demo, add the 3 scenes to build settings + // - add OnStartLocalPlayer log to RoomPlayer prefab + // - build, run server-only + // - in editor, connect, press ready + // - in server, start game + // - notice multiple OnStartLocalPlayer logs in editor client + // + // 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 + if (previousLocalPlayer == this) + return; + previousLocalPlayer = this; + + 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); + } + } + } + + // build dirty mask for server owner & observers (= all dirty components). + // faster to do it in one iteration instead of iterating separately. + (ulong, ulong) ServerDirtyMasks(bool initialState) + { + ulong ownerMask = 0; + ulong observerMask = 0; + + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour component = components[i]; + ulong nthBit = 1ul << i; + + bool dirty = component.IsDirty(); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for initial, it should always sync owner. + // for delta, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty)) + ownerMask |= nthBit; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) + { + // for initial, it should always sync to observers. + // for delta, only if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (initialState || dirty) + observerMask |= nthBit; + } + } + + return (ownerMask, observerMask); + } + + // build dirty mask for client. + // server always knows initialState, so we don't need it here. + ulong ClientDirtyMask() + { + ulong mask = 0; + + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) + { + // on the client, we need to consider different sync scenarios: + // + // ServerToClient SyncDirection: + // do nothing. + // ClientToServer SyncDirection: + // serialize only if owned. + + // on client, only consider owned components with SyncDirection to server + NetworkBehaviour component = components[i]; + ulong nthBit = 1ul << i; + + if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + // set the n-th bit if dirty + // shifting from small to large numbers is varint-efficient. + if (component.IsDirty()) mask |= nthBit; + } + } + + return mask; + } + + // check if n-th component is dirty. + // in other words, if it has the n-th bit set in the dirty mask. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirty(ulong mask, int index) + { + ulong nthBit = 1ul << index; + return (mask & nthBit) != 0; + } + + // serialize components into writer on the server. + // check ownerWritten/observersWritten to know if anything was written + internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) + { + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // check which components are dirty for owner / observers. + // this is quite complicated with SyncMode + SyncDirection. + // see the function for explanation. + // + // instead of writing a 1 byte index per component, + // we limit components to 64 bits and write one ulong instead. + // the ulong is also varint compressed for minimum bandwidth. + (ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState); + + // if nothing dirty, then don't even write the mask. + // otherwise, every unchanged object would send a 1 byte dirty mask! + if (ownerMask != 0) Compression.CompressVarUInt(ownerWriter, ownerMask); + if (observerMask != 0) Compression.CompressVarUInt(observersWriter, observerMask); + + // serialize all components + // perf: only iterate if either dirty mask has dirty bits. + if ((ownerMask | observerMask) != 0) + { + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour comp = components[i]; + + // is the component dirty for anyone (owner or observers)? + // may be serialized to owner, observer, both, or neither. + // + // OnSerialize should only be called once. + // this is faster, and it cleaner because it may set + // internal state, counters, logs, etc. + // + // previously we always serialized to owner and then copied + // the serialization to observers. however, since + // SyncDirection it's not guaranteed to be in owner anymore. + // so we need to serialize to temporary writer first. + // and then copy as needed. + bool ownerDirty = IsDirty(ownerMask, i); + bool observersDirty = IsDirty(observerMask, i); + if (ownerDirty || observersDirty) + { + // serialize into helper writer + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, initialState); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirty) ownerWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); + } + + // clear dirty bits for the components that we serialized. + // do not clear for _all_ components, only the ones that + // were dirty and had their syncInterval elapsed. + // + // we don't want to clear bits before the syncInterval + // was elapsed, as then they wouldn't be synced. + // + // only clear for delta, not for full (spawn messages). + // otherwise if a player joins, we serialize monster, + // and shouldn't clear dirty bits not yet synced to + // other players. + if (!initialState) comp.ClearAllDirtyBits(); + } + } + } + } + + // serialize components into writer on the client. + internal void SerializeClient(NetworkWriter writer) + { + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // check which components are dirty. + // this is quite complicated with SyncMode + SyncDirection. + // see the function for explanation. + // + // instead of writing a 1 byte index per component, + // we limit components to 64 bits and write one ulong instead. + // the ulong is also varint compressed for minimum bandwidth. + ulong dirtyMask = ClientDirtyMask(); + + // varint compresses the mask to 1 byte in most cases. + // instead of writing an 8 byte ulong. + // 7 components fit into 1 byte. (previously 7 bytes) + // 11 components fit into 2 bytes. (previously 11 bytes) + // 16 components fit into 3 bytes. (previously 16 bytes) + // TODO imer: server knows amount of comps, write N bytes instead + + // if nothing dirty, then don't even write the mask. + // otherwise, every unchanged object would send a 1 byte dirty mask! + if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask); + + // serialize all components + // perf: only iterate if dirty mask has dirty bits. + if (dirtyMask != 0) + { + // serialize all components + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour comp = components[i]; + + // is this component dirty? + // reuse the mask instead of calling comp.IsDirty() again here. + if (IsDirty(dirtyMask, i)) + // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + // serialize into writer. + // server always knows initialState, we never need to send it + comp.Serialize(writer, false); + + // clear dirty bits for the components that we serialized. + // do not clear for _all_ components, only the ones that + // were dirty and had their syncInterval elapsed. + // + // we don't want to clear bits before the syncInterval + // was elapsed, as then they wouldn't be synced. + comp.ClearAllDirtyBits(); + } + } + } + } + + // deserialize components from the client on the server. + // there's no 'initialState'. server always knows the initial state. + internal bool DeserializeServer(NetworkReader reader) + { + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // first we deserialize the varinted dirty mask + ulong mask = Compression.DecompressVarUInt(reader); + + // now deserialize every dirty component + for (int i = 0; i < components.Length; ++i) + { + // was this one dirty? + if (IsDirty(mask, i)) + { + NetworkBehaviour comp = components[i]; + + // safety check to ensure clients can only modify their own + // ClientToServer components, nothing else. + if (comp.syncDirection == SyncDirection.ClientToServer) + { + // deserialize this component + // server always knows the initial state (initial=false) + // disconnect if failed, to prevent exploits etc. + if (!comp.Deserialize(reader, false)) return false; + + // server received state from the owner client. + // set dirty so it's broadcast to other clients too. + // + // note that we set the _whole_ component as dirty. + // everything will be broadcast to others. + // SetSyncVarDirtyBits() would be nicer, but not all + // components use [SyncVar]s. + comp.SetDirty(); + } + } + } + + // successfully deserialized everything + return true; + } + + // deserialize components from server on the client. + internal void DeserializeClient(NetworkReader reader, bool initialState) + { + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // first we deserialize the varinted dirty mask + ulong mask = Compression.DecompressVarUInt(reader); + + // now deserialize every dirty component + for (int i = 0; i < components.Length; ++i) + { + // was this one dirty? + if (IsDirty(mask, i)) + { + // deserialize this component + NetworkBehaviour comp = components[i]; + comp.Deserialize(reader, initialState); + } + } + } + + // get cached serialization for this tick (or serialize if none yet). + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks. + // calls SerializeServer, so this function is to be called on server. + internal NetworkIdentitySerialization GetServerSerializationAtTick(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 +#if UNITY_EDITOR + || !Application.isPlaying +#endif + ) + { + // reset + lastSerialization.ResetWriters(); + + // serialize + SerializeServer(false, + lastSerialization.ownerWriter, + lastSerialization.observersWriter); + + // set tick + lastSerialization.tick = tick; + //Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}"); + } + + // return it + return lastSerialization; + } + + internal void AddObserver(NetworkConnectionToClient conn) + { + 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); + } + + // clear all component's dirty bits no matter what + internal void ClearAllComponentsDirtyBits() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + comp.ClearAllDirtyBits(); + } + } + + // this is used when a connection is destroyed, since the "observers" property is read-only + internal void RemoveObserver(NetworkConnectionToClient conn) + { + observers.Remove(conn.connectionId); + } + + /// 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; + } + + // 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; + } + + /// 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(). + // + // Do not reset SyncObjects from Reset + // - Unspawned objects need to retain their list contents + // - They may be respawned, especially players, but others as well. + // + // 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 ResetState() + { + 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. + isOwned = 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; + } + + bool hadAuthority; + internal void NotifyAuthority() + { + if (!hadAuthority && isOwned) + OnStartAuthority(); + if (hadAuthority && !isOwned) + OnStopAuthority(); + hadAuthority = isOwned; + } + + 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); + } + } + } + + // Called when NetworkIdentity is destroyed + internal void ClearObservers() + { + foreach (NetworkConnectionToClient conn in observers.Values) + { + conn.RemoveFromObserving(this, true); + } + observers.Clear(); + } + } +} diff --git a/Assets/Mirror/Core/NetworkIdentity.cs.meta b/Assets/Mirror/Core/NetworkIdentity.cs.meta new file mode 100644 index 0000000..2ca784b --- /dev/null +++ b/Assets/Mirror/Core/NetworkIdentity.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 9b91ecbcc199f4492b9a91e820070131 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkIdentity.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs b/Assets/Mirror/Core/NetworkLoop.cs similarity index 87% rename from Assets/Mirror/Runtime/NetworkLoop.cs rename to Assets/Mirror/Core/NetworkLoop.cs index 50d9e95..a9cd490 100644 --- a/Assets/Mirror/Runtime/NetworkLoop.cs +++ b/Assets/Mirror/Core/NetworkLoop.cs @@ -26,17 +26,8 @@ // 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 { @@ -45,7 +36,7 @@ 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. + // callbacks for others to hook into if they need Early/LateUpdate. public static Action OnEarlyUpdate; public static Action OnLateUpdate; @@ -69,7 +60,7 @@ internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction fun // recursively keep looking if (playerLoop.subSystemList != null) { - for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + for (int i = 0; i < playerLoop.subSystemList.Length; ++i) { int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType); if (index != -1) return index; @@ -108,6 +99,15 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T //foreach (PlayerLoopSystem sys in playerLoop.subSystemList) // Debug.Log($" ->{sys.type}"); + // make sure the function wasn't added yet. + // with domain reload disabled, it would otherwise be added twice: + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3392 + if (Array.FindIndex(playerLoop.subSystemList, (s => s.updateDelegate == function)) != -1) + { + // loop contains the function, so return true. + return true; + } + // resize & expand subSystemList to fit one more entry int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0; Array.Resize(ref playerLoop.subSystemList, oldListLength + 1); @@ -128,7 +128,6 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T // 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) @@ -148,7 +147,7 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T // recursively keep looking if (playerLoop.subSystemList != null) { - for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + for (int i = 0; i < playerLoop.subSystemList.Length; ++i) { if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode)) return true; @@ -167,12 +166,7 @@ static void RuntimeInitializeOnLoad() // 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 + PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop(); // add NetworkEarlyUpdate to the end of EarlyUpdate so it runs after // any Unity initializations but before the first Update/FixedUpdate @@ -189,6 +183,11 @@ static void RuntimeInitializeOnLoad() static void NetworkEarlyUpdate() { + // loop functions run in edit mode and in play mode. + // however, we only want to call NetworkServer/Client in play mode. + if (!Application.isPlaying) return; + + NetworkTime.EarlyUpdate(); //Debug.Log($"NetworkEarlyUpdate {Time.time}"); NetworkServer.NetworkEarlyUpdate(); NetworkClient.NetworkEarlyUpdate(); @@ -198,6 +197,10 @@ static void NetworkEarlyUpdate() static void NetworkLateUpdate() { + // loop functions run in edit mode and in play mode. + // however, we only want to call NetworkServer/Client in play mode. + if (!Application.isPlaying) return; + //Debug.Log($"NetworkLateUpdate {Time.time}"); // invoke event before mirror does its final late updating. OnLateUpdate?.Invoke(); diff --git a/Assets/Mirror/Core/NetworkLoop.cs.meta b/Assets/Mirror/Core/NetworkLoop.cs.meta new file mode 100644 index 0000000..522313b --- /dev/null +++ b/Assets/Mirror/Core/NetworkLoop.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 2c6cec4e279774b919386e05545317b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkLoop.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs similarity index 79% rename from Assets/Mirror/Runtime/NetworkManager.cs rename to Assets/Mirror/Core/NetworkManager.cs index 37be9ae..ed0494d 100644 --- a/Assets/Mirror/Runtime/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using kcp2k; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Serialization; @@ -10,6 +9,7 @@ namespace Mirror { public enum PlayerSpawnMethod { Random, RoundRobin } public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host } + public enum HeadlessStartOptions { DoNothing, AutoStartServer, AutoStartClient } [DisallowMultipleComponent] [AddComponentMenu("Network/Network Manager")] @@ -29,13 +29,23 @@ public class NetworkManager : MonoBehaviour 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; + [Header("Auto-Start Options")] + + [Tooltip("Choose whether Server or Client should auto-start in headless builds")] + public HeadlessStartOptions headlessStartMode = HeadlessStartOptions.DoNothing; + + [Tooltip("Headless Start Mode in Editor\nwhen enabled, headless start mode will be used in editor as well.")] + public bool editorAutoStart; /// 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; + [Tooltip("Server / Client send rate per second.\nUse 60-100Hz for fast paced games like Counter-Strike to minimize latency.\nUse around 30Hz for games like WoW to minimize computations.\nUse around 1-10Hz for slow paced games like EVE.")] + [FormerlySerializedAs("serverTickRate")] + public int sendRate = 60; + + // client send rate follows server send rate to avoid errors for now + /// Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + // [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] + // public int clientSendRate = 30; // 33 ms /// Automatically switch to this scene upon going offline (on start / on disconnect / on shutdown). [Header("Scene Management")] @@ -50,11 +60,13 @@ public class NetworkManager : MonoBehaviour [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 = ""; + [Range(0, 60), Tooltip("Optional delay that can be used after disconnecting to show a 'Connection lost...' message or similar before loading the offline scene, which may take a long time in big projects.")] + public float offlineSceneLoadDelay = 0; + // transport layer [Header("Network Info")] [Tooltip("Transport component attached to this object that server and client will use to connect")] - [SerializeField] - protected Transport transport; + public Transport transport; /// Server's address for clients to connect to. [FormerlySerializedAs("m_NetworkAddress")] @@ -66,6 +78,15 @@ public class NetworkManager : MonoBehaviour [Tooltip("Maximum number of concurrent connections.")] public int maxConnections = 100; + // Mirror global disconnect inactive option, independent of Transport. + // not all Transports do this properly, and it's easiest to configure this just once. + // this is very useful for some projects, keep it. + [Tooltip("When enabled, the server automatically disconnects inactive connections after the configured timeout.")] + public bool disconnectInactiveConnections; + + [Tooltip("Timeout in seconds for server to automatically disconnect inactive connections if 'disconnectInactiveConnections' is enabled.")] + public float disconnectInactiveTimeout = 60f; + [Header("Authentication")] [Tooltip("Authentication component attached to this object")] public NetworkAuthenticator authenticator; @@ -96,6 +117,25 @@ public class NetworkManager : MonoBehaviour public static List startPositions = new List(); public static int startPositionIndex; + [Header("Security")] + [Tooltip("For security, it is recommended to disconnect a player if a networked action triggers an exception\nThis could prevent components being accessed in an undefined state, which may be an attack vector for exploits.\nHowever, some games may want to allow exceptions in order to not interrupt the player's experience.")] + public bool exceptionsDisconnect = true; // security by default + + [Header("Snapshot Interpolation")] + public SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings(); + + [Header("Connection Quality")] + [Tooltip("Method to use for connection quality evaluation.\nSimple: based on rtt and jitter.\nPragmatic: based on snapshot interpolation adjustment.")] + public ConnectionQualityMethod evaluationMethod; + + [Tooltip("Interval in seconds to evaluate connection quality.\nSet to 0 to disable connection quality evaluation.")] + [Range(0, 60)] + [FormerlySerializedAs("connectionQualityInterval")] + public float evaluationInterval = 3; + + [Header("Interpolation UI - Requires Editor / Dev Build")] + public bool timeInterpolationGui = false; + /// The one and only NetworkManager public static NetworkManager singleton { get; internal set; } @@ -128,7 +168,7 @@ public virtual void OnValidate() // always >= 0 maxConnections = Mathf.Max(maxConnections, 0); - if (playerPrefab != null && playerPrefab.GetComponent() == null) + if (playerPrefab != null && !playerPrefab.TryGetComponent(out NetworkIdentity _)) { Debug.LogError("NetworkManager - Player Prefab must have a NetworkIdentity."); playerPrefab = null; @@ -137,7 +177,7 @@ public virtual void OnValidate() // 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."); + Debug.LogWarning("NetworkManager - Player Prefab doesn't need to be in Spawnable Prefabs list too. Removing it."); spawnPrefabs.Remove(playerPrefab); } } @@ -161,24 +201,6 @@ public virtual void Reset() 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 @@ -187,7 +209,8 @@ 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"); + // Apply configuration in Awake once already + ApplyConfiguration(); // Set the networkSceneName to prevent a scene reload // if client connection to server fails. @@ -200,36 +223,60 @@ public virtual void Awake() // 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. + // Auto-start headless server or client. + // + // We can't do this in Awake because Awake is for initialization + // and some transports might not be ready until Start. // - // (tick rate is applied in StartServer!) -#if UNITY_SERVER - if (autoStartServerBuild) + // Auto-starting in Editor is useful for debugging, so that can + // be enabled with editorAutoStart. + if (Utils.IsHeadless()) { - StartServer(); + if (!Application.isEditor || editorAutoStart) + switch (headlessStartMode) + { + case HeadlessStartOptions.AutoStartServer: + StartServer(); + break; + case HeadlessStartOptions.AutoStartClient: + StartClient(); + break; + } } -#endif } - // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too - public virtual void LateUpdate() + // make sure to call base.Update() when overwriting + public virtual void Update() { - UpdateScene(); + ApplyConfiguration(); } - // keep the online scene change check in a separate function - bool IsServerOnlineSceneChangeNeeded() + // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too + public virtual void LateUpdate() { - // Only change scene if the requested online scene is not blank, and is not already loaded - return !string.IsNullOrWhiteSpace(onlineScene) && !IsSceneActive(onlineScene) && onlineScene != offlineScene; + UpdateScene(); } - public static bool IsSceneActive(string scene) + //////////////////////////////////////////////////////////////////////// + + // keep the online scene change check in a separate function. + // only change scene if the requested online scene is not blank, and is not already loaded. + bool IsServerOnlineSceneChangeNeeded() => + !string.IsNullOrWhiteSpace(onlineScene) && + !Utils.IsSceneActive(onlineScene) && + onlineScene != offlineScene; + + // NetworkManager exposes some NetworkServer/Client configuration. + // we apply it every Update() in order to avoid two sources of truth. + // fixes issues where NetworkServer.sendRate was never set because + // NetworkManager.StartServer was never called, etc. + // => all exposed settings should be applied at all times if NM exists. + void ApplyConfiguration() { - Scene activeScene = SceneManager.GetActiveScene(); - return activeScene.path == scene || activeScene.name == scene; + NetworkServer.tickRate = sendRate; + NetworkClient.snapshotSettings = snapshotSettings; + NetworkClient.connectionQualityInterval = evaluationInterval; + NetworkClient.connectionQualityMethod = evaluationMethod; } // full server setup code, without spawning objects yet @@ -238,6 +285,11 @@ void SetupServer() // Debug.Log("NetworkManager SetupServer"); InitializeSingleton(); + // apply settings before initializing anything + NetworkServer.disconnectInactiveConnections = disconnectInactiveConnections; + NetworkServer.disconnectInactiveTimeout = disconnectInactiveTimeout; + NetworkServer.exceptionsDisconnect = exceptionsDisconnect; + if (runInBackground) Application.runInBackground = true; @@ -252,18 +304,11 @@ void SetupServer() // 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(); + + // do not call OnStartServer here yet. + // this is up to the caller. different for server-only vs. host mode. } /// Starts the server, listening for incoming connections. @@ -295,6 +340,16 @@ public void StartServer() SetupServer(); + // 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(); + // scene change needed? then change scene and spawn afterwards. if (IsServerOnlineSceneChangeNeeded()) { @@ -307,19 +362,14 @@ public void StartServer() } } - /// Starts the client, connects it to the server with networkAddress. - public void StartClient() + void SetupClient() { - if (NetworkClient.active) - { - Debug.LogWarning("Client already started."); - return; - } - - mode = NetworkManagerMode.ClientOnly; - InitializeSingleton(); + // apply settings before initializing anything + NetworkClient.exceptionsDisconnect = exceptionsDisconnect; + // NetworkClient.sendRate = clientSendRate; + if (runInBackground) Application.runInBackground = true; @@ -329,17 +379,33 @@ public void StartClient() authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); } - // In case this is a headless client... - ConfigureHeadlessFrameRate(); + } - RegisterClientMessages(); + /// Starts the client, connects it to the server with networkAddress. + public void StartClient() + { + // Do checks and short circuits before setting anything up. + // If / when we retry, we won't have conflict issues. + if (NetworkClient.active) + { + Debug.LogWarning("Client already started."); + return; + } if (string.IsNullOrWhiteSpace(networkAddress)) { Debug.LogError("Must set the Network Address field in the manager"); return; } - // Debug.Log($"NetworkManager StartClient address:{networkAddress}"); + + mode = NetworkManagerMode.ClientOnly; + + SetupClient(); + + // In case this is a headless client... + ConfigureHeadlessFrameRate(); + + RegisterClientMessages(); NetworkClient.Connect(networkAddress); @@ -357,16 +423,7 @@ public void StartClient(Uri uri) mode = NetworkManagerMode.ClientOnly; - InitializeSingleton(); - - if (runInBackground) - Application.runInBackground = true; - - if (authenticator != null) - { - authenticator.OnStartClient(); - authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); - } + SetupClient(); RegisterClientMessages(); @@ -413,11 +470,6 @@ public void StartHost() // 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 @@ -474,6 +526,21 @@ void FinishStartHost() // TODO call this after spawnobjects and worry about the syncvar hook fix later? NetworkClient.ConnectHost(); + // invoke user callbacks AFTER ConnectHost has set .activeHost. + // this way initialization can properly handle host mode. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3302 + // where [SyncVar] hooks wouldn't be called for objects spawned in + // NetworkManager.OnStartServer, because .activeHost was still false. + // + // TODO is there a risk of someone connecting between Listen() and FinishStartHost()? + OnStartServer(); + + // 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(); + // server scene was loaded. now spawn all the objects NetworkServer.SpawnObjects(); @@ -482,26 +549,12 @@ void FinishStartHost() // 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(); + SetupClient(); RegisterClientMessages(); - // ConnectLocalServer needs to be called AFTER RegisterClientMessages + // InvokeOnConnected needs to be called AFTER RegisterClientMessages // (https://github.com/vis2k/Mirror/pull/1249/) - NetworkClient.ConnectLocalServer(); + HostMode.InvokeOnConnected(); OnStartClient(); } @@ -510,14 +563,6 @@ void StartHostClient() 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(); } @@ -570,50 +615,29 @@ 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; + // For Host client, call OnServerDisconnect before NetworkClient.Disconnect + // because we need NetworkServer.localConnection to not be null + // NetworkClient.Disconnect will set it null. + if (mode == NetworkManagerMode.Host) + OnServerDisconnect(NetworkServer.localConnection); - // shutdown client + // ask client -> transport to disconnect. + // handle voluntary and involuntary disconnects in OnClientDisconnect. + // + // StopClient + // NetworkClient.Disconnect + // Transport.Disconnect + // ... + // Transport.OnClientDisconnect + // NetworkClient.OnTransportDisconnect + // NetworkManager.OnClientDisconnect 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 + // (this can't be in OnDestroy: https://github.com/MirrorNetworking/Mirror/issues/3952) public virtual void OnApplicationQuit() { // stop client first @@ -641,10 +665,11 @@ public virtual void OnApplicationQuit() // 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 + if (Utils.IsHeadless()) + { + Application.targetFrameRate = sendRate; + // Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz."); + } } bool InitializeSingleton() @@ -680,7 +705,21 @@ bool InitializeSingleton() // set active transport AFTER setting singleton. // so only if we didn't destroy ourselves. - Transport.activeTransport = transport; + + // This tries to avoid missing transport errors and more clearly tells user what to fix. + if (transport == null) + if (TryGetComponent(out Transport newTransport)) + { + Debug.LogWarning($"No Transport assigned to Network Manager - Using {newTransport} found on same object."); + transport = newTransport; + } + else + { + Debug.LogError("No Transport on Network Manager...add a transport and assign it."); + return false; + } + + Transport.active = transport; return true; } @@ -689,6 +728,7 @@ void RegisterServerMessages() NetworkServer.OnConnectedEvent = OnServerConnectInternal; NetworkServer.OnDisconnectedEvent = OnServerDisconnect; NetworkServer.OnErrorEvent = OnServerError; + NetworkServer.OnTransportExceptionEvent = OnServerTransportException; NetworkServer.RegisterHandler(OnServerAddPlayerInternal); // Network Server initially registers its own handler for this, so we replace it here. @@ -700,7 +740,10 @@ void RegisterClientMessages() NetworkClient.OnConnectedEvent = OnClientConnectInternal; NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal; NetworkClient.OnErrorEvent = OnClientError; - NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal); + NetworkClient.OnTransportExceptionEvent = OnClientTransportException; + + // Don't require authentication because server may send NotReadyMessage from ServerChangeScene + NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal, false); NetworkClient.RegisterHandler(OnClientSceneInternal, false); if (playerPrefab != null) @@ -763,6 +806,14 @@ public virtual void ServerChangeScene(string newSceneName) return; } + // Throw error if called from client + // Allow changing scene while stopping the server + if (!NetworkServer.active && newSceneName != offlineScene) + { + Debug.LogError("ServerChangeScene can only be called on an active server."); + return; + } + // Debug.Log($"ServerChangeScene {newSceneName}"); NetworkServer.SetAllClientsNotReady(); networkSceneName = newSceneName; @@ -781,7 +832,10 @@ public virtual void ServerChangeScene(string newSceneName) if (NetworkServer.active) { // notify all clients about the new scene - NetworkServer.SendToAll(new SceneMessage { sceneName = newSceneName }); + NetworkServer.SendToAll(new SceneMessage + { + sceneName = newSceneName + }); } startPositionIndex = 0; @@ -955,10 +1009,6 @@ void FinishLoadSceneHost() 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; } @@ -992,13 +1042,7 @@ void FinishLoadSceneHost() 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 - } + OnClientSceneChanged(); } } @@ -1024,21 +1068,12 @@ void FinishLoadSceneClientOnly() 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 - } + OnClientSceneChanged(); } /// @@ -1070,7 +1105,7 @@ public static void UnRegisterStartPosition(Transform start) } /// Get the next NetworkStartPosition based on the selected PlayerSpawnMethod. - public Transform GetStartPosition() + public virtual Transform GetStartPosition() { // first remove any dead transforms startPositions.RemoveAll(t => t == null); @@ -1118,7 +1153,10 @@ void OnServerAuthenticated(NetworkConnectionToClient conn) // proceed with the login handshake by calling OnServerConnect if (networkSceneName != "" && networkSceneName != offlineScene) { - SceneMessage msg = new SceneMessage() { sceneName = networkSceneName }; + SceneMessage msg = new SceneMessage() + { + sceneName = networkSceneName + }; conn.Send(msg); } @@ -1141,7 +1179,7 @@ void OnServerAddPlayerInternal(NetworkConnectionToClient conn, AddPlayerMessage return; } - if (autoCreatePlayer && playerPrefab.GetComponent() == null) + if (autoCreatePlayer && !playerPrefab.TryGetComponent(out NetworkIdentity _)) { Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); return; @@ -1180,39 +1218,88 @@ void 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)) + // Set flag to wait for scene change? + if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || Utils.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. + // Scene message expected from server. clientLoadedScene = true; clientReadyConnection = NetworkClient.connection; } + + // Call virtual method regardless of whether a scene change is expected or not. + OnClientConnect(); } + // Transport callback, invoked after client fully disconnected. + // the call order should always be: + // Disconnect() -> ask Transport -> Transport.OnDisconnected -> Cleanup 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 + + // Only let this run once. StopClient in Host mode changes to ServerOnly + if (mode == NetworkManagerMode.ServerOnly || mode == NetworkManagerMode.Offline) + return; + + // user callback + OnClientDisconnect(); + + if (authenticator != null) + { + authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); + authenticator.OnStopClient(); + } + + // set mode BEFORE changing scene so FinishStartScene doesn't re-initialize anything. + // set mode BEFORE NetworkClient.Disconnect so StopClient only runs once. + // set mode BEFORE OnStopClient so StopClient only runs once. + // If we got here from StopClient in Host mode, change to ServerOnly. + // - If StopHost was called, StopServer will put us in Offline mode. + if (mode == NetworkManagerMode.Host) + mode = NetworkManagerMode.ServerOnly; + else + mode = NetworkManagerMode.Offline; + + //Debug.Log("NetworkManager StopClient"); + OnStopClient(); + + // shutdown client + NetworkClient.Shutdown(); + + // Exit here if we're now in ServerOnly mode (StopClient called in Host mode). + if (mode == NetworkManagerMode.ServerOnly) return; + + // 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()); + + // If StopHost called in Host mode, StopServer will change scenes after this. + // 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) && !Utils.IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) + Invoke(nameof(ClientChangeOfflineScene), offlineSceneLoadDelay); + + networkSceneName = ""; } + // wrap ClientChangeScene call without parameters for use in Invoke. + void ClientChangeOfflineScene() => + ClientChangeScene(offlineScene, SceneOperation.Normal); + 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. @@ -1224,13 +1311,11 @@ void OnClientSceneInternal(SceneMessage msg) // 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) {} + public virtual void OnServerConnect(NetworkConnectionToClient conn) { } /// Called on the server when a client disconnects. // Called by NetworkServer.OnTransportDisconnect! @@ -1270,13 +1355,16 @@ public virtual void OnServerAddPlayer(NetworkConnectionToClient conn) } /// Called on server when transport raises an exception. NetworkConnection may be null. - public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) {} + public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) { } + + /// Called on server when transport raises an exception. NetworkConnection may be null. + public virtual void OnServerTransportException(NetworkConnectionToClient conn, Exception exception) { } /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed - public virtual void OnServerChangeScene(string newSceneName) {} + public virtual void OnServerChangeScene(string newSceneName) { } /// Called on server after a scene load with ServerChangeScene() is completed. - public virtual void OnServerSceneChanged(string sceneName) {} + 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() @@ -1296,36 +1384,21 @@ public virtual void OnClientConnect() } } - // 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(); - } + public virtual void OnClientDisconnect() { } - // 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(TransportError error, string reason) { } /// Called on client when transport raises an exception. - public virtual void OnClientError(Exception exception) {} + public virtual void OnClientTransportException(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) {} + public virtual void OnClientNotReady() { } /// 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) {} + 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 @@ -1334,41 +1407,46 @@ public virtual void OnClientChangeScene(string newSceneName, SceneOperation scen public virtual void OnClientSceneChanged() { // always become ready. - if (!NetworkClient.ready) NetworkClient.Ready(); + if (NetworkClient.connection.isAuthenticated && !NetworkClient.ready) NetworkClient.Ready(); // Only call AddPlayer for normal scene changes, not additive load/unload - if (clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) + if (NetworkClient.connection.isAuthenticated && 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() {} + public virtual void OnStartHost() { } /// This is invoked when a server is started - including when a host is started. - public virtual void OnStartServer() {} + public virtual void OnStartServer() { } /// This is invoked when the client is started. - public virtual void OnStartClient() {} + public virtual void OnStartClient() { } /// This is called when a server is stopped - including when a host is stopped. - public virtual void OnStopServer() {} + public virtual void OnStopServer() { } /// This is called when a client is stopped. - public virtual void OnStopClient() {} + public virtual void OnStopClient() { } /// This is called when a host is stopped. - public virtual void OnStopHost() {} + public virtual void OnStopHost() { } + +#if DEBUG + // keep OnGUI even in builds. useful to debug snap interp. + void OnGUI() + { + if (!timeInterpolationGui) return; + NetworkClient.OnGUI(); + } +#endif } } diff --git a/Assets/Mirror/Core/NetworkManager.cs.meta b/Assets/Mirror/Core/NetworkManager.cs.meta new file mode 100644 index 0000000..6e81bbe --- /dev/null +++ b/Assets/Mirror/Core/NetworkManager.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkManager.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkManagerHUD.cs b/Assets/Mirror/Core/NetworkManagerHUD.cs new file mode 100644 index 0000000..f78eede --- /dev/null +++ b/Assets/Mirror/Core/NetworkManagerHUD.cs @@ -0,0 +1,162 @@ +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() + { + // If this width is changed, also change offsetX in GUIConsole::OnGUI + int width = 300; + + GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, width, 9999)); + + if (!NetworkClient.isConnected && !NetworkServer.active) + StartButtons(); + else + StatusLabels(); + + if (NetworkClient.isConnected && !NetworkClient.ready) + { + if (GUILayout.Button("Client Ready")) + { + // client ready + NetworkClient.Ready(); + if (NetworkClient.localPlayer == null) + NetworkClient.AddPlayer(); + } + } + + StopButtons(); + + GUILayout.EndArea(); + } + + void StartButtons() + { + if (!NetworkClient.active) + { +#if UNITY_WEBGL + // cant be a server in webgl build + if (GUILayout.Button("Single Player")) + { + NetworkServer.listen = false; + manager.StartHost(); + } +#else + // Server + Client + if (GUILayout.Button("Host (Server + Client)")) + manager.StartHost(); +#endif + + // Client + IP (+ PORT) + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Client")) + manager.StartClient(); + + manager.networkAddress = GUILayout.TextField(manager.networkAddress); + // only show a port field if we have a port transport + // we can't have "IP:PORT" in the address field since this only + // works for IPV4:PORT. + // for IPV6:PORT it would be misleading since IPV6 contains ":": + // 2001:0db8:0000:0000:0000:ff00:0042:8329 + if (Transport.active is PortTransport portTransport) + { + // use TryParse in case someone tries to enter non-numeric characters + if (ushort.TryParse(GUILayout.TextField(portTransport.Port.ToString()), out ushort port)) + portTransport.Port = port; + } + + GUILayout.EndHorizontal(); + + // Server Only +#if UNITY_WEBGL + // cant be a server in webgl build + GUILayout.Box("( WebGL cannot be server )"); +#else + if (GUILayout.Button("Server Only")) + manager.StartServer(); +#endif + } + 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) + { + // host mode + GUILayout.Label($"Host: running via {Transport.active}"); + } + else if (NetworkServer.active) + { + // server only + GUILayout.Label($"Server: running via {Transport.active}"); + } + else if (NetworkClient.isConnected) + { + // client only + GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.active}"); + } + } + + void StopButtons() + { + if (NetworkServer.active && NetworkClient.isConnected) + { + GUILayout.BeginHorizontal(); +#if UNITY_WEBGL + if (GUILayout.Button("Stop Single Player")) + manager.StopHost(); +#else + // stop host if host mode + if (GUILayout.Button("Stop Host")) + manager.StopHost(); + + // stop client if host mode, leaving server up + if (GUILayout.Button("Stop Client")) + manager.StopClient(); +#endif + GUILayout.EndHorizontal(); + } + else if (NetworkClient.isConnected) + { + // stop client if client-only + if (GUILayout.Button("Stop Client")) + manager.StopClient(); + } + else if (NetworkServer.active) + { + // stop server if server-only + if (GUILayout.Button("Stop Server")) + manager.StopServer(); + } + } + } +} diff --git a/Assets/Mirror/Core/NetworkManagerHUD.cs.meta b/Assets/Mirror/Core/NetworkManagerHUD.cs.meta new file mode 100644 index 0000000..fb72f54 --- /dev/null +++ b/Assets/Mirror/Core/NetworkManagerHUD.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6442dc8070ceb41f094e44de0bf87274 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkManagerHUD.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkMessage.cs b/Assets/Mirror/Core/NetworkMessage.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkMessage.cs rename to Assets/Mirror/Core/NetworkMessage.cs diff --git a/Assets/Mirror/Core/NetworkMessage.cs.meta b/Assets/Mirror/Core/NetworkMessage.cs.meta new file mode 100644 index 0000000..3afc348 --- /dev/null +++ b/Assets/Mirror/Core/NetworkMessage.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: eb04e4848a2e4452aa2dbd7adb801c51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkMessage.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkMessages.cs b/Assets/Mirror/Core/NetworkMessages.cs new file mode 100644 index 0000000..7a70d94 --- /dev/null +++ b/Assets/Mirror/Core/NetworkMessages.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using UnityEngine; + +namespace Mirror +{ + // for performance, we (ab)use c# generics to cache the message id in a static field + // this is significantly faster than doing the computation at runtime or looking up cached results via Dictionary + // generic classes have separate static fields per type specification + public static class NetworkMessageId where T : struct, NetworkMessage + { + // automated message id from type hash. + // platform independent via stable hashcode. + // => convenient so we don't need to track messageIds across projects + // => addons can work with each other without knowing their ids before + // => 2 bytes is enough to avoid collisions. + // registering a messageId twice will log a warning anyway. + public static readonly ushort Id = CalculateId(); + + // Gets the 32bit fnv1a hash + // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort + // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits + // This will create a more uniform 16bit hash, the method is described in: + // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding" + static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16(); + } + + // 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 NetworkMessages + { + // size of message id header in bytes + public const int IdSize = sizeof(ushort); + + // Id <> Type lookup for debugging, profiler, etc. + // important when debugging messageId errors! + public static readonly Dictionary Lookup = + new Dictionary(); + + // dump all types for debugging + public static void LogTypes() + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine("NetworkMessageIds:"); + foreach (KeyValuePair kvp in Lookup) + { + builder.AppendLine($" Id={kvp.Key} = {kvp.Value}"); + } + Debug.Log(builder.ToString()); + } + + // 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 one timestamp per batch + // -> Every message in a batch has a varuint size header. + // use the worst case VarUInt size for the largest possible + // message size = int.max. + public static int MaxContentSize(int channelId) + { + // calculate the max possible size that can fit in a batch + int transportMax = Transport.active.GetMaxPacketSize(channelId); + return transportMax - IdSize - Batcher.MaxMessageOverhead(transportMax); + } + + // max message size which includes header + content. + public static int MaxMessageSize(int channelId) => + MaxContentSize(channelId) + IdSize; + + // automated message id from type hash. + // platform independent via stable hashcode. + // => convenient so we don't need to track messageIds across projects + // => addons can work with each other without knowing their ids before + // => 2 bytes is enough to avoid collisions. + // registering a messageId twice will log a warning anyway. + // keep this for convenience. easier to use than NetworkMessageId.Id. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort GetId() where T : struct, NetworkMessage => + NetworkMessageId.Id; + + // 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 + { + writer.WriteUShort(NetworkMessageId.Id); + writer.Write(message); + } + + // read only the message id. + // common function in case we ever change the header size. + public static bool UnpackId(NetworkReader reader, out ushort messageId) + { + // read message type + try + { + messageId = reader.ReadUShort(); + return true; + } + catch (System.IO.EndOfStreamException) + { + messageId = 0; + return false; + } + } + + // version for handlers with channelId + // inline! only exists for 20-30 messages and they call it all the time. + internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication, bool exceptionsDisconnect) + 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($"Disconnecting 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) + { + // should we disconnect on exceptions? + if (exceptionsDisconnect) + { + Debug.LogError($"Disconnecting connection: {conn} because reading a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); + conn.Disconnect(); + return; + } + // otherwise log it but allow the connection to keep playing + else + { + Debug.LogError($"Caught an Exception when reading a message from: {conn} of type {typeof(T)}. Reason: {exception}"); + 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 exception) + { + // should we disconnect on exceptions? + if (exceptionsDisconnect) + { + Debug.LogError($"Disconnecting connection: {conn} because handling a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); + conn.Disconnect(); + } + // otherwise log it but allow the connection to keep playing + else + { + Debug.LogError($"Caught an Exception when handling a message from: {conn} of type {typeof(T)}. Reason: {exception}"); + } + } + }; + + // 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, bool exceptionsDisconnect) + 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, exceptionsDisconnect); + } + } +} diff --git a/Assets/Mirror/Core/NetworkMessages.cs.meta b/Assets/Mirror/Core/NetworkMessages.cs.meta new file mode 100644 index 0000000..d2600a3 --- /dev/null +++ b/Assets/Mirror/Core/NetworkMessages.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 2db134099f0df4d96a84ae7a0cd9b4bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkMessages.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkReader.cs b/Assets/Mirror/Core/NetworkReader.cs new file mode 100644 index 0000000..82fb7cd --- /dev/null +++ b/Assets/Mirror/Core/NetworkReader.cs @@ -0,0 +1,249 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +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. + // + // Note that NetworkWriter can be passed in constructor thanks to implicit + // ArraySegment conversion: + // NetworkReader reader = new NetworkReader(writer); + public class NetworkReader + { + // internal buffer + // byte[] pointer would work, but we use ArraySegment to also support + // the ArraySegment constructor + internal 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; + + /// Remaining bytes that can be read, for convenience. + public int Remaining => buffer.Count - Position; + + /// Total buffer capacity, independent of reader position. + public int Capacity => buffer.Count; + + // cache encoding for ReadString instead of creating it with each time + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + // member(!) to avoid static state. + // + // throwOnInvalidBytes is true. + // if false, it would silently ignore the invalid bytes but continue + // with the valid ones, creating strings like "a�������". + // instead, we want to catch it manually and return String.Empty. + // this is safer. see test: ReadString_InvalidUTF8(). + internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true); + + // while allocation free ReadArraySegment is encouraged, + // some functions can allocate a new byte[], List, Texture, etc. + // we should keep a reasonable allocation size limit: + // -> server won't accidentally allocate 2GB on a mobile device + // -> client won't allocate 2GB on server for ClientToServer [SyncVar]s + // -> unlike max string length of 64 KB, we need a larger limit here. + // large enough to not break existing projects, + // small enough to reasonably limit allocation attacks. + // -> we don't know the exact size of ReadList etc. because is + // managed. instead, this is considered a 'collection length' limit. + public const int AllocationLimit = 1024 * 1024 * 16; // 16 MB * sizeof(T) + + public NetworkReader(ArraySegment segment) + { + buffer = segment; + } + +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have the implicit byte[] to segment conversion yet + public NetworkReader(byte[] bytes) + { + buffer = new ArraySegment(bytes, 0, bytes.Length); + } +#endif + + // 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(ArraySegment segment) + { + buffer = segment; + Position = 0; + } + +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have the implicit byte[] to segment conversion yet + public void SetBuffer(byte[] bytes) + { + buffer = new ArraySegment(bytes, 0, bytes.Length); + Position = 0; + } +#endif + + // 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. + // + // This is not safe to expose to random structs. + // * StructLayout.Sequential is the default, which is safe. + // if the struct contains a reference type, it is converted to Auto. + // but since all structs here are unmanaged blittable, it's safe. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential + // * StructLayout.Pack depends on CPU word size. + // this may be different 4 or 8 on some ARM systems, etc. + // this is not safe, and would cause bytes/shorts etc. to be padded. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0 + // * If we force pack all to '1', they would have no padding which is + // great for bandwidth. but on some android systems, CPU can't read + // unaligned memory. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // * The only option would be to force explicit layout with multiples + // of word size. but this requires lots of weaver checking and is + // still questionable (IL2CPP etc.). + // + // Note: inlining ReadBlittable is enough. don't inline ReadInt etc. + // we don't want ReadBlittable to be copied in place everywhere. + 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); + + // ensure remaining + if (Remaining < size) + { + throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> not enough data in buffer to read {size} bytes: {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?); + + public byte ReadByte() => ReadBlittable(); + + /// Read 'count' bytes into the bytes array + // NOTE: returns byte[] because all reader functions return something. + public byte[] ReadBytes(byte[] bytes, int count) + { + // user may call ReadBytes(ReadInt()). ensure positive count. + if (count < 0) throw new ArgumentOutOfRangeException("ReadBytes requires count >= 0"); + + // 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}"); + } + // ensure remaining + if (Remaining < 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. + public ArraySegment ReadBytesSegment(int count) + { + // user may call ReadBytes(ReadInt()). ensure positive count. + if (count < 0) throw new ArgumentOutOfRangeException("ReadBytesSegment requires count >= 0"); + + // ensure remaining + if (Remaining < 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; + } + + /// Reads any data type that mirror supports. Uses weaver populated Reader(T).read + 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 extension for {typeof(T)}."); + return default; + } + return readerDelegate(this); + } + + // print the full buffer with position / capacity. + public override string ToString() => + $"[{buffer.ToHexString()} @ {Position}/{Capacity}]"; + } + + /// 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/Core/NetworkReader.cs.meta b/Assets/Mirror/Core/NetworkReader.cs.meta new file mode 100644 index 0000000..f1bd5bd --- /dev/null +++ b/Assets/Mirror/Core/NetworkReader.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 1610f05ec5bd14d6882e689f7372596a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReader.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkReaderExtensions.cs b/Assets/Mirror/Core/NetworkReaderExtensions.cs new file mode 100644 index 0000000..dd366f6 --- /dev/null +++ b/Assets/Mirror/Core/NetworkReaderExtensions.cs @@ -0,0 +1,420 @@ +using System; +using System.Collections.Generic; +using System.IO; +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 + { + public static byte ReadByte(this NetworkReader reader) => reader.ReadBlittable(); + public static byte? ReadByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static sbyte ReadSByte(this NetworkReader reader) => reader.ReadBlittable(); + public static sbyte? ReadSByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + // bool is not blittable. read as ushort. + public static char ReadChar(this NetworkReader reader) => (char)reader.ReadBlittable(); + public static char? ReadCharNullable(this NetworkReader reader) => (char?)reader.ReadBlittableNullable(); + + // bool is not blittable. read as byte. + public static bool ReadBool(this NetworkReader reader) => reader.ReadBlittable() != 0; + public static bool? ReadBoolNullable(this NetworkReader reader) + { + byte? value = reader.ReadBlittableNullable(); + return value.HasValue ? (value.Value != 0) : default(bool?); + } + + public static short ReadShort(this NetworkReader reader) => (short)reader.ReadUShort(); + public static short? ReadShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static ushort ReadUShort(this NetworkReader reader) => reader.ReadBlittable(); + public static ushort? ReadUShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static int ReadInt(this NetworkReader reader) => reader.ReadBlittable(); + public static int? ReadIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static uint ReadUInt(this NetworkReader reader) => reader.ReadBlittable(); + public static uint? ReadUIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static long ReadLong(this NetworkReader reader) => reader.ReadBlittable(); + public static long? ReadLongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable(); + public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + // ReadInt/UInt/Long/ULong writes full bytes by default. + // define additional "VarInt" versions that Weaver will automatically prefer. + // 99% of the time [SyncVar] ints are small values, which makes this very much worth it. + [WeaverPriority] public static int ReadVarInt(this NetworkReader reader) => (int)Compression.DecompressVarInt(reader); + [WeaverPriority] public static uint ReadVarUInt(this NetworkReader reader) => (uint)Compression.DecompressVarUInt(reader); + [WeaverPriority] public static long ReadVarLong(this NetworkReader reader) => Compression.DecompressVarInt(reader); + [WeaverPriority] public static ulong ReadVarULong(this NetworkReader reader) => Compression.DecompressVarUInt(reader); + + public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable(); + public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static double ReadDouble(this NetworkReader reader) => reader.ReadBlittable(); + public static double? ReadDoubleNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static decimal ReadDecimal(this NetworkReader reader) => reader.ReadBlittable(); + public static decimal? ReadDecimalNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Half ReadHalf(this NetworkReader reader) => new Half(reader.ReadUShort()); + + /// if an invalid utf8 string is sent + public static string ReadString(this NetworkReader reader) + { + // read number of bytes + ushort size = reader.ReadUShort(); + + // null support, see NetworkWriter + if (size == 0) + return null; + + ushort realSize = (ushort)(size - 1); + + // make sure it's within limits to avoid allocation attacks etc. + if (realSize > NetworkWriter.MaxStringLength) + throw new EndOfStreamException($"NetworkReader.ReadString - Value too long: {realSize} bytes. Limit is: {NetworkWriter.MaxStringLength} bytes"); + + ArraySegment data = reader.ReadBytesSegment(realSize); + + // convert directly from buffer to string via encoding + // throws in case of invalid utf8. + // see test: ReadString_InvalidUTF8() + return reader.encoding.GetString(data.Array, data.Offset, data.Count); + } + + public static byte[] ReadBytes(this NetworkReader reader, int count) + { + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + if (count > NetworkReader.AllocationLimit) + { + // throw EndOfStream for consistency with ReadBlittable when out of data + throw new EndOfStreamException($"NetworkReader attempted to allocate {count} bytes, which is larger than the allowed limit of {NetworkReader.AllocationLimit} bytes."); + } + + byte[] bytes = new byte[count]; + reader.ReadBytes(bytes, count); + return bytes; + } + + /// if count is invalid + public static byte[] ReadBytesAndSize(this NetworkReader reader) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint count = (uint)Compression.DecompressVarUInt(reader); + // 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))); + } + // Reads ArraySegment and size header + /// if count is invalid + public static ArraySegment ReadArraySegmentAndSize(this NetworkReader reader) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint count = (uint)Compression.DecompressVarUInt(reader); + // 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))); + } + + public static Vector2 ReadVector2(this NetworkReader reader) => reader.ReadBlittable(); + public static Vector2? ReadVector2Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Vector3 ReadVector3(this NetworkReader reader) => reader.ReadBlittable(); + public static Vector3? ReadVector3Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Vector4 ReadVector4(this NetworkReader reader) => reader.ReadBlittable(); + public static Vector4? ReadVector4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Vector2Int ReadVector2Int(this NetworkReader reader) => reader.ReadBlittable(); + public static Vector2Int? ReadVector2IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Vector3Int ReadVector3Int(this NetworkReader reader) => reader.ReadBlittable(); + public static Vector3Int? ReadVector3IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Color ReadColor(this NetworkReader reader) => reader.ReadBlittable(); + public static Color? ReadColorNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Color32 ReadColor32(this NetworkReader reader) => reader.ReadBlittable(); + public static Color32? ReadColor32Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Quaternion ReadQuaternion(this NetworkReader reader) => reader.ReadBlittable(); + public static Quaternion? ReadQuaternionNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + // Rect is a struct with properties instead of fields + public static Rect ReadRect(this NetworkReader reader) => new Rect(reader.ReadVector2(), reader.ReadVector2()); + public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRect(reader) : default(Rect?); + + // Plane is a struct with properties instead of fields + public static Plane ReadPlane(this NetworkReader reader) => new Plane(reader.ReadVector3(), reader.ReadFloat()); + public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBool() ? ReadPlane(reader) : default(Plane?); + + // Ray is a struct with properties instead of fields + public static Ray ReadRay(this NetworkReader reader) => new Ray(reader.ReadVector3(), reader.ReadVector3()); + public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRay(reader) : default(Ray?); + + // LayerMask is a struct with properties instead of fields + public static LayerMask ReadLayerMask(this NetworkReader reader) + { + // LayerMask doesn't have a constructor that takes an initial value. + // 32 layers as a flags enum, max value of 496, we only need a UShort. + LayerMask layerMask = default; + layerMask.value = reader.ReadUShort(); + return layerMask; + } + + public static LayerMask? ReadLayerMaskNullable(this NetworkReader reader) => reader.ReadBool() ? ReadLayerMask(reader) : default(LayerMask?); + + public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader) => reader.ReadBlittable(); + public static Matrix4x4? ReadMatrix4x4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + + public static Guid ReadGuid(this NetworkReader reader) + { +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have Span yet + return new Guid(reader.ReadBytes(16)); +#else + // ReadBlittable(Guid) isn't safe. see ReadBlittable comments. + // Guid is Sequential, but we can't guarantee packing. + if (reader.Remaining >= 16) + { + ReadOnlySpan span = new ReadOnlySpan(reader.buffer.Array, reader.buffer.Offset + reader.Position, 16); + reader.Position += 16; + return new Guid(span); + } + throw new EndOfStreamException($"ReadGuid out of range: {reader}"); +#endif + } + public static Guid? ReadGuidNullable(this NetworkReader reader) => reader.ReadBool() ? ReadGuid(reader) : default(Guid?); + + 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); + } + + 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; + } + + public static T ReadNetworkBehaviour(this NetworkReader reader) where T : NetworkBehaviour + { + return reader.ReadNetworkBehaviour() as T; + } + + public static 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 NetworkBehaviourSyncVar(netId, componentIndex); + } + + 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; + } + + 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; + } + + // while SyncList is recommended for NetworkBehaviours, + // structs may have .List members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Readers/GenerateReader() handles this manually. + public static List ReadList(this NetworkReader reader) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint length = (uint)Compression.DecompressVarUInt(reader); + // uint length = reader.ReadUInt(); + if (length == 0) return null; + length -= 1; + + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + if (length > NetworkReader.AllocationLimit) + { + // throw EndOfStream for consistency with ReadBlittable when out of data + throw new EndOfStreamException($"NetworkReader attempted to allocate a List<{typeof(T)}> {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}."); + } + + List result = new List((checked((int)length))); + for (int i = 0; i < length; i++) + { + result.Add(reader.Read()); + } + return result; + } + + // while SyncSet is recommended for NetworkBehaviours, + // structs may have .Set members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Readers/GenerateReader() handles this manually. + public static HashSet ReadHashSet(this NetworkReader reader) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint length = (uint)Compression.DecompressVarUInt(reader); + //uint length = reader.ReadUInt(); + if (length == 0) return null; + length -= 1; + + HashSet result = new HashSet(); + for (int i = 0; i < length; i++) + { + result.Add(reader.Read()); + } + return result; + } + + public static T[] ReadArray(this NetworkReader reader) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint length = (uint)Compression.DecompressVarUInt(reader); + //uint length = reader.ReadUInt(); + if (length == 0) return null; + length -= 1; + + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + if (length > NetworkReader.AllocationLimit) + { + // throw EndOfStream for consistency with ReadBlittable when out of data + throw new EndOfStreamException($"NetworkReader attempted to allocate an Array<{typeof(T)}> with {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}."); + } + + // we can't check if reader.Remaining < length, + // because we don't know sizeof(T) since it's a managed type. + // if (length > reader.Remaining) 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; + } + + public static Uri ReadUri(this NetworkReader reader) + { + string uriString = reader.ReadString(); + return (string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString)); + } + + public static Texture2D ReadTexture2D(this NetworkReader reader) + { + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + short width = reader.ReadShort(); + if (width == -1) return null; + + // read height + short height = reader.ReadShort(); + + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + // log an error and return default. + // we don't want attackers to be able to trigger exceptions. + int totalSize = width * height; + if (totalSize > NetworkReader.AllocationLimit) + { + Debug.LogWarning($"NetworkReader attempted to allocate a Texture2D with total size (width * height) of {totalSize}, which is larger than the allowed limit of {NetworkReader.AllocationLimit}."); + return null; + } + + Texture2D texture2D = new Texture2D(width, height); + + // read pixel content + Color32[] pixels = reader.ReadArray(); + texture2D.SetPixels32(pixels); + texture2D.Apply(); + return texture2D; + } + + public static Sprite ReadSprite(this NetworkReader reader) + { + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + Texture2D texture = reader.ReadTexture2D(); + if (texture == null) return null; + + // otherwise create a valid sprite + return Sprite.Create(texture, reader.ReadRect(), reader.ReadVector2()); + } + + public static DateTime ReadDateTime(this NetworkReader reader) => DateTime.FromOADate(reader.ReadDouble()); + public static DateTime? ReadDateTimeNullable(this NetworkReader reader) => reader.ReadBool() ? ReadDateTime(reader) : default(DateTime?); + } +} diff --git a/Assets/Mirror/Core/NetworkReaderExtensions.cs.meta b/Assets/Mirror/Core/NetworkReaderExtensions.cs.meta new file mode 100644 index 0000000..97f24e5 --- /dev/null +++ b/Assets/Mirror/Core/NetworkReaderExtensions.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 364a9f7ccd5541e19aa2ae0b81f0b3cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReaderExtensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkReaderPool.cs b/Assets/Mirror/Core/NetworkReaderPool.cs new file mode 100644 index 0000000..f44adb8 --- /dev/null +++ b/Assets/Mirror/Core/NetworkReaderPool.cs @@ -0,0 +1,48 @@ +// 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 + ); + + // expose count for testing + public static int Count => Pool.Count; + + /// Get the next reader in the pool. If pool is empty, creates a new Reader + public static NetworkReaderPooled Get(byte[] bytes) + { + // grab from pool & set buffer + NetworkReaderPooled reader = Pool.Get(); + reader.SetBuffer(bytes); + return reader; + } + + /// Get the next reader in the pool. If pool is empty, creates a new Reader + public static NetworkReaderPooled Get(ArraySegment segment) + { + // grab from pool & set buffer + NetworkReaderPooled reader = Pool.Get(); + reader.SetBuffer(segment); + 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/Core/NetworkReaderPool.cs.meta b/Assets/Mirror/Core/NetworkReaderPool.cs.meta new file mode 100644 index 0000000..60bf9f6 --- /dev/null +++ b/Assets/Mirror/Core/NetworkReaderPool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 2bacff63613ad634a98f9e4d15d29dbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReaderPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkReaderPooled.cs b/Assets/Mirror/Core/NetworkReaderPooled.cs new file mode 100644 index 0000000..1508701 --- /dev/null +++ b/Assets/Mirror/Core/NetworkReaderPooled.cs @@ -0,0 +1,12 @@ +using System; + +namespace Mirror +{ + /// Pooled NetworkReader, automatically returned to pool when using 'using' + public sealed 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/Core/NetworkReaderPooled.cs.meta b/Assets/Mirror/Core/NetworkReaderPooled.cs.meta new file mode 100644 index 0000000..0f7dee9 --- /dev/null +++ b/Assets/Mirror/Core/NetworkReaderPooled.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: faafa97c32e44adf8e8888de817a370a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReaderPooled.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs new file mode 100644 index 0000000..5df6b9c --- /dev/null +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -0,0 +1,2111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mirror.RemoteCalls; +using UnityEngine; + +namespace Mirror +{ + public enum ReplacePlayerOptions + { + /// Player Object remains active on server and clients. Ownership is not removed + KeepAuthority, + /// Player Object remains active on server and clients. Only ownership is removed + KeepActive, + /// Player Object is unspawned on clients but remains on server + Unspawn, + /// Player Object is destroyed on server and clients + Destroy + } + + public enum RemovePlayerOptions + { + /// Player Object remains active on server and clients. Only ownership is removed + KeepActive, + /// Player Object is unspawned on clients but remains on server + Unspawn, + /// Player Object is destroyed on server and clients + Destroy + } + + /// NetworkServer handles remote connections and has a local connection for a local client. + public static partial class NetworkServer + { + static bool initialized; + public static int maxConnections; + + /// 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. + // overwritten by NetworkManager (if any) + public static int tickRate = 60; + + // tick rate is in Hz. + // convert to interval in seconds for convenience where needed. + // + // send interval is 1 / sendRate. + // but for tests we need a way to set it to exactly 0. + // 1 / int.max would not be exactly 0, so handel that manually. + public static float tickInterval => tickRate < int.MaxValue ? 1f / tickRate : 0; // for 30 Hz, that's 33ms + + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + public static int sendRate => tickRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; + + /// Connection to host mode client (if any) + public static LocalConnectionToClient localConnection { get; private set; } + + /// Dictionary of all server connections, with connectionId as key + public static Dictionary connections = + new Dictionary(); + + /// Message Handlers dictionary, with messageId 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 set listen=false to not accept incoming connections. + public static bool listen; + + // DEPRECATED 2024-10-14 + [Obsolete("NetworkServer.dontListen was replaced with NetworkServer.listen. The new value is the opposite, and avoids double negatives like 'dontListen=false'")] + public static bool dontListen + { + get => !listen; + set => listen = !value; + } + + /// active checks if the server has been started either has standalone or as host server. + public static bool active { get; internal set; } + + /// active checks if the server has been started in host mode. + // naming consistent with NetworkClient.activeHost. + public static bool activeHost => localConnection != null; + + // scene loading + public static bool isLoadingScene; + + // interest management component (optional) + // by default, everyone observes everyone + public static InterestManagementBase aoi; + + // For security, it is recommended to disconnect a player if a networked + // action triggers an exception\nThis could prevent components being + // accessed in an undefined state, which may be an attack vector for + // exploits. + // + // However, some games may want to allow exceptions in order to not + // interrupt the player's experience. + public static bool exceptionsDisconnect = true; // security by default + + // Mirror global disconnect inactive option, independent of Transport. + // not all Transports do this properly, and it's easiest to configure this just once. + // this is very useful for some projects, keep it. + public static bool disconnectInactiveConnections; + public static float disconnectInactiveTimeout = 60; + + // 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; + public static Action OnTransportExceptionEvent; + + // keep track of actual achieved tick rate. + // might become lower under heavy load. + // very useful for profiling etc. + // measured over 1s each, same as frame rate. no EMA here. + public static int actualTickRate; + static double actualTickRateStart; // start time when counting + static int actualTickRateCounter; // current counter since start + + // profiling + // includes transport update time, because transport calls handlers etc. + // averaged over 1s by passing 'tickRate' to constructor. + public static TimeSample earlyUpdateDuration; + public static TimeSample lateUpdateDuration; + + // capture full Unity update time from before Early- to after LateUpdate + public static TimeSample fullUpdateDuration; + + /// 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 (listen) + { + Transport.active.ServerStart(); + + if (Transport.active is PortTransport portTransport) + { + if (Utils.IsHeadless()) + { +#if !UNITY_EDITOR + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Server listening on port {portTransport.Port}"); + Console.ResetColor(); +#else + Debug.Log($"Server listening on port {portTransport.Port}"); +#endif + } + } + else + Debug.Log("Server started listening"); + } + + active = true; + RegisterMessageHandlers(); + } + + // initialization / shutdown /////////////////////////////////////////// + static void Initialize() + { + if (initialized) + return; + + // safety: ensure Weaving succeded. + // if it silently failed, we would get lots of 'writer not found' + // and other random errors at runtime instead. this is cleaner. + if (!WeaverFuse.Weaved()) + { + // if it failed, throw an exception to early exit all Listen calls. + throw new Exception("NetworkServer won't start because Weaving failed or didn't run."); + } + + // 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.ResetState(); + + // reset NetworkTime + NetworkTime.ResetStatics(); + + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.active' first"); + AddTransportHandlers(); + + initialized = true; + + // profiling + earlyUpdateDuration = new TimeSample(sendRate); + lateUpdateDuration = new TimeSample(sendRate); + fullUpdateDuration = new TimeSample(sendRate); + } + + static void AddTransportHandlers() + { + // += so that other systems can also hook into it (i.e. statistics) +#pragma warning disable CS0618 // Type or member is obsolete + Transport.active.OnServerConnected += OnTransportConnected; +#pragma warning restore CS0618 // Type or member is obsolete + Transport.active.OnServerConnectedWithAddress += OnTransportConnectedWithAddress; + Transport.active.OnServerDataReceived += OnTransportData; + Transport.active.OnServerDisconnected += OnTransportDisconnected; + Transport.active.OnServerError += OnTransportError; + Transport.active.OnServerTransportException += OnTransportException; + } + + /// 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.active.ServerStop(); + + // transport handlers are hooked into when initializing. + // so only remove them when shutting down. + RemoveTransportHandlers(); + + initialized = false; + } + + // Reset all statics here.... + listen = true; + isLoadingScene = false; + lastSendTime = 0; + actualTickRate = 0; + + localConnection = null; + + connections.Clear(); + connectionsCopy.Clear(); + handlers.Clear(); + + // destroy all spawned objects, _then_ set inactive. + // make sure .active is still true before calling this. + // otherwise modifying SyncLists in OnStopServer would throw + // because .IsWritable() check checks if NetworkServer.active. + // https://github.com/MirrorNetworking/Mirror/issues/3344 + CleanupSpawned(); + active = false; + + // 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; + OnTransportExceptionEvent = null; + + if (aoi != null) aoi.ResetState(); + } + + static void RemoveTransportHandlers() + { + // -= so that other systems can also hook into it (i.e. statistics) +#pragma warning disable CS0618 // Type or member is obsolete + Transport.active.OnServerConnected -= OnTransportConnected; +#pragma warning restore CS0618 // Type or member is obsolete + Transport.active.OnServerConnectedWithAddress -= OnTransportConnectedWithAddress; + Transport.active.OnServerDataReceived -= OnTransportData; + Transport.active.OnServerDisconnected -= OnTransportDisconnected; + Transport.active.OnServerError -= OnTransportError; + } + + // 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) + { + // NetworkServer.Destroy resets if scene object, destroys if prefab. + Destroy(identity.gameObject); + } + } + + spawned.Clear(); + } + + internal static void RegisterMessageHandlers() + { + RegisterHandler(OnClientReadyMessage); + RegisterHandler(OnCommandMessage); + RegisterHandler(NetworkTime.OnServerPing, false); + RegisterHandler(NetworkTime.OnServerPong, false); + RegisterHandler(OnEntityStateMessage, true); + RegisterHandler(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through + } + + // remote calls //////////////////////////////////////////////////////// + // Handle command from specific player, this could be one of multiple + // players on a single client + // default ready handler. + static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) + { + // Debug.Log($"Default handler for ready message from {conn}"); + SetClientReady(conn); + } + + 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) + { + // Attempt to identify the target object, component, and method to narrow down the cause of the error. + if (spawned.TryGetValue(msg.netId, out NetworkIdentity netIdentity)) + if (msg.componentIndex < netIdentity.NetworkBehaviours.Length && netIdentity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component) + if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName)) + { + Debug.LogWarning($"Command {methodName} received for {netIdentity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] when client not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } + + if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string method)) + { + Debug.LogWarning($"Command {method} received from {conn} when client was not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } + + Debug.LogWarning($"Command received from {conn} 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) + { + // Attempt to identify the component and method to narrow down the cause of the error. + if (msg.componentIndex < identity.NetworkBehaviours.Length && identity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component) + if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName)) + { + Debug.LogWarning($"Command {methodName} received for {identity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] without authority"); + return; + } + + Debug.LogWarning($"Command received for {identity.name} [netId={msg.netId}] without authority"); + 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); + } + + // client to server broadcast ////////////////////////////////////////// + // for client's owned ClientToServer components. + static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message) + { + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. + + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + if (!identity.DeserializeServer(reader)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}."); + } + } + } + // An attacker may attempt to modify another connection's entity + // This could also be a race condition of message in flight when + // RemoveClientAuthority is called, so not malicious. + // Don't disconnect, just log the warning. + else + Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority."); + } + // no warning. don't spam server logs. + // 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."); + } + + // client sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. + + // TODO validation? + // maybe we shouldn't allow timeline to deviate more than a certain %. + // for now, this is only used for client authority movement. + + // Unity 2019 doesn't have Time.timeAsDouble yet + // + // NetworkTime uses unscaled time and ignores Time.timeScale. + // fixes Time.timeScale getting server & client time out of sync: + // https://github.com/MirrorNetworking/Mirror/issues/3409 + connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); + } + + // 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 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 + NetworkMessages.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkServer.SendToAll: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + + // 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.Count == 0) + return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message into byte[] once + NetworkMessages.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkServer.SendToObservers: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + + foreach (NetworkConnectionToClient 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 obsolete this later. it's not used anymore + 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.Count == 0) + return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message only once + NetworkMessages.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkServer.SendToReadyObservers: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + + int count = 0; + foreach (NetworkConnectionToClient 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); + } + } + + /// Send a message to only clients which are ready including the owner of the NetworkIdentity + // TODO obsolete this later. it's not used anymore + public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) + where T : struct, NetworkMessage + { + SendToReadyObservers(identity, message, true, channelId); + } + + // transport events //////////////////////////////////////////////////// + // called by transport + static void OnTransportConnected(int connectionId) + => OnTransportConnectedWithAddress(connectionId, Transport.active.ServerGetClientAddress(connectionId)); + + static void OnTransportConnectedWithAddress(int connectionId, string clientAddress) + { + if (IsConnectionAllowed(connectionId, clientAddress)) + { + // create a connection + NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId, clientAddress); + OnConnected(conn); + } + else + { + // kick the client immediately + Transport.active.ServerDisconnect(connectionId); + } + } + + static bool IsConnectionAllowed(int connectionId, string address) + { + // only accept connections while listening + if (!listen) + { + Debug.Log($"Server not listening, rejecting connectionId={connectionId} with address={address}"); + return false; + } + + // 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."); + return false; + } + + // connectionId not in use yet? + if (connections.ContainsKey(connectionId)) + { + Debug.LogError($"Server connectionId={connectionId} already in use. Client with address={address} will be kicked"); + return false; + } + + // 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) + { + Debug.LogError($"Server full, client connectionId={connectionId} with address={address} will be kicked"); + return false; + } + + return true; + } + + 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 (NetworkMessages.UnpackId(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)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id)."); + + 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 ArraySegment message, out double remoteTimestamp)) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message)) + { + // enough to read at least header size? + if (reader.Remaining >= NetworkMessages.IdSize) + { + // 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. + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: failed to unpack and invoke message from connectionId:{connectionId}."); + + return; + } + } + // otherwise disconnect + else + { + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id)."); + + 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)) + { + conn.Cleanup(); + 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); + } + } + } + + // transport errors are forwarded to high level + static void OnTransportError(int connectionId, TransportError error, string reason) + { + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Server Transport Error for connId={connectionId}: {error}: {reason}. This is fine."); + // try get connection. passes null otherwise. + connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); + OnErrorEvent?.Invoke(conn, error, reason); + } + + // transport errors are forwarded to high level + static void OnTransportException(int connectionId, Exception exception) + { + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Server Transport Exception for connId={connectionId}: {exception}"); + // try get connection. passes null otherwise. + connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); + OnTransportExceptionEvent?.Invoke(conn, exception); + } + + /// 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 + // NetworkConnection.Disconnect() + conn.RemoveFromObservingsObservers(); + conn.identity = null; + } + + // 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 = NetworkMessageId.Id; + 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."); + } + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); + } + + /// 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 = NetworkMessageId.Id; + 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."); + } + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); + } + + /// 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); + } + + /// 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 = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); + } + + /// 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 = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); + } + + /// Unregister a handler for a message type T. + public static void UnregisterHandler() + where T : struct, NetworkMessage + { + ushort msgType = NetworkMessageId.Id; + handlers.Remove(msgType); + } + + /// Clears all registered message handlers. + public static void ClearHandlers() => handlers.Clear(); + + internal static bool GetNetworkIdentity(GameObject go, out NetworkIdentity identity) + { + if (!go.TryGetComponent(out identity)) + { + 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 InvalidOperationException. + // 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; + // this used to set active=false. + // however, then Shutdown can't properly destroy objects: + // https://github.com/MirrorNetworking/Mirror/issues/3344 + // "DisconnectAll" should only disconnect all, not set inactive. + // 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, uint assetId) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return AddPlayerForConnection(conn, 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) + { + if (!player.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogWarning($"AddPlayer: player GameObject 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.isOwned = 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; + } + + // Deprecated 2024-008-09 + [Obsolete("Use ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, ReplacePlayerOptions replacePlayerOptions) instead")] + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, bool keepAuthority = false) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + identity.assetId = assetId; + + return ReplacePlayerForConnection(conn, player, keepAuthority ? ReplacePlayerOptions.KeepAuthority : ReplacePlayerOptions.KeepActive); + } + + // Deprecated 2024-008-09 + [Obsolete("Use ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, ReplacePlayerOptions replacePlayerOptions) instead")] + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, bool keepAuthority = false) + { + return ReplacePlayerForConnection(conn, player, keepAuthority ? ReplacePlayerOptions.KeepAuthority : ReplacePlayerOptions.KeepActive); + } + + /// 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, uint assetId, ReplacePlayerOptions replacePlayerOptions) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + identity.assetId = assetId; + + return ReplacePlayerForConnection(conn, player, replacePlayerOptions); + } + + /// 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, ReplacePlayerOptions replacePlayerOptions) + { + if (!player.TryGetComponent(out NetworkIdentity identity)) + { + 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.isOwned = 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); + + switch (replacePlayerOptions) + { + case ReplacePlayerOptions.KeepAuthority: + // This needs to be sent to clear isLocalPlayer on + // client while keeping hasAuthority true + SendChangeOwnerMessage(previousPlayer, conn); + break; + case ReplacePlayerOptions.KeepActive: + // This clears both isLocalPlayer and hasAuthority on client + previousPlayer.RemoveClientAuthority(); + break; + case ReplacePlayerOptions.Unspawn: + UnSpawn(previousPlayer.gameObject); + break; + case ReplacePlayerOptions.Destroy: + Destroy(previousPlayer.gameObject); + break; + } + + return true; + } + + /// Removes the player object from the connection + // destroyServerObject: Indicates whether the server object should be destroyed + // Deprecated 2024-06-06 + [Obsolete("Use RemovePlayerForConnection(NetworkConnectionToClient conn, RemovePlayerOptions removeOptions) instead")] + public static void RemovePlayerForConnection(NetworkConnectionToClient conn, bool destroyServerObject) + { + if (destroyServerObject) + RemovePlayerForConnection(conn, RemovePlayerOptions.Destroy); + else + RemovePlayerForConnection(conn, RemovePlayerOptions.Unspawn); + } + + /// Removes player object for the connection. Options to keep the object in play, unspawn it, or destroy it. + public static void RemovePlayerForConnection(NetworkConnectionToClient conn, RemovePlayerOptions removeOptions = RemovePlayerOptions.KeepActive) + { + if (conn.identity == null) return; + + switch (removeOptions) + { + case RemovePlayerOptions.KeepActive: + conn.identity.connectionToClient = null; + conn.owned.Remove(conn.identity); + SendChangeOwnerMessage(conn.identity, conn); + break; + case RemovePlayerOptions.Unspawn: + UnSpawn(conn.identity.gameObject); + break; + case RemovePlayerOptions.Destroy: + Destroy(conn.identity.gameObject); + break; + } + + conn.identity = null; + } + + // 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); + } + + 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.visibility == Visibility.ForceShown) + { + identity.AddObserver(conn); + } + // ForceHidden: don't show no matter what + else if (identity.visibility == Visibility.ForceHidden) + { + // do nothing + } + // default: legacy system / new system / no system support + else if (identity.visibility == 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()); + } + + /// 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); + } + } + + // show / hide for connection ////////////////////////////////////////// + internal static void ShowForConnection(NetworkIdentity identity, NetworkConnectionToClient conn) + { + if (conn.isReady) + SendSpawnMessage(identity, conn); + } + + internal static void HideForConnection(NetworkIdentity identity, NetworkConnectionToClient conn) + { + ObjectHideMessage msg = new ObjectHideMessage + { + netId = identity.netId + }; + conn.Send(msg); + } + + // spawning //////////////////////////////////////////////////////////// + internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectionToClient 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); + } + } + + static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) + { + // Only call SerializeAll if there are NetworkBehaviours + if (identity.NetworkBehaviours.Length == 0) + { + return default; + } + + // serialize all components with initialState = true + // (can be null if has none) + identity.SerializeServer(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 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 && identity.connectionToClient == conn) + }); + } + + // check NetworkIdentity parent before spawning it. + // - without parent, they are spawned + // - with parent, only if the parent is active in hierarchy + // + // note that active parents may have inactive parents of their own. + // we need to check .activeInHierarchy. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + static bool ValidParent(NetworkIdentity identity) => + identity.transform.parent == null || + identity.transform.parent.gameObject.activeInHierarchy; + + /// 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; + + // find all NetworkIdentities in the scene. + // all of them are disabled because of NetworkScenePostProcess. + NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); + + // first pass: activate all scene objects + foreach (NetworkIdentity identity in identities) + { + // only spawn scene objects which haven't been spawned yet. + // SpawnObjects may be called multiple times for additive scenes. + // https://github.com/MirrorNetworking/Mirror/issues/3318 + // + // note that we even activate objects under inactive parents. + // while they are not spawned, they do need to be activated + // in order to be spawned later. so here, we don't check parents. + // https://github.com/MirrorNetworking/Mirror/issues/3330 + if (Utils.IsSceneObject(identity) && identity.netId == 0) + { + // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); + identity.gameObject.SetActive(true); + } + } + + // second pass: spawn all scene objects + foreach (NetworkIdentity identity in identities) + { + // scene objects may be children of inactive parents. + // users would put them under disabled parents to 'deactivate' them. + // those should not be used by Mirror at all. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + if (Utils.IsSceneObject(identity) && identity.netId == 0 && ValidParent(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; + } + + /// 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) + { + if (!ownerPlayer.TryGetComponent(out NetworkIdentity identity)) + { + 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); + } + + 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); + } + } + + /// 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, NetworkConnectionToClient 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, uint assetId, NetworkConnectionToClient ownerConnection = null) + { + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + SpawnObject(obj, ownerConnection); + } + + static void SpawnObject(GameObject obj, NetworkConnectionToClient 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.", obj); + return; + } + + if (!active) + { + Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server.", obj); + return; + } + + if (!obj.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}", 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; + } + + // Spawn should only be called once per netId. + // calling it twice would lead to undefined behaviour. + // https://github.com/MirrorNetworking/Mirror/pull/3205 + if (spawned.ContainsKey(identity.netId)) + { + Debug.LogWarning($"{identity.name} [netId={identity.netId}] was already spawned.", identity.gameObject); + return; + } + + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; + + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.isOwned = true; + + // NetworkServer.Unspawn sets object as inactive. + // NetworkServer.Spawn needs to set them active again in case they were previously unspawned / inactive. + identity.gameObject.SetActive(true); + + // only call OnStartServer if not spawned yet. + // check used to be in NetworkIdentity. may not be necessary anymore. + if (!identity.isServer && identity.netId == 0) + { + // configure NetworkIdentity + // this may be called in host mode, so we need to initialize + // isLocalPlayer/isClient flags too. + identity.isLocalPlayer = NetworkClient.localPlayer == identity; + identity.isClient = NetworkClient.active; + identity.isServer = true; + identity.netId = NetworkIdentity.GetNextNetworkId(); + + // add to spawned (after assigning netId) + spawned[identity.netId] = identity; + + // callback after all fields were set + 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); + } + + // internal Unspawn function which has the 'resetState' parameter. + // resetState calls .ResetState() on the object after unspawning. + // this is necessary for scene objects, but not for prefabs since we + // don't want to reset their isServer flags etc. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3832 + static void UnSpawnInternal(GameObject obj, bool resetState) + { + // Debug.Log($"DestroyObject instance:{identity.netId}"); + + // NetworkServer.Unspawn should only be called on server or host. + // on client, show a warning to explain what it does. + if (!active) + { + Debug.LogWarning("NetworkServer.Unspawn() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients."); + return; + } + + if (obj == null) + { + Debug.Log("NetworkServer.Unspawn(): object is null"); + return; + } + + if (!GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + return; + } + + // 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 && activeHost) + { + // fix: #3962 custom unspawn handler for this prefab (for prefab pools etc.) + NetworkClient.InvokeUnSpawnHandler(identity.assetId, identity.gameObject); + + 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.isOwned = false; + identity.NotifyAuthority(); + + // remove from NetworkClient dictionary + NetworkClient.connection.owned.Remove(identity); + NetworkClient.spawned.Remove(identity.netId); + } + + // we are on the server. call OnStopServer. + identity.OnStopServer(); + + // finally reset the state and deactivate it + if (resetState) + { + identity.ResetState(); + identity.gameObject.SetActive(false); + } + } + + /// 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) => UnSpawnInternal(obj, resetState: true); + + // destroy ///////////////////////////////////////////////////////////// + /// 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) + { + // NetworkServer.Destroy should only be called on server or host. + // on client, show a warning to explain what it does. + if (!active) + { + Debug.LogWarning("NetworkServer.Destroy() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients."); + return; + } + + if (obj == null) + { + Debug.Log("NetworkServer.Destroy(): object is null"); + return; + } + + // get the NetworkIdentity component first + if (!GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + Debug.LogWarning($"NetworkServer.Destroy() called on {obj.name} which doesn't have a NetworkIdentity component."); + return; + } + + // is this a scene object? + // then we simply unspawn & reset it so it can still be spawned again. + // we never destroy scene objects on server or on client, since once + // they are gone, they are gone forever and can't be instantiate again. + // for example, server may Destroy() a scene object and once a match + // restarts, the scene objects would be gone from the new match. + if (identity.sceneId != 0) + { + UnSpawnInternal(obj, resetState: true); + } + // is this a prefab? + // then we destroy it completely. + else + { + // unspawn without calling ResetState. + // otherwise isServer/isClient flags might be reset in OnDestroy. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3832 + UnSpawnInternal(obj, resetState: false); + identity.destroyCalled = true; + + // Destroy if application is running + if (Application.isPlaying) + { + UnityEngine.Object.Destroy(obj); + } + // Destroy can't be used in Editor during tests. use DestroyImmediate. + else + { + GameObject.DestroyImmediate(obj); + } + } + } + + // interest management ///////////////////////////////////////////////// + // Helper function to add all server connections as observers. + // This is used if none of the components provides their own + // OnRebuildObservers function. + // 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.visibility != Visibility.ForceHidden) + { + AddAllReadyServerConnectionsToObservers(identity); + } + else if (identity.connectionToClient != null) + { + // force hidden, but add owner connection + identity.AddObserver(identity.connectionToClient); + } + } + } + + 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); + } + } + + // 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) + { + // if there is no interest management system, + // or if 'force shown' then add all connections + if (aoi == null || identity.visibility == Visibility.ForceShown) + { + RebuildObserversDefault(identity, initialize); + } + // otherwise let interest management system rebuild + else + { + aoi.Rebuild(identity, initialize); + } + } + + + // broadcasting //////////////////////////////////////////////////////// + // helper function to get the right serialization for a connection + static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + { + // get serialization for this entity (cached) + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(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 + bool hasNull = false; + 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 = SerializeForConnection(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 + { + hasNull = true; + 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."); + } + } + + // recover from null entries. + // otherwise every broadcast will spam the warning and slow down performance until restart. + if (hasNull) connection.observing.RemoveWhere(identity => identity == null); + } + + // helper function to check a connection for inactivity and disconnect if necessary + // returns true if disconnected + static bool DisconnectIfInactive(NetworkConnectionToClient connection) + { + // check for inactivity + if (disconnectInactiveConnections && + !connection.IsAlive(disconnectInactiveTimeout)) + { + Debug.LogWarning($"Disconnecting {connection} for inactivity!"); + connection.Disconnect(); + return true; + } + return false; + } + + // 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) + { + // check for inactivity. disconnects if necessary. + if (DisconnectIfInactive(connection)) + continue; + + // has this connection joined the world yet? + // for each READY connection: + // pull in UpdateVarsMessage for each entity it observes + if (connection.isReady) + { + // send time for snapshot interpolation every sendInterval. + // BroadcastToConnection() may not send if nothing is new. + // + // sent over unreliable. + // NetworkTime / Transform both use unreliable. + // + // make sure Broadcast() is only called every sendInterval, + // even if targetFrameRate isn't set in host mode (!) + // (done via AccurateInterval) + connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); + + // broadcast world state to this connection + BroadcastToConnection(connection); + } + + // update connection to flush out batched messages + connection.Update(); + } + } + + // update ////////////////////////////////////////////////////////////// + // NetworkEarlyUpdate called before any Update/FixedUpdate + // (we add this to the UnityEngine in NetworkLoop) + internal static void NetworkEarlyUpdate() + { + // measure update time for profiling. + if (active) + { + earlyUpdateDuration.Begin(); + fullUpdateDuration.Begin(); + } + + // process all incoming messages first before updating the world + if (Transport.active != null) + Transport.active.ServerEarlyUpdate(); + + // step each connection's local time interpolation in early update. + foreach (NetworkConnectionToClient connection in connections.Values) + connection.UpdateTimeInterpolation(); + + if (active) earlyUpdateDuration.End(); + } + + internal static void NetworkLateUpdate() + { + if (active) + { + // measure update time for profiling. + lateUpdateDuration.Begin(); + + // only broadcast world if active + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + // Unity 2019 doesn't have Time.timeAsDouble yet + bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + if (!Application.isPlaying || sendIntervalElapsed) + Broadcast(); + } + + // process all outgoing messages after updating the world + // (even if not active. still want to process disconnects etc.) + if (Transport.active != null) + Transport.active.ServerLateUpdate(); + + // measure actual tick rate every second. + if (active) + { + ++actualTickRateCounter; + + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= actualTickRateStart + 1) + { + // calculate avg by exact elapsed time. + // assuming 1s wouldn't be accurate, usually a few more ms passed. + float elapsed = (float)(NetworkTime.localTime - actualTickRateStart); + actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed); + actualTickRateStart = NetworkTime.localTime; + actualTickRateCounter = 0; + } + + // measure total update time. including transport. + // because in early update, transport update calls handlers. + lateUpdateDuration.End(); + fullUpdateDuration.End(); + } + } + } +} diff --git a/Assets/Mirror/Core/NetworkServer.cs.meta b/Assets/Mirror/Core/NetworkServer.cs.meta new file mode 100644 index 0000000..84d41e8 --- /dev/null +++ b/Assets/Mirror/Core/NetworkServer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a5f5ec068f5604c32b160bc49ee97b75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkStartPosition.cs b/Assets/Mirror/Core/NetworkStartPosition.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkStartPosition.cs rename to Assets/Mirror/Core/NetworkStartPosition.cs diff --git a/Assets/Mirror/Core/NetworkStartPosition.cs.meta b/Assets/Mirror/Core/NetworkStartPosition.cs.meta new file mode 100644 index 0000000..64a7fe4 --- /dev/null +++ b/Assets/Mirror/Core/NetworkStartPosition.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 41f84591ce72545258ea98cb7518d8b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkStartPosition.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkTime.cs b/Assets/Mirror/Core/NetworkTime.cs new file mode 100644 index 0000000..6319970 --- /dev/null +++ b/Assets/Mirror/Core/NetworkTime.cs @@ -0,0 +1,243 @@ +// NetworkTime now uses NetworkClient's snapshot interpolated timeline. +// this gives ideal results & ensures everything is on the same timeline. +// previously, NetworkTransforms were on separate timelines. +// +// however, some of the old NetworkTime code remains for ping time (rtt). +// some users may still be using that. +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 interval, used to calculate latency / RTT and predicted time. + // 2s was enough to get a good average RTT. + // for prediction, we want to react to latency changes more rapidly. + const float DefaultPingInterval = 0.1f; // for resets + public static float PingInterval = DefaultPingInterval; + + /// Average out the last few results from Ping + // const because it's used immediately in _rtt constructor. + public const int PingWindowSize = 50; // average over 50 * 100ms = 5s + + static double lastPingTime; + + static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(PingWindowSize); + + /// Returns double precision clock time _in this system_, unaffected by the network. +#if UNITY_2020_3_OR_NEWER + public static double localTime + { + // NetworkTime uses unscaled time and ignores Time.timeScale. + // fixes Time.timeScale getting server & client time out of sync: + // https://github.com/MirrorNetworking/Mirror/issues/3409 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Time.unscaledTimeAsDouble; + } +#else + // need stopwatch for older Unity versions, but it's quite slow. + // CAREFUL: unlike Time.time, the stopwatch time is not a FRAME time. + // it changes during the frame, so we have an extra step to "cache" it in EarlyUpdate. + static readonly Stopwatch stopwatch = new Stopwatch(); + static NetworkTime() => stopwatch.Start(); + static double localFrameTime; + public static double localTime => localFrameTime; +#endif + + /// The time in seconds since the server started. + // via global NetworkClient snapshot interpolated timeline (if client). + // on server, this is simply Time.timeAsDouble. + // + // 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. + public static double time + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => NetworkServer.active + ? localTime + : NetworkClient.localTimeline; + } + + // prediction ////////////////////////////////////////////////////////// + // NetworkTime.time is server time, behind by bufferTime. + // for prediction, we want server time, ahead by latency. + // so that client inputs at predictedTime=2 arrive on server at time=2. + // the more accurate this is, the more closesly will corrections be + // be applied and the less jitter we will see. + // + // we'll use a two step process to calculate predicted time: + // 1. move snapshot interpolated time to server time, without being behind by bufferTime + // 2. constantly send this time to server (included in ping message) + // server replies with how far off it was. + // client averages that offset and applies it to predictedTime to get ever closer. + // + // this is also very easy to test & verify: + // - add LatencySimulation with 50ms latency + // - log predictionError on server in OnServerPing, see if it gets closer to 0 + // + // credits: FakeByte, imer, NinjaKickja, mischa + // const because it's used immediately in _predictionError constructor. + + static int PredictionErrorWindowSize = 20; // average over 20 * 100ms = 2s + static ExponentialMovingAverage _predictionErrorUnadjusted = new ExponentialMovingAverage(PredictionErrorWindowSize); + public static double predictionErrorUnadjusted => _predictionErrorUnadjusted.Value; + public static double predictionErrorAdjusted { get; private set; } // for debugging + + /// Predicted timeline in order for client inputs to be timestamped with the exact time when they will most likely arrive on the server. This is the basis for all prediction like PredictedRigidbody. + // on client, this is based on localTime (aka Time.time) instead of the snapshot interpolated timeline. + // this gives much better and immediately accurate results. + // -> snapshot interpolation timeline tries to emulate a server timeline without hard offset corrections. + // -> predictedTime does have hard offset corrections, so might as well use Time.time directly for this. + // + // note that predictedTime over unreliable is enough! + // even with reliable components, it gives better results than if we were + // to implemented predictedTime over reliable channel. + public static double predictedTime + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => NetworkServer.active + ? localTime // server always uses it's own timeline + : localTime + predictionErrorUnadjusted; // add the offset that the server told us we are off by + } + //////////////////////////////////////////////////////////////////////// + + /// Clock difference in seconds between the client and the server. Always 0 on server. + // original implementation used 'client - server' time. keep it this way. + // TODO obsolete later. people shouldn't worry about this. + public static double offset => localTime - time; + + /// 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 aka jitter, in seconds. + // "rttVariance" instead of "rttVar" for consistency with older versions. + public static double rttVariance => _rtt.Variance; + + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod] + public static void ResetStatics() + { + PingInterval = DefaultPingInterval; + lastPingTime = 0; + _rtt = new ExponentialMovingAverage(PingWindowSize); +#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 + PingInterval) + SendPing(); + } + + // Separate method so we can call it from NetworkClient directly. + internal static void SendPing() + { + // send raw predicted time without the offset applied yet. + // we then apply the offset to it after. + NetworkPingMessage pingMessage = new NetworkPingMessage + ( + localTime, + predictedTime + ); + NetworkClient.Send(pingMessage, Channels.Unreliable); + lastPingTime = localTime; + } + + // client rtt calculation ////////////////////////////////////////////// + // 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) + { + // calculate the prediction offset that the client needs to apply to unadjusted time to reach server time. + // this will be sent back to client for corrections. + double unadjustedError = localTime - message.localTime; + + // to see how well the client's final prediction worked, compare with adjusted time. + // this is purely for debugging. + // >0 means: server is ... seconds ahead of client's prediction (good if small) + // <0 means: server is ... seconds behind client's prediction. + // in other words, client is predicting too far ahead (not good) + double adjustedError = localTime - message.predictedTimeAdjusted; + // Debug.Log($"[Server] unadjustedError:{(unadjustedError*1000):F1}ms adjustedError:{(adjustedError*1000):F1}ms"); + + // Debug.Log($"OnServerPing conn:{conn}"); + NetworkPongMessage pongMessage = new NetworkPongMessage + ( + message.localTime, + unadjustedError, + adjustedError + ); + 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 & prediction offset. + internal static void OnClientPong(NetworkPongMessage message) + { + // prevent attackers from sending timestamps which are in the future + if (message.localTime > localTime) return; + + // how long did this message take to come back + double newRtt = localTime - message.localTime; + _rtt.Add(newRtt); + + // feed unadjusted prediction error into our exponential moving average + // store adjusted prediction error for debug / GUI purposes + _predictionErrorUnadjusted.Add(message.predictionErrorUnadjusted); + predictionErrorAdjusted = message.predictionErrorAdjusted; + // Debug.Log($"[Client] predictionError avg={(_predictionErrorUnadjusted.Value*1000):F1} ms"); + } + + // server rtt calculation ////////////////////////////////////////////// + // Executed at the client when we receive a ping message from the server. + // in other words, this is for server sided ping + rtt calculation. + // reply with a pong containing the time from the server + internal static void OnClientPing(NetworkPingMessage message) + { + // Debug.Log($"OnClientPing conn:{conn}"); + NetworkPongMessage pongMessage = new NetworkPongMessage + ( + message.localTime, + 0, 0 // server doesn't predict + ); + NetworkClient.Send(pongMessage, Channels.Unreliable); + } + + // Executed at the server when we receive a Pong message back. + // find out how long it took since we sent the Ping + // and update time offset + internal static void OnServerPong(NetworkConnectionToClient conn, NetworkPongMessage message) + { + // prevent attackers from sending timestamps which are in the future + if (message.localTime > localTime) return; + + // how long did this message take to come back + double newRtt = localTime - message.localTime; + conn._rtt.Add(newRtt); + } + + internal static void EarlyUpdate() + { +#if !UNITY_2020_3_OR_NEWER + localFrameTime = stopwatch.Elapsed.TotalSeconds; +#endif + } + } +} diff --git a/Assets/Mirror/Core/NetworkTime.cs.meta b/Assets/Mirror/Core/NetworkTime.cs.meta new file mode 100644 index 0000000..0049ede --- /dev/null +++ b/Assets/Mirror/Core/NetworkTime.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 09a0c241fc4a5496dbf4a0ab6e9a312c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkTime.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkWriter.cs b/Assets/Mirror/Core/NetworkWriter.cs new file mode 100644 index 0000000..7ecf126 --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriter.cs @@ -0,0 +1,249 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +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 + { + // the limit of ushort is so we can write string size prefix as only 2 bytes. + // -1 so we can still encode 'null' into it too. + public const ushort MaxStringLength = ushort.MaxValue - 1; + + // 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 + public const int DefaultCapacity = 1500; + internal byte[] buffer = new byte[DefaultCapacity]; + + /// Next position to write to the buffer + public int Position; + + /// Current capacity. Automatically resized if necessary. + public int Capacity => buffer.Length; + + // cache encoding for WriteString instead of creating it each time. + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + // not(!) static for thread safety. + // + // throwOnInvalidBytes is true. + // writer should throw and user should fix if this ever happens. + // unlike reader, which needs to expect it to happen from attackers. + internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true); + + /// 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)] + internal 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. + // Try to use ToArraySegment instead to avoid allocations! + 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() => + new ArraySegment(buffer, 0, Position); + + // implicit conversion for convenience + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ArraySegment(NetworkWriter w) => + w.ToArraySegment(); + + // 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! + // + // This is not safe to expose to random structs. + // * StructLayout.Sequential is the default, which is safe. + // if the struct contains a reference type, it is converted to Auto. + // but since all structs here are unmanaged blittable, it's safe. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential + // * StructLayout.Pack depends on CPU word size. + // this may be different 4 or 8 on some ARM systems, etc. + // this is not safe, and would cause bytes/shorts etc. to be padded. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0 + // * If we force pack all to '1', they would have no padding which is + // great for bandwidth. but on some android systems, CPU can't read + // unaligned memory. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // * The only option would be to force explicit layout with multiples + // of word size. but this requires lots of weaver checking and is + // still questionable (IL2CPP etc.). + // + // Note: inlining WriteBlittable is enough. don't inline WriteInt etc. + // we don't want WriteBlittable to be copied in place everywhere. + 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); + } + + 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) + public void WriteBytes(byte[] array, int offset, int count) + { + EnsureCapacity(Position + count); + Array.ConstrainedCopy(array, offset, this.buffer, Position, count); + Position += count; + } + // write an unsafe byte* array. + // useful for bit tree compression, etc. + public unsafe bool WriteBytes(byte* ptr, int offset, int size) + { + EnsureCapacity(Position + size); + + fixed (byte* destination = &buffer[Position]) + { + // write 'size' bytes at position + // 10 mio writes: 868ms + // Array.Copy(value.Array, value.Offset, buffer, Position, value.Count); + // 10 mio writes: 775ms + // Buffer.BlockCopy(value.Array, value.Offset, buffer, Position, value.Count); + // 10 mio writes: 637ms + UnsafeUtility.MemCpy(destination, ptr + offset, size); + } + + Position += size; + return true; + } + + /// Writes any type that mirror supports. Uses weaver populated Writer(T).write. + 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); + } + } + + // print with buffer content for easier debugging. + // [content, position / capacity]. + // showing "position / space" would be too confusing. + public override string ToString() => + $"[{ToArraySegment().ToHexString()} @ {Position}/{Capacity}]"; + } + + /// 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/Core/NetworkWriter.cs.meta b/Assets/Mirror/Core/NetworkWriter.cs.meta new file mode 100644 index 0000000..17cba61 --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriter.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 48d2207bcef1f4477b624725f075f9bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriter.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkWriterExtensions.cs b/Assets/Mirror/Core/NetworkWriterExtensions.cs new file mode 100644 index 0000000..1f6588f --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriterExtensions.cs @@ -0,0 +1,471 @@ +using System; +using System.Collections.Generic; +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 + { + public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteBlittable(value); + public static void WriteByteNullable(this NetworkWriter writer, byte? value) => writer.WriteBlittableNullable(value); + + public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteBlittable(value); + public static void WriteSByteNullable(this NetworkWriter writer, sbyte? value) => writer.WriteBlittableNullable(value); + + // char is not blittable. convert to ushort. + public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteBlittable((ushort)value); + public static void WriteCharNullable(this NetworkWriter writer, char? value) => writer.WriteBlittableNullable((ushort?)value); + + // bool is not blittable. convert to byte. + public static void WriteBool(this NetworkWriter writer, bool value) => writer.WriteBlittable((byte)(value ? 1 : 0)); + public static void WriteBoolNullable(this NetworkWriter writer, bool? value) => writer.WriteBlittableNullable(value.HasValue ? ((byte)(value.Value ? 1 : 0)) : new byte?()); + + public static void WriteShort(this NetworkWriter writer, short value) => writer.WriteBlittable(value); + public static void WriteShortNullable(this NetworkWriter writer, short? value) => writer.WriteBlittableNullable(value); + + public static void WriteUShort(this NetworkWriter writer, ushort value) => writer.WriteBlittable(value); + public static void WriteUShortNullable(this NetworkWriter writer, ushort? value) => writer.WriteBlittableNullable(value); + + public static void WriteInt(this NetworkWriter writer, int value) => writer.WriteBlittable(value); + public static void WriteIntNullable(this NetworkWriter writer, int? value) => writer.WriteBlittableNullable(value); + + public static void WriteUInt(this NetworkWriter writer, uint value) => writer.WriteBlittable(value); + public static void WriteUIntNullable(this NetworkWriter writer, uint? value) => writer.WriteBlittableNullable(value); + + public static void WriteLong(this NetworkWriter writer, long value) => writer.WriteBlittable(value); + public static void WriteLongNullable(this NetworkWriter writer, long? value) => writer.WriteBlittableNullable(value); + + public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value); + public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value); + + // WriteInt/UInt/Long/ULong writes full bytes by default. + // define additional "VarInt" versions that Weaver will automatically prefer. + // 99% of the time [SyncVar] ints are small values, which makes this very much worth it. + [WeaverPriority] public static void WriteVarInt(this NetworkWriter writer, int value) => Compression.CompressVarInt(writer, value); + [WeaverPriority] public static void WriteVarUInt(this NetworkWriter writer, uint value) => Compression.CompressVarUInt(writer, value); + [WeaverPriority] public static void WriteVarLong(this NetworkWriter writer, long value) => Compression.CompressVarInt(writer, value); + [WeaverPriority] public static void WriteVarULong(this NetworkWriter writer, ulong value) => Compression.CompressVarUInt(writer, value); + + public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value); + public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value); + + public static void WriteDouble(this NetworkWriter writer, double value) => writer.WriteBlittable(value); + public static void WriteDoubleNullable(this NetworkWriter writer, double? value) => writer.WriteBlittableNullable(value); + + public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value); + public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value); + + public static void WriteHalf(this NetworkWriter writer, Half value) => writer.WriteUShort(value._value); + + public static void WriteString(this NetworkWriter writer, string value) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (value == null) + { + writer.WriteUShort(0); + return; + } + + // WriteString copies into the buffer manually. + // need to ensure capacity here first, manually. + int maxSize = writer.encoding.GetMaxByteCount(value.Length); + writer.EnsureCapacity(writer.Position + 2 + maxSize); // 2 bytes position + N bytes encoding + + // encode it into the buffer first. + // reserve 2 bytes for header after we know how much was written. + int written = writer.encoding.GetBytes(value, 0, value.Length, writer.buffer, writer.Position + 2); + + // check if within max size, otherwise Reader can't read it. + if (written > NetworkWriter.MaxStringLength) + throw new IndexOutOfRangeException($"NetworkWriter.WriteString - Value too long: {written} bytes. Limit: {NetworkWriter.MaxStringLength} bytes"); + + // .Position is unchanged, so fill in the size header now. + // we already ensured that max size fits into ushort.max-1. + writer.WriteUShort(checked((ushort)(written + 1))); // Position += 2 + + // now update position by what was written above + writer.Position += written; + } + + // 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) + 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.) + 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. + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (buffer == null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + // writer.WriteUInt(0u); + return; + } + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)count) + 1u); + // writer.WriteUInt(checked((uint)count) + 1u); + writer.WriteBytes(buffer, offset, count); + } + + // writes ArraySegment of byte (most common type) and size header + public static void WriteArraySegmentAndSize(this NetworkWriter writer, ArraySegment segment) + { + writer.WriteBytesAndSize(segment.Array, segment.Offset, segment.Count); + } + + // writes ArraySegment of any type, and size header + public static void WriteArraySegment(this NetworkWriter writer, ArraySegment segment) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + // + // ArraySegment technically can't be null, but users may call: + // - WriteArraySegment + // - ReadArray + // in which case ReadArray needs null support. both need to be compatible. + int count = segment.Count; + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)count) + 1u); + // writer.WriteUInt(checked((uint)count) + 1u); + for (int i = 0; i < count; i++) + { + writer.Write(segment.Array[segment.Offset + i]); + } + } + + public static void WriteVector2(this NetworkWriter writer, Vector2 value) => writer.WriteBlittable(value); + public static void WriteVector2Nullable(this NetworkWriter writer, Vector2? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector3(this NetworkWriter writer, Vector3 value) => writer.WriteBlittable(value); + public static void WriteVector3Nullable(this NetworkWriter writer, Vector3? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector4(this NetworkWriter writer, Vector4 value) => writer.WriteBlittable(value); + public static void WriteVector4Nullable(this NetworkWriter writer, Vector4? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) => writer.WriteBlittable(value); + public static void WriteVector2IntNullable(this NetworkWriter writer, Vector2Int? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) => writer.WriteBlittable(value); + public static void WriteVector3IntNullable(this NetworkWriter writer, Vector3Int? value) => writer.WriteBlittableNullable(value); + + public static void WriteColor(this NetworkWriter writer, Color value) => writer.WriteBlittable(value); + public static void WriteColorNullable(this NetworkWriter writer, Color? value) => writer.WriteBlittableNullable(value); + + public static void WriteColor32(this NetworkWriter writer, Color32 value) => writer.WriteBlittable(value); + public static void WriteColor32Nullable(this NetworkWriter writer, Color32? value) => writer.WriteBlittableNullable(value); + + public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value); + public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value); + + // Rect is a struct with properties instead of fields + public static void WriteRect(this NetworkWriter writer, Rect value) + { + writer.WriteVector2(value.position); + writer.WriteVector2(value.size); + } + public static void WriteRectNullable(this NetworkWriter writer, Rect? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WriteRect(value.Value); + } + + // Plane is a struct with properties instead of fields + public static void WritePlane(this NetworkWriter writer, Plane value) + { + writer.WriteVector3(value.normal); + writer.WriteFloat(value.distance); + } + public static void WritePlaneNullable(this NetworkWriter writer, Plane? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WritePlane(value.Value); + } + + // Ray is a struct with properties instead of fields + public static void WriteRay(this NetworkWriter writer, Ray value) + { + writer.WriteVector3(value.origin); + writer.WriteVector3(value.direction); + } + public static void WriteRayNullable(this NetworkWriter writer, Ray? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WriteRay(value.Value); + } + + // LayerMask is a struct with properties instead of fields + public static void WriteLayerMask(this NetworkWriter writer, LayerMask layerMask) + { + // 32 layers as a flags enum, max value of 496, we only need a UShort. + writer.WriteUShort((ushort)layerMask.value); + } + public static void WriteLayerMaskNullable(this NetworkWriter writer, LayerMask? layerMask) + { + writer.WriteBool(layerMask.HasValue); + if (layerMask.HasValue) + writer.WriteLayerMask(layerMask.Value); + } + + public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value); + public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value); + + public static void WriteGuid(this NetworkWriter writer, Guid value) + { +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have Span yet + byte[] data = value.ToByteArray(); + writer.WriteBytes(data, 0, data.Length); +#else + // WriteBlittable(Guid) isn't safe. see WriteBlittable comments. + // Guid is Sequential, but we can't guarantee packing. + // TryWriteBytes is safe and allocation free. + writer.EnsureCapacity(writer.Position + 16); + value.TryWriteBytes(new Span(writer.buffer, writer.Position, 16)); + writer.Position += 16; +#endif + } + public static void WriteGuidNullable(this NetworkWriter writer, Guid? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WriteGuid(value.Value); + } + + 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); + } + + public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + + // users might try to use unspawned / prefab NetworkBehaviours 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 + // and more recently https://github.com/MirrorNetworking/Mirror/issues/3399 + // + // => warning (instead of exception) because we also use a warning + // when writing an unspawned NetworkIdentity + if (value.netId == 0) + { + Debug.LogWarning($"Attempted to serialize unspawned NetworkBehaviour: of type {value.GetType()} on 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(0); + return; + } + + writer.WriteUInt(value.netId); + writer.WriteByte(value.ComponentIndex); + } + + public static void WriteTransform(this NetworkWriter writer, Transform value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + if (value.TryGetComponent(out NetworkIdentity identity)) + { + writer.WriteUInt(identity.netId); + } + else + { + // if users attempt to pass a transform without NetworkIdentity + // to a [Command] or [SyncVar], it should show an obvious warning. + Debug.LogWarning($"Attempted to sync a Transform ({value}) which isn't networked. Transforms without a NetworkIdentity component can't be synced."); + writer.WriteUInt(0); + } + } + + 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, + if (!value.TryGetComponent(out NetworkIdentity identity)) + Debug.LogWarning($"Attempted to sync a GameObject ({value}) which isn't networked. GameObject without a NetworkIdentity component can't be synced."); + + // 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); + } + + // while SyncList is recommended for NetworkBehaviours, + // structs may have .List members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Writers/GenerateWriter() handles this manually. + public static void WriteList(this NetworkWriter writer, List list) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (list is null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + // writer.WriteUInt(0); + return; + } + + // check if within max size, otherwise Reader can't read it. + if (list.Count > NetworkReader.AllocationLimit) + throw new IndexOutOfRangeException($"NetworkWriter.WriteList - List<{typeof(T)}> too big: {list.Count} elements. Limit: {NetworkReader.AllocationLimit}"); + + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)list.Count) + 1u); + // writer.WriteUInt(checked((uint)list.Count) + 1u); + for (int i = 0; i < list.Count; i++) + writer.Write(list[i]); + } + + // while SyncSet is recommended for NetworkBehaviours, + // structs may have .Set members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Writers/GenerateWriter() handles this manually. + public static void WriteHashSet(this NetworkWriter writer, HashSet hashSet) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (hashSet is null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + //writer.WriteUInt(0); + return; + } + + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)hashSet.Count) + 1u); + //writer.WriteUInt(checked((uint)hashSet.Count) + 1u); + foreach (T item in hashSet) + writer.Write(item); + } + + public static void WriteArray(this NetworkWriter writer, T[] array) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (array is null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + // writer.WriteUInt(0); + return; + } + + // check if within max size, otherwise Reader can't read it. + if (array.Length > NetworkReader.AllocationLimit) + throw new IndexOutOfRangeException($"NetworkWriter.WriteArray - Array<{typeof(T)}> too big: {array.Length} elements. Limit: {NetworkReader.AllocationLimit}"); + + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)array.Length) + 1u); + // writer.WriteUInt(checked((uint)array.Length) + 1u); + for (int i = 0; i < array.Length; i++) + writer.Write(array[i]); + } + + public static void WriteUri(this NetworkWriter writer, Uri uri) + { + writer.WriteString(uri?.ToString()); + } + + public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D) + { + // TODO allocation protection when sending textures to server. + // currently can allocate 32k x 32k x 4 byte = 3.8 GB + + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + // simply send -1 for width. + if (texture2D == null) + { + writer.WriteShort(-1); + return; + } + + // check if within max size, otherwise Reader can't read it. + int totalSize = texture2D.width * texture2D.height; + if (totalSize > NetworkReader.AllocationLimit) + throw new IndexOutOfRangeException($"NetworkWriter.WriteTexture2D - Texture2D total size (width*height) too big: {totalSize}. Limit: {NetworkReader.AllocationLimit}"); + + // write dimensions first so reader can create the texture with size + // 32k x 32k short is more than enough + writer.WriteShort((short)texture2D.width); + writer.WriteShort((short)texture2D.height); + writer.WriteArray(texture2D.GetPixels32()); + } + + public static void WriteSprite(this NetworkWriter writer, Sprite sprite) + { + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + // simply send a 'null' for texture content. + if (sprite == null) + { + writer.WriteTexture2D(null); + return; + } + + writer.WriteTexture2D(sprite.texture); + writer.WriteRect(sprite.rect); + writer.WriteVector2(sprite.pivot); + } + + public static void WriteDateTime(this NetworkWriter writer, DateTime dateTime) + { + writer.WriteDouble(dateTime.ToOADate()); + } + + public static void WriteDateTimeNullable(this NetworkWriter writer, DateTime? dateTime) + { + writer.WriteBool(dateTime.HasValue); + if (dateTime.HasValue) + writer.WriteDouble(dateTime.Value.ToOADate()); + } + } +} diff --git a/Assets/Mirror/Core/NetworkWriterExtensions.cs.meta b/Assets/Mirror/Core/NetworkWriterExtensions.cs.meta new file mode 100644 index 0000000..f0c4e31 --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriterExtensions.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 94259792df2a404892c3e2377f58d0cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriterExtensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkWriterPool.cs b/Assets/Mirror/Core/NetworkWriterPool.cs similarity index 77% rename from Assets/Mirror/Runtime/NetworkWriterPool.cs rename to Assets/Mirror/Core/NetworkWriterPool.cs index c63323d..23f6026 100644 --- a/Assets/Mirror/Runtime/NetworkWriterPool.cs +++ b/Assets/Mirror/Core/NetworkWriterPool.cs @@ -1,5 +1,4 @@ // API consistent with Microsoft's ObjectPool. -using System; using System.Runtime.CompilerServices; namespace Mirror @@ -19,12 +18,10 @@ public static class NetworkWriterPool 1000 ); - // DEPRECATED 2022-03-10 - [Obsolete("GetWriter() was renamed to Get()")] - public static NetworkWriterPooled GetWriter() => Get(); + // expose count for testing + public static int Count => Pool.Count; /// 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 @@ -33,10 +30,6 @@ public static NetworkWriterPooled Get() 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) diff --git a/Assets/Mirror/Core/NetworkWriterPool.cs.meta b/Assets/Mirror/Core/NetworkWriterPool.cs.meta new file mode 100644 index 0000000..530777c --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriterPool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3f34b53bea38e4f259eb8dc211e4fdb6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriterPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkWriterPooled.cs b/Assets/Mirror/Core/NetworkWriterPooled.cs new file mode 100644 index 0000000..963ce33 --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriterPooled.cs @@ -0,0 +1,10 @@ +using System; + +namespace Mirror +{ + /// Pooled NetworkWriter, automatically returned to pool when using 'using' + public sealed class NetworkWriterPooled : NetworkWriter, IDisposable + { + public void Dispose() => NetworkWriterPool.Return(this); + } +} diff --git a/Assets/Mirror/Core/NetworkWriterPooled.cs.meta b/Assets/Mirror/Core/NetworkWriterPooled.cs.meta new file mode 100644 index 0000000..cd170b4 --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriterPooled.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a9fab936bf3c4716a452d94ad5ecbebe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriterPooled.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/PortTransport.cs b/Assets/Mirror/Core/PortTransport.cs new file mode 100644 index 0000000..19a7dfd --- /dev/null +++ b/Assets/Mirror/Core/PortTransport.cs @@ -0,0 +1,13 @@ +// convenience interface for transports which use a port. +// useful for cases where someone wants to 'just set the port' independent of +// which transport it is. +// +// note that not all transports have ports, but most do. + +namespace Mirror +{ + public interface PortTransport + { + ushort Port { get; set; } + } +} diff --git a/Assets/Mirror/Core/PortTransport.cs.meta b/Assets/Mirror/Core/PortTransport.cs.meta new file mode 100644 index 0000000..682143b --- /dev/null +++ b/Assets/Mirror/Core/PortTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: f7c7c2820d7974cb28c7bfe9aae890a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/PortTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Prediction.meta b/Assets/Mirror/Core/Prediction.meta new file mode 100644 index 0000000..0483f5a --- /dev/null +++ b/Assets/Mirror/Core/Prediction.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7e8e801f9c7f4b858d9a6c162e64ca84 +timeCreated: 1694005962 diff --git a/Assets/Mirror/Core/Prediction/Prediction.cs b/Assets/Mirror/Core/Prediction/Prediction.cs new file mode 100644 index 0000000..d669945 --- /dev/null +++ b/Assets/Mirror/Core/Prediction/Prediction.cs @@ -0,0 +1,195 @@ +// standalone, easy to test algorithms for prediction +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // prediction may capture Rigidbody3D/2D/etc. state + // have a common interface. + public interface PredictedState + { + double timestamp { get; } + + // use Vector3 for both Rigidbody3D and Rigidbody2D, that's fine + Vector3 position { get; set; } + Vector3 positionDelta { get; set; } + + Quaternion rotation { get; set; } + Quaternion rotationDelta { get; set; } + + Vector3 velocity { get; set; } + Vector3 velocityDelta { get; set; } + + Vector3 angularVelocity { get; set; } + Vector3 angularVelocityDelta { get; set; } + } + + public static class Prediction + { + // get the two states closest to a given timestamp. + // those can be used to interpolate the exact state at that time. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! + public static bool Sample( + SortedList history, + double timestamp, // current server time + out T before, + out T after, + out int afterIndex, + out double t) // interpolation factor + { + before = default; + after = default; + t = 0; + afterIndex = -1; + + // can't sample an empty history + // interpolation needs at least two entries. + // can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0. + if (history.Count < 2) { + return false; + } + + // older than oldest + if (timestamp < history.Keys[0]) { + return false; + } + + // iterate through the history + // TODO this needs to be faster than O(N) + // search around that area. + // should be O(1) most of the time, unless sampling was off. + int index = 0; // manually count when iterating. easier than for-int loop. + KeyValuePair prev = new KeyValuePair(); + + // SortedList foreach iteration allocates a LOT. use for-int instead. + // foreach (KeyValuePair entry in history) { + for (int i = 0; i < history.Count; ++i) + { + double key = history.Keys[i]; + T value = history.Values[i]; + + // exact match? + if (timestamp == key) + { + before = value; + after = value; + afterIndex = index; + t = Mathd.InverseLerp(key, key, timestamp); + return true; + } + + // did we check beyond timestamp? then return the previous two. + if (key > timestamp) + { + before = prev.Value; + after = value; + afterIndex = index; + t = Mathd.InverseLerp(prev.Key, key, timestamp); + return true; + } + + // remember the last + prev = new KeyValuePair(key, value); + index += 1; + } + + return false; + } + + // inserts a server state into the client's history. + // readjust the deltas of the states after the inserted one. + // returns the corrected final position. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! + public static T CorrectHistory( + SortedList history, + int stateHistoryLimit, + T corrected, // corrected state with timestamp + T before, // state in history before the correction + T after, // state in history after the correction + int afterIndex) // index of the 'after' value so we don't need to find it again here + where T: PredictedState + { + // respect the limit + // TODO unit test to check if it respects max size + if (history.Count >= stateHistoryLimit) + { + history.RemoveAt(0); + afterIndex -= 1; // we removed the first value so all indices are off by one now + } + + // PERFORMANCE OPTIMIZATION: avoid O(N) insertion, only readjust all values after. + // the end result is the same since after.delta and after.position are both recalculated. + // it's technically not correct if we were to reconstruct final position from 0..after..end but + // we never do, we only ever iterate from after..end! + // + // insert the corrected state into the history, or overwrite if already exists + // SortedList insertions are O(N)! + // history[corrected.timestamp] = corrected; + // afterIndex += 1; // we inserted the corrected value before the previous index + + // the entry behind the inserted one still has the delta from (before, after). + // we need to correct it to (corrected, after). + // + // for example: + // before: (t=1.0, delta=10, position=10) + // after: (t=3.0, delta=20, position=30) + // + // then we insert: + // corrected: (t=2.5, delta=__, position=25) + // + // previous delta was from t=1.0 to t=3.0 => 2.0 + // inserted delta is from t=2.5 to t=3.0 => 0.5 + // multiplier is 0.5 / 2.0 = 0.25 + // multiply 'after.delta(20)' by 0.25 to get the new 'after.delta(5) + // + // so the new history is: + // before: (t=1.0, delta=10, position=10) + // corrected: (t=2.5, delta=__, position=25) + // after: (t=3.0, delta= 5, position=__) + // + // so when we apply the correction, the new after.position would be: + // corrected.position(25) + after.delta(5) = 30 + // + double previousDeltaTime = after.timestamp - before.timestamp; // 3.0 - 1.0 = 2.0 + double correctedDeltaTime = after.timestamp - corrected.timestamp; // 3.0 - 2.5 = 0.5 + + // fix multiplier becoming NaN if previousDeltaTime is 0: + // double multiplier = correctedDeltaTime / previousDeltaTime; + double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25 + + // recalculate 'after.delta' with the multiplier + after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier); + after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier); + after.angularVelocityDelta = Vector3.Lerp(Vector3.zero, after.angularVelocityDelta, (float)multiplier); + // Quaternions always need to be normalized in order to be a valid rotation after operations + after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier).normalized; + + // changes aren't saved until we overwrite them in the history + history[after.timestamp] = after; + + // second step: readjust all absolute values by rewinding client's delta moves on top of it. + T last = corrected; + for (int i = afterIndex; i < history.Count; ++i) + { + double key = history.Keys[i]; + T value = history.Values[i]; + + // correct absolute position based on last + delta. + value.position = last.position + value.positionDelta; + value.velocity = last.velocity + value.velocityDelta; + value.angularVelocity = last.angularVelocity + value.angularVelocityDelta; + // Quaternions always need to be normalized in order to be a valid rotation after operations + value.rotation = (value.rotationDelta * last.rotation).normalized; // quaternions add delta by multiplying in this order + + // save the corrected entry into history. + history[key] = value; + + // save last + last = value; + } + + // third step: return the final recomputed state. + return last; + } + } +} diff --git a/Assets/Mirror/Core/Prediction/Prediction.cs.meta b/Assets/Mirror/Core/Prediction/Prediction.cs.meta new file mode 100644 index 0000000..37e078f --- /dev/null +++ b/Assets/Mirror/Core/Prediction/Prediction.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 216d494d910445ea8a7acc7c889212d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Prediction/Prediction.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/RemoteCalls.cs b/Assets/Mirror/Core/RemoteCalls.cs similarity index 75% rename from Assets/Mirror/Runtime/RemoteCalls.cs rename to Assets/Mirror/Core/RemoteCalls.cs index 127e241..5dbe52f 100644 --- a/Assets/Mirror/Runtime/RemoteCalls.cs +++ b/Assets/Mirror/Core/RemoteCalls.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using UnityEngine; namespace Mirror.RemoteCalls @@ -31,6 +30,8 @@ public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCa /// Used to help manage remote calls for NetworkBehaviours public static class RemoteProcedureCalls { + public const string InvokeRpcPrefix = "InvokeUserCode_"; + // 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] @@ -38,13 +39,14 @@ public static class RemoteProcedureCalls // 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: + // guaranteed. keep hashes to avoid: // 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(); + // BUT: 2 byte hash is enough if we check for collisions. that's what we + // do for NetworkMessage as well. + static readonly Dictionary remoteCallDelegates = new Dictionary(); - static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, int functionHash) + static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, ushort functionHash) { if (remoteCallDelegates.ContainsKey(functionHash)) { @@ -58,17 +60,17 @@ static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallT // 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"); + Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them. To save bandwidth, we only use 2 bytes for the hash, which has a small chance of collisions."); } 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) + internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) { // type+func so Inventory.RpcUse != Equipment.RpcUse - int hash = functionFullName.GetStableHashCode(); + ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF); if (CheckIfDelegateExists(componentType, remoteCallType, func, hash)) return hash; @@ -96,19 +98,30 @@ public static void RegisterRpc(Type componentType, string functionFullName, Remo RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func); // to clean up tests - internal static void RemoveDelegate(int hash) => + internal static void RemoveDelegate(ushort hash) => remoteCallDelegates.Remove(hash); + internal static bool GetFunctionMethodName(ushort functionHash, out string methodName) + { + if (remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker)) + { + methodName = invoker.function.GetMethodName().Replace(InvokeRpcPrefix, ""); + return true; + } + methodName = ""; + return false; + } + // 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) => + static bool GetInvokerForHash(ushort 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) + internal static bool Invoke(ushort 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 @@ -124,12 +137,12 @@ internal static bool Invoke(int functionHash, RemoteCallType remoteCallType, Net } // check if the command 'requiresAuthority' which is set in the attribute - internal static bool CommandRequiresAuthority(int cmdHash) => + internal static bool CommandRequiresAuthority(ushort 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) => + public static RemoteCallDelegate GetDelegate(ushort functionHash) => remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker) ? invoker.function : null; diff --git a/Assets/Mirror/Core/RemoteCalls.cs.meta b/Assets/Mirror/Core/RemoteCalls.cs.meta new file mode 100644 index 0000000..f3f822d --- /dev/null +++ b/Assets/Mirror/Core/RemoteCalls.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: d2cdbcbd1e377d6408a91acbec31ba16 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/RemoteCalls.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation.meta b/Assets/Mirror/Core/SnapshotInterpolation.meta similarity index 100% rename from Assets/Mirror/Runtime/SnapshotInterpolation.meta rename to Assets/Mirror/Core/SnapshotInterpolation.meta diff --git a/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs new file mode 100644 index 0000000..6b8ba8a --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs @@ -0,0 +1,17 @@ +// 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 + { + // the remote timestamp (when it was sent by the remote) + double remoteTime { get; set; } + + // the local timestamp (when it was received on our end) + // technically not needed for basic snapshot interpolation. + // only for dynamic buffer time adjustment. + double localTime { get; set; } + } +} diff --git a/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta new file mode 100644 index 0000000..de82446 --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 12afea28fdb94154868a0a3b7a9df55b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs new file mode 100644 index 0000000..50339fb --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs @@ -0,0 +1,390 @@ +// snapshot interpolation V2 by mischa +// +// Unity independent to be engine agnostic & easy to test. +// boxing: in C#, uses does not box! passing the interface would box! +// +// credits: +// glenn fiedler: https://gafferongames.com/post/snapshot_interpolation/ +// fholm: netcode streams +// fakebyte: standard deviation for dynamic adjustment +// ninjakicka: math & debugging +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class SortedListExtensions + { + // removes the first 'amount' elements from the sorted list + public static void RemoveRange(this SortedList list, int amount) + { + // remove the first element 'amount' times. + // handles -1 and > count safely. + for (int i = 0; i < amount && i < list.Count; ++i) + list.RemoveAt(0); + } + } + + public static class SnapshotInterpolation + { + // calculate timescale for catch-up / slow-down + // note that negative threshold should be <0. + // caller should verify (i.e. Unity OnValidate). + // improves branch prediction. + public static double Timescale( + double drift, // how far we are off from bufferTime + double catchupSpeed, // in % [0,1] + double slowdownSpeed, // in % [0,1] + double absoluteCatchupNegativeThreshold, // in seconds (careful, we may run out of snapshots) + double absoluteCatchupPositiveThreshold) // in seconds + { + // if the drift time is too large, it means we are behind more time. + // so we need to speed up the timescale. + // note the threshold should be sendInterval * catchupThreshold. + if (drift > absoluteCatchupPositiveThreshold) + { + // localTimeline += 0.001; // too simple, this would ping pong + return 1 + catchupSpeed; // n% faster + } + + // if the drift time is too small, it means we are ahead of time. + // so we need to slow down the timescale. + // note the threshold should be sendInterval * catchupThreshold. + if (drift < absoluteCatchupNegativeThreshold) + { + // localTimeline -= 0.001; // too simple, this would ping pong + return 1 - slowdownSpeed; // n% slower + } + + // keep constant timescale while within threshold. + // this way we have perfectly smooth speed most of the time. + return 1; + } + + // calculate dynamic buffer time adjustment + public static double DynamicAdjustment( + double sendInterval, + double jitterStandardDeviation, + double dynamicAdjustmentTolerance) + { + // jitter is equal to delivery time standard variation. + // delivery time is made up of 'sendInterval+jitter'. + // .Average would be dampened by the constant sendInterval + // .StandardDeviation is the changes in 'jitter' that we want + // so add it to send interval again. + double intervalWithJitter = sendInterval + jitterStandardDeviation; + + // how many multiples of sendInterval is that? + // we want to convert to bufferTimeMultiplier later. + double multiples = intervalWithJitter / sendInterval; + + // add the tolerance + double safezone = multiples + dynamicAdjustmentTolerance; + // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}"); + return safezone; + } + + // helper function to insert a snapshot if it doesn't exist yet. + // extra function so we can use it for both cases: + // NetworkClient global timeline insertions & adjustments via Insert. + // NetworkBehaviour local insertion without any time adjustments. + public static bool InsertIfNotExists( + SortedList buffer, // snapshot buffer + int bufferLimit, // don't grow infinitely + T snapshot) // the newly received snapshot + where T : Snapshot + { + // slow clients may not be able to process incoming snapshots fast enough. + // infinitely growing snapshots would make it even worse. + // for example, run NetworkRigidbodyBenchmark while deep profiling client. + // the client just grows and reallocates the buffer forever. + if (buffer.Count >= bufferLimit) return false; + + // SortedList does not allow duplicates. + // we don't need to check ContainsKey (which is expensive). + // simply add and compare count before/after for the return value. + + //if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive + // buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists + + int before = buffer.Count; + buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists + return buffer.Count > before; + } + + // clamp timeline for cases where it gets too far behind. + // for example, a client app may go into the background and get updated + // with 1hz for a while. by the time it's back it's at least 30 frames + // behind, possibly more if the transport also queues up. In this + // scenario, at 1% catch up it took around 20+ seconds to finally catch + // up. For these kinds of scenarios it will be better to snap / clamp. + // + // to reproduce, try snapshot interpolation demo and press the button to + // simulate the client timeline at multiple seconds behind. it'll take + // a long time to catch up if the timeline is a long time behind. + public static double TimelineClamp( + double localTimeline, + double bufferTime, + double latestRemoteTime) + { + // we want local timeline to always be 'bufferTime' behind remote. + double targetTime = latestRemoteTime - bufferTime; + + // we define a boundary of 'bufferTime' around the target time. + // this is where catchup / slowdown will happen. + // outside of the area, we clamp. + double lowerBound = targetTime - bufferTime; // how far behind we can get + double upperBound = targetTime + bufferTime; // how far ahead we can get + return Mathd.Clamp(localTimeline, lowerBound, upperBound); + } + + // call this for every received snapshot. + // adds / inserts it to the list & initializes local time if needed. + public static void InsertAndAdjust( + SortedList buffer, // snapshot buffer + int bufferLimit, // don't grow infinitely + T snapshot, // the newly received snapshot + ref double localTimeline, // local interpolation time based on server time + ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time + float sendInterval, // for debugging + double bufferTime, // offset for buffering + double catchupSpeed, // in % [0,1] + double slowdownSpeed, // in % [0,1] + ref ExponentialMovingAverage driftEma, // for catchup / slowdown + float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) + float catchupPositiveThreshold, // in % of sendInterval + ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment + where T : Snapshot + { + // first snapshot? + // initialize local timeline. + // we want it to be behind by 'offset'. + // + // note that the first snapshot may be a lagging packet. + // so we would always be behind by that lag. + // this requires catchup later. + if (buffer.Count == 0) + localTimeline = snapshot.remoteTime - bufferTime; + + // insert into the buffer. + // + // note that we might insert it between our current interpolation + // which is fine, it adds another data point for accuracy. + // + // note that insert may be called twice for the same key. + // by default, this would throw. + // need to handle it silently. + if (InsertIfNotExists(buffer, bufferLimit, snapshot)) + { + // dynamic buffer adjustment needs delivery interval jitter + if (buffer.Count >= 2) + { + // note that this is not entirely accurate for scrambled inserts. + // + // we always use the last two, not what we just inserted + // even if we were to use the diff for what we just inserted, + // a scrambled insert would still not be 100% accurate: + // => assume a buffer of AC, with delivery time C-A + // => we then insert B, with delivery time B-A + // => but then technically the first C-A wasn't correct, + // as it would have to be C-B + // + // in practice, scramble is rare and won't make much difference + double previousLocalTime = buffer.Values[buffer.Count - 2].localTime; + double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime; + + // this is the delivery time since last snapshot + double localDeliveryTime = lastestLocalTime - previousLocalTime; + + // feed the local delivery time to the EMA. + // this is what the original stream did too. + // our final dynamic buffer adjustment is different though. + // we use standard deviation instead of average. + deliveryTimeEma.Add(localDeliveryTime); + } + + // adjust timescale to catch up / slow down after each insertion + // because that is when we add new values to our EMA. + + // we want localTimeline to be about 'bufferTime' behind. + // for that, we need the delivery time EMA. + // snapshots may arrive out of order, we can not use last-timeline. + // we need to use the inserted snapshot's time - timeline. + double latestRemoteTime = snapshot.remoteTime; + + // ensure timeline stays within a reasonable bound behind/ahead. + localTimeline = TimelineClamp(localTimeline, bufferTime, latestRemoteTime); + + // calculate timediff after localTimeline override changes + double timeDiff = latestRemoteTime - localTimeline; + + // next, calculate average of a few seconds worth of timediffs. + // this gives smoother results. + // + // to calculate the average, we could simply loop through the + // last 'n' seconds worth of timediffs, but: + // - our buffer may only store a few snapshots (bufferTime) + // - looping through seconds worth of snapshots every time is + // expensive + // + // to solve this, we use an exponential moving average. + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + // which is basically fancy math to do the same but faster. + // additionally, it allows us to look at more timeDiff values + // than we sould have access to in our buffer :) + driftEma.Add(timeDiff); + + // timescale depends on driftEma. + // driftEma only changes when inserting. + // therefore timescale only needs to be calculated when inserting. + // saves CPU cycles in Update. + + // next up, calculate how far we are currently away from bufferTime + double drift = driftEma.Value - bufferTime; + + // convert relative thresholds to absolute values based on sendInterval + double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold; + double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold; + + // next, set localTimescale to catchup consistently in Update(). + // we quantize between default/catchup/slowdown, + // this way we have 'default' speed most of the time(!). + // and only catch up / slow down for a little bit occasionally. + // a consistent multiplier would never be exactly 1.0. + localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold); + + // debug logging + // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}"); + } + } + + // sample snapshot buffer to find the pair around the given time. + // returns indices so we can use it with RemoveRange to clear old snaps. + // make sure to use use buffer.Values[from/to], not buffer[from/to]. + // make sure to only call this is we have > 0 snapshots. + public static void Sample( + SortedList buffer, // snapshot buffer + double localTimeline, // local interpolation time based on server time. this is basically remoteTime-bufferTime. + out int from, // the snapshot <= time + out int to, // the snapshot >= time + out double t) // interpolation factor + where T : Snapshot + { + from = -1; + to = -1; + t = 0; + + // sample from [0,count-1] so we always have two at 'i' and 'i+1'. + for (int i = 0; i < buffer.Count - 1; ++i) + { + // is local time between these two? + T first = buffer.Values[i]; + T second = buffer.Values[i + 1]; + if (localTimeline >= first.remoteTime && + localTimeline <= second.remoteTime) + { + // use these two snapshots + from = i; + to = i + 1; + t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline); + return; + } + } + + // didn't find two snapshots around local time. + // so pick either the first or last, depending on which is closer. + + // oldest snapshot ahead of local time? + if (buffer.Values[0].remoteTime > localTimeline) + { + from = to = 0; + t = 0; + } + // otherwise initialize both to the last one + else + { + from = to = buffer.Count - 1; + t = 0; + } + } + + // progress local timeline every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // decoupled from Step for easier testing and so we can progress + // time only once in NetworkClient, while stepping for each component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StepTime( + double deltaTime, // engine delta time (unscaled) + ref double localTimeline, // local interpolation time based on server time + double localTimescale) // catchup / slowdown is applied to time every update) + { + // move local forward in time, scaled with catchup / slowdown applied + localTimeline += deltaTime * localTimescale; + } + + // sample, clear old. + // call this every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // returns true if there is anything to apply (requires at least 1 snap) + // from/to/t are out parameters instead of an interpolated 'computed'. + // this allows us to store from/to/t globally (i.e. in NetworkClient) + // and have each component apply the interpolation manually. + // besides, passing "Func Interpolate" would allocate anyway. + public static void StepInterpolation( + SortedList buffer, // snapshot buffer + double localTimeline, // local interpolation time based on server time. this is basically remoteTime-bufferTime. + out T fromSnapshot, // we interpolate 'from' this snapshot + out T toSnapshot, // 'to' this snapshot + out double t) // at ratio 't' [0,1] + where T : Snapshot + { + // check this in caller: + // nothing to do if there are no snapshots at all yet + // if (buffer.Count == 0) return false; + + // sample snapshot buffer at local interpolation time + Sample(buffer, localTimeline, out int from, out int to, out t); + + // save from/to + fromSnapshot = buffer.Values[from]; + toSnapshot = buffer.Values[to]; + + // remove older snapshots that we definitely don't need anymore. + // after(!) using the indices. + // + // if we have 3 snapshots and we are between 2nd and 3rd: + // from = 1, to = 2 + // then we need to remove the first one, which is exactly 'from'. + // because 'from-1' = 0 would remove none. + buffer.RemoveRange(from); + } + + // update time, sample, clear old. + // call this every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // returns true if there is anything to apply (requires at least 1 snap) + // from/to/t are out parameters instead of an interpolated 'computed'. + // this allows us to store from/to/t globally (i.e. in NetworkClient) + // and have each component apply the interpolation manually. + // besides, passing "Func Interpolate" would allocate anyway. + public static void Step( + SortedList buffer, // snapshot buffer + double deltaTime, // engine delta time (unscaled) + ref double localTimeline, // local interpolation time based on server time + double localTimescale, // catchup / slowdown is applied to time every update + out T fromSnapshot, // we interpolate 'from' this snapshot + out T toSnapshot, // 'to' this snapshot + out double t) // at ratio 't' [0,1] + where T : Snapshot + { + StepTime(deltaTime, ref localTimeline, localTimescale); + StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t); + } + } +} diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta new file mode 100644 index 0000000..6cb24d5 --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 72c16070d85334011853813488ab1431 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs new file mode 100644 index 0000000..74feae4 --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs @@ -0,0 +1,70 @@ +// snapshot interpolation settings struct. +// can easily be exposed in Unity inspectors. +using System; +using UnityEngine; + +namespace Mirror +{ + // class so we can define defaults easily + [Serializable] + public class SnapshotInterpolationSettings + { + // decrease bufferTime at runtime to see the catchup effect. + // increase to see slowdown. + // 'double' so we can have very precise dynamic adjustment without rounding + [Header("Buffering")] + [Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")] + public double bufferTimeMultiplier = 2; + + [Tooltip("If a client can't process snapshots fast enough, don't store too many.")] + public int bufferLimit = 32; + + // catchup ///////////////////////////////////////////////////////////// + // catchup thresholds in 'frames'. + // half a frame might be too aggressive. + [Header("Catchup / Slowdown")] + [Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")] + public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots + + [Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")] + public float catchupPositiveThreshold = 1; + + [Tooltip("Local timeline acceleration in % while catching up.")] + [Range(0, 1)] + public double catchupSpeed = 0.02f; // see snap interp demo. 1% is too slow. + + [Tooltip("Local timeline slowdown in % while slowing down.")] + [Range(0, 1)] + public double slowdownSpeed = 0.04f; // slow down a little faster so we don't encounter empty buffer (= jitter) + + [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")] + public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway + + // dynamic buffer time adjustment ////////////////////////////////////// + // dynamically adjusts bufferTimeMultiplier for smooth results. + // to understand how this works, try this manually: + // + // - disable dynamic adjustment + // - set jitter = 0.2 (20% is a lot!) + // - notice some stuttering + // - disable interpolation to see just how much jitter this really is(!) + // - enable interpolation again + // - manually increase bufferTimeMultiplier to 3-4 + // ... the cube slows down (blue) until it's smooth + // - with dynamic adjustment enabled, it will set 4 automatically + // ... the cube slows down (blue) until it's smooth as well + // + // note that 20% jitter is extreme. + // for this to be perfectly smooth, set the safety tolerance to '2'. + // but realistically this is not necessary, and '1' is enough. + [Header("Dynamic Adjustment")] + [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")] + public bool dynamicAdjustment = true; + + [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")] + public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments) + + [Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")] + public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time + } +} diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs.meta new file mode 100644 index 0000000..f90b1da --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: f955b76b7956417088c03992b3622dc9 +timeCreated: 1678507210 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs new file mode 100644 index 0000000..5bfdd3a --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs @@ -0,0 +1,15 @@ +namespace Mirror +{ + // empty snapshot that is only used to progress client's local timeline. + public struct TimeSnapshot : Snapshot + { + public double remoteTime { get; set; } + public double localTime { get; set; } + + public TimeSnapshot(double remoteTime, double localTime) + { + this.remoteTime = remoteTime; + this.localTime = localTime; + } + } +} diff --git a/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta new file mode 100644 index 0000000..2215879 --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: afe2b5ed49634971a2aec720ad74e5cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SyncDictionary.cs b/Assets/Mirror/Core/SyncDictionary.cs new file mode 100644 index 0000000..29bc931 --- /dev/null +++ b/Assets/Mirror/Core/SyncDictionary.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Mirror +{ + public class SyncIDictionary : SyncObject, IDictionary, IReadOnlyDictionary + { + /// This is called after the item is added with TKey + public Action OnAdd; + + /// This is called after the item is changed with TKey. TValue is the OLD item + public Action OnSet; + + /// This is called after the item is removed with TKey. TValue is the OLD item + public Action OnRemove; + + /// This is called before the data is cleared + public Action OnClear; + + public enum Operation : byte + { + OP_ADD, + OP_SET, + OP_REMOVE, + OP_CLEAR + } + + /// + /// This is called for all changes to the Dictionary. + /// For OP_ADD, TValue is the NEW value of the entry. + /// For OP_SET and OP_REMOVE, TValue is the OLD value of the entry. + /// For OP_CLEAR, both TKey and TValue are default. + /// + public Action OnChange; + + protected readonly IDictionary objects; + + public SyncIDictionary(IDictionary objects) + { + this.objects = objects; + } + + public int Count => objects.Count; + public bool IsReadOnly => !IsWritable(); + + 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 causes 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 ICollection Keys => objects.Keys; + + public ICollection Values => objects.Values; + + IEnumerable IReadOnlyDictionary.Keys => objects.Keys; + + IEnumerable IReadOnlyDictionary.Values => objects.Values; + + 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_SET: + writer.Write(change.key); + writer.Write(change.item); + break; + case Operation.OP_REMOVE: + writer.Write(change.key); + break; + case Operation.OP_CLEAR: + break; + } + } + } + + public override void OnDeserializeAll(NetworkReader reader) + { + // 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) + { + 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) + { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + if (objects.TryGetValue(key, out TValue oldItem)) + { + objects[key] = item; // assign after TryGetValue + AddOperation(Operation.OP_SET, key, item, oldItem, false); + } + else + { + objects[key] = item; // assign after TryGetValue + AddOperation(Operation.OP_ADD, key, item, default, false); + } + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, default, default, default, false); + // clear after invoking the callback so users can iterate the dictionary + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + break; + + case Operation.OP_REMOVE: + key = reader.Read(); + if (apply) + { + if (objects.TryGetValue(key, out TValue oldItem)) + { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + objects.Remove(key); + AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false); + } + } + break; + } + + if (!apply) + { + // we just skipped this change + changesAhead--; + } + } + } + + // throw away all the changes + // this should be called after a successful sync + public override void ClearChanges() => changes.Clear(); + + public override void Reset() + { + changes.Clear(); + changesAhead = 0; + objects.Clear(); + } + + public TValue this[TKey i] + { + get => objects[i]; + set + { + if (ContainsKey(i)) + { + TValue oldItem = objects[i]; + objects[i] = value; + AddOperation(Operation.OP_SET, i, value, oldItem, true); + } + else + { + objects[i] = value; + AddOperation(Operation.OP_ADD, i, value, default, true); + } + } + } + + public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value); + + public bool ContainsKey(TKey key) => objects.ContainsKey(key); + + public bool Contains(KeyValuePair item) => 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 void Add(KeyValuePair item) => Add(item.Key, item.Value); + + public void Add(TKey key, TValue value) + { + objects.Add(key, value); + AddOperation(Operation.OP_ADD, key, value, default, true); + } + + public bool Remove(TKey key) + { + if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key)) + { + AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true); + return true; + } + return false; + } + + public bool Remove(KeyValuePair item) + { + bool result = objects.Remove(item.Key); + if (result) + AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true); + + return result; + } + + public void Clear() + { + AddOperation(Operation.OP_CLEAR, default, default, default, true); + // clear after invoking the callback so users can iterate the dictionary + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + + void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess) + { + if (checkAccess && IsReadOnly) + throw new InvalidOperationException("SyncDictionaries can only be modified by the owner."); + + Change change = new Change + { + operation = op, + key = key, + item = item + }; + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(key); + OnChange?.Invoke(op, key, item); + break; + case Operation.OP_SET: + OnSet?.Invoke(key, oldItem); + OnChange?.Invoke(op, key, oldItem); + break; + case Operation.OP_REMOVE: + OnRemove?.Invoke(key, oldItem); + OnChange?.Invoke(op, key, oldItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + OnChange?.Invoke(op, default, default); + break; + } + } + + 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/Core/SyncDictionary.cs.meta b/Assets/Mirror/Core/SyncDictionary.cs.meta new file mode 100644 index 0000000..d9362fe --- /dev/null +++ b/Assets/Mirror/Core/SyncDictionary.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4b346c49cfdb668488a364c3023590e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncDictionary.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SyncList.cs b/Assets/Mirror/Core/SyncList.cs new file mode 100644 index 0000000..3986725 --- /dev/null +++ b/Assets/Mirror/Core/SyncList.cs @@ -0,0 +1,473 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Mirror +{ + public class SyncList : SyncObject, IList, IReadOnlyList + { + /// This is called after the item is added with index + public Action OnAdd; + + /// This is called after the item is inserted with index + public Action OnInsert; + + /// This is called after the item is set with index and OLD Value + public Action OnSet; + + /// This is called after the item is removed with index and OLD Value + public Action OnRemove; + + /// This is called before the list is cleared so the list can be iterated + public Action OnClear; + + public enum Operation : byte + { + OP_ADD, + OP_SET, + OP_INSERT, + OP_REMOVEAT, + OP_CLEAR + } + + /// + /// This is called for all changes to the List. + /// For OP_ADD and OP_INSERT, T is the NEW value of the entry. + /// For OP_SET and OP_REMOVE, T is the OLD value of the entry. + /// For OP_CLEAR, T is default. + /// + // TODO deprecate in favor of explicit Callback, later rename Callback to OnChange for consistency with other SyncCollections. + public Action OnChange; + + /// + /// This is called for all changes to the List. + /// Parameters: Operation, index, oldItem, newItem. + /// Sometimes we need both oldItem and newItem. + /// Keep for compatibility since 10 years of projects use this. + /// + public Action Callback; + + readonly IList objects; + readonly IEqualityComparer comparer; + + public int Count => objects.Count; + public bool IsReadOnly => !IsWritable(); + + 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() + { + changes.Clear(); + changesAhead = 0; + objects.Clear(); + } + + void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkAccess) + { + if (checkAccess && IsReadOnly) + throw new InvalidOperationException("Synclists can only be modified by the owner."); + + Change change = new Change + { + operation = op, + index = itemIndex, + item = newItem + }; + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(itemIndex); + OnChange?.Invoke(op, itemIndex, newItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_INSERT: + OnInsert?.Invoke(itemIndex); + OnChange?.Invoke(op, itemIndex, newItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_SET: + OnSet?.Invoke(itemIndex, oldItem); + OnChange?.Invoke(op, itemIndex, oldItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_REMOVEAT: + OnRemove?.Invoke(itemIndex, oldItem); + OnChange?.Invoke(op, itemIndex, oldItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + OnChange?.Invoke(op, itemIndex, default); + Callback?.Invoke(op, itemIndex, default, default); + break; + } + } + + 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) + { + // 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) + { + 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); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_ADD, objects.Count - 1, default, newItem, false); + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, 0, default, default, false); + // clear after invoking the callback so users can iterate the list + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + break; + + case Operation.OP_INSERT: + index = (int)reader.ReadUInt(); + newItem = reader.Read(); + if (apply) + { + objects.Insert(index, newItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_INSERT, index, default, newItem, false); + } + break; + + case Operation.OP_REMOVEAT: + index = (int)reader.ReadUInt(); + if (apply) + { + oldItem = objects[index]; + objects.RemoveAt(index); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_REMOVEAT, index, oldItem, default, false); + } + break; + + case Operation.OP_SET: + index = (int)reader.ReadUInt(); + newItem = reader.Read(); + if (apply) + { + oldItem = objects[index]; + objects[index] = newItem; + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_SET, index, oldItem, newItem, false); + } + break; + } + + if (!apply) + { + // we just skipped this change + changesAhead--; + } + } + } + + public void Add(T item) + { + objects.Add(item); + AddOperation(Operation.OP_ADD, objects.Count - 1, default, item, true); + } + + public void AddRange(IEnumerable range) + { + foreach (T entry in range) + Add(entry); + } + + public void Clear() + { + AddOperation(Operation.OP_CLEAR, 0, default, default, true); + // clear after invoking the callback so users can iterate the list + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + + 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, true); + } + + 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, true); + } + + 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, true); + } + } + } + + 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/Core/SyncList.cs.meta b/Assets/Mirror/Core/SyncList.cs.meta new file mode 100644 index 0000000..2a1d886 --- /dev/null +++ b/Assets/Mirror/Core/SyncList.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 744fc71f748fe40d5940e04bf42b29f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncList.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/SyncObject.cs b/Assets/Mirror/Core/SyncObject.cs similarity index 83% rename from Assets/Mirror/Runtime/SyncObject.cs rename to Assets/Mirror/Core/SyncObject.cs index 7df3b67..405e240 100644 --- a/Assets/Mirror/Runtime/SyncObject.cs +++ b/Assets/Mirror/Core/SyncObject.cs @@ -17,22 +17,24 @@ public abstract class SyncObject /// 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, + // if a monster has no observers but we keep modifying 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 + // => virtual so it simply always records by default public Func IsRecording = () => true; + // SyncList/Set/etc. shouldn't be modifiable if not owned. + // otherwise they would silently get out of sync. + // need a lambda because InitSyncObject is called in ctor, when + // 'isClient' etc. aren't initialized yet. + public Func IsWritable = () => 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); diff --git a/Assets/Mirror/Core/SyncObject.cs.meta b/Assets/Mirror/Core/SyncObject.cs.meta new file mode 100644 index 0000000..1b78301 --- /dev/null +++ b/Assets/Mirror/Core/SyncObject.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ae226d17a0c844041aa24cc2c023dd49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncObject.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SyncSet.cs b/Assets/Mirror/Core/SyncSet.cs new file mode 100644 index 0000000..7e9fee7 --- /dev/null +++ b/Assets/Mirror/Core/SyncSet.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Mirror +{ + public class SyncSet : SyncObject, ISet + { + /// This is called after the item is added. T is the new item. + public Action OnAdd; + + /// This is called after the item is removed. T is the OLD item + public Action OnRemove; + + /// This is called BEFORE the data is cleared + public Action OnClear; + + public enum Operation : byte + { + OP_ADD, + OP_REMOVE, + OP_CLEAR + } + + /// + /// This is called for all changes to the Set. + /// For OP_ADD, T is the NEW value of the entry. + /// For OP_REMOVE, T is the OLD value of the entry. + /// For OP_CLEAR, T is default. + /// + public Action OnChange; + + protected readonly ISet objects; + + public int Count => objects.Count; + public bool IsReadOnly => !IsWritable(); + + 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() + { + 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 oldItem, T newItem, bool checkAccess) + { + if (checkAccess && IsReadOnly) + throw new InvalidOperationException("SyncSets can only be modified by the owner."); + + Change change = default; + switch (op) + { + case Operation.OP_ADD: + change = new Change + { + operation = op, + item = newItem + }; + break; + case Operation.OP_REMOVE: + change = new Change + { + operation = op, + item = oldItem + }; + break; + case Operation.OP_CLEAR: + change = new Change + { + operation = op, + item = default + }; + break; + } + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(newItem); + OnChange?.Invoke(op, newItem); + break; + case Operation.OP_REMOVE: + OnRemove?.Invoke(oldItem); + OnChange?.Invoke(op, oldItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + OnChange?.Invoke(op, default); + break; + } + } + + void AddOperation(Operation op, bool checkAccess) => AddOperation(op, default, default, checkAccess); + + 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_REMOVE: + writer.Write(change.item); + break; + case Operation.OP_CLEAR: + break; + } + } + } + + public override void OnDeserializeAll(NetworkReader reader) + { + // 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) + { + 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 oldItem = default; + T newItem = default; + + switch (operation) + { + case Operation.OP_ADD: + newItem = reader.Read(); + if (apply) + { + objects.Add(newItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_ADD, default, newItem, false); + } + break; + + case Operation.OP_REMOVE: + oldItem = reader.Read(); + if (apply) + { + objects.Remove(oldItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_REMOVE, oldItem, default, false); + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, false); + // clear after invoking the callback so users can iterate the set + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + break; + } + + if (!apply) + { + // we just skipped this change + changesAhead--; + } + } + } + + public bool Add(T item) + { + if (objects.Add(item)) + { + AddOperation(Operation.OP_ADD, default, item, true); + return true; + } + return false; + } + + void ICollection.Add(T item) + { + if (objects.Add(item)) + AddOperation(Operation.OP_ADD, default, item, true); + } + + public void Clear() + { + AddOperation(Operation.OP_CLEAR, true); + // clear after invoking the callback so users can iterate the set + // and take appropriate action on the items before they are wiped. + objects.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, default, true); + 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/Core/SyncSet.cs.meta b/Assets/Mirror/Core/SyncSet.cs.meta new file mode 100644 index 0000000..8eb0efe --- /dev/null +++ b/Assets/Mirror/Core/SyncSet.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8a31599d9f9dd4ef9999f7b9707c832c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncSet.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading.meta b/Assets/Mirror/Core/Threading.meta new file mode 100644 index 0000000..037993e --- /dev/null +++ b/Assets/Mirror/Core/Threading.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 752fcafbee1ec45c9a43c0cf65da39de +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs new file mode 100644 index 0000000..c0bc926 --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs @@ -0,0 +1,45 @@ +// API consistent with Microsoft's ObjectPool. +// thread safe. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class ConcurrentNetworkWriterPool + { + // initial capacity to avoid allocations in the first few frames + // 1000 * 1200 bytes = around 1 MB. + public const int InitialCapacity = 1000; + + + // reuse ConcurrentPool + // 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 ConcurrentPool pool = + new ConcurrentPool( + // new object function + () => new ConcurrentNetworkWriterPooled(), + // initial capacity to avoid allocations in the first few frames + // 1000 * 1200 bytes = around 1 MB. + InitialCapacity + ); + + // pool size access for debugging & tests + public static int Count => pool.Count; + + public static ConcurrentNetworkWriterPooled Get() + { + // grab from pool & reset position + ConcurrentNetworkWriterPooled writer = pool.Get(); + writer.Position = 0; + return writer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(ConcurrentNetworkWriterPooled writer) + { + pool.Return(writer); + } + } +} diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs.meta b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs.meta new file mode 100644 index 0000000..8ff2757 --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fdf46e334f52400c854c9732f6fcf005 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs new file mode 100644 index 0000000..4baa7df --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs @@ -0,0 +1,10 @@ +using System; + +namespace Mirror +{ + /// Pooled (not threadsafe) NetworkWriter used from Concurrent pool (thread safe). Automatically returned to concurrent pool when using 'using' + public sealed class ConcurrentNetworkWriterPooled : NetworkWriter, IDisposable + { + public void Dispose() => ConcurrentNetworkWriterPool.Return(this); + } +} diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs.meta b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs.meta new file mode 100644 index 0000000..8a1e745 --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9163d963b36b4e389318f312bfd8e488 +timeCreated: 1691485295 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/ConcurrentPool.cs b/Assets/Mirror/Core/Threading/ConcurrentPool.cs new file mode 100644 index 0000000..eeac0cc --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentPool.cs @@ -0,0 +1,44 @@ +// Pool to avoid allocations (from libuv2k) +// API consistent with Microsoft's ObjectPool. +// concurrent for thread safe access. +// +// currently not in use. keep it in case we need it again. +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public class ConcurrentPool + { + // Mirror is single threaded, no need for concurrent collections + // concurrent bag is for items who's order doesn't matter. + // just about right for our use case here. + readonly ConcurrentBag objects = new ConcurrentBag(); + + // some types might need additional parameters in their constructor, so + // we use a Func generator + readonly Func objectGenerator; + + public ConcurrentPool(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.Add(objectGenerator()); + } + + // take an element from the pool, or create a new one if empty + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Get() => objects.TryTake(out T obj) ? obj : objectGenerator(); + + // return an element to the pool + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Return(T item) => objects.Add(item); + + // count to see how many objects are in the pool. useful for tests. + public int Count => objects.Count; + } +} diff --git a/Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta b/Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta new file mode 100644 index 0000000..cb36abf --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ed304bd790ff478ca37233f66d04d1c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ConcurrentPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/ThreadLog.cs b/Assets/Mirror/Core/Threading/ThreadLog.cs new file mode 100644 index 0000000..36dca5f --- /dev/null +++ b/Assets/Mirror/Core/Threading/ThreadLog.cs @@ -0,0 +1,112 @@ +// threaded Debug.Log support (mischa 2022) +// +// Editor shows Debug.Logs from different threads. +// Builds don't show Debug.Logs from different threads. +// +// need to hook into logMessageReceivedThreaded to receive them in builds too. +using System.Collections.Concurrent; +using System.Threading; +using UnityEngine; + +namespace Mirror +{ + public static class ThreadLog + { + // queue log messages from threads + struct LogEntry + { + public int threadId; + public LogType type; + public string message; + public string stackTrace; + + public LogEntry(int threadId, LogType type, string message, string stackTrace) + { + this.threadId = threadId; + this.type = type; + this.message = message; + this.stackTrace = stackTrace; + } + } + + // ConcurrentQueue allocations are fine here. + // logs allocate anywway. + static readonly ConcurrentQueue logs = + new ConcurrentQueue(); + + // main thread id + static int mainThreadId; + +#if !UNITY_EDITOR + // Editor as of Unity 2021 does log threaded messages. + // only builds don't. + // do nothing in editor, otherwise we would log twice. + // before scene load ensures thread logs are all caught. + // otherwise some component's Awake may be called before we hooked it up. + // for example, ThreadedTransport's early logs wouldn't be caught. + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void Initialize() + { + + // set main thread id + mainThreadId = Thread.CurrentThread.ManagedThreadId; + + // receive threaded log calls + Application.logMessageReceivedThreaded -= OnLog; // remove old first. TODO unnecessary? + Application.logMessageReceivedThreaded += OnLog; + + // process logs on main thread Update + NetworkLoop.OnLateUpdate -= OnLateUpdate; // remove old first. TODO unnecessary? + NetworkLoop.OnLateUpdate += OnLateUpdate; + + // log for debugging + Debug.Log("ThreadLog initialized."); + } +#endif + + static bool IsMainThread() => + Thread.CurrentThread.ManagedThreadId == mainThreadId; + + // callback runs on the same thread where the Debug.Log is called. + // we can use this to buffer messages for main thread here. + static void OnLog(string message, string stackTrace, LogType type) + { + // only enqueue messages from other threads. + // otherwise OnLateUpdate main thread logging would be enqueued + // as well, causing deadlock. + if (IsMainThread()) return; + + // queue for logging from main thread later + logs.Enqueue(new LogEntry(Thread.CurrentThread.ManagedThreadId, type, message, stackTrace)); + } + + static void OnLateUpdate() + { + // process queued logs on main thread + while (logs.TryDequeue(out LogEntry entry)) + { + switch (entry.type) + { + // add [Thread#] prefix to make it super obvious where this log message comes from. + // some projects may see unexpected messages that were previously hidden, + // since Unity wouldn't log them without ThreadLog.cs. + case LogType.Log: + Debug.Log($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Warning: + Debug.LogWarning($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Error: + Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Exception: + Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Assert: + Debug.LogAssertion($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + } + } + } + } +} diff --git a/Assets/Mirror/Core/Threading/ThreadLog.cs.meta b/Assets/Mirror/Core/Threading/ThreadLog.cs.meta new file mode 100644 index 0000000..432955d --- /dev/null +++ b/Assets/Mirror/Core/Threading/ThreadLog.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 22360406b3844808b0a305486758a703 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ThreadLog.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/WorkerThread.cs b/Assets/Mirror/Core/Threading/WorkerThread.cs new file mode 100644 index 0000000..f570dcc --- /dev/null +++ b/Assets/Mirror/Core/Threading/WorkerThread.cs @@ -0,0 +1,169 @@ +// worker thread for Unity (mischa 2022) +// thread with proper exception handling, profling, init, cleanup, etc. for Unity. +// use this from main thread. +using System; +using System.Diagnostics; +using System.Threading; +using UnityEngine.Profiling; +using Debug = UnityEngine.Debug; + +namespace Mirror +{ + public class WorkerThread + { + readonly Thread thread; + + protected volatile bool active; + + // stopwatch so we don't need to use Unity's Time (engine independent) + readonly Stopwatch watch = new Stopwatch(); + + // callbacks need to be set after constructor. + // inheriting classes can't pass their member funcs to base ctor. + // don't set them while the thread is running! + // -> Tick() returns a bool so it can easily stop the thread + // without needing to throw InterruptExceptions or similar. + public Action Init; + public Func Tick; + public Action Cleanup; + + public WorkerThread(string identifier) + { + // start the thread wrapped in safety guard + // if main application terminates, this thread needs to terminate too + thread = new Thread( + () => Guard(identifier) + ); + thread.IsBackground = true; + } + + public void Start() + { + // only if thread isn't already running + if (thread.IsAlive) + { + Debug.LogWarning("WorkerThread is still active, can't start it again."); + return; + } + + active = true; + thread.Start(); + } + + // signal the thread to stop gracefully. + // returns immediately, but the thread may take a while to stop. + // may be overwritten to clear more flags like 'computing' etc. + public virtual void SignalStop() => active = false; + + // wait for the thread to fully stop + public bool StopBlocking(float timeout) + { + // only if alive + if (!thread.IsAlive) return true; + + // double precision for long running servers. + watch.Restart(); + + // signal to stop + SignalStop(); + + // wait while thread is still alive + while (IsAlive) + { + // simply wait.. + Thread.Sleep(0); + + // deadlock detection + if (watch.Elapsed.TotalSeconds >= timeout) + { + // force kill all threads as last resort to stop them. + // return false to indicate deadlock. + Interrupt(); + return false; + } + } + return true; + } + + public bool IsAlive => thread.IsAlive; + + // signal an interrupt in the thread. + // this function is very safe to use. + // https://stackoverflow.com/questions/5950994/thread-abort-vs-thread-interrupt + // + // note this does not always kill the thread: + // "If this thread is not currently blocked in a wait, sleep, or join + // state, it will be interrupted when it next begins to block." + // https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.interrupt?view=net-6.0 + // + // in other words, "while (true) {}" wouldn't throw an interrupt exception. + // and that's _okay_. using interrupt is safe & best practice. + // => Unity still aborts deadlocked threads on script reload. + // => and we catch + warn on AbortException. + public void Interrupt() => thread.Interrupt(); + + // thread constructor needs callbacks. + // always define them, and make them call actions. + // those can be set at any time. + void OnInit() => Init?.Invoke(); + bool OnTick() => Tick?.Invoke() ?? false; + void OnCleanup() => Cleanup?.Invoke(); + + // guarded wrapper for thread code. + // catches exceptions which would otherwise be silent. + // shows in Unity profiler. + // etc. + public void Guard(string identifier) + { + try + { + // log when work begins = thread starts. + // very important for debugging threads. + Debug.Log($"{identifier}: started."); + + // show this thread in Unity profiler + Profiler.BeginThreadProfiling("Mirror Worker Threads", $"{identifier}"); + + // run init once + OnInit(); + + // run thread func while active + while (active) + { + // Tick() returns a bool so it can easily stop the thread + // without needing to throw InterruptExceptions or similar. + if (!OnTick()) break; + } + } + // Thread.Interrupt() will gracefully raise a InterruptedException. + catch (ThreadInterruptedException) + { + Debug.Log($"{identifier}: interrupted. That's okay."); + } + // Unity domain reload will cause a ThreadAbortException. + // for example, when saving a changed script while in play mode. + catch (ThreadAbortException) + { + Debug.LogWarning($"{identifier}: aborted. This may happen after domain reload. That's okay."); + } + catch (Exception e) + { + Debug.LogException(e); + } + finally + { + // run cleanup (if any) + active = false; + OnCleanup(); + + // remove this thread from Unity profiler + Profiler.EndThreadProfiling(); + + // log when work ends = thread terminates. + // very important for debugging threads. + // 'finally' to log no matter what (even if exceptions) + Debug.Log($"{identifier}: ended."); + } + } + } +} diff --git a/Assets/Mirror/Core/Threading/WorkerThread.cs.meta b/Assets/Mirror/Core/Threading/WorkerThread.cs.meta new file mode 100644 index 0000000..694601d --- /dev/null +++ b/Assets/Mirror/Core/Threading/WorkerThread.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 605fa1d7e32f40a08e5549bb43fc5c07 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/WorkerThread.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools.meta b/Assets/Mirror/Core/Tools.meta new file mode 100644 index 0000000..b8128f6 --- /dev/null +++ b/Assets/Mirror/Core/Tools.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d9c73cc2749d43268600b80df0b55c4d +timeCreated: 1667486812 diff --git a/Assets/Mirror/Core/Tools/AccurateInterval.cs b/Assets/Mirror/Core/Tools/AccurateInterval.cs new file mode 100644 index 0000000..9d2ce0e --- /dev/null +++ b/Assets/Mirror/Core/Tools/AccurateInterval.cs @@ -0,0 +1,86 @@ +// accurate interval from Mirror II. +// for sync / send intervals where it matters. +// does not(!) do catch-up. +// +// first, let's understand the problem. +// say we need an interval of 10 Hz, so every 100ms in Update we do: +// if (Time.time >= lastTime + interval) +// { +// lastTime = Time.time; +// ... +// } +// +// this seems fine, but actually Time.time will always be a few ms beyond +// the interval. but since lastTime is reset to Time.time, the remainder +// is always ignored away. +// with fixed tickRate servers (say 30 Hz), the remainder is significant! +// +// in practice if we have a 30 Hz tickRate server with a 30 Hz sendRate, +// the above way to measure the interval would result in a 18-19 Hz sendRate! +// => this is not just a little off. this is _way_ off, by almost half. +// => displaying actual + target tick/send rate will show this very easily. +// +// we need an accurate way to measure intervals for where it matters. +// and it needs to be testable to guarantee results. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class AccurateInterval + { + // static func instead of storing interval + lastTime struct. + // + don't need to initialize struct ctor with interval in Awake + // + allows for interval changes at runtime too + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Elapsed(double time, double interval, ref double lastTime) + { + // enough time elapsed? + if (time < lastTime + interval) + return false; + + // naive implementation: + //lastTime = time; + + // accurate but doesn't handle heavy load situations: + //lastTime += interval; + + // heavy load edge case: + // * interval is 100ms + // * server is under heavy load, Updates slow down to 1/s + // * Elapsed(1.000) returns true. + // technically 10 intervals have elapsed. + // * server recovers to normal, Updates are every 10ms again + // * Elapsed(1.010) should return false again until 1.100. + // + // increasing lastTime by interval would require 10 more calls + // to ever catch up again: + // lastTime += interval + // + // as result, the next 10 calls to Elapsed would return true. + // Elapsed(1.001) => true + // Elapsed(1.002) => true + // Elapsed(1.003) => true + // ... + // even though technically the delta was not >= interval. + // + // this would keep the server under heavy load, and it may never + // catch-up. this is not ideal for large virtual worlds. + // + // instead, we want to skip multiples of 'interval' and only + // keep the remainder. + // + // see also: AccurateIntervalTests.Slowdown() + + // easy to understand: + //double elapsed = time - lastTime; + //double remainder = elapsed % interval; + //lastTime = time - remainder; + + // easier: set to rounded multiples of interval (fholm). + // long to match double time. + long multiplier = (long)(time / interval); + lastTime = multiplier * interval; + return true; + } + } +} diff --git a/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta b/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta new file mode 100644 index 0000000..4c1f473 --- /dev/null +++ b/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c1b18064e25046f28b88db65a4012ec1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/AccurateInterval.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Compression.cs b/Assets/Mirror/Core/Tools/Compression.cs new file mode 100644 index 0000000..f6bd06f --- /dev/null +++ b/Assets/Mirror/Core/Tools/Compression.cs @@ -0,0 +1,596 @@ +// Quaternion compression from DOTSNET +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + /// Functions to Compress Quaternions and Floats + public static class Compression + { + // divide by precision (functions backported from Mirror II) + // for example, 0.1 cm precision converts '5.0f' float to '50' long. + // + // 'long' instead of 'int' to allow for large enough worlds. + // value / precision exceeds int.max range too easily. + // Convert.ToInt32/64 would throw. + // https://github.com/vis2k/DOTSNET/issues/59 + // + // 'long' and 'int' will result in the same bandwidth though. + // for example, ScaleToLong(10.5, 0.1) = 105. + // int: 0x00000069 + // long: 0x0000000000000069 + // delta compression will reduce both to 1 byte. + // + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + public static bool ScaleToLong(float value, float precision, out long result) + { + // user might try to pass precision = 0 to disable rounding. + // this is not supported. + // throw to make the user fix this immediately. + // otherwise we would have to reinterpret-cast if ==0 etc. + // this function should be kept simple. + // if rounding isn't wanted, this function shouldn't be called. + if (precision == 0) throw new DivideByZeroException($"ScaleToLong: precision=0 would cause null division. If rounding isn't wanted, don't call this function."); + + // catch OverflowException if value/precision > long.max. + // attackers should never be able to throw exceptions. + try + { + result = Convert.ToInt64(value / precision); + return true; + } + // clamp to .max/.min. + // returning '0' would make far away entities reset to origin. + // returning 'max' would keep them stuck at the end of the world. + // the latter is much easier to debug. + catch (OverflowException) + { + result = value > 0 ? long.MaxValue : long.MinValue; + return false; + } + } + + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Vector3 value, float precision, out long x, out long y, out long z) + { + // attempt to convert every component. + // do not return early if one conversion returned 'false'. + // the return value is optional. always attempt to convert all. + bool result = true; + result &= ScaleToLong(value.x, precision, out x); + result &= ScaleToLong(value.y, precision, out y); + result &= ScaleToLong(value.z, precision, out z); + return result; + } + + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Quaternion value, float precision, out long x, out long y, out long z, out long w) + { + // attempt to convert every component. + // do not return early if one conversion returned 'false'. + // the return value is optional. always attempt to convert all. + bool result = true; + result &= ScaleToLong(value.x, precision, out x); + result &= ScaleToLong(value.y, precision, out y); + result &= ScaleToLong(value.z, precision, out z); + result &= ScaleToLong(value.w, precision, out w); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long quantized) + { + quantized = Vector3Long.zero; + return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Quaternion value, float precision, out Vector4Long quantized) + { + quantized = Vector4Long.zero; + return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z, out quantized.w); + } + + // multiple by precision. + // for example, 0.1 cm precision converts '50' long to '5.0f' float. + public static float ScaleToFloat(long value, float precision) + { + // user might try to pass precision = 0 to disable rounding. + // this is not supported. + // throw to make the user fix this immediately. + // otherwise we would have to reinterpret-cast if ==0 etc. + // this function should be kept simple. + // if rounding isn't wanted, this function shouldn't be called. + if (precision == 0) throw new DivideByZeroException($"ScaleToLong: precision=0 would cause null division. If rounding isn't wanted, don't call this function."); + + return value * precision; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ScaleToFloat(long x, long y, long z, float precision) + { + Vector3 v; + v.x = ScaleToFloat(x, precision); + v.y = ScaleToFloat(y, precision); + v.z = ScaleToFloat(z, precision); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion ScaleToFloat(long x, long y, long z, long w, float precision) + { + Quaternion v; + v.x = ScaleToFloat(x, precision); + v.y = ScaleToFloat(y, precision); + v.z = ScaleToFloat(z, precision); + v.w = ScaleToFloat(w, precision); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ScaleToFloat(Vector3Long value, float precision) => + ScaleToFloat(value.x, value.y, value.z, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion ScaleToFloat(Vector4Long value, float precision) => + ScaleToFloat(value.x, value.y, value.z, value.w, precision); + + // 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); + } + + // 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; + } + + const float QuaternionMinRange = -0.707107f; + const float QuaternionMaxRange = 0.707107f; + const ushort TenBitsMax = 0b11_1111_1111; + + // 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 (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 ////////////////////////////////////////////////// + // helper function to predict varint size for a given number. + // useful when checking if a message + size header will fit, etc. + public static int VarUIntSize(ulong value) + { + if (value <= 240) + return 1; + if (value <= 2287) + return 2; + if (value <= 67823) + return 3; + if (value <= 16777215) + return 4; + if (value <= 4294967295) + return 5; + if (value <= 1099511627775) + return 6; + if (value <= 281474976710655) + return 7; + if (value <= 72057594037927935) + return 8; + return 9; + } + + // helper function to predict varint size for a given number. + // useful when checking if a message + size header will fit, etc. + public static int VarIntSize(long value) + { + // CompressVarInt zigzags it first + ulong zigzagged = (ulong)((value >> 63) ^ (value << 1)); + return VarUIntSize(zigzagged); + } + + // compress ulong varint. + // same result for ulong, uint, ushort and byte. only need one function. + // NOT an extension. otherwise weaver might accidentally use it. + public static void CompressVarUInt(NetworkWriter writer, ulong value) + { + // straight forward implementation: + // keep this for understanding & debugging. + /* + 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)); + } + */ + + // faster implementation writes multiple bytes at once. + // avoids extra Space, WriteBlittable overhead. + // VarInt is in hot path, performance matters here. + if (value <= 240) + { + byte a = (byte)value; + writer.WriteByte(a); + return; + } + if (value <= 2287) + { + byte a = (byte)(((value - 240) >> 8) + 241); + byte b = (byte)((value - 240) & 0xFF); + writer.WriteUShort((ushort)(b << 8 | a)); + return; + } + if (value <= 67823) + { + byte a = (byte)249; + byte b = (byte)((value - 2288) >> 8); + byte c = (byte)((value - 2288) & 0xFF); + writer.WriteByte(a); + writer.WriteUShort((ushort)(c << 8 | b)); + return; + } + if (value <= 16777215) + { + byte a = (byte)250; + uint b = (uint)(value << 8); + writer.WriteUInt(b | a); + return; + } + if (value <= 4294967295) + { + byte a = (byte)251; + uint b = (uint)value; + writer.WriteByte(a); + writer.WriteUInt(b); + return; + } + if (value <= 1099511627775) + { + byte a = (byte)252; + byte b = (byte)(value & 0xFF); + uint c = (uint)(value >> 8); + writer.WriteUShort((ushort)(b << 8 | a)); + writer.WriteUInt(c); + return; + } + if (value <= 281474976710655) + { + byte a = (byte)253; + byte b = (byte)(value & 0xFF); + byte c = (byte)((value >> 8) & 0xFF); + uint d = (uint)(value >> 16); + writer.WriteByte(a); + writer.WriteUShort((ushort)(c << 8 | b)); + writer.WriteUInt(d); + return; + } + if (value <= 72057594037927935) + { + byte a = 254; + ulong b = value << 8; + writer.WriteULong(b | a); + return; + } + + // all others + { + writer.WriteByte(255); + writer.WriteULong(value); + } + } + + // 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/Core/Tools/Compression.cs.meta b/Assets/Mirror/Core/Tools/Compression.cs.meta new file mode 100644 index 0000000..e6308ca --- /dev/null +++ b/Assets/Mirror/Core/Tools/Compression.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 5c28963f9c4b97e418252a55500fb91e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Compression.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/DeltaCompression.cs b/Assets/Mirror/Core/Tools/DeltaCompression.cs new file mode 100644 index 0000000..50b9b2f --- /dev/null +++ b/Assets/Mirror/Core/Tools/DeltaCompression.cs @@ -0,0 +1,58 @@ +// manual delta compression for some types. +// varint(b-a) +// Mirror can't use Mirror II's bit-tree delta compression. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class DeltaCompression + { + // delta (usually small), then zigzag varint to support +- changes + // parameter order: (last, current) makes most sense (Q3 does this too). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, long last, long current) => + Compression.CompressVarInt(writer, current - last); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Decompress(NetworkReader reader, long last) => + last + Compression.DecompressVarInt(reader); + + // delta (usually small), then zigzag varint to support +- changes + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, Vector3Long last, Vector3Long current) + { + Compress(writer, last.x, current.x); + Compress(writer, last.y, current.y); + Compress(writer, last.z, current.z); + } + + // delta (usually small), then zigzag varint to support +- changes + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, Vector4Long last, Vector4Long current) + { + Compress(writer, last.x, current.x); + Compress(writer, last.y, current.y); + Compress(writer, last.z, current.z); + Compress(writer, last.w, current.w); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long Decompress(NetworkReader reader, Vector3Long last) + { + long x = Decompress(reader, last.x); + long y = Decompress(reader, last.y); + long z = Decompress(reader, last.z); + return new Vector3Long(x, y, z); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long Decompress(NetworkReader reader, Vector4Long last) + { + long x = Decompress(reader, last.x); + long y = Decompress(reader, last.y); + long z = Decompress(reader, last.z); + long w = Decompress(reader, last.w); + return new Vector4Long(x, y, z, w); + } + } +} diff --git a/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta b/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta new file mode 100644 index 0000000..83a4d41 --- /dev/null +++ b/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6b8f3fffcb4754c15bc5ed4c33e2497b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/DeltaCompression.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs new file mode 100644 index 0000000..674abb8 --- /dev/null +++ b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs @@ -0,0 +1,53 @@ +// N-day EMA implementation from Mirror with a few changes (struct etc.) +// it calculates an exponential moving average roughly equivalent to the last n observations +// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average +using System; + +namespace Mirror +{ + public struct ExponentialMovingAverage + { + readonly double alpha; + bool initialized; + + public double Value; + public double Variance; + public double StandardDeviation; // absolute value, see test + + public ExponentialMovingAverage(int n) + { + // standard N-day EMA alpha calculation + alpha = 2.0 / (n + 1); + initialized = false; + Value = 0; + Variance = 0; + StandardDeviation = 0; + } + + 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; + Variance = (1 - alpha) * (Variance + alpha * delta * delta); + StandardDeviation = Math.Sqrt(Variance); + } + else + { + Value = newValue; + initialized = true; + } + } + + public void Reset() + { + initialized = false; + Value = 0; + Variance = 0; + StandardDeviation = 0; + } + } +} diff --git a/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta new file mode 100644 index 0000000..533ab12 --- /dev/null +++ b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 05e858cbaa54b4ce4a48c8c7f50c1914 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Extensions.cs b/Assets/Mirror/Core/Tools/Extensions.cs new file mode 100644 index 0000000..e9399b3 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Extensions.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public static class Extensions + { + public static string ToHexString(this ArraySegment segment) => + BitConverter.ToString(segment.Array, segment.Offset, segment.Count); + + // string.GetHashCode is not guaranteed to be the same on all + // machines, but we need one that is the same on all machines. + // Uses fnv1a as hash function for more uniform distribution http://www.isthe.com/chongo/tech/comp/fnv/ + // Tests: https://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed + // NOTE: Do not call this from hot path because it's slow O(N) for long method names. + // - As of 2012-02-16 There are 2 design-time callers (weaver) and 1 runtime caller that caches. + public static int GetStableHashCode(this string text) + { + unchecked + { + uint hash = 0x811c9dc5; + uint prime = 0x1000193; + + for (int i = 0; i < text.Length; ++i) + { + byte value = (byte)text[i]; + hash = hash ^ value; + hash *= prime; + } + + //UnityEngine.Debug.Log($"Created stable hash {(ushort)hash} for {text}"); + return (int)hash; + } + } + + // smaller version of our GetStableHashCode. + // careful, this significantly increases chance of collisions. + public static ushort GetStableHashCode16(this string text) + { + // deterministic hash + int hash = GetStableHashCode(text); + + // Gets the 32bit fnv1a hash + // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort + // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits + // This will create a more uniform 16bit hash, the method is described in: + // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding" + return (ushort)((hash >> 16) ^ hash); + } + + // previously in DotnetCompatibility.cs + // leftover from the UNET days. supposedly for windows store? + public 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 2020 and earlier don't have Queue.TryDequeue 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 + +#if !UNITY_2021_OR_NEWER + // Unity 2020 and earlier don't have ConcurrentQueue.Clear which we need for ThreadedTransport. + public static void Clear(this ConcurrentQueue source) + { + // while count > 0 risks deadlock if other thread write at the same time. + // our safest solution is a best-effort approach to clear 'Count' once. + int count = source.Count; // get it only once + for (int i = 0; i < count; ++i) + { + source.TryDequeue(out _); + } + } +#endif + +#if !UNITY_2021_3_OR_NEWER + // Some patch versions of Unity 2021.3 and earlier don't have transform.GetPositionAndRotation which we use for performance in some places + public static void GetPositionAndRotation(this Transform transform, out Vector3 position, out Quaternion rotation) + { + position = transform.position; + rotation = transform.rotation; + } + + public static void SetPositionAndRotation(this Transform transform, Vector3 position, Quaternion rotation) + { + transform.position = position; + transform.rotation = rotation; + } + + public static void GetLocalPositionAndRotation(this Transform transform, out Vector3 position, out Quaternion rotation) + { + position = transform.localPosition; + rotation = transform.localRotation; + } + + public static void SetLocalPositionAndRotation(this Transform transform, Vector3 position, Quaternion rotation) + { + transform.localPosition = position; + transform.localRotation = rotation; + } +#endif + + // IPEndPoint address only to pretty string. + // useful for to get a connection's address for IP bans etc. + public static string PrettyAddress(this IPEndPoint endPoint) + { + if (endPoint == null) return ""; + + // Map to IPv4 if "IsIPv4MappedToIPv6" for readability + // "::ffff:127.0.0.1" -> "127.0.0.1" + return + endPoint.Address.IsIPv4MappedToIPv6 + ? endPoint.Address.MapToIPv4().ToString() + : endPoint.Address.ToString(); + } + } +} diff --git a/Assets/Mirror/Core/Tools/Extensions.cs.meta b/Assets/Mirror/Core/Tools/Extensions.cs.meta new file mode 100644 index 0000000..13bd75f --- /dev/null +++ b/Assets/Mirror/Core/Tools/Extensions.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: decf32fd053744d18f35712b7a6f5116 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Extensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Half.cs b/Assets/Mirror/Core/Tools/Half.cs new file mode 100644 index 0000000..33eb08e --- /dev/null +++ b/Assets/Mirror/Core/Tools/Half.cs @@ -0,0 +1,773 @@ +// half float from .NET 5: +// https://devblogs.microsoft.com/dotnet/introducing-the-half-type/ +// +// drop in from dotnet/runtime source: +// https://github.com/dotnet/runtime/blob/e188d6ac90fe56320cca51c53709ef1c72f063d5/src/libraries/System.Private.CoreLib/src/System/Half.cs#L17 +// removing all the stuff that's not in Unity though. + + + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace System +{ + // Portions of the code implemented below are based on the 'Berkeley SoftFloat Release 3e' algorithms. + + /// + /// Represents a half-precision floating-point number. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Half + : IComparable, + IComparable, + IEquatable + { + private const NumberStyles DefaultParseStyle = NumberStyles.Float | NumberStyles.AllowThousands; + + // Constants for manipulating the private bit-representation + + internal const ushort SignMask = 0x8000; + internal const int SignShift = 15; + internal const byte ShiftedSignMask = SignMask >> SignShift; + + internal const ushort BiasedExponentMask = 0x7C00; + internal const int BiasedExponentShift = 10; + internal const int BiasedExponentLength = 5; + internal const byte ShiftedBiasedExponentMask = BiasedExponentMask >> BiasedExponentShift; + + internal const ushort TrailingSignificandMask = 0x03FF; + + internal const byte MinSign = 0; + internal const byte MaxSign = 1; + + internal const byte MinBiasedExponent = 0x00; + internal const byte MaxBiasedExponent = 0x1F; + + internal const byte ExponentBias = 15; + + internal const sbyte MinExponent = -14; + internal const sbyte MaxExponent = +15; + + internal const ushort MinTrailingSignificand = 0x0000; + internal const ushort MaxTrailingSignificand = 0x03FF; + + internal const int TrailingSignificandLength = 10; + internal const int SignificandLength = TrailingSignificandLength + 1; + + // Constants representing the private bit-representation for various default values + + private const ushort PositiveZeroBits = 0x0000; + private const ushort NegativeZeroBits = 0x8000; + + private const ushort EpsilonBits = 0x0001; + + private const ushort PositiveInfinityBits = 0x7C00; + private const ushort NegativeInfinityBits = 0xFC00; + + private const ushort PositiveQNaNBits = 0x7E00; + private const ushort NegativeQNaNBits = 0xFE00; + + private const ushort MinValueBits = 0xFBFF; + private const ushort MaxValueBits = 0x7BFF; + + private const ushort PositiveOneBits = 0x3C00; + private const ushort NegativeOneBits = 0xBC00; + + private const ushort SmallestNormalBits = 0x0400; + + private const ushort EBits = 0x4170; + private const ushort PiBits = 0x4248; + private const ushort TauBits = 0x4648; + + // Well-defined and commonly used values + + public static Half Epsilon => new Half(EpsilonBits); // 5.9604645E-08 + + public static Half PositiveInfinity => new Half(PositiveInfinityBits); // 1.0 / 0.0; + + public static Half NegativeInfinity => new Half(NegativeInfinityBits); // -1.0 / 0.0 + + public static Half NaN => new Half(NegativeQNaNBits); // 0.0 / 0.0 + + public static Half MinValue => new Half(MinValueBits); // -65504 + + public static Half MaxValue => new Half(MaxValueBits); // 65504 + + internal readonly ushort _value; // internal representation + + internal Half(ushort value) + { + _value = value; + } + + private Half(bool sign, ushort exp, ushort sig) => _value = (ushort)(((sign ? 1 : 0) << SignShift) + (exp << BiasedExponentShift) + sig); + + internal byte BiasedExponent + { + get + { + ushort bits = _value; + return ExtractBiasedExponentFromBits(bits); + } + } + + internal sbyte Exponent + { + get + { + return (sbyte)(BiasedExponent - ExponentBias); + } + } + + internal ushort Significand + { + get + { + return (ushort)(TrailingSignificand | ((BiasedExponent != 0) ? (1U << BiasedExponentShift) : 0U)); + } + } + + internal ushort TrailingSignificand + { + get + { + ushort bits = _value; + return ExtractTrailingSignificandFromBits(bits); + } + } + + internal static byte ExtractBiasedExponentFromBits(ushort bits) + { + return (byte)((bits >> BiasedExponentShift) & ShiftedBiasedExponentMask); + } + + internal static ushort ExtractTrailingSignificandFromBits(ushort bits) + { + return (ushort)(bits & TrailingSignificandMask); + } + + public static bool operator <(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative && !AreZero(left, right); + } + + return (left._value != right._value) && ((left._value < right._value) ^ leftIsNegative); + } + + public static bool operator >(Half left, Half right) + { + return right < left; + } + + public static bool operator <=(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative || AreZero(left, right); + } + + return (left._value == right._value) || ((left._value < right._value) ^ leftIsNegative); + } + + public static bool operator >=(Half left, Half right) + { + return right <= left; + } + + public static bool operator ==(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is not equal to anything, including itself. + return false; + } + + // IEEE defines that positive and negative zero are equivalent. + return (left._value == right._value) || AreZero(left, right); + } + + public static bool operator !=(Half left, Half right) + { + return !(left == right); + } + + /// Determines whether the specified value is finite (zero, subnormal, or normal). + /// This effectively checks the value is not NaN and not infinite. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(Half value) + { + uint bits = value._value; + return (~bits & PositiveInfinityBits) != 0; + } + + /// Determines whether the specified value is infinite. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInfinity(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) == PositiveInfinityBits; + } + + /// Determines whether the specified value is NaN. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) > PositiveInfinityBits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsNaNOrZero(Half value) + { + uint bits = value._value; + return ((bits - 1) & ~SignMask) >= PositiveInfinityBits; + } + + /// Determines whether the specified value is negative. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNegative(Half value) + { + return (short)(value._value) < 0; + } + + /// Determines whether the specified value is negative infinity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNegativeInfinity(Half value) + { + return value._value == NegativeInfinityBits; + } + + /// Determines whether the specified value is normal (finite, but not zero or subnormal). + /// This effectively checks the value is not NaN, not infinite, not subnormal, and not zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNormal(Half value) + { + uint bits = value._value; + return (ushort)((bits & ~SignMask) - SmallestNormalBits) < (PositiveInfinityBits - SmallestNormalBits); + } + + /// Determines whether the specified value is positive infinity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPositiveInfinity(Half value) + { + return value._value == PositiveInfinityBits; + } + + /// Determines whether the specified value is subnormal (finite, but not zero or normal). + /// This effectively checks the value is not NaN, not infinite, not normal, and not zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSubnormal(Half value) + { + uint bits = value._value; + return (ushort)((bits & ~SignMask) - 1) < MaxTrailingSignificand; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsZero(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) == 0; + } + + private static bool AreZero(Half left, Half right) + { + // IEEE defines that positive and negative zero are equal, this gives us a quick equality check + // for two values by or'ing the private bits together and stripping the sign. They are both zero, + // and therefore equivalent, if the resulting value is still zero. + return ((left._value | right._value) & ~SignMask) == 0; + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + public int CompareTo(object obj) + { + if (obj is Half other) + { + return CompareTo(other); + } + return (obj is null) ? 1 : throw new ArgumentException("SR.Arg_MustBeHalf"); + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + public int CompareTo(Half other) + { + if (this < other) + { + return -1; + } + + if (this > other) + { + return 1; + } + + if (this == other) + { + return 0; + } + + if (IsNaN(this)) + { + return IsNaN(other) ? 0 : -1; + } + + return 1; + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified . + /// + public override bool Equals(object obj) + { + return (obj is Half other) && Equals(other); + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified value. + /// + public bool Equals(Half other) + { + return _value == other._value + || AreZero(this, other) + || (IsNaN(this) && IsNaN(other)); + } + + /// + /// Serves as the default hash function. + /// + public override int GetHashCode() + { + uint bits = _value; + + if (IsNaNOrZero(this)) + { + // Ensure that all NaNs and both zeros have the same hash code + bits &= PositiveInfinityBits; + } + + return (int)bits; + } + + /// + /// Returns a string representation of the current value. + /// + public override string ToString() + { + return ((float)this).ToString(); + } + + // + // Explicit Convert To Half + // + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + public static explicit operator Half(char value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + public static explicit operator Half(decimal value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(short value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(int value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(long value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(float value) + { + // Unity implement this! + return new Half(Mathf.FloatToHalf(value)); + } + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(ushort value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(uint value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(ulong value) => (Half)(float)value; + + // + // Explicit Convert From Half + // + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator byte(Half value) => (byte)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator char(Half value) => (char)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator decimal(Half value) => (decimal)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator short(Half value) => (short)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator int(Half value) => (int)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator long(Half value) => (long)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator sbyte(Half value) => (sbyte)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator ushort(Half value) => (ushort)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator uint(Half value) => (uint)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator ulong(Half value) => (ulong)(float)value; + + // + // Implicit Convert To Half + // + + /// Implicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static implicit operator Half(byte value) => (Half)(float)value; + + /// Implicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static implicit operator Half(sbyte value) => (Half)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator float(Half value) + { + return Mathf.HalfToFloat(value._value); + } + + // IEEE 754 specifies NaNs to be propagated + internal static Half Negate(Half value) + { + return IsNaN(value) ? value : new Half((ushort)(value._value ^ SignMask)); + } + + public static Half operator +(Half left, Half right) => (Half)((float)left + (float)right); + + // + // IDecrementOperators + // + + public static Half operator --(Half value) + { + var tmp = (float)value; + --tmp; + return (Half)tmp; + } + + // + // IDivisionOperators + // + + public static Half operator /(Half left, Half right) => (Half)((float)left / (float)right); + + // + // IExponentialFunctions + // + + public static Half Exp(Half x) => (Half)Math.Exp((float)x); + + // + // IFloatingPoint + // + + public static Half Ceiling(Half x) => (Half)Math.Ceiling((float)x); + + public static Half Floor(Half x) => (Half)Math.Floor((float)x); + + public static Half Round(Half x) => (Half)Math.Round((float)x); + + public static Half Round(Half x, int digits) => (Half)Math.Round((float)x, digits); + + public static Half Round(Half x, MidpointRounding mode) => (Half)Math.Round((float)x, mode); + + public static Half Round(Half x, int digits, MidpointRounding mode) => (Half)Math.Round((float)x, digits, mode); + + public static Half Truncate(Half x) => (Half)Math.Truncate((float)x); + + // + // IFloatingPointConstants + // + + public static Half E => new Half(EBits); + + public static Half Pi => new Half(PiBits); + + public static Half Tau => new Half(TauBits); + + // + // IFloatingPointIeee754 + // + + public static Half NegativeZero => new Half(NegativeZeroBits); + + public static Half Atan2(Half y, Half x) => (Half)Math.Atan2((float)y, (float)x); + + public static Half Lerp(Half value1, Half value2, Half amount) => (Half)Mathf.Lerp((float)value1, (float)value2, (float)amount); + + // + // IHyperbolicFunctions + // + + public static Half Cosh(Half x) => (Half)Math.Cosh((float)x); + + public static Half Sinh(Half x) => (Half)Math.Sinh((float)x); + + public static Half Tanh(Half x) => (Half)Math.Tanh((float)x); + + // + // IIncrementOperators + // + + public static Half operator ++(Half value) + { + var tmp = (float)value; + ++tmp; + return (Half)tmp; + } + + // + // ILogarithmicFunctions + // + + public static Half Log(Half x) => (Half)Math.Log((float)x); + + public static Half Log(Half x, Half newBase) => (Half)Math.Log((float)x, (float)newBase); + + // + // IModulusOperators + // + + public static Half operator %(Half left, Half right) => (Half)((float)left % (float)right); + + // + // IMultiplicativeIdentity + // + + public static Half MultiplicativeIdentity => new Half(PositiveOneBits); + + // + // IMultiplyOperators + // + + public static Half operator *(Half left, Half right) => (Half)((float)left * (float)right); + + // + // INumber + // + + public static Half Clamp(Half value, Half min, Half max) => (Half)Mathf.Clamp((float)value, (float)min, (float)max); + + public static Half CopySign(Half value, Half sign) + { + // This method is required to work for all inputs, + // including NaN, so we operate on the raw bits. + uint xbits = value._value; + uint ybits = sign._value; + + // Remove the sign from x, and remove everything but the sign from y + // Then, simply OR them to get the correct sign + return new Half((ushort)((xbits & ~SignMask) | (ybits & SignMask))); + } + + public static Half Max(Half x, Half y) => (Half)Math.Max((float)x, (float)y); + + public static Half MaxNumber(Half x, Half y) + { + // This matches the IEEE 754:2019 `maximumNumber` function + // + // It does not propagate NaN inputs back to the caller and + // otherwise returns the larger of the inputs. It + // treats +0 as larger than -0 as per the specification. + + if (x != y) + { + if (!IsNaN(y)) + { + return y < x ? x : y; + } + + return x; + } + + return IsNegative(y) ? x : y; + } + + public static Half Min(Half x, Half y) => (Half)Math.Min((float)x, (float)y); + + public static Half MinNumber(Half x, Half y) + { + // This matches the IEEE 754:2019 `minimumNumber` function + // + // It does not propagate NaN inputs back to the caller and + // otherwise returns the larger of the inputs. It + // treats +0 as larger than -0 as per the specification. + + if (x != y) + { + if (!IsNaN(y)) + { + return x < y ? x : y; + } + + return x; + } + + return IsNegative(x) ? x : y; + } + + public static int Sign(Half value) + { + if (IsNaN(value)) + { + throw new ArithmeticException("SR.Arithmetic_NaN"); + } + + if (IsZero(value)) + { + return 0; + } + else if (IsNegative(value)) + { + return -1; + } + + return +1; + } + + // + // INumberBase + // + + public static Half One => new Half(PositiveOneBits); + + public static Half Zero => new Half(PositiveZeroBits); + + public static Half Abs(Half value) => new Half((ushort)(value._value & ~SignMask)); + + public static bool IsPositive(Half value) => (short)(value._value) >= 0; + + public static bool IsRealNumber(Half value) + { + // A NaN will never equal itself so this is an + // easy and efficient way to check for a real number. + +#pragma warning disable CS1718 + return value == value; +#pragma warning restore CS1718 + } + + // + // IPowerFunctions + // + + public static Half Pow(Half x, Half y) => (Half)Math.Pow((float)x, (float)y); + + // + // IRootFunctions + // + + public static Half Sqrt(Half x) => (Half)Math.Sqrt((float)x); + + // + // ISignedNumber + // + + public static Half NegativeOne => new Half(NegativeOneBits); + + // + // ISubtractionOperators + // + + public static Half operator -(Half left, Half right) => (Half)((float)left - (float)right); + + // + // ITrigonometricFunctions + // + + public static Half Acos(Half x) => (Half)Math.Acos((float)x); + + public static Half Asin(Half x) => (Half)Math.Asin((float)x); + + public static Half Atan(Half x) => (Half)Math.Atan((float)x); + + public static Half Cos(Half x) => (Half)Math.Cos((float)x); + + public static Half Sin(Half x) => (Half)Math.Sin((float)x); + + public static Half Tan(Half x) => (Half)Math.Tan((float)x); + + // + // IUnaryNegationOperators + // + + public static Half operator -(Half value) => (Half)(-(float)value); + + // + // IUnaryPlusOperators + // + + public static Half operator +(Half value) => value; + } +} diff --git a/Assets/Mirror/Core/Tools/Half.cs.meta b/Assets/Mirror/Core/Tools/Half.cs.meta new file mode 100644 index 0000000..030a09a --- /dev/null +++ b/Assets/Mirror/Core/Tools/Half.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 70b5947c49174f3c90121edd21ea6b15 +timeCreated: 1729783714 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Half.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Mathd.cs b/Assets/Mirror/Core/Tools/Mathd.cs new file mode 100644 index 0000000..374471a --- /dev/null +++ b/Assets/Mirror/Core/Tools/Mathd.cs @@ -0,0 +1,32 @@ +// 'double' precision variants for some of Unity's Mathf functions. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class Mathd + { + // Unity 2020 doesn't have Math.Clamp yet. + /// Clamps value between 0 and 1 and returns value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp(double value, double min, double max) + { + if (value < min) return min; + if (value > max) return max; + return value; + } + + /// Clamps value between 0 and 1 and returns value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp01(double value) => Clamp(value, 0, 1); + + /// 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; + + /// 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; + } +} diff --git a/Assets/Mirror/Core/Tools/Mathd.cs.meta b/Assets/Mirror/Core/Tools/Mathd.cs.meta new file mode 100644 index 0000000..cfc4b51 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Mathd.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 5f74084b91c74df2839b426c4a381373 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Mathd.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Pool.cs b/Assets/Mirror/Core/Tools/Pool.cs similarity index 75% rename from Assets/Mirror/Runtime/Pool.cs rename to Assets/Mirror/Core/Tools/Pool.cs index e526139..f26d49b 100644 --- a/Assets/Mirror/Runtime/Pool.cs +++ b/Assets/Mirror/Core/Tools/Pool.cs @@ -8,7 +8,8 @@ namespace Mirror { public class Pool { - // Mirror is single threaded, no need for concurrent collections + // Mirror is single threaded, no need for concurrent collections. + // stack increases the chance that a reused writer remains in cache. readonly Stack objects = new Stack(); // some types might need additional parameters in their constructor, so @@ -25,17 +26,21 @@ public Pool(Func objectGenerator, int initialCapacity) 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); + public void Return(T item) + { + // make sure we can't accidentally insert null values into the pool. + // debugging this would be hard since it would only show on get(). + if (item == null) + throw new ArgumentNullException(nameof(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/Core/Tools/Pool.cs.meta b/Assets/Mirror/Core/Tools/Pool.cs.meta new file mode 100644 index 0000000..1434250 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Pool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 845bb05fa349344c3811022f4f15dfbc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Pool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Readme.txt b/Assets/Mirror/Core/Tools/Readme.txt new file mode 100644 index 0000000..09bd920 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Readme.txt @@ -0,0 +1 @@ +Standalone algorithms & structs to help build Mirror. \ No newline at end of file diff --git a/Assets/Mirror/Core/Tools/Readme.txt.meta b/Assets/Mirror/Core/Tools/Readme.txt.meta new file mode 100644 index 0000000..b8d44b0 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Readme.txt.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: da033671de7d49e0838223a997c56bf1 +timeCreated: 1667486850 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Readme.txt + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/TimeSample.cs b/Assets/Mirror/Core/Tools/TimeSample.cs new file mode 100644 index 0000000..111e971 --- /dev/null +++ b/Assets/Mirror/Core/Tools/TimeSample.cs @@ -0,0 +1,61 @@ +// TimeSample from Mirror II. +// simple profiling sample, averaged for display in statistics. +// usable in builds without unitiy profiler overhead etc. +// +// .average may safely be called from main thread while Begin/End is in another. +// i.e. worker threads, transport, etc. +using System.Diagnostics; +using System.Threading; + +namespace Mirror +{ + public struct TimeSample + { + // UnityEngine.Time isn't thread safe. use stopwatch instead. + readonly Stopwatch watch; + + // remember when Begin was called + double beginTime; + + // keep accumulating times over the given interval. + // (not readonly. we modify its contents.) + ExponentialMovingAverage ema; + + // average in seconds. + // code often runs in sub-millisecond time. float is more precise. + // + // set with Interlocked for thread safety. + // can be read from main thread while sampling happens in other thread. + public double average; // THREAD SAFE + + // average over N begin/end captures + public TimeSample(int n) + { + watch = new Stopwatch(); + watch.Start(); + ema = new ExponentialMovingAverage(n); + beginTime = 0; + average = 0; + } + + // begin is called before the code to be sampled + public void Begin() + { + // remember when Begin was called. + // keep StopWatch running so we can average over the given interval. + beginTime = watch.Elapsed.TotalSeconds; + // Debug.Log($"Begin @ {beginTime:F4}"); + } + + // end is called after the code to be sampled + public void End() + { + // add duration in seconds to accumulated durations + double elapsed = watch.Elapsed.TotalSeconds - beginTime; + ema.Add(elapsed); + + // expose new average thread safely + Interlocked.Exchange(ref average, ema.Value); + } + } +} diff --git a/Assets/Mirror/Core/Tools/TimeSample.cs.meta b/Assets/Mirror/Core/Tools/TimeSample.cs.meta new file mode 100644 index 0000000..1aad767 --- /dev/null +++ b/Assets/Mirror/Core/Tools/TimeSample.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 26c32f6429554546a88d800c846c74ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/TimeSample.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Utils.cs b/Assets/Mirror/Core/Tools/Utils.cs new file mode 100644 index 0000000..406484f --- /dev/null +++ b/Assets/Mirror/Core/Tools/Utils.cs @@ -0,0 +1,222 @@ +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.SceneManagement; + +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, uint 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 + { + // detect headless / dedicated server mode + // SystemInfo.graphicsDeviceType is never null in the editor. + // UNITY_SERVER works in builds for all Unity versions 2019 LTS and later. + // For Unity 2019 / 2020, there is no way to detect Server Build checkbox + // state in Build Settings, so they never auto-start headless server / client. + // UNITY_SERVER works in the editor in Unity 2021 LTS and later + // because that's when Dedicated Server platform was added. + // It is intentional for editor play mode to auto-start headless server / client + // when Dedicated Server platform is selected in the editor so that editor + // acts like a headless build to every extent possible for testing / debugging. + public static bool IsHeadless() => +#if UNITY_SERVER + true; +#else + SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null; +#endif + + // detect WebGL mode + public const bool IsWebGL = +#if UNITY_WEBGL + true; +#else + false; +#endif + + // detect Debug mode + public const bool IsDebug = +#if DEBUG + true; +#else + false; +#endif + + 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 + } + + // simplified IsSceneObject check from Mirror II + public static bool IsSceneObject(NetworkIdentity identity) + { + // original UNET / Mirror still had the IsPersistent check. + // it never fires though. even for Prefabs dragged to the Scene. + // (see Scene Objects example scene.) + // #if UNITY_EDITOR + // if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) + // return false; + // #endif + + return identity.gameObject.hideFlags != HideFlags.NotEditable && + identity.gameObject.hideFlags != HideFlags.HideAndDontSave && + identity.sceneId != 0; + } + + 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"; + } + + // pretty print seconds as hours:minutes:seconds(.milliseconds/100)s. + // double for long running servers. + public static string PrettySeconds(double seconds) + { + TimeSpan t = TimeSpan.FromSeconds(seconds); + string res = ""; + if (t.Days > 0) res += $"{t.Days}d"; + if (t.Hours > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Hours}h"; + if (t.Minutes > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Minutes}m"; + // 0.5s, 1.5s etc. if any milliseconds. 1s, 2s etc. if any seconds + if (t.Milliseconds > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Seconds}.{(t.Milliseconds / 100)}s"; + else if (t.Seconds > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Seconds}s"; + // if the string is still empty because the value was '0', then at least + // return the seconds instead of returning an empty string + return res != "" ? res : "0s"; + } + + // universal .spawned function + 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; + } + + // keep a GUI window in screen. + // for example. if it's at x=1000 and screen is resized to w=500, + // it won't get lost in the invisible area etc. + public static Rect KeepInScreen(Rect rect) + { + // ensure min + rect.x = Math.Max(rect.x, 0); + rect.y = Math.Max(rect.y, 0); + + // ensure max + rect.x = Math.Min(rect.x, Screen.width - rect.width); + rect.y = Math.Min(rect.y, Screen.width - rect.height); + + return rect; + } + + // create local connections pair and connect them + public static void CreateLocalConnections( + out LocalConnectionToClient connectionToClient, + out LocalConnectionToServer connectionToServer) + { + connectionToServer = new LocalConnectionToServer(); + connectionToClient = new LocalConnectionToClient(); + connectionToServer.connectionToClient = connectionToClient; + connectionToClient.connectionToServer = connectionToServer; + } + + public static bool IsSceneActive(string scene) + { + Scene activeScene = SceneManager.GetActiveScene(); + return activeScene.path == scene || + activeScene.name == scene; + } + } +} diff --git a/Assets/Mirror/Core/Tools/Utils.cs.meta b/Assets/Mirror/Core/Tools/Utils.cs.meta new file mode 100644 index 0000000..aa65c05 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Utils.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: b530ce39098b54374a29ad308c8e4554 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Utils.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Vector3Long.cs b/Assets/Mirror/Core/Tools/Vector3Long.cs new file mode 100644 index 0000000..d54a46c --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector3Long.cs @@ -0,0 +1,125 @@ +#pragma warning disable CS0659 // 'Vector3Long' overrides Object.Equals(object o) but does not override Object.GetHashCode() +#pragma warning disable CS0661 // 'Vector3Long' defines operator == or operator != but does not override Object.GetHashCode() + +// Vector3Long by mischa (based on game engine project) +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public struct Vector3Long + { + public long x; + public long y; + public long z; + + public static readonly Vector3Long zero = new Vector3Long(0, 0, 0); + public static readonly Vector3Long one = new Vector3Long(1, 1, 1); + public static readonly Vector3Long forward = new Vector3Long(0, 0, 1); + public static readonly Vector3Long back = new Vector3Long(0, 0, -1); + public static readonly Vector3Long left = new Vector3Long(-1, 0, 0); + public static readonly Vector3Long right = new Vector3Long(1, 0, 0); + public static readonly Vector3Long up = new Vector3Long(0, 1, 0); + public static readonly Vector3Long down = new Vector3Long(0, -1, 0); + + // constructor ///////////////////////////////////////////////////////// + public Vector3Long(long x, long y, long z) + { + this.x = x; + this.y = y; + this.z = z; + } + + // operators /////////////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator +(Vector3Long a, Vector3Long b) => + new Vector3Long(a.x + b.x, a.y + b.y, a.z + b.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator -(Vector3Long a, Vector3Long b) => + new Vector3Long(a.x - b.x, a.y - b.y, a.z - b.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator -(Vector3Long v) => + new Vector3Long(-v.x, -v.y, -v.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator *(Vector3Long a, long n) => + new Vector3Long(a.x * n, a.y * n, a.z * n); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator *(long n, Vector3Long a) => + new Vector3Long(a.x * n, a.y * n, a.z * n); + + // == returns true if approximately equal (with epsilon). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector3Long a, Vector3Long b) => + a.x == b.x && + a.y == b.y && + a.z == b.z; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector3Long a, Vector3Long b) => !(a == b); + + // NO IMPLICIT System.Numerics.Vector3Long conversion because double<->float + // would silently lose precision in large worlds. + + // [i] component index. useful for iterating all components etc. + public long this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + default: throw new IndexOutOfRangeException($"Vector3Long[{index}] out of range."); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + default: throw new IndexOutOfRangeException($"Vector3Long[{index}] out of range."); + } + } + } + + // instance functions ////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"({x} {y} {z})"; + + // equality //////////////////////////////////////////////////////////// + // implement Equals & HashCode explicitly for performance. + // calling .Equals (instead of "==") checks for exact equality. + // (API compatibility) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Vector3Long other) => + x == other.x && y == other.y && z == other.z; + + // Equals(object) can reuse Equals(Vector4) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object other) => + other is Vector3Long vector4 && Equals(vector4); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(x, y, z); +#endif + } +} diff --git a/Assets/Mirror/Core/Tools/Vector3Long.cs.meta b/Assets/Mirror/Core/Tools/Vector3Long.cs.meta new file mode 100644 index 0000000..68e89d5 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector3Long.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 18efa4e349254185ad257401dd24628b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Vector3Long.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Vector4Long.cs b/Assets/Mirror/Core/Tools/Vector4Long.cs new file mode 100644 index 0000000..0eba99a --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector4Long.cs @@ -0,0 +1,126 @@ +#pragma warning disable CS0659 // 'Vector4Long' overrides Object.Equals(object o) but does not override Object.GetHashCode() +#pragma warning disable CS0661 // 'Vector4Long' defines operator == or operator != but does not override Object.GetHashCode() + +// Vector4Long by mischa (based on game engine project) +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public struct Vector4Long + { + public long x; + public long y; + public long z; + public long w; + + public static readonly Vector4Long zero = new Vector4Long(0, 0, 0, 0); + public static readonly Vector4Long one = new Vector4Long(1, 1, 1, 1); + + // constructor ///////////////////////////////////////////////////////// + public Vector4Long(long x, long y, long z, long w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + // operators /////////////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator +(Vector4Long a, Vector4Long b) => + new Vector4Long(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator -(Vector4Long a, Vector4Long b) => + new Vector4Long(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator -(Vector4Long v) => + new Vector4Long(-v.x, -v.y, -v.z, -v.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator *(Vector4Long a, long n) => + new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator *(long n, Vector4Long a) => + new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n); + + // == returns true if approximately equal (with epsilon). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector4Long a, Vector4Long b) => + a.x == b.x && + a.y == b.y && + a.z == b.z && + a.w == b.w; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector4Long a, Vector4Long b) => !(a == b); + + // NO IMPLICIT System.Numerics.Vector4Long conversion because double<->float + // would silently lose precision in large worlds. + + // [i] component index. useful for iterating all components etc. + public long this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + case 3: return w; + default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range."); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + case 3: + w = value; + break; + default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range."); + } + } + } + + // instance functions ////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"({x} {y} {z} {w})"; + + // equality //////////////////////////////////////////////////////////// + // implement Equals & HashCode explicitly for performance. + // calling .Equals (instead of "==") checks for exact equality. + // (API compatibility) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Vector4Long other) => + x == other.x && y == other.y && z == other.z && w == other.w; + + // Equals(object) can reuse Equals(Vector4) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object other) => + other is Vector4Long vector4 && Equals(vector4); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(x, y, z, w); +#endif + } +} diff --git a/Assets/Mirror/Core/Tools/Vector4Long.cs.meta b/Assets/Mirror/Core/Tools/Vector4Long.cs.meta new file mode 100644 index 0000000..b6ed4d8 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector4Long.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9f69bf6b6d73476ab3f31dd635bb6497 +timeCreated: 1726777293 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Vector4Long.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transport.cs b/Assets/Mirror/Core/Transport.cs similarity index 88% rename from Assets/Mirror/Runtime/Transport.cs rename to Assets/Mirror/Core/Transport.cs index f831bf1..926a5c0 100644 --- a/Assets/Mirror/Runtime/Transport.cs +++ b/Assets/Mirror/Core/Transport.cs @@ -30,13 +30,18 @@ namespace Mirror /// Abstract transport layer component public abstract class Transport : MonoBehaviour { - // common ////////////////////////////////////////////////////////////// /// The current transport used by Mirror. - public static Transport activeTransport; + public static Transport active; /// Is this transport available in the current platform? public abstract bool Available(); + /// Is this transport encrypted for secure communication? + public virtual bool IsEncrypted => false; + + /// If encrypted, which cipher is used? + public virtual string EncryptionCipher => ""; + // client ////////////////////////////////////////////////////////////// /// Called by Transport when the client connected to the server. public Action OnClientConnected; @@ -52,15 +57,23 @@ public abstract class Transport : MonoBehaviour public Action, int> OnClientDataSent; /// Called by Transport when the client encountered an error. - public Action OnClientError; + public Action OnClientError; + + /// Called by Transport when the client encountered an error. + public Action OnClientTransportException; /// 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. + + // Deprecated 2024-07-20 + [Obsolete("Use OnServerConnectedWithAddress and pass the remote client address instead")] public Action OnServerConnected; + /// Called by Transport when a new client connected to the server. + public Action OnServerConnectedWithAddress; + /// Called by Transport when the server received a message from a client. public Action, int> OnServerDataReceived; @@ -73,7 +86,11 @@ public abstract class Transport : MonoBehaviour /// 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; + public Action OnServerError; + + /// Called by Transport when a server's connection encountered a problem. + /// If a Disconnect will also be raised, raise the Error first. + public Action OnServerTransportException; /// Called by Transport when a client disconnected from the server. public Action OnServerDisconnected; @@ -180,6 +197,7 @@ public virtual void ServerLateUpdate() {} public abstract void Shutdown(); /// Called by Unity when quitting. Inheriting Transports should call base for proper Shutdown. + // (this can't be in OnDestroy: https://github.com/MirrorNetworking/Mirror/issues/3952) public virtual void OnApplicationQuit() { // stop transport (e.g. to shut down threads) diff --git a/Assets/Mirror/Core/Transport.cs.meta b/Assets/Mirror/Core/Transport.cs.meta new file mode 100644 index 0000000..4931067 --- /dev/null +++ b/Assets/Mirror/Core/Transport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: cfffcac25d6d64ced9de620159e221b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Transport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/TransportError.cs b/Assets/Mirror/Core/TransportError.cs new file mode 100644 index 0000000..b452015 --- /dev/null +++ b/Assets/Mirror/Core/TransportError.cs @@ -0,0 +1,17 @@ +// Mirror transport error code enum. +// most transport implementations should use a subset of this, +// and then translate the transport error codes to mirror error codes. +namespace Mirror +{ + public enum TransportError : byte + { + DnsResolve, // failed to resolve a host name + Refused, // connection refused by other end. server full etc. + 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. + } +} diff --git a/Assets/Mirror/Core/TransportError.cs.meta b/Assets/Mirror/Core/TransportError.cs.meta new file mode 100644 index 0000000..1b56a9b --- /dev/null +++ b/Assets/Mirror/Core/TransportError.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ce162bdedd704db9b8c35d163f0c1d54 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/TransportError.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/WeaverFuse.cs b/Assets/Mirror/Core/WeaverFuse.cs new file mode 100644 index 0000000..f850323 --- /dev/null +++ b/Assets/Mirror/Core/WeaverFuse.cs @@ -0,0 +1,22 @@ +// safety fuse for weaver to flip. +// runtime can check this to ensure weaving succeded. +// otherwise running server/client would give lots of random 'writer not found' etc. errors. +// this is much cleaner. +// +// note that ILPostProcessor errors already block entering playmode. +// however, issues could still stop the weaving from running at all. +// WeaverFuse can check if it actually ran. +namespace Mirror +{ + public static class WeaverFuse + { + // this trick only works for ILPostProcessor. + // CompilationFinishedHook can't weaver Mirror.dll. + public static bool Weaved() => +#if UNITY_2020_3_OR_NEWER + false; +#else + true; +#endif + } +} diff --git a/Assets/Mirror/Core/WeaverFuse.cs.meta b/Assets/Mirror/Core/WeaverFuse.cs.meta new file mode 100644 index 0000000..b4572a5 --- /dev/null +++ b/Assets/Mirror/Core/WeaverFuse.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4de3dfbcbd2e41fcac947c04bcac52c9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/WeaverFuse.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/AndroidManifestHelper.cs b/Assets/Mirror/Editor/AndroidManifestHelper.cs index 78f408d..bae0082 100644 --- a/Assets/Mirror/Editor/AndroidManifestHelper.cs +++ b/Assets/Mirror/Editor/AndroidManifestHelper.cs @@ -10,104 +10,107 @@ using UnityEditor.Android; #endif - -[InitializeOnLoad] -public class AndroidManifestHelper : IPreprocessBuildWithReport, IPostprocessBuildWithReport -#if UNITY_ANDROID - , IPostGenerateGradleAndroidProject -#endif +namespace Mirror { - 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); + [InitializeOnLoad] + public class AndroidManifestHelper : IPreprocessBuildWithReport, IPostprocessBuildWithReport + #if UNITY_ANDROID + , IPostGenerateGradleAndroidProject + #endif + { + public int callbackOrder { get { return 99999; } } - string androidNamepsaceURI; - XmlElement element = (XmlElement)doc.SelectSingleNode("/manifest"); - if (element == null) - { - UnityEngine.Debug.LogError("Could not find manifest tag in android manifest."); - return; - } + #if UNITY_ANDROID + public void OnPostGenerateGradleAndroidProject(string path) + { + string manifestFolder = Path.Combine(path, "src/main"); + string sourceFile = manifestFolder + "/AndroidManifest.xml"; + // Load android manifest file + XmlDocument doc = new XmlDocument(); + doc.Load(sourceFile); - // 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 + string androidNamespaceURI; + XmlElement element = (XmlElement)doc.SelectSingleNode("/manifest"); + if (element == null) + { + UnityEngine.Debug.LogError("Could not find manifest tag in android manifest."); + return; + } - 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)) + // Get android namespace URI from the manifest + androidNamespaceURI = element.GetAttribute("xmlns:android"); + if (string.IsNullOrEmpty(androidNamespaceURI)) { - element = e; - break; + UnityEngine.Debug.LogError("Could not find Android Namespace in manifest."); + return; } + AddOrRemoveTag(doc, + androidNamespaceURI, + "/manifest", + "uses-permission", + "android.permission.CHANGE_WIFI_MULTICAST_STATE", + true, + false); + AddOrRemoveTag(doc, + androidNamespaceURI, + "/manifest", + "uses-permission", + "android.permission.INTERNET", + true, + false); + doc.Save(sourceFile); } + #endif - if (required) + static void AddOrRemoveTag(XmlDocument doc, string @namespace, string path, string elementName, string name, bool required, bool modifyIfFound, params string[] attrs) // name, value pairs { - if (element == null) + var nodes = doc.SelectNodes(path + "/" + elementName); + XmlElement element = null; + foreach (XmlElement e in nodes) { - var parent = doc.SelectSingleNode(path); - element = doc.CreateElement(elementName); - element.SetAttribute("name", @namespace, name); - parent.AppendChild(element); + if (name == null || name == e.GetAttribute("name", @namespace)) + { + element = e; + break; + } } - for (int i = 0; i < attrs.Length; i += 2) + if (required) { - if (modifyIfFound || string.IsNullOrEmpty(element.GetAttribute(attrs[i], @namespace))) + if (element == null) { - if (attrs[i + 1] != null) - { - element.SetAttribute(attrs[i], @namespace, attrs[i + 1]); - } - else + 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))) { - element.RemoveAttribute(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) + else { - element.ParentNode.RemoveChild(element); + if (element != null && modifyIfFound) + { + element.ParentNode.RemoveChild(element); + } } } - } - public void OnPostprocessBuild(BuildReport report) {} - public void OnPreprocessBuild(BuildReport report) {} + 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 index 1281aea..996e3e9 100644 --- a/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta +++ b/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/AndroidManifestHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/EditorHelper.cs b/Assets/Mirror/Editor/EditorHelper.cs index b10c7b0..c3551ab 100644 --- a/Assets/Mirror/Editor/EditorHelper.cs +++ b/Assets/Mirror/Editor/EditorHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; @@ -27,5 +28,14 @@ public static string FindPath() return string.Empty; } } + + + public static IEnumerable IterateOverProject(string filter) + { + foreach (string guid in AssetDatabase.FindAssets(filter)) + { + yield return AssetDatabase.GUIDToAssetPath(guid); + } + } } } diff --git a/Assets/Mirror/Editor/EditorHelper.cs.meta b/Assets/Mirror/Editor/EditorHelper.cs.meta index a1cd814..c2a4577 100644 --- a/Assets/Mirror/Editor/EditorHelper.cs.meta +++ b/Assets/Mirror/Editor/EditorHelper.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/EditorHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Empty.meta b/Assets/Mirror/Editor/Empty.meta deleted file mode 100644 index ee87976..0000000 --- a/Assets/Mirror/Editor/Empty.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 18ab111..0000000 --- a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-12-12 diff --git a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta b/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta deleted file mode 100644 index 79a200d..0000000 --- a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 82e5275..0000000 --- a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index b8cbaeb..0000000 --- a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 257467f..0000000 --- a/Assets/Mirror/Editor/Empty/Logging.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta deleted file mode 100644 index 832876f..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta deleted file mode 100644 index 3214b08..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta deleted file mode 100644 index 2c1fac4..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta b/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta deleted file mode 100644 index b4c277d..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta b/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta deleted file mode 100644 index a1a0af3..0000000 --- a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4d54a29ddd5b52b4eaa07ed39c0e3e83 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Icon/MirrorIcon.png b/Assets/Mirror/Editor/Icon/MirrorIcon.png new file mode 100644 index 0000000..a77ca23 --- /dev/null +++ b/Assets/Mirror/Editor/Icon/MirrorIcon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f1ee2247319c7a4e5d1ab13ad4981c21c5a408299ae4158050a63ac7f1eba47 +size 138247 diff --git a/Assets/Mirror/Editor/Icon/MirrorIcon.png.meta b/Assets/Mirror/Editor/Icon/MirrorIcon.png.meta new file mode 100644 index 0000000..18e01bd --- /dev/null +++ b/Assets/Mirror/Editor/Icon/MirrorIcon.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 7453abfe9e8b2c04a8a47eb536fe21eb +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: -1 + wrapV: -1 + wrapW: -1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Icon/MirrorIcon.png + uploadId: 736421 diff --git a/Assets/Mirror/Editor/InspectorHelper.cs.meta b/Assets/Mirror/Editor/InspectorHelper.cs.meta index 852ff71..91205ff 100644 --- a/Assets/Mirror/Editor/InspectorHelper.cs.meta +++ b/Assets/Mirror/Editor/InspectorHelper.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/InspectorHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/LagCompensatorInspector.cs b/Assets/Mirror/Editor/LagCompensatorInspector.cs new file mode 100644 index 0000000..f706384 --- /dev/null +++ b/Assets/Mirror/Editor/LagCompensatorInspector.cs @@ -0,0 +1,14 @@ +using UnityEditor; + +namespace Mirror +{ + [CustomEditor(typeof(LagCompensator))] + public class LagCompensatorInspector : Editor + { + public override void OnInspectorGUI() + { + EditorGUILayout.HelpBox("Preview Component - Feedback appreciated on GitHub or Discord!", MessageType.Warning); + DrawDefaultInspector(); + } + } +} diff --git a/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta b/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta new file mode 100644 index 0000000..0dba2ed --- /dev/null +++ b/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 703e39b5385ae2e479987ff4ec0707a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/LagCompensatorInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef b/Assets/Mirror/Editor/Mirror.Editor.asmdef index 0d59f9f..800e67b 100644 --- a/Assets/Mirror/Editor/Mirror.Editor.asmdef +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef @@ -2,8 +2,9 @@ "name": "Mirror.Editor", "rootNamespace": "", "references": [ - "Mirror", - "Unity.Mirror.CodeGen" + "GUID:30817c1a0e6d646d99c048fc403f5979", + "GUID:72872094b21c16e48b631b2224833d49", + "GUID:1d0b9d21c3ff546a4aa32399dfd33474" ], "includePlatforms": [ "Editor" diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta b/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta index e2e6f2a..2d148c7 100644 --- a/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Mirror.Editor.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs index 54b3ae7..52c56d6 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -43,7 +43,8 @@ bool SyncsAnything(Type scriptClass) void OnEnable() { - if (target == null) { Debug.LogWarning("NetworkBehaviourInspector had no target object"); return; } + // sometimes target is null. just return early. + if (target == null) return; // If target's base class is changed from NetworkBehaviour to MonoBehaviour // then Unity temporarily keep using this Inspector causing things to break @@ -85,7 +86,15 @@ protected void DrawDefaultSyncSettings() EditorGUILayout.Space(); EditorGUILayout.LabelField("Sync Settings", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + // sync direction + SerializedProperty syncDirection = serializedObject.FindProperty("syncDirection"); + EditorGUILayout.PropertyField(syncDirection); + + // sync mdoe: only show for ServerToClient components + if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient) + EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + + // sync interval EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval")); // apply diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta index 78d9fa8..428f07d 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkBehaviourInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs b/Assets/Mirror/Editor/NetworkInformationPreview.cs index 2c8874b..b4dbe5e 100644 --- a/Assets/Mirror/Editor/NetworkInformationPreview.cs +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs @@ -126,7 +126,9 @@ float DrawNetworkIdentityInfo(NetworkIdentity identity, float initialX, float Y) 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); + + // height needs a +1 to line up nicely + Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y + 1); foreach (NetworkIdentityInfo info in infos) { @@ -171,7 +173,7 @@ float DrawNetworkBehaviors(NetworkIdentity identity, float initialX, float Y) float DrawObservers(NetworkIdentity identity, float initialX, float Y) { - if (identity.observers != null && identity.observers.Count > 0) + if (identity.observers.Count > 0) { Rect observerRect = new Rect(initialX, Y + 10, 200, 20); @@ -252,7 +254,7 @@ IEnumerable GetNetworkIdentityInfo(NetworkIdentity identity 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 Owned", identity.isOwned)); infos.Add(GetBoolean("Is Local Player", identity.isLocalPlayer)); } return infos; diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta b/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta index 9bf2de4..e67d4ea 100644 --- a/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkInformationPreview.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkManagerEditor.cs b/Assets/Mirror/Editor/NetworkManagerEditor.cs index 94b0844..a7aa9bc 100644 --- a/Assets/Mirror/Editor/NetworkManagerEditor.cs +++ b/Assets/Mirror/Editor/NetworkManagerEditor.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEditorInternal; using UnityEngine; @@ -42,6 +45,98 @@ public override void OnInspectorGUI() { serializedObject.ApplyModifiedProperties(); } + + if (GUILayout.Button("Populate Spawnable Prefabs")) + { + ScanForNetworkIdentities(); + } + + // clicking the Populate button in a large project can add hundreds of entries. + // have a clear button in case that wasn't intended. + GUI.enabled = networkManager.spawnPrefabs.Count > 0; + if (GUILayout.Button("Clear Spawnable Prefabs")) + { + ClearNetworkIdentities(); + } + GUI.enabled = true; + } + + void ScanForNetworkIdentities() + { + List identities = new List(); + bool cancelled = false; + try + { + string[] paths = EditorHelper.IterateOverProject("t:prefab").ToArray(); + int count = 0; + foreach (string path in paths) + { + // ignore test & example prefabs. + // users sometimes keep the folders in their projects. + if (path.Contains("Mirror/Tests/") || + path.Contains("Mirror/Examples/")) + { + continue; + } + + if (EditorUtility.DisplayCancelableProgressBar("Searching for NetworkIdentities..", + $"Scanned {count}/{paths.Length} prefabs. Found {identities.Count} new ones", + count / (float)paths.Length)) + { + cancelled = true; + break; + } + + count++; + + NetworkIdentity ni = AssetDatabase.LoadAssetAtPath(path); + if (!ni) + { + continue; + } + + if (!networkManager.spawnPrefabs.Contains(ni.gameObject)) + { + identities.Add(ni.gameObject); + } + + } + } + finally + { + + EditorUtility.ClearProgressBar(); + if (!cancelled) + { + // RecordObject is needed for "*" to show up in Scene. + // however, this only saves List.Count without the entries. + Undo.RecordObject(networkManager, "NetworkManager: populated prefabs"); + + // add the entries + networkManager.spawnPrefabs.AddRange(identities); + + // sort alphabetically for better UX + networkManager.spawnPrefabs = networkManager.spawnPrefabs.OrderBy(go => go.name).ToList(); + + // SetDirty is required to save the individual entries properly. + EditorUtility.SetDirty(target); + } + // Loading assets might use a lot of memory, so try to unload them after + Resources.UnloadUnusedAssets(); + } + } + + void ClearNetworkIdentities() + { + // RecordObject is needed for "*" to show up in Scene. + // however, this only saves List.Count without the entries. + Undo.RecordObject(networkManager, "NetworkManager: cleared prefabs"); + + // add the entries + networkManager.spawnPrefabs.Clear(); + + // SetDirty is required to save the individual entries properly. + EditorUtility.SetDirty(target); } static void DrawHeader(Rect headerRect) diff --git a/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta b/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta index 7fe8dbc..78cc6d8 100644 --- a/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta +++ b/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkManagerEditor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkScenePostProcess.cs b/Assets/Mirror/Editor/NetworkScenePostProcess.cs index c60493d..9baad1b 100644 --- a/Assets/Mirror/Editor/NetworkScenePostProcess.cs +++ b/Assets/Mirror/Editor/NetworkScenePostProcess.cs @@ -33,9 +33,7 @@ public static void OnPostProcessScene() // 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, @@ -57,19 +55,28 @@ public static void OnPostProcessScene() 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. + // if we have a prefab open in the prefab scene string path = identity.gameObject.scene.path; if (string.IsNullOrWhiteSpace(path)) + { + // pressing play while in prefab edit mode used to freeze/crash Unity 2019. + // this seems fine now so we don't need to stop the editor anymore. +#if UNITY_2020_3_OR_NEWER + Debug.LogWarning($"{identity.name} was open in Prefab Edit Mode while launching with Mirror. If this causes issues, please let us know."); +#else Debug.LogError($"{identity.name} is currently open in Prefab Edit Mode. Please open the actual scene before launching Mirror."); + EditorApplication.isPlaying = false; +#endif + } + // if an unopened scene needs resaving 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; + // nothing good will happen when trying to launch with invalid sceneIds. + // show an error and stop playing immediately. + Debug.LogError($"Scene {path} needs to be opened and resaved, because the scene object {identity.name} has no valid sceneId yet."); + EditorApplication.isPlaying = false; + } } } } @@ -80,28 +87,20 @@ static void PrepareSceneObject(NetworkIdentity identity) // set scene hash identity.SetSceneIdSceneHashPartInternal(); - // disable it + // spawnable scene objects are force disabled on scene load to + // ensure Start/Update/etc. aren't called until actually spawned. + // // 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 index b567cc9..2ea2e63 100644 --- a/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta +++ b/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkScenePostProcess.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/ReadOnlyDrawer.cs b/Assets/Mirror/Editor/ReadOnlyDrawer.cs new file mode 100644 index 0000000..4a09707 --- /dev/null +++ b/Assets/Mirror/Editor/ReadOnlyDrawer.cs @@ -0,0 +1,19 @@ +using UnityEngine; +using UnityEditor; + +namespace Mirror +{ + [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] + public class ReadOnlyDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + // Cache the current GUI enabled state + bool prevGuiEnabledState = GUI.enabled; + + GUI.enabled = false; + EditorGUI.PropertyField(position, property, label, true); + GUI.enabled = prevGuiEnabledState; + } + } +} diff --git a/Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta b/Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta new file mode 100644 index 0000000..81d626f --- /dev/null +++ b/Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 22f17bdd21f104c41bc175937fefbdec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/ReadOnlyDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SceneDrawer.cs.meta b/Assets/Mirror/Editor/SceneDrawer.cs.meta index 6a996dc..04d6cdc 100644 --- a/Assets/Mirror/Editor/SceneDrawer.cs.meta +++ b/Assets/Mirror/Editor/SceneDrawer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/SceneDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs index 2c95bcf..735b018 100644 --- a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs +++ b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Reflection; +using System.Text.RegularExpressions; using UnityEditor; namespace Mirror @@ -18,7 +19,12 @@ public SyncObjectCollectionField(FieldInfo field) { this.field = field; visible = false; - label = $"{field.Name} [{field.FieldType.Name}]"; + + // field.FieldType.Name has a backtick and number at the end, e.g. SyncList`1 + // so we split it and only take the first part so it looks nicer. + // e.g. SyncList`1 -> SyncList + // Better to do it one time here than in hot path in OnInspectorGUI + label = $"{field.Name} [{Regex.Replace(field.FieldType.Name, @"`\d+", "")}]"; } } diff --git a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta index 44ba75d..b830f9a 100644 --- a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta +++ b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 6f90afab12e04f0e945d83e9d38308a3 -timeCreated: 1632556645 \ No newline at end of file +timeCreated: 1632556645 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta index 6311f1d..3a9216d 100644 --- a/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta +++ b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/SyncVarAttributeDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SyncVarDrawer.cs b/Assets/Mirror/Editor/SyncVarDrawer.cs deleted file mode 100644 index b0532ae..0000000 --- a/Assets/Mirror/Editor/SyncVarDrawer.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 0ee91aa..0000000 --- a/Assets/Mirror/Editor/SyncVarDrawer.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 874812594431423b84f763b987ff9681 -timeCreated: 1632553007 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta index d356af8..a58871b 100644 --- a/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta +++ b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Empty.meta b/Assets/Mirror/Editor/Weaver/Empty.meta deleted file mode 100644 index 6e29ee7..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index a88144a..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index 685f914..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index b38f171..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index cbea4d6..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index a214b81..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/Program.cs +++ /dev/null @@ -1 +0,0 @@ -// Removed 05/09/20 diff --git a/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta deleted file mode 100644 index 0a14018..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index a88144a..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index 0a7c2aa..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2fdbc52..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2020-09 diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta deleted file mode 100644 index 81b9576..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index a88144a..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index b73b047..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 index 81827c5..54b718a 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 251338e67afb4cefa38da924f8c50a6e -timeCreated: 1628851818 \ No newline at end of file +timeCreated: 1628851818 diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs index 9016949..b5db851 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs @@ -57,11 +57,7 @@ public static void WeaveExistingAssemblies() } } -#if UNITY_2019_3_OR_NEWER EditorUtility.RequestScriptReload(); -#else - UnityEditorInternal.InternalEditorUtility.RequestScriptReload(); -#endif } static Assembly FindCompilationPipelineAssembly(string assemblyName) => @@ -79,13 +75,16 @@ public static void OnCompilationFinished(string assemblyPath, CompilerMessage[] return; } - // Should not run on the editor only assemblies - if (assemblyPath.Contains("-Editor") || assemblyPath.Contains(".Editor")) + // Should not run on the editor only assemblies (test ones still need to be weaved) + if (assemblyPath.Contains("-Editor") || + (assemblyPath.Contains(".Editor") && !assemblyPath.Contains(".Tests"))) { return; } - // don't weave mirror files + // skip Mirror.dll because CompilationFinishedHook can't weave itself. + // this would cause a sharing violation. + // skip Mirror.Weaver.dll too. string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); if (assemblyName == MirrorRuntimeAssemblyName || assemblyName == MirrorWeaverAssemblyName) { diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta index ed537ab..33e73f7 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta index f8c7139..3d07ffc 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 47026732f0fa475c94bd1dd41f1de559 -timeCreated: 1629379868 \ No newline at end of file +timeCreated: 1629379868 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta index eca31e3..af6dddc 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: b73d0f106ba84aa983baa5142b08a0a9 -timeCreated: 1628851346 \ No newline at end of file +timeCreated: 1628851346 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta index 6ef7bf3..d1fa101 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 09082db63d1d48d9ab91320165c1b684 -timeCreated: 1628859005 \ No newline at end of file +timeCreated: 1628859005 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta index 1e5091e..c442f3b 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 9009d1db4ed44f6694a92bf8ad7738e9 -timeCreated: 1630129423 \ No newline at end of file +timeCreated: 1630129423 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs index cbc8e41..0823234 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs @@ -14,7 +14,7 @@ // we need a custom resolver for ILPostProcessor. #if UNITY_2020_3_OR_NEWER using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Threading; @@ -26,12 +26,23 @@ namespace Mirror.Weaver class ILPostProcessorAssemblyResolver : IAssemblyResolver { readonly string[] assemblyReferences; - readonly Dictionary assemblyCache = - new Dictionary(); + + // originally we used Dictionary + lock. + // Resolve() is called thousands of times for large projects. + // ILPostProcessor is multithreaded, so best to use ConcurrentDictionary without the lock here. + readonly ConcurrentDictionary assemblyCache = + new ConcurrentDictionary(); + + // Resolve() calls FindFile() every time. + // thousands of times for String => mscorlib alone in large projects. + // cache the results! ILPostProcessor is multithreaded, so use a ConcurrentDictionary here. + readonly ConcurrentDictionary fileNameCache = + new ConcurrentDictionary(); + readonly ICompiledAssembly compiledAssembly; AssemblyDefinition selfAssembly; - Logger Log; + readonly Logger Log; public ILPostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly, Logger Log) { @@ -54,56 +65,82 @@ protected virtual void Dispose(bool disposing) public AssemblyDefinition Resolve(AssemblyNameReference name) => Resolve(name, new ReaderParameters(ReadingMode.Deferred)); + // here is an example on when this is called: + // Player : NetworkBehaviour has a [SyncVar] of type String. + // Weaver's SyncObjectInitializer checks if ImplementsSyncObject() + // which needs to resolve the type 'String' from mscorlib. + // Resolve() lives in CecilX.MetadataResolver.Resolve() + // which calls assembly_resolver.Resolve(). + // which uses our ILPostProcessorAssemblyResolver here. + // + // for large projects, this is called thousands of times for mscorlib alone. + // initially ILPostProcessorAssemblyResolver took 30x longer than with CompilationFinishedHook. + // we need to cache and speed up everything we can here! public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) { - lock (assemblyCache) + if (name.Name == compiledAssembly.Name) + return selfAssembly; + + // cache FindFile. + // in large projects, this is called thousands(!) of times for String=>mscorlib alone. + // reduces a single String=>mscorlib resolve from 0.771ms to 0.015ms. + // => 50x improvement in TypeReference.Resolve() speed! + // => 22x improvement in Weaver speed! + if (!fileNameCache.TryGetValue(name.Name, out string fileName)) { - if (name.Name == compiledAssembly.Name) - return selfAssembly; + fileName = FindFile(name.Name); + fileNameCache.TryAdd(name.Name, fileName); + } - string fileName = FindFile(name); - if (fileName == null) + 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! + + // the fix for #2503 started showing this warning for Bee.BeeDriver on mac, + // which is for compilation. we can ignore that one. + if (!name.Name.StartsWith("Bee.BeeDriver")) { - // 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; } + return null; + } - DateTime lastWriteTime = File.GetLastWriteTime(fileName); - - string cacheKey = fileName + lastWriteTime; - - if (assemblyCache.TryGetValue(cacheKey, out AssemblyDefinition result)) - return result; - - parameters.AssemblyResolver = this; + // try to get cached assembly by filename + writetime + DateTime lastWriteTime = File.GetLastWriteTime(fileName); + string cacheKey = fileName + lastWriteTime; + if (assemblyCache.TryGetValue(cacheKey, out AssemblyDefinition result)) + return result; - MemoryStream ms = MemoryStreamFor(fileName); + // otherwise resolve and cache a new assembly + parameters.AssemblyResolver = this; + MemoryStream ms = MemoryStreamFor(fileName); - string pdb = fileName + ".pdb"; - if (File.Exists(pdb)) - parameters.SymbolStream = MemoryStreamFor(pdb); + string pdb = fileName + ".pdb"; + if (File.Exists(pdb)) + parameters.SymbolStream = MemoryStreamFor(pdb); - AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters); - assemblyCache.Add(cacheKey, assemblyDefinition); - return assemblyDefinition; - } + AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters); + assemblyCache.TryAdd(cacheKey, assemblyDefinition); + return assemblyDefinition; } // find assemblyname in assembly's references - string FindFile(AssemblyNameReference name) + string FindFile(string name) { - string fileName = assemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == name.Name + ".dll"); - if (fileName != null) - return fileName; + // perhaps the type comes from a .dll or .exe + // check both in one call without Linq instead of iterating twice like originally + foreach (string r in assemblyReferences) + { + if (Path.GetFileNameWithoutExtension(r) == name) + return r; + } - // perhaps the type comes from an exe instead - fileName = assemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == name.Name + ".exe"); - if (fileName != null) - return fileName; + // this is called thousands(!) of times. + // constructing strings only once saves ~0.1ms per call for mscorlib. + string dllName = name + ".dll"; // 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 @@ -114,7 +151,7 @@ string FindFile(AssemblyNameReference name) // 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"); + string candidate = Path.Combine(parentDir, dllName); if (File.Exists(candidate)) return candidate; } @@ -122,8 +159,9 @@ string FindFile(AssemblyNameReference name) return null; } - // open file as MemoryStream - // attempts multiple times, not sure why.. + // open file as MemoryStream. + // ILPostProcessor is multithreaded. + // retry a few times in case another thread is still accessing the file. static MemoryStream MemoryStreamFor(string fileName) { return Retry(10, TimeSpan.FromSeconds(1), () => diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta index 07289dd..1ece15f 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 0b3e94696e22440ead0b3a42411bbe14 -timeCreated: 1629693784 \ No newline at end of file +timeCreated: 1629693784 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta index e06dfa7..b7dabcc 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 2a4b115486b74d27a9540f3c39ae2d46 -timeCreated: 1630152191 \ No newline at end of file +timeCreated: 1630152191 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta index 9d7e0a2..7b16277 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 5f113eb695b348b5b28cd85358c8959a -timeCreated: 1628859074 \ No newline at end of file +timeCreated: 1628859074 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs index 2c070cc..e8595fd 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs @@ -24,6 +24,7 @@ void Add(string message, DiagnosticType logType) public void LogDiagnostics(string message, DiagnosticType logType = DiagnosticType.Warning) { + // TODO IN-44868 FIX IS IN 2021.3.32f1, 2022.3.11f1, 2023.2.0b13 and 2023.3.0a8 // 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. diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta index 8bb72e0..dee8c87 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: e7b56e7826664e34a415e4b70d958f2a -timeCreated: 1629533154 \ No newline at end of file +timeCreated: 1629533154 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta index d361e21..40788da 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 6403a7e3b3ae4e009ae282f111d266e0 -timeCreated: 1629709256 \ No newline at end of file +timeCreated: 1629709256 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta index d9b6f6b..ff5c4ca 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: a1003b568bad4e69b961c4c81d5afd96 -timeCreated: 1629709223 \ No newline at end of file +timeCreated: 1629709223 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Extensions.cs b/Assets/Mirror/Editor/Weaver/Extensions.cs index e5ddb1f..566a51a 100644 --- a/Assets/Mirror/Editor/Weaver/Extensions.cs +++ b/Assets/Mirror/Editor/Weaver/Extensions.cs @@ -12,8 +12,18 @@ public static bool Is(this TypeReference td, Type type) => ? td.GetElementType().FullName == type.FullName : td.FullName == type.FullName; + // check if 'td' is exactly of type T. + // it does not check if any base type is of , only the specific type. + // for example: + // NetworkConnection Is NetworkConnection: true + // NetworkConnectionToClient Is NetworkConnection: false public static bool Is(this TypeReference td) => Is(td, typeof(T)); + // check if 'tr' is derived from T. + // it does not check if 'tr' is exactly T. + // for example: + // NetworkConnection IsDerivedFrom: false + // NetworkConnectionToClient IsDerivedFrom: true public static bool IsDerivedFrom(this TypeReference tr) => IsDerivedFrom(tr, typeof(T)); public static bool IsDerivedFrom(this TypeReference tr, Type baseClass) @@ -79,7 +89,10 @@ public static bool IsMultidimensionalArray(this TypeReference tr) => public static bool IsNetworkIdentityField(this TypeReference tr) => tr.Is() || tr.Is() || - tr.IsDerivedFrom(); + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + tr.IsDerivedFrom() || + tr.Is(); public static bool CanBeResolved(this TypeReference parent) { @@ -229,7 +242,16 @@ public static IEnumerable FindAllPublicFields(this TypeDefiniti { foreach (FieldDefinition field in typeDefinition.Fields) { - if (field.IsStatic || field.IsPrivate) + // ignore static, private, protected fields + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3485 + // credit: James Frowen + if (field.IsStatic || field.IsPrivate || field.IsFamily) + continue; + + // also ignore internal fields + // we dont want to create different writers for this type if they are in current dll or another dll + // so we have to ignore internal in all cases + if (field.IsAssembly) continue; if (field.IsNotSerialized) @@ -266,7 +288,7 @@ public static AssemblyNameReference FindReference(this ModuleDefinition module, // 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 + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 public static TypeReference ApplyGenericParameters(this TypeReference parentReference, TypeReference childReference) @@ -306,7 +328,7 @@ public static TypeReference ApplyGenericParameters(this TypeReference parentRefe } // Finds the type reference for a generic parameter with the provided name in the child reference - // Originally by James-Frowen under MIT + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 static TypeReference FindMatchingGenericArgument(TypeReference childReference, string paramName) { diff --git a/Assets/Mirror/Editor/Weaver/Extensions.cs.meta b/Assets/Mirror/Editor/Weaver/Extensions.cs.meta index 78660f9..d337f22 100644 --- a/Assets/Mirror/Editor/Weaver/Extensions.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Extensions.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Extensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Helpers.cs.meta b/Assets/Mirror/Editor/Weaver/Helpers.cs.meta index 231f539..c8bf3d1 100644 --- a/Assets/Mirror/Editor/Weaver/Helpers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Helpers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Helpers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Logger.cs.meta b/Assets/Mirror/Editor/Weaver/Logger.cs.meta index 3f62978..9aebf80 100644 --- a/Assets/Mirror/Editor/Weaver/Logger.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Logger.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Logger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs index 55893f7..db2d5af 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs @@ -10,10 +10,11 @@ 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); + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.Write(thrusting); + writer.WritePackedUInt32((uint)spin); + base.SendCommandInternal(cmdName, cmdHash, writer, channel); + NetworkWriterPool.Return(writer); } public void CallCmdThrust(float thrusting, int spin) @@ -52,6 +53,11 @@ public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Write worker.Emit(OpCodes.Ldarg_0); // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, channel); @@ -78,7 +84,7 @@ protected static void InvokeCmdCmdThrust(NetworkBehaviour obj, NetworkReader rea */ 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); + string cmdName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, method); MethodDefinition cmd = new MethodDefinition(cmdName, MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, diff --git a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta index 20c3e15..488c9c6 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta index 3c81894..9cbd357 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta index ef3f5f4..154c73e 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs index ac00f65..47f1b94 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -26,6 +26,9 @@ class NetworkBehaviourProcessor List syncObjects = new List(); // Dictionary syncVarNetIds = new Dictionary(); + // - Every syncvar with a hook has a new field created to store the Action delegate so we don't allocate on every hook invocation + // This dictionary maps each syncvar field to the field that will store the hook method delegate instance, and the method from which the delegate instance is constructed from + Dictionary syncVarHookDelegates = new Dictionary(); readonly List commands = new List(); readonly List clientRpcs = new List(); readonly List targetRpcs = new List(); @@ -71,7 +74,7 @@ public bool Process(ref bool WeavingFailed) MarkAsProcessed(netBehaviourSubclass); // deconstruct tuple and set fields - (syncVars, syncVarNetIds) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed); + (syncVars, syncVarNetIds, syncVarHookDelegates) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed); syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed); @@ -205,20 +208,29 @@ public static bool WriteArguments(ILProcessor worker, Writers writers, Logger Lo } #region mark / check type as processed - public const string ProcessedFunctionName = "MirrorProcessed"; + public const string ProcessedFunctionName = "Weaved"; - // by adding an empty MirrorProcessed() function + // check if the type has a "Weaved" function already public static bool WasProcessed(TypeDefinition td) { return td.GetMethod(ProcessedFunctionName) != null; } + // add the Weaved() function which returns true. + // can be called at runtime and from tests to check if weaving succeeded. public void MarkAsProcessed(TypeDefinition td) { if (!WasProcessed(td)) { - MethodDefinition versionMethod = new MethodDefinition(ProcessedFunctionName, MethodAttributes.Private, weaverTypes.Import(typeof(void))); + // add a function: + // public override bool MirrorProcessed() { return true; } + // ReuseSlot means 'override'. + MethodDefinition versionMethod = new MethodDefinition( + ProcessedFunctionName, + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.ReuseSlot, + weaverTypes.Import(typeof(bool))); ILProcessor worker = versionMethod.Body.GetILProcessor(); + worker.Emit(OpCodes.Ldc_I4_1); worker.Emit(OpCodes.Ret); td.Methods.Add(versionMethod); } @@ -312,7 +324,7 @@ void InjectIntoStaticConstructor(ref bool WeavingFailed) // we need to inject several initializations into NetworkBehaviour ctor void InjectIntoInstanceConstructor(ref bool WeavingFailed) { - if (syncObjects.Count == 0) + if ((syncObjects.Count == 0) && (syncVarHookDelegates.Count == 0)) return; // find instance constructor @@ -340,6 +352,14 @@ void InjectIntoInstanceConstructor(ref bool WeavingFailed) SyncObjectInitializer.GenerateSyncObjectInitializer(ctorWorker, weaverTypes, fd); } + // initialize all delegate fields in ctor + foreach(KeyValuePair entry in syncVarHookDelegates) + { + FieldDefinition syncVarField = entry.Key; + (FieldDefinition hookDelegate, MethodDefinition hookMethod) = entry.Value; + syncVarAttributeProcessor.GenerateSyncVarHookDelegateInitializer(ctorWorker, syncVarField, hookDelegate, hookMethod); + } + // add final 'Ret' instruction to ctor ctorWorker.Append(ctorWorker.Create(OpCodes.Ret)); } @@ -397,7 +417,7 @@ void GenerateSerialization(ref bool WeavingFailed) MethodDefinition serialize = new MethodDefinition(SerializeMethodName, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - weaverTypes.Import()); + weaverTypes.Import(typeof(void))); serialize.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, weaverTypes.Import())); serialize.Parameters.Add(new ParameterDefinition("forceAll", ParameterAttributes.None, weaverTypes.Import())); @@ -405,10 +425,7 @@ void GenerateSerialization(ref bool WeavingFailed) 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); - + // base.SerializeSyncVars(writer, forceAll); MethodReference baseSerialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, SerializeMethodName); if (baseSerialize != null) { @@ -419,16 +436,20 @@ void GenerateSerialization(ref bool WeavingFailed) // 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); + // Generates: + // if (forceAll) + // { + // writer.WriteInt(health); + // ... + // } Instruction initialStateLabel = worker.Create(OpCodes.Nop); // forceAll - worker.Emit(OpCodes.Ldarg_2); - worker.Emit(OpCodes.Brfalse, initialStateLabel); + worker.Emit(OpCodes.Ldarg_2); // load 'forceAll' flag + worker.Emit(OpCodes.Brfalse, initialStateLabel); // start the 'if forceAll' branch + // generates write.Write(syncVar) for each SyncVar in forceAll case foreach (FieldDefinition syncVarDef in syncVars) { FieldReference syncVar = syncVarDef; @@ -442,7 +463,21 @@ void GenerateSerialization(ref bool WeavingFailed) // this worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, syncVar); - MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + MethodReference writeFunc; + // For NBs we always need to use the default NetworkBehaviour write func + // since the reader counter part uses that exact layout which is not easy to change + // without introducing more edge cases + // effectively this disallows custom NB-type writers/readers on SyncVars + // see: https://github.com/MirrorNetworking/Mirror/issues/2680 + if (syncVar.FieldType.IsDerivedFrom()) + { + writeFunc = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + } + else + { + writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + } + if (writeFunc != null) { worker.Emit(OpCodes.Call, writeFunc); @@ -455,22 +490,21 @@ void GenerateSerialization(ref bool WeavingFailed) } } - // always return true if forceAll - - // Generates: return true - worker.Emit(OpCodes.Ldc_I4_1); + // if (forceAll) then always return at the end of the 'if' case worker.Emit(OpCodes.Ret); - // Generates: end if (forceAll); + // end the 'if' case for "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); + worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference); MethodReference writeUint64Func = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); worker.Emit(OpCodes.Call, writeUint64Func); @@ -480,7 +514,6 @@ void GenerateSerialization(ref bool WeavingFailed) int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); foreach (FieldDefinition syncVarDef in syncVars) { - FieldReference syncVar = syncVarDef; if (netBehaviourSubclass.HasGenericParameters) { @@ -491,7 +524,7 @@ void GenerateSerialization(ref bool WeavingFailed) // Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL) // base worker.Emit(OpCodes.Ldarg_0); - worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference); + worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference); // 8 bytes = long worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit); worker.Emit(OpCodes.And); @@ -504,7 +537,21 @@ void GenerateSerialization(ref bool WeavingFailed) worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, syncVar); - MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + MethodReference writeFunc; + // For NBs we always need to use the default NetworkBehaviour write func + // since the reader counter part uses that exact layout which is not easy to change + // without introducing more edge cases + // effectively this disallows custom NB-type writers/readers on SyncVars + // see: https://github.com/MirrorNetworking/Mirror/issues/2680 + if (syncVar.FieldType.IsDerivedFrom()) + { + writeFunc = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + } + else + { + writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + } + if (writeFunc != null) { worker.Emit(OpCodes.Call, writeFunc); @@ -516,11 +563,6 @@ void GenerateSerialization(ref bool WeavingFailed) return; } - // something was dirty - worker.Emit(OpCodes.Ldc_I4_1); - // set dirtyLocal to true - worker.Emit(OpCodes.Stloc_0); - worker.Append(varLabel); dirtyBit += 1; } @@ -529,8 +571,7 @@ void GenerateSerialization(ref bool WeavingFailed) //worker.Emit(OpCodes.Ldstr, $"Injected Serialize {netBehaviourSubclass.Name}"); //worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference); - // generate: return dirtyLocal - worker.Emit(OpCodes.Ldloc_0); + // generate: return worker.Emit(OpCodes.Ret); netBehaviourSubclass.Methods.Add(serialize); } @@ -552,15 +593,18 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldflda, syncVar); } - // hook? then push 'new Action(Hook)' onto stack - MethodDefinition hookMethod = syncVarAttributeProcessor.GetHookMethod(netBehaviourSubclass, syncVar, ref WeavingFailed); - if (hookMethod != null) + // If a hook exists, then we need to load the hook delegate on the stack + // The hook delegate is created once in the constructor and stored in an instance field + // We load the delegate from this instance field to avoid instantiating a new delegate instance every time (drastically reduces allocations) + if(syncVarHookDelegates.TryGetValue(syncVar, out (FieldDefinition hookDelegateField, MethodDefinition) value)) { - syncVarAttributeProcessor.GenerateNewActionFromHookMethod(syncVar, worker, hookMethod); + // A hook exists. Push this.hookDelegateField onto the stack + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, value.hookDelegateField); } - // otherwise push 'null' as hook else { + // No hook exists. Push 'null' as hook worker.Emit(OpCodes.Ldnull); } @@ -589,10 +633,9 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav 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()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (syncVar.FieldType.IsDerivedFrom() || syncVar.FieldType.Is()) { // reader worker.Emit(OpCodes.Ldarg_1); diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta index 67c27dc..a48253f 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs index 280240c..2a919d2 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs @@ -1,4 +1,5 @@ // finds all readers and writers and register them +using System.Collections.Generic; using System.Linq; using Mono.CecilX; using Mono.CecilX.Cil; @@ -17,10 +18,53 @@ public static bool Process(AssemblyDefinition CurrentAssembly, IAssemblyResolver // 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. + // process dependencies first, this way weaver can process types of other assemblies properly. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2503 + // + // find NetworkReader/Writer extensions in referenced assemblies + IEnumerable assemblyReferences = FindProcessTargetAssemblies(CurrentAssembly, resolver) + .Where(assembly => assembly != null && assembly != CurrentAssembly); + + foreach (AssemblyDefinition referencedAssembly in assemblyReferences) + ProcessAssemblyClasses(CurrentAssembly, referencedAssembly, writers, readers, ref WeavingFailed); + return ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly, writers, readers, ref WeavingFailed); } + // look for assembly instead of relying on CurrentAssembly.MainModule. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3816 + static List FindProcessTargetAssemblies(AssemblyDefinition assembly, IAssemblyResolver resolver) + { + HashSet processedAssemblies = new HashSet(); + List assemblies = new List(); + ProcessAssembly(assembly); + return assemblies; + + void ProcessAssembly(AssemblyDefinition current) + { + // If the assembly has already been processed, we skip it + if (current.FullName == Weaver.MirrorAssemblyName || !processedAssemblies.Add(current.FullName)) + return; + + IEnumerable references = current.MainModule.AssemblyReferences; + + // If there is no Mirror reference, there will be no ReaderWriter or NetworkMessage, so skip + if (references.All(reference => reference.Name != Weaver.MirrorAssemblyName)) + return; + + // Add the assembly to the processed set and list + assemblies.Add(current); + + // Process the references of the current assembly + foreach (AssemblyNameReference reference in references) + { + AssemblyDefinition referencedAssembly = resolver.Resolve(reference); + if (referencedAssembly != null) + ProcessAssembly(referencedAssembly); + } + } + } + static void ProcessMirrorAssemblyClasses(AssemblyDefinition CurrentAssembly, IAssemblyResolver resolver, Logger Log, Writers writers, Readers readers, ref bool WeavingFailed) { // find Mirror.dll in assembly's references. diff --git a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta index c14d6fa..cd06284 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs index df44f20..ca2d7b9 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs @@ -8,7 +8,7 @@ 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); + string rpcName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, md); MethodDefinition rpc = new MethodDefinition(rpcName, MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, weaverTypes.Import(typeof(void))); @@ -82,6 +82,11 @@ public static MethodDefinition ProcessRpcCall(WeaverTypes weaverTypes, Writers w worker.Emit(OpCodes.Ldarg_0); // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, channel); diff --git a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta index 22375ba..c47fb64 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs index 50df598..a0b7739 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs @@ -25,7 +25,7 @@ static bool ProcessSiteMethod(WeaverTypes weaverTypes, Logger Log, MethodDefinit { if (md.Name == ".cctor" || md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || - md.Name.StartsWith(Weaver.InvokeRpcPrefix)) + md.Name.StartsWith(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix)) return false; if (md.IsAbstract) @@ -123,7 +123,16 @@ static void InjectGuardParameters(MethodDefinition md, ILProcessor worker, Instr ParameterDefinition param = md.Parameters[index]; if (param.IsOut) { - TypeReference elementType = param.ParameterType.GetElementType(); + // this causes IL2CPP build issues with generic out parameters: + // https://github.com/MirrorNetworking/Mirror/issues/3482 + // TypeReference elementType = param.ParameterType.GetElementType(); + // + // instead we need to use ElementType not GetElementType() + // GetElementType() will get the element type of the inner elementType + // which will return wrong type for arrays and generic + // credit: JamesFrowen + ByReferenceType byRefType = (ByReferenceType)param.ParameterType; + TypeReference elementType = byRefType.ElementType; md.Body.Variables.Add(new VariableDefinition(elementType)); md.Body.InitLocals = true; diff --git a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta index 5a5451d..697a431 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta index 22f976e..97e66ae 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs index 2cec8a4..8143e86 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs @@ -16,7 +16,7 @@ public static List FindSyncObjectsFields(Writers writers, Reade foreach (FieldDefinition fd in td.Fields) { - if (fd.FieldType.IsGenericParameter) + if (fd.FieldType.IsGenericParameter || fd.ContainsGenericParameter) { // can't call .Resolve on generic ones continue; diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta index 0efe434..1408ab3 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs index 0a6f376..73a9526 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs @@ -11,7 +11,7 @@ namespace Mirror.Weaver public static class SyncVarAttributeAccessReplacer { // process the module - public static void Process(ModuleDefinition moduleDef, SyncVarAccessLists syncVarAccessLists) + public static void Process(Logger Log, ModuleDefinition moduleDef, SyncVarAccessLists syncVarAccessLists) { DateTime startTime = DateTime.Now; @@ -20,31 +20,31 @@ public static void Process(ModuleDefinition moduleDef, SyncVarAccessLists syncVa { if (td.IsClass) { - ProcessClass(syncVarAccessLists, td); + ProcessClass(Log, syncVarAccessLists, td); } } Console.WriteLine($" ProcessSitesModule {moduleDef.Name} elapsed time:{(DateTime.Now - startTime)}"); } - static void ProcessClass(SyncVarAccessLists syncVarAccessLists, TypeDefinition td) + static void ProcessClass(Logger Log, SyncVarAccessLists syncVarAccessLists, TypeDefinition td) { //Console.WriteLine($" ProcessClass {td}"); // process all methods in this class foreach (MethodDefinition md in td.Methods) { - ProcessMethod(syncVarAccessLists, md); + ProcessMethod(Log, syncVarAccessLists, md); } // processes all nested classes in this class recursively foreach (TypeDefinition nested in td.NestedTypes) { - ProcessClass(syncVarAccessLists, nested); + ProcessClass(Log, syncVarAccessLists, nested); } } - static void ProcessMethod(SyncVarAccessLists syncVarAccessLists, MethodDefinition md) + static void ProcessMethod(Logger Log, SyncVarAccessLists syncVarAccessLists, MethodDefinition md) { // process all references to replaced members with properties //Log.Warning($" ProcessSiteMethod {md}"); @@ -52,7 +52,7 @@ static void ProcessMethod(SyncVarAccessLists syncVarAccessLists, MethodDefinitio // skip static constructor, "MirrorProcessed", "InvokeUserCode_" if (md.Name == ".cctor" || md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || - md.Name.StartsWith(Weaver.InvokeRpcPrefix)) + md.Name.StartsWith(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix)) return; // skip abstract @@ -67,32 +67,64 @@ static void ProcessMethod(SyncVarAccessLists syncVarAccessLists, MethodDefinitio for (int i = 0; i < md.Body.Instructions.Count;) { Instruction instr = md.Body.Instructions[i]; - i += ProcessInstruction(syncVarAccessLists, md, instr, i); + i += ProcessInstruction(Log, syncVarAccessLists, md, instr, i); } } } - static int ProcessInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, int iCount) + static int ProcessInstruction(Logger Log, SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, int iCount) { // stfld (sets value of a field)? - if (instr.OpCode == OpCodes.Stfld && instr.Operand is FieldDefinition opFieldst) + if (instr.OpCode == OpCodes.Stfld) { - ProcessSetInstruction(syncVarAccessLists, md, instr, opFieldst); + // operand is a FieldDefinition in the same assembly? + if (instr.Operand is FieldDefinition opFieldst) + { + ProcessSetInstruction(syncVarAccessLists, md, instr, opFieldst); + } + // operand is a FieldReference in another assembly? + // this is not supported just yet. + // compilation error is better than silently failing SyncVar serialization at runtime. + // https://github.com/MirrorNetworking/Mirror/issues/3525 + else if (instr.Operand is FieldReference opFieldstRef) + { + // resolve it from the other assembly + FieldDefinition field = opFieldstRef.Resolve(); + + // [SyncVar]? + if (field.HasCustomAttribute()) + { + // ILPostProcessor would need to Process() the assembly's + // references before processing this one. + // we can not control the order. + // instead, Log an error to suggest adding a SetSyncVar(value) function. + // this is a very easy solution for a very rare edge case. + Log.Error($"'[SyncVar] {opFieldstRef.DeclaringType.Name}.{opFieldstRef.Name}' in '{field.Module.Name}' is modified by '{md.FullName}' in '{md.Module.Name}'. Modifying a [SyncVar] from another assembly is not supported. Please add a: 'public void Set{opFieldstRef.Name}(value) {{ this.{opFieldstRef.Name} = value; }}' method in '{opFieldstRef.DeclaringType.Name}' and call this function from '{md.FullName}' instead."); + } + } } // ldfld (load value of a field)? - if (instr.OpCode == OpCodes.Ldfld && instr.Operand is FieldDefinition opFieldld) + if (instr.OpCode == OpCodes.Ldfld) { - // this instruction gets the value of a field. cache the field reference. - ProcessGetInstruction(syncVarAccessLists, md, instr, opFieldld); + // operand is a FieldDefinition in the same assembly? + if (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) + if (instr.OpCode == OpCodes.Ldflda) { - // watch out for initobj instruction - // see https://github.com/vis2k/Mirror/issues/696 - return ProcessLoadAddressInstruction(syncVarAccessLists, md, instr, opFieldlda, iCount); + // operand is a FieldDefinition in the same assembly? + if (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) diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta index e8c2500..ce80ec2 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs index a5f95cd..76e3ac7 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs @@ -46,15 +46,26 @@ public MethodDefinition GetHookMethod(TypeDefinition td, FieldDefinition syncVar return FindHookMethod(td, syncVar, hookFunctionName, ref WeavingFailed); } + // Create a field definition for a field that will store the Action delegate instance for the syncvar hook method (only instantiate delegate once) + public FieldDefinition CreateNewActionFieldDefinitionFromHookMethod(FieldDefinition syncVarField) + { + TypeReference actionRef = assembly.MainModule.ImportReference(typeof(Action<,>)); + GenericInstanceType syncVarHookActionDelegateType = actionRef.MakeGenericInstanceType(syncVarField.FieldType, syncVarField.FieldType); + string syncVarHookDelegateFieldName = $"_Mirror_SyncVarHookDelegate_{syncVarField.Name}"; + return new FieldDefinition(syncVarHookDelegateFieldName, FieldAttributes.Public, syncVarHookActionDelegateType); + } + // push hook from GetHookMethod() onto the stack as a new Action. // allows for reuse without handling static/virtual cases every time. + // perf warning: it is recommended to use this method only when generating IL to create a new Action() in order to store it into a field + // avoid using this to emit IL to instantiate a new action instance every single time one is needed for the same method 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. + // we support static hooks and instance hooks. if (hookMethod.IsStatic) { // for static hooks, we need to push 'null' first. @@ -95,15 +106,23 @@ public void GenerateNewActionFromHookMethod(FieldDefinition syncVar, ILProcessor // 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)); } + // generates CIL to set an Action instance field to a new Action(hookMethod) + // this.hookDelegate = new Action(HookMethod); + public void GenerateSyncVarHookDelegateInitializer(ILProcessor worker, FieldDefinition syncVar, FieldDefinition hookDelegate, MethodDefinition hookMethod) + { + // push this + worker.Emit(OpCodes.Ldarg_0); + // push new Action(hookMethod) + GenerateNewActionFromHookMethod(syncVar, worker, hookMethod); + // set field + worker.Emit(OpCodes.Stfld, hookDelegate); + } + MethodDefinition FindHookMethod(TypeDefinition td, FieldDefinition syncVar, string hookFunctionName, ref bool WeavingFailed) { List methods = td.GetMethods(hookFunctionName); @@ -203,7 +222,9 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference); worker.Emit(OpCodes.Ret); } - else if (fd.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { // return this.GetSyncVarNetworkBehaviour(ref field, uint netId); // this. @@ -240,7 +261,7 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina // } // // 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) + public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition fd, string originalName, long dirtyBit, FieldDefinition netFieldId, Dictionary syncVarHookDelegates, ref bool WeavingFailed) { //Create the set method MethodDefinition set = new MethodDefinition($"set_Network{originalName}", MethodAttributes.Public | @@ -302,11 +323,17 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition // push the dirty bit for this SyncVar worker.Emit(OpCodes.Ldc_I8, dirtyBit); - // hook? then push 'new Action(Hook)' onto stack + // hook? then push 'this.HookDelegate' onto stack MethodDefinition hookMethod = GetHookMethod(td, fd, ref WeavingFailed); if (hookMethod != null) { - GenerateNewActionFromHookMethod(fd, worker, hookMethod); + // Create the field that will store a single instance of the hook as a delegate (field will be set in constructor) + FieldDefinition hookActionDelegateField = CreateNewActionFieldDefinitionFromHookMethod(fd); + syncVarHookDelegates[fd] = (hookActionDelegateField, hookMethod); + + // push this.hookActionDelegateField + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, hookActionDelegateField); } // otherwise push 'null' as hook else @@ -331,10 +358,9 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition 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()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { // NetworkIdentity setter needs one more parameter: netId field ref // (actually its a NetworkBehaviourSyncVar type) @@ -361,18 +387,20 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition return set; } - public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVarNetIds, long dirtyBit, ref bool WeavingFailed) + public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVarNetIds, Dictionary syncVarHookDelegates, 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()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { netIdField = new FieldDefinition($"___{fd.Name}NetId", FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed - weaverTypes.Import()); + weaverTypes.Import()); netIdField.DeclaringType = td; syncVarNetIds[fd] = netIdField; @@ -388,7 +416,7 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVars, Dictionary syncVarNetIds) ProcessSyncVars(TypeDefinition td, ref bool WeavingFailed) + public (List syncVars, Dictionary syncVarNetIds, Dictionary syncVarHookDelegates) ProcessSyncVars(TypeDefinition td, ref bool WeavingFailed) { List syncVars = new List(); Dictionary syncVarNetIds = new Dictionary(); + Dictionary syncVarHookDelegates = 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 @@ -442,13 +471,6 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary SyncVarLimit) @@ -475,9 +497,19 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary fields + foreach((FieldDefinition hookDelegateInstanceField, MethodDefinition) entry in syncVarHookDelegates.Values) + { + td.Fields.Add(entry.hookDelegateInstanceField); + } + + // include parent class syncvars + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3457 + int parentSyncVarCount = syncVarAccessLists.GetSyncVarStart(td.BaseType.FullName); + syncVarAccessLists.SetNumSyncVars(td.FullName, parentSyncVarCount + syncVars.Count); + + return (syncVars, syncVarNetIds, syncVarHookDelegates); } } } diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta index 982f768..3b77329 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs index 8afba94..8d56040 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs @@ -9,13 +9,21 @@ 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(); + if (md.Parameters.Count > 0) + { + // we need to allow both NetworkConnection, and inheriting types. + // NetworkBehaviour.SendTargetRpc takes a NetworkConnection parameter. + // fixes https://github.com/vis2k/Mirror/issues/3290 + TypeReference type = md.Parameters[0].ParameterType; + return type.Is() || + type.IsDerivedFrom(); + } + return false; } 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); + string trgName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, md); MethodDefinition rpc = new MethodDefinition(trgName, MethodAttributes.Family | MethodAttributes.Static | @@ -34,16 +42,25 @@ public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, R // 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 + // TargetRpcs are sent from server to client. + // on server, we currently support two types: + // TargetRpc(NetworkConnection) + // TargetRpc(NetworkConnectionToClient) + // however, it's always a connection to client. + // in the future, only NetworkConnectionToClient will be supported. + // explicit typing helps catch issues at compile time. + // + // on client, InvokeTargetRpc calls the original code. + // we need to fill in the NetworkConnection parameter. + // NetworkClient.connection is always a connection to server. // - // 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); + // we used to pass NetworkClient.connection as the TargetRpc parameter. + // which caused: https://github.com/MirrorNetworking/Mirror/issues/3455 + // when the parameter is defined as a NetworkConnectionToClient. + // + // a client's connection never fits into a NetworkConnectionToClient. + // we need to always pass null here. + worker.Emit(OpCodes.Ldnull); } // process reader parameters and skip first one if first one is NetworkConnection @@ -122,6 +139,11 @@ public static MethodDefinition ProcessTargetRpcCall(WeaverTypes weaverTypes, Wri } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, targetRpcAttr.GetField("channel", 0)); diff --git a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta index 0ff7cc5..f4019ff 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Readers.cs b/Assets/Mirror/Editor/Weaver/Readers.cs index fa888c9..875f8b7 100644 --- a/Assets/Mirror/Editor/Weaver/Readers.cs +++ b/Assets/Mirror/Editor/Weaver/Readers.cs @@ -33,12 +33,24 @@ public Readers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinit internal void Register(TypeReference dataType, MethodReference methodReference) { - if (readFuncs.ContainsKey(dataType)) + // sometimes we define multiple read methods for the same type. + // for example: + // ReadInt() // alwasy writes 4 bytes: should be available to the user for binary protocols etc. + // ReadVarInt() // varint compression: we may want Weaver to always use this for minimal bandwidth + // give the user a way to define the weaver prefered one if two exists: + // "[WeaverPriority]" attribute is automatically detected and prefered. + MethodDefinition methodDefinition = methodReference.Resolve(); + bool priority = methodDefinition.HasCustomAttribute(); + // if (priority) Log.Warning($"Weaver: Registering priority Read<{dataType.FullName}> with {methodReference.FullName}.", methodReference); + + // Weaver sometimes calls Register for multiple times because we resolve assemblies multiple times. + // if the function name is the same: always use the latest one. + // if the function name differes: use the priority one. + if (readFuncs.TryGetValue(dataType, out MethodReference existingMethod) && // if it was already defined + existingMethod.FullName != methodReference.FullName && // and this one is a different name + !priority) // and it's not the priority one { - // 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); + return; // then skip } // we need to import type when we Initialize Readers so import here in case it is used anywhere else @@ -113,7 +125,16 @@ MethodReference GenerateReader(TypeReference variableReference, ref bool Weaving return GenerateReadCollection(variableReference, elementType, nameof(NetworkReaderExtensions.ReadList), ref WeavingFailed); } - else if (variableReference.IsDerivedFrom()) + else if (variableDefinition.Is(typeof(HashSet<>))) + { + GenericInstanceType genericInstance = (GenericInstanceType)variableReference; + TypeReference elementType = genericInstance.GenericArguments[0]; + + return GenerateReadCollection(variableReference, elementType, nameof(NetworkReaderExtensions.ReadHashSet), ref WeavingFailed); + } + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (variableReference.IsDerivedFrom() || variableReference.Is()) { return GetNetworkBehaviourReader(variableReference); } diff --git a/Assets/Mirror/Editor/Weaver/Readers.cs.meta b/Assets/Mirror/Editor/Weaver/Readers.cs.meta index 838ff59..5831615 100644 --- a/Assets/Mirror/Editor/Weaver/Readers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Readers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Readers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Resolvers.cs b/Assets/Mirror/Editor/Weaver/Resolvers.cs index a9d551b..0af32ca 100644 --- a/Assets/Mirror/Editor/Weaver/Resolvers.cs +++ b/Assets/Mirror/Editor/Weaver/Resolvers.cs @@ -42,6 +42,38 @@ public static MethodReference ResolveMethod(TypeReference t, AssemblyDefinition return null; } + public static FieldReference ResolveField(TypeReference tr, AssemblyDefinition assembly, Logger Log, string name, ref bool WeavingFailed) + { + if (tr == null) + { + Log.Error($"Cannot resolve Field {name} without a class"); + WeavingFailed = true; + return null; + } + FieldReference field = ResolveField(tr, assembly, Log, m => m.Name == name, ref WeavingFailed); + if (field == null) + { + Log.Error($"Field not found with name {name} in type {tr.Name}", tr); + WeavingFailed = true; + } + return field; + } + + public static FieldReference ResolveField(TypeReference t, AssemblyDefinition assembly, Logger Log, System.Func predicate, ref bool WeavingFailed) + { + foreach (FieldDefinition fieldRef in t.Resolve().Fields) + { + if (predicate(fieldRef)) + { + return assembly.MainModule.ImportReference(fieldRef); + } + } + + Log.Error($"Field not found in type {t.Name}", t); + WeavingFailed = true; + return null; + } + public static MethodReference TryResolveMethodInParents(TypeReference tr, AssemblyDefinition assembly, string name) { if (tr == null) diff --git a/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta b/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta index f4f6602..49235c1 100644 --- a/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Resolvers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta index 9a4da44..31a2ed3 100644 --- a/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta +++ b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 6905230c3c4c4e158760065a93380e83 -timeCreated: 1629348618 \ No newline at end of file +timeCreated: 1629348618 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta index 890b4dc..ec97f9d 100644 --- a/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta +++ b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef index 4566bb2..987382c 100644 --- a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef +++ b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef @@ -2,7 +2,7 @@ "name": "Unity.Mirror.CodeGen", "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], "includePlatforms": [ "Editor" diff --git a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta index b65a0cd..5fc0fe1 100644 --- a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta +++ b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Weaver.cs b/Assets/Mirror/Editor/Weaver/Weaver.cs index 2644e68..3aef7b8 100644 --- a/Assets/Mirror/Editor/Weaver/Weaver.cs +++ b/Assets/Mirror/Editor/Weaver/Weaver.cs @@ -2,14 +2,14 @@ using System.Collections.Generic; using System.Diagnostics; using Mono.CecilX; +using Mono.CecilX.Cil; +using Mono.CecilX.Rocks; 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"; @@ -118,7 +118,17 @@ bool WeaveModule(ModuleDefinition moduleDefinition) Stopwatch watch = Stopwatch.StartNew(); watch.Start(); - foreach (TypeDefinition td in moduleDefinition.Types) + // ModuleDefinition.Types only finds top level types. + // GetAllTypes recursively finds all nested types as well. + // fixes nested types not being weaved, for example: + // class Parent { // ModuleDefinition.Types finds this + // class Child { // .Types.NestedTypes finds this + // class GrandChild {} // only GetAllTypes finds this too + // } + // } + // note this is not about inheritance, only about type definitions. + // see test: NetworkBehaviourTests.DeeplyNested() + foreach (TypeDefinition td in moduleDefinition.GetAllTypes()) { if (td.IsClass && td.BaseType.CanBeResolved()) { @@ -142,6 +152,16 @@ void CreateGeneratedCodeClass() weaverTypes.Import()); } + void ToggleWeaverFuse() + { + // // find Weaved() function + MethodDefinition func = weaverTypes.weaverFuseMethod.Resolve(); + // // change return 0 to return 1 + + ILProcessor worker = func.Body.GetILProcessor(); + func.Body.Instructions[0] = worker.Create(OpCodes.Ldc_I4_1); + } + // Weave takes an AssemblyDefinition to be compatible with both old and // new weavers: // * old takes a filepath, new takes a in-memory byte[] @@ -214,7 +234,7 @@ public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out b if (modified) { - SyncVarAttributeAccessReplacer.Process(moduleDefinition, syncVarAccessLists); + SyncVarAttributeAccessReplacer.Process(Log, moduleDefinition, syncVarAccessLists); // add class that holds read/write functions moduleDefinition.Types.Add(GeneratedCodeClass); @@ -228,6 +248,12 @@ public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out b //CurrentAssembly.Write(new WriterParameters{ WriteSymbols = true }); } + // if weaving succeeded, switch on the Weaver Fuse in Mirror.dll + if (CurrentAssembly.Name.Name == MirrorAssemblyName) + { + ToggleWeaverFuse(); + } + return true; } catch (Exception e) diff --git a/Assets/Mirror/Editor/Weaver/Weaver.cs.meta b/Assets/Mirror/Editor/Weaver/Weaver.cs.meta index 0ea2dfe..2eaf569 100644 --- a/Assets/Mirror/Editor/Weaver/Weaver.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Weaver.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Weaver.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta index 68643b2..4ef61d6 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta +++ b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/WeaverExceptions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs index 173af58..aa0d42d 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs @@ -10,7 +10,7 @@ public class WeaverTypes { public MethodReference ScriptableObjectCreateInstanceMethod; - public MethodReference NetworkBehaviourDirtyBitsReference; + public FieldReference NetworkBehaviourDirtyBitsReference; public MethodReference GetWriterReference; public MethodReference ReturnWriterReference; @@ -53,6 +53,9 @@ public class WeaverTypes public MethodReference readNetworkBehaviourGeneric; + public TypeReference weaverFuseType; + public MethodReference weaverFuseMethod; + // attributes public TypeDefinition initializeOnLoadMethodAttribute; public TypeDefinition runtimeInitializeOnLoadMethodAttribute; @@ -75,30 +78,19 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail TypeReference ActionType = Import(typeof(Action<,>)); ActionT_T = Resolvers.ResolveMethod(ActionType, assembly, Log, ".ctor", ref WeavingFailed); + weaverFuseType = Import(typeof(WeaverFuse)); + weaverFuseMethod = Resolvers.ResolveMethod(weaverFuseType, assembly, Log, "Weaved", 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); + NetworkClientConnectionReference = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_connection", 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); + NetworkBehaviourDirtyBitsReference = Resolvers.ResolveField(NetworkBehaviourType, assembly, Log, "syncVarDirtyBits", ref WeavingFailed); generatedSyncVarSetter = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter", ref WeavingFailed); generatedSyncVarSetter_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_GameObject", ref WeavingFailed); @@ -114,9 +106,25 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkIdentity", ref WeavingFailed); getSyncVarNetworkBehaviourReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkBehaviour", 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 RemoteProcedureCallsType = Import(typeof(RemoteCalls.RemoteProcedureCalls)); registerCommandReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterCommand", ref WeavingFailed); registerRpcReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterRpc", ref WeavingFailed); + TypeReference RemoteCallDelegateType = Import(); + RemoteCallDelegateConstructor = Resolvers.ResolveMethod(RemoteCallDelegateType, assembly, Log, ".ctor", ref WeavingFailed); + + TypeReference ScriptableObjectType = Import(); + ScriptableObjectCreateInstanceMethod = Resolvers.ResolveMethod( + ScriptableObjectType, assembly, Log, + md => md.Name == "CreateInstance" && md.HasGenericParameters, + 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 => @@ -133,11 +141,10 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail 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 NetworkWriterPoolType = Import(typeof(NetworkWriterPool)); + GetWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Get", ref WeavingFailed); + ReturnWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Return", ref WeavingFailed); TypeReference readerExtensions = Import(typeof(NetworkReaderExtensions)); readNetworkBehaviourGeneric = Resolvers.ResolveMethod(readerExtensions, assembly, Log, (md => diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta index d71c33f..ea2cec8 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 2585961bf7fe4c10a9143f4087efdf6f -timeCreated: 1596486854 \ No newline at end of file +timeCreated: 1596486854 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/WeaverTypes.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Writers.cs b/Assets/Mirror/Editor/Weaver/Writers.cs index 51d514e..31a4ac7 100644 --- a/Assets/Mirror/Editor/Weaver/Writers.cs +++ b/Assets/Mirror/Editor/Weaver/Writers.cs @@ -33,12 +33,24 @@ public Writers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinit public void Register(TypeReference dataType, MethodReference methodReference) { - if (writeFuncs.ContainsKey(dataType)) + // sometimes we define multiple write methods for the same type. + // for example: + // WriteInt() // alwasy writes 4 bytes: should be available to the user for binary protocols etc. + // WriteVarInt() // varint compression: we may want Weaver to always use this for minimal bandwidth + // give the user a way to define the weaver prefered one if two exists: + // "[WeaverPriority]" attribute is automatically detected and prefered. + MethodDefinition methodDefinition = methodReference.Resolve(); + bool priority = methodDefinition.HasCustomAttribute(); + // if (priority) Log.Warning($"Weaver: Registering priority Write<{dataType.FullName}> with {methodReference.FullName}.", methodReference); + + // Weaver sometimes calls Register for multiple times because we resolve assemblies multiple times. + // if the function name is the same: always use the latest one. + // if the function name differes: use the priority one. + if (writeFuncs.TryGetValue(dataType, out MethodReference existingMethod) && // if it was already defined + existingMethod.FullName != methodReference.FullName && // and this one is a different name + !priority) // and it's not the priority one { - // 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); + return; // then skip } // we need to import type when we Initialize Writers so import here in case it is used anywhere else @@ -114,8 +126,17 @@ MethodReference GenerateWriter(TypeReference variableReference, ref bool Weaving return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteList), ref WeavingFailed); } + if (variableReference.Is(typeof(HashSet<>))) + { + GenericInstanceType genericInstance = (GenericInstanceType)variableReference; + TypeReference elementType = genericInstance.GenericArguments[0]; + + return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteHashSet), ref WeavingFailed); + } - if (variableReference.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + if (variableReference.IsDerivedFrom() || variableReference.Is()) { return GetNetworkBehaviourWriter(variableReference); } diff --git a/Assets/Mirror/Editor/Weaver/Writers.cs.meta b/Assets/Mirror/Editor/Weaver/Writers.cs.meta index 3769f7f..1bec2a8 100644 --- a/Assets/Mirror/Editor/Weaver/Writers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Writers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Writers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Welcome.cs b/Assets/Mirror/Editor/Welcome.cs new file mode 100644 index 0000000..49bd379 --- /dev/null +++ b/Assets/Mirror/Editor/Welcome.cs @@ -0,0 +1,23 @@ +// Shows either a welcome message, only once per session. +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + static class Welcome + { + [InitializeOnLoadMethod] + static void OnInitializeOnLoad() + { + // InitializeOnLoad is called on start and after each rebuild, + // but we only want to show this once per editor session. + if (!SessionState.GetBool("MIRROR_WELCOME", false)) + { + SessionState.SetBool("MIRROR_WELCOME", true); + Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Mirror | mirror-networking.com | discord.gg/N9QVxbM"); + } + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Welcome.cs.meta b/Assets/Mirror/Editor/Welcome.cs.meta new file mode 100644 index 0000000..82dd027 --- /dev/null +++ b/Assets/Mirror/Editor/Welcome.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 180619c3887314f56bf396769c0a23ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Welcome.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE b/Assets/Mirror/LICENSE similarity index 90% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE rename to Assets/Mirror/LICENSE index 0330370..63a97b2 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE +++ b/Assets/Mirror/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2021 Mirror Networking (vis2k, FakeByte) +Copyright (c) 2015, Unity Technologies +Copyright (c) 2019, Mischa, Paul, Chris, Robin, Stephen 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 diff --git a/Assets/Mirror/LICENSE.meta b/Assets/Mirror/LICENSE.meta new file mode 100644 index 0000000..a3285f2 --- /dev/null +++ b/Assets/Mirror/LICENSE.meta @@ -0,0 +1,9 @@ +guid: E3D46192E4CB96FDDE36997FE62D5973 +fileFormatVersion: 2 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/LICENSE + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta b/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta index 9477cb6..a02e246 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/License.txt + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta index d5555bf..91e5737 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta @@ -90,3 +90,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta index 3ab420f..322faf6 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta @@ -90,3 +90,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta index aff0237..09ecb6b 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta @@ -90,3 +90,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta index f87dc69..2b667c6 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta @@ -92,3 +92,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll + uploadId: 736421 diff --git a/Assets/Mirror/Presets.meta b/Assets/Mirror/Presets.meta new file mode 100644 index 0000000..b8cf771 --- /dev/null +++ b/Assets/Mirror/Presets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 80d00a982048947fa93f2be2ea402b4e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Presets/Network Transform (Reliable).meta b/Assets/Mirror/Presets/Network Transform (Reliable).meta new file mode 100644 index 0000000..c710730 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable).meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 599f312ab09244b8e9b5c475e2634d26 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset new file mode 100644 index 0000000..23f5f11 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset.meta new file mode 100644 index 0000000..841f1b9 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: d2265a8e4f13f4cabada48c26038cd61 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset new file mode 100644 index 0000000..c7dcc7d --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Casual + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset.meta new file mode 100644 index 0000000..233119d --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: bdd9c4732b76c4a6e955711cbf5123de +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset new file mode 100644 index 0000000..029c618 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Responsive + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset.meta new file mode 100644 index 0000000..32b858d --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: b6ef89bc6332145cabf562c53295145a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset new file mode 100644 index 0000000..a267f8e --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ServerAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset.meta new file mode 100644 index 0000000..23f4f73 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 13adbacdaa535458cb4f558cccd3de6f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable).meta b/Assets/Mirror/Presets/Network Transform (Unreliable).meta new file mode 100644 index 0000000..561aec5 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable).meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb1ce23d9d0fa494d919aefb1f1fc1d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset new file mode 100644 index 0000000..4df0198 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 3 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset.meta new file mode 100644 index 0000000..6650728 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 0a20d07504f3744079937213136ef7a3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset new file mode 100644 index 0000000..6a83c1c --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Casual + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 4 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset.meta new file mode 100644 index 0000000..ad1d531 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 11dbda2b964884128b2bd209f9e5cead +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset new file mode 100644 index 0000000..88ee71c --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Responsive + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset.meta new file mode 100644 index 0000000..e3dc9ae --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 393416c7c2b2f4366be81f82886c280c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset new file mode 100644 index 0000000..f77a015 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ServerAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 3 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset.meta new file mode 100644 index 0000000..17ca248 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 66a33afb7bec84e30b79a8673719ee3d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Readme.txt.meta b/Assets/Mirror/Readme.txt.meta index d52ccce..3be4e9e 100644 --- a/Assets/Mirror/Readme.txt.meta +++ b/Assets/Mirror/Readme.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Readme.txt + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs b/Assets/Mirror/Runtime/AssemblyInfo.cs deleted file mode 100644 index f342716..0000000 --- a/Assets/Mirror/Runtime/AssemblyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 50cc028..0000000 --- a/Assets/Mirror/Runtime/AssemblyInfo.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b06fd..0000000 --- a/Assets/Mirror/Runtime/Attributes.cs +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index c50a489..0000000 --- a/Assets/Mirror/Runtime/Attributes.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/Batcher.cs b/Assets/Mirror/Runtime/Batching/Batcher.cs deleted file mode 100644 index 3a8d457..0000000 --- a/Assets/Mirror/Runtime/Batching/Batcher.cs +++ /dev/null @@ -1,127 +0,0 @@ -// 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 deleted file mode 100644 index a774908..0000000 --- a/Assets/Mirror/Runtime/Batching/Batcher.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 495ada9..0000000 --- a/Assets/Mirror/Runtime/Batching/Unbatcher.cs +++ /dev/null @@ -1,142 +0,0 @@ -// 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 deleted file mode 100644 index 26038b0..0000000 --- a/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 3e4b0f6..0000000 --- a/Assets/Mirror/Runtime/Compression.cs +++ /dev/null @@ -1,361 +0,0 @@ -// 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 deleted file mode 100644 index e35474b..0000000 --- a/Assets/Mirror/Runtime/Compression.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index e702402..0000000 --- a/Assets/Mirror/Runtime/Empty.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 0d1b96e..0000000 --- a/Assets/Mirror/Runtime/Empty/ClientScene.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index 82b617e..0000000 --- a/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index e2c44de..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta deleted file mode 100644 index 9279c0c..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta deleted file mode 100644 index 98a4c11..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta deleted file mode 100644 index a6fc272..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta deleted file mode 100644 index b914a33..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta deleted file mode 100644 index f66b84e..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta deleted file mode 100644 index 150d85b..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta deleted file mode 100644 index 6bf6291..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta deleted file mode 100644 index f1149a9..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta deleted file mode 100644 index 966c503..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta deleted file mode 100644 index 7cb2a59..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta deleted file mode 100644 index 4b7219b..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta deleted file mode 100644 index 2c04009..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta deleted file mode 100644 index 519876d..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta deleted file mode 100644 index a9d32ea..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta deleted file mode 100644 index 306bf7c..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta deleted file mode 100644 index 7e206f1..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta deleted file mode 100644 index 82e23fd..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta deleted file mode 100644 index 5984ce3..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta deleted file mode 100644 index 86775df..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta deleted file mode 100644 index 5c4294f..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta deleted file mode 100644 index 1c85828..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta deleted file mode 100644 index 4a22565..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta deleted file mode 100644 index 67341ea..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta deleted file mode 100644 index eb139af..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta deleted file mode 100644 index 74c6a0f..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta deleted file mode 100644 index f7fe4f2..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta deleted file mode 100644 index d8857e8..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta b/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta deleted file mode 100644 index 8742197..0000000 --- a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 57f3344..0000000 --- a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index 509a58f..0000000 --- a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFactory.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta b/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta deleted file mode 100644 index 0715501..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 391c5bd..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFilter.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-03-08 diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta b/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta deleted file mode 100644 index aab4fa0..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 867da74..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta deleted file mode 100644 index 329c6eb..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta deleted file mode 100644 index 81b33e9..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta deleted file mode 100644 index acf3b63..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 264a1cd..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs +++ /dev/null @@ -1,2 +0,0 @@ -// removed 2021-02-16 - diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta deleted file mode 100644 index 90c4e4d..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta deleted file mode 100644 index 221a61b..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta deleted file mode 100644 index 2f7ecdf..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta deleted file mode 100644 index 7c7d6cf..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 712833c..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta deleted file mode 100644 index fee7725..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta deleted file mode 100644 index c5aa112..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta deleted file mode 100644 index b451655..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta deleted file mode 100644 index f71b7be..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/StringHash.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs.meta b/Assets/Mirror/Runtime/Empty/StringHash.cs.meta deleted file mode 100644 index 6198581..0000000 --- a/Assets/Mirror/Runtime/Empty/StringHash.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 64b91e1..0000000 --- a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index d0d8210..0000000 --- a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 3d285e9..0000000 --- a/Assets/Mirror/Runtime/Extensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index c2a18b7..0000000 --- a/Assets/Mirror/Runtime/Extensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index ab149c3..0000000 --- a/Assets/Mirror/Runtime/InterestManagement.cs +++ /dev/null @@ -1,99 +0,0 @@ -// 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 deleted file mode 100644 index bfabf6b..0000000 --- a/Assets/Mirror/Runtime/InterestManagement.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 67c9649..0000000 --- a/Assets/Mirror/Runtime/LocalConnectionToClient.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 42243ed..0000000 --- a/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta deleted file mode 100644 index 856b255..0000000 --- a/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2dfa2f9..0000000 --- a/Assets/Mirror/Runtime/Mathd.cs +++ /dev/null @@ -1,28 +0,0 @@ -// '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 deleted file mode 100644 index 927c55a..0000000 --- a/Assets/Mirror/Runtime/Mathd.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index af7fca6..0000000 --- a/Assets/Mirror/Runtime/MessagePacking.cs +++ /dev/null @@ -1,148 +0,0 @@ -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 deleted file mode 100644 index 910b75c..0000000 --- a/Assets/Mirror/Runtime/MessagePacking.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index d3816f8..0000000 --- a/Assets/Mirror/Runtime/Messages.cs +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index 5d119e2..0000000 --- a/Assets/Mirror/Runtime/Messages.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0f38055..0000000 --- a/Assets/Mirror/Runtime/Mirror.asmdef +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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 deleted file mode 100644 index 202009b..0000000 --- a/Assets/Mirror/Runtime/Mirror.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 30817c1a0e6d646d99c048fc403f5979 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta b/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta deleted file mode 100644 index d37db68..0000000 --- a/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 94cd930..0000000 --- a/Assets/Mirror/Runtime/NetworkBehaviour.cs +++ /dev/null @@ -1,1094 +0,0 @@ -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 deleted file mode 100644 index f0bc195..0000000 --- a/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index e5dabe3..0000000 --- a/Assets/Mirror/Runtime/NetworkClient.cs +++ /dev/null @@ -1,1544 +0,0 @@ -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 deleted file mode 100644 index 20cb211..0000000 --- a/Assets/Mirror/Runtime/NetworkClient.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 14729c6..0000000 --- a/Assets/Mirror/Runtime/NetworkConnection.cs +++ /dev/null @@ -1,233 +0,0 @@ -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 deleted file mode 100644 index 32c4ba2..0000000 --- a/Assets/Mirror/Runtime/NetworkConnection.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 4cb56f5..0000000 --- a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index 6001a71..0000000 --- a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta deleted file mode 100644 index 3424b58..0000000 --- a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta deleted file mode 100644 index fe37316..0000000 --- a/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 6c3c122..0000000 --- a/Assets/Mirror/Runtime/NetworkIdentity.cs +++ /dev/null @@ -1,1317 +0,0 @@ -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 deleted file mode 100644 index 7b96521..0000000 --- a/Assets/Mirror/Runtime/NetworkIdentity.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/NetworkLoop.cs.meta deleted file mode 100644 index 52b6e6a..0000000 --- a/Assets/Mirror/Runtime/NetworkLoop.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/NetworkManager.cs.meta deleted file mode 100644 index 0a7564a..0000000 --- a/Assets/Mirror/Runtime/NetworkManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index cba968d..0000000 --- a/Assets/Mirror/Runtime/NetworkManagerHUD.cs +++ /dev/null @@ -1,149 +0,0 @@ -// 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 deleted file mode 100644 index a720b9c..0000000 --- a/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/NetworkMessage.cs.meta deleted file mode 100644 index 73d3d8f..0000000 --- a/Assets/Mirror/Runtime/NetworkMessage.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 86eeef4..0000000 --- a/Assets/Mirror/Runtime/NetworkReader.cs +++ /dev/null @@ -1,202 +0,0 @@ -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 deleted file mode 100644 index 65ad3f0..0000000 --- a/Assets/Mirror/Runtime/NetworkReader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 6137866..0000000 --- a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs +++ /dev/null @@ -1,354 +0,0 @@ -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 deleted file mode 100644 index 66536c9..0000000 --- a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index ebbfac5..0000000 --- a/Assets/Mirror/Runtime/NetworkReaderPool.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index 2c94768..0000000 --- a/Assets/Mirror/Runtime/NetworkReaderPool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index fcfa792..0000000 --- a/Assets/Mirror/Runtime/NetworkReaderPooled.cs +++ /dev/null @@ -1,22 +0,0 @@ -// "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 deleted file mode 100644 index 4eb6e9d..0000000 --- a/Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 45d00c4..0000000 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ /dev/null @@ -1,1732 +0,0 @@ -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 deleted file mode 100644 index 9861342..0000000 --- a/Assets/Mirror/Runtime/NetworkServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta deleted file mode 100644 index ae9ab89..0000000 --- a/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 1721524..0000000 --- a/Assets/Mirror/Runtime/NetworkTime.cs +++ /dev/null @@ -1,159 +0,0 @@ -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 deleted file mode 100644 index 1dc9e0a..0000000 --- a/Assets/Mirror/Runtime/NetworkTime.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 442075f..0000000 --- a/Assets/Mirror/Runtime/NetworkWriter.cs +++ /dev/null @@ -1,187 +0,0 @@ -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 deleted file mode 100644 index c938496..0000000 --- a/Assets/Mirror/Runtime/NetworkWriter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index cf0954e..0000000 --- a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs +++ /dev/null @@ -1,372 +0,0 @@ -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 deleted file mode 100644 index 9bbdaf0..0000000 --- a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta deleted file mode 100644 index 19d2bb7..0000000 --- a/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index ce113bc..0000000 --- a/Assets/Mirror/Runtime/NetworkWriterPooled.cs +++ /dev/null @@ -1,17 +0,0 @@ -// "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 deleted file mode 100644 index 5571d6f..0000000 --- a/Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Pool.cs.meta deleted file mode 100644 index 7d12a20..0000000 --- a/Assets/Mirror/Runtime/Pool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/RemoteCalls.cs.meta deleted file mode 100644 index 7bbc087..0000000 --- a/Assets/Mirror/Runtime/RemoteCalls.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/Snapshot.cs b/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs deleted file mode 100644 index fbf2c24..0000000 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 24eedd7..0000000 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index bc685d7..0000000 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs +++ /dev/null @@ -1,325 +0,0 @@ -// 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 deleted file mode 100644 index 244c5fb..0000000 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index c63077c..0000000 --- a/Assets/Mirror/Runtime/SyncDictionary.cs +++ /dev/null @@ -1,310 +0,0 @@ -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 deleted file mode 100644 index 1c20b57..0000000 --- a/Assets/Mirror/Runtime/SyncDictionary.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 9eb0a59..0000000 --- a/Assets/Mirror/Runtime/SyncList.cs +++ /dev/null @@ -1,406 +0,0 @@ -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 deleted file mode 100644 index 088ef1e..0000000 --- a/Assets/Mirror/Runtime/SyncList.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/SyncObject.cs.meta deleted file mode 100644 index 736c651..0000000 --- a/Assets/Mirror/Runtime/SyncObject.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 94e353f..0000000 --- a/Assets/Mirror/Runtime/SyncSet.cs +++ /dev/null @@ -1,348 +0,0 @@ -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 deleted file mode 100644 index 6eeef1c..0000000 --- a/Assets/Mirror/Runtime/SyncSet.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 5c1790b..0000000 --- a/Assets/Mirror/Runtime/SyncVar.cs +++ /dev/null @@ -1,148 +0,0 @@ -// 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 deleted file mode 100644 index fffb472..0000000 --- a/Assets/Mirror/Runtime/SyncVar.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 81961c2..0000000 --- a/Assets/Mirror/Runtime/SyncVarGameObject.cs +++ /dev/null @@ -1,145 +0,0 @@ -// 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 deleted file mode 100644 index 4e924f0..0000000 --- a/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 623b890..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs +++ /dev/null @@ -1,168 +0,0 @@ -// 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 deleted file mode 100644 index 0ceab50..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0f3fdf2..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs +++ /dev/null @@ -1,118 +0,0 @@ -// 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 deleted file mode 100644 index 53271d4..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transport.cs.meta deleted file mode 100644 index 55072e1..0000000 --- a/Assets/Mirror/Runtime/Transport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/KCP/MirrorTransport.meta b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta deleted file mode 100644 index dedea2f..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 026e66b..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs +++ /dev/null @@ -1,357 +0,0 @@ -//#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 deleted file mode 100644 index f7280c8..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport/KcpTransport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/KCP.asmdef.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta deleted file mode 100644 index 1d70e80..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 6806a62c384838046a3c66c44f06d75f -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE.meta deleted file mode 100644 index 49dc767..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE.meta +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 992fe9f..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION +++ /dev/null @@ -1,136 +0,0 @@ -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 deleted file mode 100644 index 2a07daa..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: ed3f2cf1bbf1b4d53a6f2c103d311f71 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta deleted file mode 100644 index 42f163f..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 6115bc8..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 36d3193..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: c0649195e5ba4fcf8e0e1231fee7d5f6 -timeCreated: 1641701011 \ 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 deleted file mode 100644 index 2721025..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 58249e7..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs +++ /dev/null @@ -1,148 +0,0 @@ -// 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 deleted file mode 100644 index e55306b..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index a843a8d..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs +++ /dev/null @@ -1,156 +0,0 @@ -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 deleted file mode 100644 index 3369918..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index e5bc0f3..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs +++ /dev/null @@ -1,668 +0,0 @@ -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 deleted file mode 100644 index fa5dcff..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index bc4b047..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 9e81c94..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 5e48688..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs +++ /dev/null @@ -1,375 +0,0 @@ -// 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 deleted file mode 100644 index ef720d4..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index a902865..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 10d9803..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 939dae7..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index 333bee5..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Log.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 4cbc909..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index b3e1b27..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index 9d4a42e..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 2417408..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 deleted file mode 100644 index 266dafb..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 7986bea..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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 deleted file mode 100644 index 383fe02..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 001a64b..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index a878cc1..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 54b8398dcd544c8a93bcad846214cc40 -timeCreated: 1626432191 \ 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 deleted file mode 100644 index 6b442a9..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: aec6a15ac7bd43129317ea1f01f19782 -timeCreated: 1602665988 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs.meta deleted file mode 100644 index 935b423..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs.meta deleted file mode 100644 index 5eba0e0..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index b82935a..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index d14dc1a..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Segment.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 45dc1a6..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 86118bc..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Utils.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 5c72cf0..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: e9de45e025f26411bbb52d1aefc8d5a5 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta deleted file mode 100644 index 4fadbdf..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 6878ad8..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 246a5d1..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 1edb254..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index fcf18f6..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index c4fa54d..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 65eb453..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs +++ /dev/null @@ -1,208 +0,0 @@ -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 deleted file mode 100644 index ef424ba..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index a185c2b..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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 deleted file mode 100644 index ce96c63..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 8341d28..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 67ab688..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 2feb073..0000000 --- a/Assets/Mirror/Runtime/Transports/LatencySimulation.cs +++ /dev/null @@ -1,284 +0,0 @@ -// 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 deleted file mode 100644 index eabbe4a..0000000 --- a/Assets/Mirror/Runtime/Transports/LatencySimulation.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs.meta deleted file mode 100644 index dce8378..0000000 --- a/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0d0503d..0000000 --- a/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs +++ /dev/null @@ -1,306 +0,0 @@ -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 deleted file mode 100644 index 6e97b28..0000000 --- a/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/AssemblyInfo.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs deleted file mode 100644 index 7bc5c17..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 028a307..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/AssemblyInfo.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta deleted file mode 100644 index bc43099..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: b0ef23ac1c6a62546bbad5529b3bfdad -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs.meta deleted file mode 100644 index 90c361b..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/ClientHandshake.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs deleted file mode 100644 index e5fccf9..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index ad3d40d..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientHandshake.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs.meta deleted file mode 100644 index d6be2bb..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs.meta deleted file mode 100644 index 37229d3..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/SimpleWebJSLib.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs.meta deleted file mode 100644 index 9dfa12e..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs.meta deleted file mode 100644 index 3827d3a..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/Common/BufferPool.cs.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs.meta deleted file mode 100644 index 0b1070f..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index adc52db..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index d48a835..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Connection.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs.meta deleted file mode 100644 index ece602e..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs.meta deleted file mode 100644 index a91403a..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 4b7bce5..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index beb2883..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs.meta deleted file mode 100644 index 3286a2c..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs.meta deleted file mode 100644 index 7e3a7c4..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs.meta deleted file mode 100644 index 77d09c1..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs.meta deleted file mode 100644 index 47c6ff5..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs.meta deleted file mode 100644 index 09dfd1e..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs.meta deleted file mode 100644 index 62ba232..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs.meta deleted file mode 100644 index 79a1583..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta deleted file mode 100644 index 8ece59e..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 0a0cf751b4a201242ac60b4adbc54657 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta deleted file mode 100644 index b63fe39..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 0e3971d5783109f4d9ce93c7a689d701 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs deleted file mode 100644 index b138201..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs +++ /dev/null @@ -1,149 +0,0 @@ -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 deleted file mode 100644 index 6fa74da..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerHandshake.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs.meta deleted file mode 100644 index e0d133c..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs.meta deleted file mode 100644 index c8c6f5a..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs.meta deleted file mode 100644 index 0a76a9f..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 3687c5d..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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 deleted file mode 100644 index 99755b6..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 66badc3..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs +++ /dev/null @@ -1,293 +0,0 @@ -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 deleted file mode 100644 index 381a5c7..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs.meta deleted file mode 100644 index e653532..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/Telepathy/Client.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs.meta deleted file mode 100644 index 1b6d222..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs.meta deleted file mode 100644 index 5d8ab5b..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs.meta deleted file mode 100644 index 3dcceaf..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 1bc9652..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 4f7722a..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index 304866f..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 4f7722a..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index 5937bb9..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 7899911..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index f3a9310..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 85dece4..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs +++ /dev/null @@ -1 +0,0 @@ -// 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 deleted file mode 100644 index 77c885d..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs.meta deleted file mode 100644 index ac88c1b..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta deleted file mode 100644 index 4d7664e..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 0ba11103b95fd4721bffbb08440d5b8e -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs.meta deleted file mode 100644 index 8f78650..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta deleted file mode 100644 index 614bab6..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta deleted file mode 100644 index cf1415f..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta deleted file mode 100644 index e7e5744..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs.meta deleted file mode 100644 index 9a7dafc..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs.meta deleted file mode 100644 index 9cee8b7..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta deleted file mode 100644 index 572c127..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 725ee7191c021de4dbf9269590ded755 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta deleted file mode 100644 index ea536ac..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs.meta deleted file mode 100644 index 0a9253b..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 9ec0736..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 04c1c8a..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: d942af06608be434dbeeaa58207d20bd -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs.meta deleted file mode 100644 index 99cde3e..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index d39ed98..0000000 --- a/Assets/Mirror/Runtime/Utils.cs +++ /dev/null @@ -1,121 +0,0 @@ -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 deleted file mode 100644 index 7cf1557..0000000 --- a/Assets/Mirror/Runtime/Utils.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -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/Mirror/ScriptTemplates.meta b/Assets/Mirror/ScriptTemplates.meta new file mode 100644 index 0000000..0dfbf44 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8c00361129d75a941a732ef88e326a4f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt new file mode 100644 index 0000000..887e76f --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt @@ -0,0 +1,365 @@ +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 +{ + // You can adjust the parameters of the Actions below to suit your needs and pass the values through the Invoke() method. + + public event Action OnStartAction; + public event Action OnDestroyAction; + + public event Action OnApplicationQuitAction; + + public event Action ServerChangeSceneAction; + public event Action OnServerChangeSceneAction; + public event Action OnServerSceneChangedAction; + public event Action OnClientChangeSceneAction; + public event Action OnClientSceneChangedAction; + + public event Action OnServerConnectAction; + public event Action OnServerReadyAction; + public event Action OnServerAddPlayerAction; + public event Action OnServerDisconnectAction; + public event Action OnServerErrorAction; + public event Action OnServerTransportExceptionAction; + + public event Action OnClientConnectAction; + public event Action OnClientDisconnectAction; + public event Action OnClientNotReadyAction; + public event Action OnClientErrorAction; + public event Action OnClientTransportExceptionAction; + + public event Action OnStartServerAction; + public event Action OnStopServerAction; + public event Action OnStartHostAction; + public event Action OnStopHostAction; + public event Action OnStartClientAction; + public event Action OnStopClientAction; + + // Overrides the base singleton so we don't have to cast to this type everywhere. + public static new #SCRIPTNAME# singleton => (#SCRIPTNAME#)NetworkManager.singleton; + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + + // Example of adding a handler for the OnStartAction + // Multiple handlers can be added for actions + // Use -= to remove handlers + // Set the action to null to remove all handlers + OnStartAction += OnStartedActionHandler; + } + + /// + /// Example handler for OnStartAction + /// + /// Handlers can be assigned from, and exist, in any script + public void OnStartedActionHandler() + { + Debug.Log("#SCRIPTNAME#.OnStartAction invoked"); + } + + #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 Start() + { + OnStartAction?.Invoke(); + base.Start(); + } + + /// + /// Runs on both Server and Client + /// + public override void LateUpdate() + { + base.LateUpdate(); + } + + /// + /// Runs on both Server and Client + /// + public override void OnDestroy() + { + OnDestroyAction?.Invoke(); + 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() + { + OnApplicationQuitAction?.Invoke(); + 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) + { + ServerChangeSceneAction?.Invoke(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) + { + OnServerChangeSceneAction?.Invoke(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) + { + OnServerSceneChangedAction?.Invoke(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) + { + OnClientChangeSceneAction?.Invoke(newSceneName, sceneOperation, 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() + { + OnClientSceneChangedAction?.Invoke(); + 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) + { + OnServerConnectAction?.Invoke(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) + { + OnServerReadyAction?.Invoke(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) + { + OnServerAddPlayerAction?.Invoke(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) + { + OnServerDisconnectAction?.Invoke(conn); + base.OnServerDisconnect(conn); + } + + /// + /// Called on server when transport raises an error. + /// NetworkConnection may be null. + /// + /// Connection of the client...may be null + /// TransportError enum + /// String message of the error. + public override void OnServerError(NetworkConnectionToClient conn, TransportError transportError, string message) + { + OnServerErrorAction?.Invoke(conn, transportError, message); + } + + /// + /// 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 OnServerTransportException(NetworkConnectionToClient conn, Exception exception) + { + OnServerTransportExceptionAction?.Invoke(conn, 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() + { + OnClientConnectAction?.Invoke(); + 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() + { + OnClientDisconnectAction?.Invoke(); + } + + /// + /// 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() + { + OnClientNotReadyAction?.Invoke(); + } + + /// + /// Called on client when transport raises an error. + /// + /// TransportError enum. + /// String message of the error. + public override void OnClientError(TransportError transportError, string message) + { + OnClientErrorAction?.Invoke(transportError, message); + } + + /// + /// Called on client when transport raises an exception. + /// + /// Exception thrown from the Transport. + public override void OnClientTransportException(Exception exception) + { + OnClientTransportExceptionAction?.Invoke(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 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() + { + OnStartServerAction?.Invoke(); + } + + /// + /// This is called when a server is stopped - including when a host is stopped. + /// + public override void OnStopServer() + { + OnStopServerAction?.Invoke(); + } + + /// + /// 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() + { + OnStartHostAction?.Invoke(); + } + + /// + /// This is called when a host is stopped. + /// + public override void OnStopHost() + { + OnStopHostAction?.Invoke(); + } + + /// + /// This is invoked when the client is started. + /// + public override void OnStartClient() + { + OnStartClientAction?.Invoke(); + } + + /// + /// This is called when a client is stopped. + /// + public override void OnStopClient() + { + OnStopClientAction?.Invoke(); + } + + #endregion +} diff --git a/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt.meta b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt.meta new file mode 100644 index 0000000..ff0a39a --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 54c5bb307a5d80d4180010c653b2c085 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt new file mode 100644 index 0000000..9c4fc57 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt @@ -0,0 +1,106 @@ +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 OnServerConnectInternal 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); + } + + /// + /// Called when server stops, used to unregister message handlers if needed. + /// + public override void OnStopServer() + { + // Unregister the handler for the authentication request + NetworkServer.UnregisterHandler(); + } + + #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 OnClientConnectInternal 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(); + } + + /// + /// Called when client stops, used to unregister message handlers if needed. + /// + public override void OnStopClient() + { + // Unregister the handler for the authentication response + NetworkClient.UnregisterHandler(); + } + + #endregion +} diff --git a/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta new file mode 100644 index 0000000..9e44168 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 65ea3c30df1c9f949bb5d6c76e549280 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt new file mode 100644 index 0000000..5998b8f --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt @@ -0,0 +1,136 @@ +using System; +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 +*/ + +public class #SCRIPTNAME# : NetworkBehaviour +{ + // You can adjust the parameters of the Actions below to suit your needs and pass the values through the Invoke() method. + + public event Action OnStartServerAction; + public event Action OnStopServerAction; + public event Action OnStartClientAction; + public event Action OnStopClientAction; + public event Action OnStartLocalPlayerAction; + public event Action OnStopLocalPlayerAction; + public event Action OnStartAuthorityAction; + public event Action OnStopAuthorityAction; + + #region Unity Callbacks + + /// + /// Add your validation code here after the base.OnValidate(); call. + /// + protected override void OnValidate() + { + base.OnValidate(); + } + + // NOTE: Do not put objects in DontDestroyOnLoad (DDOL) in Awake. You can do that in Start instead. + void Awake() + { + // Example of adding a handler for OnStartServerAction + // Multiple handlers can be added for actions + // Use -= to remove handlers + // Set the action to null to remove all handlers + OnStartServerAction += OnStartServerHandler; + } + + void Start() + { + } + + /// + /// Example handler for OnStartServerAction + /// + /// Handlers can be assigned from, and exist, in any script + public void OnStartServerHandler(NetworkIdentity identity) + { + Debug.Log($"#SCRIPTNAME#.OnStartServerAction invoked for {identity}"); + } + + #endregion + + #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() + { + OnStartServerAction?.Invoke(netIdentity); + } + + /// + /// Invoked on the server when the object is unspawned + /// Useful for saving object data in persistent storage + /// + public override void OnStopServer() + { + OnStopServerAction?.Invoke(netIdentity); + } + + /// + /// 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() + { + OnStartClientAction?.Invoke(); + } + + /// + /// 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() + { + OnStopClientAction?.Invoke(); + } + + /// + /// 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() + { + OnStartLocalPlayerAction?.Invoke(); + } + + /// + /// 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() + { + OnStopLocalPlayerAction?.Invoke(); + } + + /// + /// 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() + { + OnStartAuthorityAction?.Invoke(); + } + + /// + /// 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() + { + OnStopAuthorityAction?.Invoke(); + } + + #endregion +} diff --git a/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt.meta b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt.meta new file mode 100644 index 0000000..1420e20 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 847fec635407d6740979cc0da3b87d40 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt new file mode 100644 index 0000000..5523ac9 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt @@ -0,0 +1,94 @@ +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 + [ServerCallback] + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // 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 ResetState() { } + + [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/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta new file mode 100644 index 0000000..d0f3108 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 81cfade18750ef748be6886f764e1e7d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/Editor.meta b/Assets/Mirror/ScriptTemplates/Editor.meta new file mode 100644 index 0000000..c9b29c6 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96993aced46357f4d9dbda89e40eabad +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs new file mode 100644 index 0000000..0b7b78a --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs @@ -0,0 +1,42 @@ +using UnityEditor; +using UnityEngine; + +[InitializeOnLoad] +public class MoveToAssetsFolder +{ + const string FirstTimeKey = "MOVE_SCRIPT_TEMPLATES_HAS_RUN"; + const string targetFolder = "ScriptTemplates"; + const string targetPath = "Assets/ScriptTemplates"; + + static MoveToAssetsFolder() + { + if (!SessionState.GetBool(FirstTimeKey, false)) + { + FindAndMoveScriptTemplatesFolder(); + SessionState.SetBool(FirstTimeKey, true); + } + } + + static void FindAndMoveScriptTemplatesFolder() + { + string[] guids = AssetDatabase.FindAssets(targetFolder, null); + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + + // Check if it's a folder and not some random asset + if (AssetDatabase.IsValidFolder(path)) + { + // Ensure exact match of the name and that it's not in the Assets folder already + string folderName = System.IO.Path.GetFileName(path); + if (folderName == targetFolder && !path.StartsWith(targetPath)) + { + AssetDatabase.MoveAsset(path, targetPath); + Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, $"Moved {targetFolder} to Assets folder."); + } + } + } + + AssetDatabase.Refresh(); + } +} diff --git a/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs.meta b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs.meta new file mode 100644 index 0000000..44f1113 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e383132f744a8c1448807b71fa31f7ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports.meta b/Assets/Mirror/Transports.meta similarity index 77% rename from Assets/Mirror/Runtime/Transports.meta rename to Assets/Mirror/Transports.meta index fc29442..72fd20d 100644 --- a/Assets/Mirror/Runtime/Transports.meta +++ b/Assets/Mirror/Transports.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 7825d46cd73fe47938869eb5427b40fa +guid: d7d3068968d80427e8cc256e22b0b0b5 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Transports/Edgegap.meta b/Assets/Mirror/Transports/Edgegap.meta new file mode 100644 index 0000000..39cd5b6 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2d2959d363903444bae4333db12a9ea1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta new file mode 100644 index 0000000..5621c20 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 447b4ad1a3db7cf4fa5a0709d297ba9b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs new file mode 100644 index 0000000..74945e1 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections; +using System.Threading; +using Mirror; +using UnityEngine; +using Random = UnityEngine.Random; +namespace Edgegap +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")] + public class EdgegapLobbyKcpTransport : EdgegapKcpTransport + { + [Header("Lobby Settings")] + [Tooltip("URL to the Edgegap lobby service, automatically filled in after completing the creation process via button below (or enter manually)")] + public string lobbyUrl; + [Tooltip("How long to wait for the relay to be assigned after starting a lobby")] + public float lobbyWaitTimeout = 60; + + public LobbyApi Api; + private LobbyCreateRequest? _request; + private string _lobbyId; + private string _playerId; + private TransportStatus _status = TransportStatus.Offline; + public enum TransportStatus + { + Offline, + CreatingLobby, + StartingLobby, + JoiningLobby, + WaitingRelay, + Connecting, + Connected, + Error, + } + public TransportStatus Status + { + get + { + if (!NetworkClient.active && !NetworkServer.active) + { + return TransportStatus.Offline; + } + if (_status == TransportStatus.Connecting) + { + if (NetworkServer.active) + { + switch (((EdgegapKcpServer)this.server).state) + { + case ConnectionState.Valid: + return TransportStatus.Connected; + case ConnectionState.Invalid: + case ConnectionState.SessionTimeout: + case ConnectionState.Error: + return TransportStatus.Error; + } + } + else if (NetworkClient.active) + { + switch (((EdgegapKcpClient)this.client).connectionState) + { + case ConnectionState.Valid: + return TransportStatus.Connected; + case ConnectionState.Invalid: + case ConnectionState.SessionTimeout: + case ConnectionState.Error: + return TransportStatus.Error; + } + } + } + return _status; + } + } + + protected override void Awake() + { + base.Awake(); + Api = new LobbyApi(lobbyUrl); + } + + private void Reset() + { + this.relayGUI = false; + } + + public override void ServerStart() + { + if (!_request.HasValue) + { + throw new Exception("No lobby request set. Call SetServerLobbyParams"); + } + _status = TransportStatus.CreatingLobby; + Api.CreateLobby(_request.Value, lobby => + { + _lobbyId = lobby.lobby_id; + _status = TransportStatus.StartingLobby; + Api.StartLobby(new LobbyIdRequest(_lobbyId), () => + { + StartCoroutine(WaitForLobbyRelay(_lobbyId, true)); + }, error => + { + _status = TransportStatus.Error; + string errorMsg = $"Could not start lobby: {error}"; + Debug.LogError(errorMsg); + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + }); + }, + error => + { + _status = TransportStatus.Error; + string errorMsg = $"Couldn't create lobby: {error}"; + Debug.LogError(errorMsg); + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + }); + } + + public override void ServerStop() + { + base.ServerStop(); + + Api.DeleteLobby(_lobbyId, () => + { + // yay + }, error => + { + OnServerError?.Invoke(0, TransportError.Unexpected, $"Failed to delete lobby: {error}"); + }); + } + + public override void ClientDisconnect() + { + base.ClientDisconnect(); + // this gets called for host mode as well + if (!NetworkServer.active) + { + Api.LeaveLobby(new LobbyJoinOrLeaveRequest + { + player = new LobbyJoinOrLeaveRequest.Player + { + id = _playerId + }, + lobby_id = _lobbyId + }, () => + { + // yay + }, error => + { + string errorMsg = $"Failed to leave lobby: {error}"; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + Debug.LogError(errorMsg); + }); + } + } + + public override void ClientConnect(string address) + { + _lobbyId = address; + _playerId = RandomPlayerId(); + _status = TransportStatus.JoiningLobby; + Api.JoinLobby(new LobbyJoinOrLeaveRequest + { + player = new LobbyJoinOrLeaveRequest.Player + { + id = _playerId, + }, + lobby_id = address + }, () => + { + StartCoroutine(WaitForLobbyRelay(_lobbyId, false)); + }, error => + { + _status = TransportStatus.Offline; + string errorMsg = $"Failed to join lobby: {error}"; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + Debug.LogError(errorMsg); + OnClientDisconnected?.Invoke(); + }); + } + + private IEnumerator WaitForLobbyRelay(string lobbyId, bool forServer) + { + _status = TransportStatus.WaitingRelay; + double startTime = NetworkTime.localTime; + bool running = true; + while (running) + { + if (NetworkTime.localTime - startTime >= lobbyWaitTimeout) + { + _status = TransportStatus.Error; + string errorMsg = "Timed out waiting for lobby."; + Debug.LogError(errorMsg); + if (forServer) + { + _status = TransportStatus.Error; + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + _status = TransportStatus.Error; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + yield break; + } + bool waitingForResponse = true; + Api.GetLobby(lobbyId, lobby => + { + waitingForResponse = false; + if (string.IsNullOrEmpty(lobby.assignment.ip)) + { + // no lobby deployed yet, have the outer loop retry + return; + } + relayAddress = lobby.assignment.ip; + foreach (Lobby.Port aport in lobby.assignment.ports) + { + if (aport.protocol == "UDP") + { + if (aport.name == "server") + { + relayGameServerPort = (ushort)aport.port; + + } + else if (aport.name == "client") + { + relayGameClientPort = (ushort)aport.port; + } + } + } + bool found = false; + foreach (Lobby.Player player in lobby.players) + { + if (player.id == _playerId) + { + userId = player.authorization_token; + sessionId = lobby.assignment.authorization_token; + found = true; + break; + } + } + running = false; + if (!found) + { + string errorMsg = $"Couldn't find my player ({_playerId})"; + Debug.LogError(errorMsg); + + if (forServer) + { + _status = TransportStatus.Error; + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + _status = TransportStatus.Error; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + return; + } + _status = TransportStatus.Connecting; + if (forServer) + { + base.ServerStart(); + } + else + { + base.ClientConnect(""); + } + }, error => + { + running = false; + waitingForResponse = false; + _status = TransportStatus.Error; + string errorMsg = $"Failed to get lobby info: {error}"; + Debug.LogError(errorMsg); + if (forServer) + { + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + }); + while (waitingForResponse) + { + yield return null; + } + yield return new WaitForSeconds(0.2f); + } + } + private static string RandomPlayerId() + { + return $"mirror-player-{Random.Range(1, int.MaxValue)}"; + } + + public void SetServerLobbyParams(string lobbyName, int capacity) + { + SetServerLobbyParams(new LobbyCreateRequest + { + player = new LobbyCreateRequest.Player + { + id = RandomPlayerId(), + }, + annotations = new LobbyCreateRequest.Annotation[] + { + }, + capacity = capacity, + is_joinable = true, + name = lobbyName, + tags = new string[] + { + } + }); + } + + public void SetServerLobbyParams(LobbyCreateRequest request) + { + _playerId = request.player.id; + _request = request; + } + + private void OnDestroy() + { + // attempt to clean up lobbies, if active + if (NetworkServer.active) + { + ServerStop(); + // Absolutely make sure there's time for the network request to hit edgegap servers. + // sorry. this can go once the lobby service can timeout lobbies itself + Thread.Sleep(300); + } + else if (NetworkClient.active) + { + ClientDisconnect(); + // Absolutely make sure there's time for the network request to hit edgegap servers. + // sorry. this can go once the lobby service can timeout lobbies itself + Thread.Sleep(300); + } + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta new file mode 100644 index 0000000..56c239c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fa9d4c3f48a245ed89f122f44e1e81ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs new file mode 100644 index 0000000..c73a707 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace Edgegap +{ + // Implements the edgegap lobby api: https://docs.edgegap.com/docs/lobby/functions + public class LobbyApi + { + [Header("Lobby Config")] + public string LobbyUrl; + public LobbyBrief[] Lobbies; + + public LobbyApi(string url) + { + LobbyUrl = url; + } + + + + private static UnityWebRequest SendJson(string url, T data, string method = "POST") + { + string body = JsonUtility.ToJson(data); + UnityWebRequest request = new UnityWebRequest(url, method); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Accept", "application/json"); + request.SetRequestHeader("Content-Type", "application/json"); + return request; + } + + private static bool CheckErrorResponse(UnityWebRequest request, Action onError) + { +#if UNITY_2020_3_OR_NEWER + if (request.result != UnityWebRequest.Result.Success) + { + // how I hate http libs that think they need to be smart and handle status code errors. + if (request.result != UnityWebRequest.Result.ProtocolError || request.responseCode == 0) + { + onError?.Invoke(request.error); + return true; + } + } +#else + if (request.isNetworkError) + { + onError?.Invoke(request.error); + return true; + } +#endif + if (request.responseCode < 200 || request.responseCode >= 300) + { + onError?.Invoke($"non-200 status code: {request.responseCode}. Body:\n {request.downloadHandler.text}"); + return true; + } + return false; + } + + public void RefreshLobbies(Action onLoaded, Action onError) + { + UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies"); + request.SendWebRequest().completed += operation => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + ListLobbiesResponse lobbies = JsonUtility.FromJson(request.downloadHandler.text); + Lobbies = lobbies.data; + onLoaded?.Invoke(lobbies.data); + } + }; + } + + public void CreateLobby(LobbyCreateRequest createData, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies", createData); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + Lobby lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void UpdateLobby(string lobbyId, LobbyUpdateRequest updateData, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", updateData, "PATCH"); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyBrief lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void GetLobby(string lobbyId, Action onResponse, Action onError) + { + UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies/{lobbyId}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + Lobby lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void JoinLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:join", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void LeaveLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:leave", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void StartLobby(LobbyIdRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:start", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void DeleteLobby(string lobbyId, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", "", "DELETE"); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + struct CreateLobbyServiceRequest + { + public string name; + } + public struct LobbyServiceResponse + { + public string name; + public string url; + public string status; + } + + public static void TrimApiKey(ref string apiKey) + { + if (apiKey == null) + { + return; + } + if (apiKey.StartsWith("token ")) + { + apiKey = apiKey.Substring("token ".Length); + } + apiKey = apiKey.Trim(); + } + + public static void CreateAndDeployLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + // try to get the lobby first + GetLobbyService(apiKey, name, response => + { + if (response == null) + { + CreateLobbyService(apiKey, name, onResponse, onError); + } + else if (!string.IsNullOrEmpty(response.Value.url)) + { + onResponse(response.Value); + } + else + { + DeployLobbyService(apiKey, name, onResponse, onError); + } + }, onError); + } + + private static void CreateLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson("https://api.edgegap.com/v1/lobbies", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + DeployLobbyService(apiKey, name, onResponse, onError); + } + }; + } + + public static void GetLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + var request = UnityWebRequest.Get($"https://api.edgegap.com/v1/lobbies/{name}"); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (request.responseCode == 404) + { + onResponse(null); + return; + } + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse(response); + } + }; + } + + public static void TerminateLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + var request = SendJson("https://api.edgegap.com/v1/lobbies:terminate", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(response); + } + }; + } + private static void DeployLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + var request = SendJson("https://api.edgegap.com/v1/lobbies:deploy", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(response); + } + }; + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta new file mode 100644 index 0000000..7ad55b0 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 64510fc75d0d75f4185fec1cf4d12206 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs new file mode 100644 index 0000000..7aee63c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs @@ -0,0 +1,138 @@ +using System; +using System.Threading; +using UnityEditor; +using UnityEngine; +#if UNITY_EDITOR +namespace Edgegap +{ + public class LobbyServiceCreateDialogue : EditorWindow + { + public Action onLobby; + public bool waitingCreate; + public bool waitingStatus; + private string _name; + private string _key; + private string _lastStatus; + + private void Awake() + { + minSize = maxSize = new Vector2(450, 300); + titleContent = new GUIContent("Edgegap Lobby Service Setup"); + } + + private void OnGUI() + { + if (waitingCreate) + { + EditorGUILayout.LabelField("Waiting for lobby to create . . . "); + return; + } + if (waitingStatus) + { + EditorGUILayout.LabelField("Waiting for lobby to deploy . . . "); + EditorGUILayout.LabelField($"Latest status: {_lastStatus}"); + return; + } + _key = EditorGUILayout.TextField("Edgegap API key", _key); + LobbyApi.TrimApiKey(ref _key); + EditorGUILayout.HelpBox(new GUIContent("Your API key won't be saved.")); + if (GUILayout.Button("I have no api key?")) + { + Application.OpenURL("https://app.edgegap.com/user-settings?tab=tokens"); + } + EditorGUILayout.Separator(); + EditorGUILayout.HelpBox("There's currently a bug where lobby names longer than 5 characters can fail to deploy correctly and will return a \"503 Service Temporarily Unavailable\"\nIt's recommended to limit your lobby names to 4-5 characters for now", UnityEditor.MessageType.Warning); + _name = EditorGUILayout.TextField("Lobby Name", _name); + EditorGUILayout.HelpBox(new GUIContent("The lobby name is your games identifier for the lobby service")); + + if (GUILayout.Button("Create")) + { + if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name)) + { + EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok"); + } + else + { + waitingCreate = true; + Repaint(); + + LobbyApi.CreateAndDeployLobbyService(_key.Trim(), _name.Trim(), res => + { + waitingCreate = false; + waitingStatus = true; + _lastStatus = res.status; + RefreshStatus(); + Repaint(); + }, error => + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) the lobby service:\n\n{error}", "Ok"); + waitingCreate = false; + }); + return; + } + + } + + if (GUILayout.Button("Cancel")) + Close(); + + EditorGUILayout.HelpBox(new GUIContent("Note: If you forgot your lobby url simply re-create it with the same name!\nIt will re-use the existing lobby service")); + EditorGUILayout.Separator(); + EditorGUILayout.Separator(); + + + if (GUILayout.Button("Terminate existing deploy")) + { + + if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name)) + { + EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok"); + } + else + { + LobbyApi.TerminateLobbyService(_key.Trim(), _name.Trim(), res => + { + EditorUtility.DisplayDialog("Success", $"The lobby service will start terminating (shutting down the deploy) now", "Ok"); + }, error => + { + EditorUtility.DisplayDialog("Failed to terminate lobby", $"The following error happened while trying to terminate the lobby service:\n\n{error}", "Ok"); + }); + } + } + EditorGUILayout.HelpBox(new GUIContent("Done with your lobby?\nEnter the same name as creation to shut it down")); + } + private void RefreshStatus() + { + // Stop if window is closed + if (!this) + { + return; + } + LobbyApi.GetLobbyService(_key, _name, res => + { + if (!res.HasValue) + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The lobby seems to have vanished while waiting for it to deploy.", "Ok"); + waitingStatus = false; + Repaint(); + return; + } + if (!string.IsNullOrEmpty(res.Value.url)) + { + onLobby(res.Value.url); + Close(); + return; + } + _lastStatus = res.Value.status; + Repaint(); + Thread.Sleep(100); // :( but this is a lazy editor script, its fiiine + RefreshStatus(); + }, error => + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) a lobby:\n\n{error}", "Ok"); + waitingStatus = false; + }); + } + } +} +#endif diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta new file mode 100644 index 0000000..3ef32f3 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 25579cc004424981bf0b05bcec65df0a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs new file mode 100644 index 0000000..3d9aa32 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Reflection; +using kcp2k; +using UnityEditor; +using UnityEngine; +#if UNITY_EDITOR +namespace Edgegap +{ + [CustomEditor(typeof(EdgegapLobbyKcpTransport))] + public class EncryptionTransportInspector : UnityEditor.Editor + { + SerializedProperty lobbyUrlProperty; + SerializedProperty lobbyWaitTimeoutProperty; + private List kcpProperties = new List(); + + + // Assuming proper SerializedProperty definitions for properties + // Add more SerializedProperty fields related to different modes as needed + + void OnEnable() + { + lobbyUrlProperty = serializedObject.FindProperty("lobbyUrl"); + lobbyWaitTimeoutProperty = serializedObject.FindProperty("lobbyWaitTimeout"); + // Get public fields from KcpTransport + kcpProperties.Clear(); + FieldInfo[] fields = typeof(KcpTransport).GetFields(BindingFlags.Public | BindingFlags.Instance); + foreach (var field in fields) + { + SerializedProperty prop = serializedObject.FindProperty(field.Name); + if (prop == null) + { + // callbacks have no property + continue; + } + kcpProperties.Add(prop); + } + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUILayout.PropertyField(lobbyUrlProperty); + if (GUILayout.Button("Create & Deploy Lobby")) + { + var input = CreateInstance(); + input.onLobby = (url) => + { + lobbyUrlProperty.stringValue = url; + serializedObject.ApplyModifiedProperties(); + }; + input.ShowUtility(); + } + EditorGUILayout.PropertyField(lobbyWaitTimeoutProperty); + EditorGUILayout.Separator(); + foreach (SerializedProperty prop in kcpProperties) + { + EditorGUILayout.PropertyField(prop); + } + serializedObject.ApplyModifiedProperties(); + } + } +} + +#endif diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta new file mode 100644 index 0000000..9c04c2e --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7d7cc53263184754a4682335440df515 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta new file mode 100644 index 0000000..feda681 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b9b459cf5e084bdd8b196df849a2c519 +timeCreated: 1709953502 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs new file mode 100644 index 0000000..dd8ac68 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#functions + [Serializable] + public struct ListLobbiesResponse + { + public int count; + public LobbyBrief[] data; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta new file mode 100644 index 0000000..c2b9160 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fdb37041d9464f8c90ac86942b940565 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs new file mode 100644 index 0000000..c08c899 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs @@ -0,0 +1,45 @@ +using System; +using UnityEngine; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#getting-a-specific-lobbys-information + [Serializable] + public struct Lobby + { + [Serializable] + public struct Player + { + public uint authorization_token; + public string id; + public bool is_host; + } + + [Serializable] + public struct Port + { + public string name; + public int port; + public string protocol; + } + + [Serializable] + public struct Assignment + { + public uint authorization_token; + public string host; + public string ip; + public Port[] ports; + } + + public Assignment assignment; + public string name; + public string lobby_id; + public bool is_joinable; + public bool is_started; + public int player_count; + public int capacity; + public int available_slots => capacity - player_count; + public string[] tags; + public Player[] players; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta new file mode 100644 index 0000000..3b4e187 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 64db55f096cd4ace83e1aa1c0c0588f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs new file mode 100644 index 0000000..1e927e3 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs @@ -0,0 +1,17 @@ +using System; +namespace Edgegap +{ + // Brief lobby data, returned by the list function + [Serializable] + public struct LobbyBrief + { + public string lobby_id; + public string name; + public bool is_joinable; + public bool is_started; + public int player_count; + public int capacity; + public int available_slots => capacity - player_count; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta new file mode 100644 index 0000000..4bf80d8 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6018ece006144e719c6b3f0d4e256d7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs new file mode 100644 index 0000000..2ed819d --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs @@ -0,0 +1,27 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#creating-a-new-lobby + [Serializable] + public struct LobbyCreateRequest + { + [Serializable] + public struct Player + { + public string id; + } + [Serializable] + public struct Annotation + { + public bool inject; + public string key; + public string value; + } + public Annotation[] annotations; // todo + public int capacity; + public bool is_joinable; + public string name; + public Player player; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta new file mode 100644 index 0000000..7b0c8cb --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4040c1adafc3449eaebd3bd22aa3ff26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs new file mode 100644 index 0000000..3647979 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs @@ -0,0 +1,14 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions/#starting-a-lobby + [Serializable] + public struct LobbyIdRequest + { + public string lobby_id; + public LobbyIdRequest(string lobbyId) + { + lobby_id = lobbyId; + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta new file mode 100644 index 0000000..1e79270 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 219c7fba8724473caf170c6254e6dc45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs new file mode 100644 index 0000000..7c2115f --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs @@ -0,0 +1,17 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby + // https://docs.edgegap.com/docs/lobby/functions#leaving-a-lobby + [Serializable] + public struct LobbyJoinOrLeaveRequest + { + [Serializable] + public struct Player + { + public string id; + } + public string lobby_id; + public Player player; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta new file mode 100644 index 0000000..a888de8 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4091d555e62341f0ac30479952d517aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs new file mode 100644 index 0000000..3b8b53c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs @@ -0,0 +1,12 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby + [Serializable] + public struct LobbyUpdateRequest + { + public int capacity; + public bool is_joinable; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta new file mode 100644 index 0000000..c233f83 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ee158bc379f44cdf9904578f37a5e7a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta new file mode 100644 index 0000000..34ff726 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 62c28e855fc644011b4079c268b46b71 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs new file mode 100644 index 0000000..ecec9f7 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs @@ -0,0 +1,141 @@ +// overwrite RawSend/Receive +using System; +using System.Net.Sockets; +using Mirror; +using UnityEngine; +using kcp2k; + +namespace Edgegap +{ + public class EdgegapKcpClient : KcpClient + { + // need buffer larger than KcpClient.rawReceiveBuffer to add metadata + readonly byte[] relayReceiveBuffer; + + // authentication + public uint userId; + public uint sessionId; + public ConnectionState connectionState = ConnectionState.Disconnected; + + // ping + double lastPingTime; + + public EdgegapKcpClient( + Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + : base(OnConnected, OnData, OnDisconnected, OnError, config) + { + relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead]; + } + + // custom start function with relay parameters; connects udp client. + public void Connect(string relayAddress, ushort relayPort, uint userId, uint sessionId) + { + // reset last state + connectionState = ConnectionState.Checking; + this.userId = userId; + this.sessionId = sessionId; + + // reuse base connect + base.Connect(relayAddress, relayPort); + } + + // parse metadata, then pass to kcp + protected override bool RawReceive(out ArraySegment segment) + { + segment = default; + if (socket == null) return false; + + try + { + if (socket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment content)) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(content)) + { + // parse message type + if (reader.Remaining == 0) + { + Debug.LogWarning($"EdgegapClient: message of {content.Count} is too small to parse."); + return false; + } + byte messageType = reader.ReadByte(); + + // handle message type + switch (messageType) + { + case (byte)MessageType.Ping: + { + // parse state + if (reader.Remaining < 1) return false; + ConnectionState last = connectionState; + connectionState = (ConnectionState)reader.ReadByte(); + + // log state changes for debugging. + if (connectionState != last) Debug.Log($"EdgegapClient: state updated to: {connectionState}"); + + // return true indicates Mirror to keep checking + // for further messages. + return true; + } + case (byte)MessageType.Data: + { + segment = reader.ReadBytesSegment(reader.Remaining); + return true; + } + // wrong message type. return false, don't throw. + default: return false; + } + } + } + } + catch (SocketException e) + { + Log.Info($"EdgegapClient: looks like the other end has closed the connection. This is fine: {e}"); + Disconnect(); + } + + return false; + } + + protected override void RawSend(ArraySegment data) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Data); + writer.WriteBytes(data.Array, data.Offset, data.Count); + base.RawSend(writer); + } + } + + void SendPing() + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Ping); + base.RawSend(writer); + } + } + + public override void TickOutgoing() + { + if (connected) + { + // ping every interval for keepalive & handshake + if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval) + { + SendPing(); + lastPingTime = NetworkTime.localTime; + } + } + + base.TickOutgoing(); + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs.meta new file mode 100644 index 0000000..28915cf --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a0d6fba7098f4ea3949d0195e8276adc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs new file mode 100644 index 0000000..cc87b14 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs @@ -0,0 +1,203 @@ +using System; +using System.Net; +using System.Net.Sockets; +using Mirror; +using UnityEngine; +using kcp2k; + +namespace Edgegap +{ + public class EdgegapKcpServer : KcpServer + { + // need buffer larger than KcpClient.rawReceiveBuffer to add metadata + readonly byte[] relayReceiveBuffer; + + // authentication + public uint userId; + public uint sessionId; + public ConnectionState state = ConnectionState.Disconnected; + + // server is an UDP client talking to relay + protected Socket relaySocket; + public EndPoint remoteEndPoint; + + // ping + double lastPingTime; + + // custom 'active'. while connected to relay + bool relayActive; + + public EdgegapKcpServer( + Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + // TODO don't call base. don't listen to local UdpServer at all? + : base(OnConnected, OnData, OnDisconnected, OnError, config) + { + relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead]; + } + + public override bool IsActive() => relayActive; + + // custom start function with relay parameters; connects udp client. + public void Start(string relayAddress, ushort relayPort, uint userId, uint sessionId) + { + // reset last state + state = ConnectionState.Checking; + this.userId = userId; + this.sessionId = sessionId; + + // try resolve host name + if (!Common.ResolveHostname(relayAddress, out IPAddress[] addresses)) + { + OnError(0, ErrorCode.DnsResolve, $"Failed to resolve host: {relayAddress}"); + return; + } + + // create socket + remoteEndPoint = new IPEndPoint(addresses[0], relayPort); + relaySocket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + relaySocket.Blocking = false; + + // configure buffer sizes + Common.ConfigureSocketBuffers(relaySocket, config.RecvBufferSize, config.SendBufferSize); + + // bind to endpoint for Send/Receive instead of SendTo/ReceiveFrom + relaySocket.Connect(remoteEndPoint); + relayActive = true; + } + + public override void Stop() + { + relayActive = false; + } + + protected override bool RawReceiveFrom(out ArraySegment segment, out int connectionId) + { + segment = default; + connectionId = 0; + + if (relaySocket == null) return false; + + try + { + // TODO need separate buffer. don't write into result yet. only payload + + if (relaySocket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment content)) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(content)) + { + // parse message type + if (reader.Remaining == 0) + { + Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse header."); + return false; + } + byte messageType = reader.ReadByte(); + + // handle message type + switch (messageType) + { + case (byte)MessageType.Ping: + { + // parse state + if (reader.Remaining < 1) return false; + ConnectionState last = state; + state = (ConnectionState)reader.ReadByte(); + + // log state changes for debugging. + if (state != last) Debug.Log($"EdgegapServer: state updated to: {state}"); + + // return true indicates Mirror to keep checking + // for further messages. + return true; + } + case (byte)MessageType.Data: + { + // parse connectionId and payload + if (reader.Remaining <= 4) + { + Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse connId."); + return false; + } + + connectionId = reader.ReadInt(); + segment = reader.ReadBytesSegment(reader.Remaining); + // Debug.Log($"EdgegapServer: receiving from connId={connectionId}: {segment.ToHexString()}"); + return true; + } + // wrong message type. return false, don't throw. + default: return false; + } + } + } + } + catch (SocketException e) + { + Log.Info($"EdgegapServer: looks like the other end has closed the connection. This is fine: {e}"); + } + return false; + } + + protected override void RawSend(int connectionId, ArraySegment data) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // Debug.Log($"EdgegapServer: sending to connId={connectionId}: {data.ToHexString()}"); + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Data); + writer.WriteInt(connectionId); + writer.WriteBytes(data.Array, data.Offset, data.Count); + ArraySegment message = writer; + + try + { + relaySocket.SendNonBlocking(message); + } + catch (SocketException e) + { + Log.Error($"KcpRleayServer: RawSend failed: {e}"); + } + } + } + + void SendPing() + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Ping); + ArraySegment message = writer; + + try + { + relaySocket.SendNonBlocking(message); + } + catch (SocketException e) + { + Debug.LogWarning($"EdgegapServer: failed to ping. perhaps the relay isn't running? {e}"); + } + } + } + + public override void TickOutgoing() + { + if (relayActive) + { + // ping every interval for keepalive & handshake + if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval) + { + SendPing(); + lastPingTime = NetworkTime.localTime; + } + } + + // base processing + base.TickOutgoing(); + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs.meta new file mode 100644 index 0000000..42a969c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fd8551078397248b0848950352c208ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs new file mode 100644 index 0000000..e0a4100 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs @@ -0,0 +1,162 @@ +// edgegap relay transport. +// reuses KcpTransport with custom KcpServer/Client. + +//#if MIRROR <- commented out because MIRROR isn't defined on first import yet +using System; +using System.Text.RegularExpressions; +using UnityEngine; +using Mirror; +using kcp2k; + +namespace Edgegap +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")] + public class EdgegapKcpTransport : KcpTransport + { + [Header("Relay")] + public string relayAddress = "127.0.0.1"; + public ushort relayGameServerPort = 8888; + public ushort relayGameClientPort = 9999; + + // mtu for kcp transport. respects relay overhead. + public const int MaxPayload = Kcp.MTU_DEF - Protocol.Overhead; + + [Header("Relay")] + public bool relayGUI = true; + public uint userId = 11111111; + public uint sessionId = 22222222; + + // helper + internal static String ReParse(String cmd, String pattern, String defaultValue) + { + Match match = Regex.Match(cmd, pattern); + return match.Success ? match.Groups[1].Value : defaultValue; + } + + protected override void Awake() + { + // logging + // Log.Info should use Debug.Log if enabled, or nothing otherwise + // (don't want to spam the console on headless servers) + if (debugLog) + Log.Info = Debug.Log; + else + Log.Info = _ => {}; + Log.Warning = Debug.LogWarning; + Log.Error = Debug.LogError; + + // create config from serialized settings. + // with MaxPayload as max size to respect relay overhead. + config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MaxPayload, NoDelay, Interval, FastResend, false, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit); + + // client (NonAlloc version is not necessary anymore) + client = new EdgegapKcpClient( + () => OnClientConnected.Invoke(), + (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)), + () => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708 + (error, reason) => OnClientError.Invoke(ToTransportError(error), reason), + config + ); + + // server + server = new EdgegapKcpServer( + (connectionId, endPoint) => OnServerConnectedWithAddress.Invoke(connectionId, endPoint.PrettyAddress()), + (connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)), + (connectionId) => OnServerDisconnected.Invoke(connectionId), + (connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason), + config); + + if (statisticsLog) + InvokeRepeating(nameof(OnLogStatistics), 1, 1); + + Debug.Log("EdgegapTransport initialized!"); + } + + protected override void OnValidate() + { + // show max message sizes in inspector for convenience. + // 'config' isn't available in edit mode yet, so use MTU define. + ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MaxPayload, ReceiveWindowSize); + UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MaxPayload); + } + + // client overwrites to use EdgegapClient instead of KcpClient + public override void ClientConnect(string address) + { + // connect to relay address:port instead of the expected server address + EdgegapKcpClient client = (EdgegapKcpClient)this.client; + client.userId = userId; + client.sessionId = sessionId; + client.connectionState = ConnectionState.Checking; // reset from last time + client.Connect(relayAddress, relayGameClientPort); + } + public override void ClientConnect(Uri uri) + { + if (uri.Scheme != Scheme) + throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri)); + + // connect to relay address:port instead of the expected server address + EdgegapKcpClient client = (EdgegapKcpClient)this.client; + client.Connect(relayAddress, relayGameClientPort, userId, sessionId); + } + + // server overwrites to use EdgegapServer instead of KcpServer + public override void ServerStart() + { + // start the server + EdgegapKcpServer server = (EdgegapKcpServer)this.server; + server.Start(relayAddress, relayGameServerPort, userId, sessionId); + } + + void OnGUIRelay() + { + // if (server.IsActive()) return; + + GUILayout.BeginArea(new Rect(300, 30, 200, 100)); + + GUILayout.BeginHorizontal(); + GUILayout.Label("SessionId:"); + sessionId = Convert.ToUInt32(GUILayout.TextField(sessionId.ToString())); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + GUILayout.Label("UserId:"); + userId = Convert.ToUInt32(GUILayout.TextField(userId.ToString())); + GUILayout.EndHorizontal(); + + if (NetworkServer.active) + { + EdgegapKcpServer server = (EdgegapKcpServer)this.server; + GUILayout.BeginHorizontal(); + GUILayout.Label("State:"); + GUILayout.Label(server.state.ToString()); + GUILayout.EndHorizontal(); + } + else if (NetworkClient.active) + { + EdgegapKcpClient client = (EdgegapKcpClient)this.client; + GUILayout.BeginHorizontal(); + GUILayout.Label("State:"); + GUILayout.Label(client.connectionState.ToString()); + GUILayout.EndHorizontal(); + } + + GUILayout.EndArea(); + } + + // base OnGUI only shows in editor & development builds. + // here we always show it because we need the sessionid & userid buttons. +#pragma warning disable CS0109 + new void OnGUI() + { +#if UNITY_EDITOR || DEVELOPMENT_BUILD + base.OnGUI(); +#endif + if (relayGUI) OnGUIRelay(); + } + + public override string ToString() => "Edgegap Kcp Transport"; + } +#pragma warning restore CS0109 +} +//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs.meta new file mode 100644 index 0000000..66be633 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c2d1e0e17f753449798fa27474d6b86b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs new file mode 100644 index 0000000..58ba478 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs @@ -0,0 +1,29 @@ +// relay protocol definitions +namespace Edgegap +{ + public enum ConnectionState : byte + { + Disconnected = 0, // until the user calls connect() + Checking = 1, // recently connected, validation in progress + Valid = 2, // validation succeeded + Invalid = 3, // validation rejected by tower + SessionTimeout = 4, // session owner timed out + Error = 5, // other error + } + + public enum MessageType : byte + { + Ping = 1, + Data = 2 + } + + public static class Protocol + { + // MTU: relay adds up to 13 bytes of metadata in the worst case. + public const int Overhead = 13; + + // ping interval should be between 100 ms and 1 second. + // faster ping gives faster authentication, but higher bandwidth. + public const float PingInterval = 0.5f; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs.meta new file mode 100644 index 0000000..900ad7d --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: eac30312ba61470b849e368af3c3b0e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md new file mode 100644 index 0000000..51add6e --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md @@ -0,0 +1,20 @@ +# Edgegap Relay for Mirror +Documentation: https://docs.edgegap.com/docs/distributed-relay-manager/ + +## Prerequisites +- Unity project set up with the Mirror networking library installed + - Supported Versions: [Mirror](https://assetstore.unity.com/packages/tools/network/mirror-129321) and [Mirror LTS](https://assetstore.unity.com/packages/tools/network/mirror-lts-102631) +- EdgegapTransport module downloaded and extracted + +## Steps +1. Open your Unity project and navigate to the "Assets" folder. +2. Locate the "Mirror" folder within "Assets" and open it. +3. Within the "Mirror" folder, open the "Transports" folder. +4. Drag and drop the "Unity" folder from the extracted EdgegapTransport files into the "Transports" folder. +5. Open your NetworkManager script in the Unity Editor and navigate to the "Inspector" panel. +6. In the "Inspector" panel, locate the "Network Manager" component and click the "+" button next to the "Transport" property. +7. In the "Add Component" menu that appears, select "Edgegap Transport" to add it to the NetworkManager. +8. Drag the newly added "Edgegap Transport" component into the "Transport" property in the "Inspector" panel. + +## Notes +- The EdgegapTransport module is only compatible with Mirror and Mirror LTS versions. diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta new file mode 100644 index 0000000..1302efd --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 8ade7c960d8fe4e94970ddd88ede3bca +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs new file mode 100644 index 0000000..670c507 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs @@ -0,0 +1,25 @@ +// parse session_id and user_id from command line args. +// mac: "open mirror.app --args session_id=123 user_id=456" +using System; +using UnityEngine; + +namespace Edgegap +{ + public class RelayCredentialsFromArgs : MonoBehaviour + { + void Awake() + { + String cmd = Environment.CommandLine; + + // parse session_id via regex + String sessionId = EdgegapKcpTransport.ReParse(cmd, "session_id=(\\d+)", "111111"); + String userID = EdgegapKcpTransport.ReParse(cmd, "user_id=(\\d+)", "222222"); + Debug.Log($"Parsed sessionId: {sessionId} user_id: {userID}"); + + // configure transport + EdgegapKcpTransport transport = GetComponent(); + transport.sessionId = UInt32.Parse(sessionId); + transport.userId = UInt32.Parse(userID); + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs.meta new file mode 100644 index 0000000..c373ec3 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e9ec7091b26c4d3882f4b42f10f9b8c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/edgegap.png b/Assets/Mirror/Transports/Edgegap/edgegap.png new file mode 100644 index 0000000..b52c295 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/edgegap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9a2bb9d1b247129cfcd1e8b41400fed37eb423ad47a2baba5e2270f67f8ce13 +size 4347 diff --git a/Assets/Mirror/Transports/Edgegap/edgegap.png.meta b/Assets/Mirror/Transports/Edgegap/edgegap.png.meta new file mode 100644 index 0000000..2be5b45 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/edgegap.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 3ea6ff15cda674a57b0c7c8b7dc1878c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 16 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/edgegap.png + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption.meta b/Assets/Mirror/Transports/Encryption.meta new file mode 100644 index 0000000..db711f4 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 741b3c7e5d0842049ff50a2f6e27ca12 +timeCreated: 1708015148 diff --git a/Assets/Mirror/Transports/Encryption/Editor.meta b/Assets/Mirror/Transports/Encryption/Editor.meta new file mode 100644 index 0000000..7111250 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0d3cd9d7d6e84a578f7e4b384ff813f1 +timeCreated: 1708793986 diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef new file mode 100644 index 0000000..0ba9c76 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "EncryptionTransportEditor", + "rootNamespace": "", + "references": [ + "GUID:627104647b9c04b4ebb8978a92ecac63" + ], + "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/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta new file mode 100644 index 0000000..bbe2e8c --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 4c9c7b0ef83e6e945b276d644816a489 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs new file mode 100644 index 0000000..8ffb428 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs @@ -0,0 +1,90 @@ +using UnityEditor; +using UnityEngine; + +namespace Mirror.Transports.Encryption +{ + [CustomEditor(typeof(EncryptionTransport), true)] + public class EncryptionTransportInspector : UnityEditor.Editor + { + SerializedProperty innerProperty; + SerializedProperty clientValidatesServerPubKeyProperty; + SerializedProperty clientTrustedPubKeySignaturesProperty; + SerializedProperty serverKeypairPathProperty; + SerializedProperty serverLoadKeyPairFromFileProperty; + + // Assuming proper SerializedProperty definitions for properties + // Add more SerializedProperty fields related to different modes as needed + + void OnEnable() + { + innerProperty = serializedObject.FindProperty("Inner"); + clientValidatesServerPubKeyProperty = serializedObject.FindProperty("ClientValidateServerPubKey"); + clientTrustedPubKeySignaturesProperty = serializedObject.FindProperty("ClientTrustedPubKeySignatures"); + serverKeypairPathProperty = serializedObject.FindProperty("ServerKeypairPath"); + serverLoadKeyPairFromFileProperty = serializedObject.FindProperty("ServerLoadKeyPairFromFile"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // Draw default inspector for the parent class + DrawDefaultInspector(); + EditorGUILayout.LabelField("Encryption Settings", EditorStyles.boldLabel); + if (innerProperty != null) + { + EditorGUILayout.LabelField("Common", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(innerProperty); + EditorGUILayout.Separator(); + } + // Client Section + EditorGUILayout.LabelField("Client", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Validating the servers public key is essential for complete man-in-the-middle (MITM) safety, but might not be feasible for all modes of hosting.", MessageType.Info); + EditorGUILayout.PropertyField(clientValidatesServerPubKeyProperty, new GUIContent("Validate Server Public Key")); + + EncryptionTransport.ValidationMode validationMode = (EncryptionTransport.ValidationMode)clientValidatesServerPubKeyProperty.enumValueIndex; + + switch (validationMode) + { + case EncryptionTransport.ValidationMode.List: + EditorGUILayout.PropertyField(clientTrustedPubKeySignaturesProperty); + break; + case EncryptionTransport.ValidationMode.Callback: + EditorGUILayout.HelpBox("Please set the EncryptionTransport.onClientValidateServerPubKey at runtime.", MessageType.Info); + break; + } + + EditorGUILayout.Separator(); + // Server Section + EditorGUILayout.LabelField("Server", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(serverLoadKeyPairFromFileProperty, new GUIContent("Load Keypair From File")); + if (serverLoadKeyPairFromFileProperty.boolValue) + { + EditorGUILayout.PropertyField(serverKeypairPathProperty, new GUIContent("Keypair File Path")); + } + if(GUILayout.Button("Generate Key Pair")) + { + EncryptionCredentials keyPair = EncryptionCredentials.Generate(); + string path = EditorUtility.SaveFilePanel("Select where to save the keypair", "", "server-keys.json", "json"); + if (!string.IsNullOrEmpty(path)) + { + keyPair.SaveToFile(path); + EditorUtility.DisplayDialog("KeyPair Saved", $"Successfully saved the keypair.\nThe fingerprint is {keyPair.PublicKeyFingerprint}, you can also retrieve it from the saved json file at any point.", "Ok"); + if (validationMode == EncryptionTransport.ValidationMode.List) + { + if (EditorUtility.DisplayDialog("Add key to trusted list?", "Do you also want to add the generated key to the trusted list?", "Yes", "No")) + { + clientTrustedPubKeySignaturesProperty.arraySize++; + clientTrustedPubKeySignaturesProperty.GetArrayElementAtIndex(clientTrustedPubKeySignaturesProperty.arraySize - 1).stringValue = keyPair.PublicKeyFingerprint; + } + } + } + } + + serializedObject.ApplyModifiedProperties(); + } + + [CustomEditor(typeof(ThreadedEncryptionKcpTransport), true)] + class EncryptionThreadedTransportInspector : EncryptionTransportInspector {} + } +} diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta new file mode 100644 index 0000000..d7bb0c1 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 871580d2094a46139279d651cec92b5d +timeCreated: 1708794004 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs new file mode 100644 index 0000000..f91ad60 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs @@ -0,0 +1,554 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Mirror.BouncyCastle.Crypto; +using Mirror.BouncyCastle.Crypto.Agreement; +using Mirror.BouncyCastle.Crypto.Digests; +using Mirror.BouncyCastle.Crypto.Generators; +using Mirror.BouncyCastle.Crypto.Modes; +using Mirror.BouncyCastle.Crypto.Parameters; +using UnityEngine.Profiling; + +namespace Mirror.Transports.Encryption +{ + public class EncryptedConnection + { + // 256-bit key + const int KeyLength = 32; + // 512-bit salt for the key derivation function + const int HkdfSaltSize = KeyLength * 2; + + // Info tag for the HKDF, this just adds more entropy + static readonly byte[] HkdfInfo = Encoding.UTF8.GetBytes("Mirror/EncryptionTransport"); + + // fixed size of the unique per-packet nonce. Defaults to 12 bytes/96 bits (not recommended to be changed) + const int NonceSize = 12; + + // this is the size of the "checksum" included in each encrypted payload + // 16 bytes/128 bytes is the recommended value for best security + // can be reduced to 12 bytes for a small space savings, but makes encryption slightly weaker. + // Setting it lower than 12 bytes is not recommended + const int MacSizeBytes = 16; + + const int MacSizeBits = MacSizeBytes * 8; + + // How much metadata overhead we have for regular packets + public const int Overhead = sizeof(OpCodes) + MacSizeBytes + NonceSize; + + // After how many seconds of not receiving a handshake packet we should time out + const double DurationTimeout = 2; // 2s + + // After how many seconds to assume the last handshake packet got lost and to resend another one + const double DurationResend = 0.05; // 50ms + + + // Static fields for allocation efficiency, makes this not thread safe + // It'd be as easy as using ThreadLocal though to fix that + + // Set up a global cipher instance, it is initialised/reset before use + // (AesFastEngine used to exist, but was removed due to side channel issues) + // use AesUtilities.CreateEngine here as it'll pick the hardware accelerated one if available (which is will not be unless on .net core) + static readonly ThreadLocal Cipher = new ThreadLocal(() => new GcmBlockCipher(AesUtilities.CreateEngine())); + + // Set up a global HKDF with a SHA-256 digest + static readonly ThreadLocal Hkdf = new ThreadLocal(() => new HkdfBytesGenerator(new Sha256Digest())); + + // Global byte array to store nonce sent by the remote side, they're used immediately after + static readonly ThreadLocal ReceiveNonce = new ThreadLocal(() => new byte[NonceSize]); + + // Buffer for the remote salt, as bouncycastle needs to take a byte[] *rolls eyes* + static readonly ThreadLocal TMPRemoteSaltBuffer = new ThreadLocal(() => new byte[HkdfSaltSize]); + + // buffer for encrypt/decrypt operations, resized larger as needed + static ThreadLocal TMPCryptBuffer = new ThreadLocal(() => new byte[2048]); + + // packet headers + enum OpCodes : byte + { + // start at 1 to maybe filter out random noise + Data = 1, + HandshakeStart = 2, + HandshakeAck = 3, + HandshakeFin = 4 + } + + enum State + { + // Waiting for a handshake to arrive + // this is for _sendsFirst: + // - false: OpCodes.HandshakeStart + // - true: Opcodes.HandshakeAck + WaitingHandshake, + + // Waiting for a handshake reply/acknowledgement to arrive + // this is for _sendsFirst: + // - false: OpCodes.HandshakeFine + // - true: Opcodes.Data (implicitly) + WaitingHandshakeReply, + + // Both sides have confirmed the keys are exchanged and data can be sent freely + Ready + } + + State state = State.WaitingHandshake; + + // Key exchange confirmed and data can be sent freely + public bool IsReady => state == State.Ready; + // Callback to send off encrypted data + readonly Action, int> send; + // Callback when received data has been decrypted + readonly Action, int> receive; + // Callback when the connection becomes ready + readonly Action ready; + // On-error callback, disconnect expected + readonly Action error; + // Optional callback to validate the remotes public key, validation on one side is necessary to ensure MITM resistance + // (usually client validates the server key) + readonly Func validateRemoteKey; + // Our asymmetric credentials for the initial DH exchange + EncryptionCredentials credentials; + readonly byte[] hkdfSalt; + NetworkReader _tmpReader = new NetworkReader(new ArraySegment()); + + // After no handshake packet in this many seconds, the handshake fails + double handshakeTimeout; + // When to assume the last handshake packet got lost and to resend another one + double nextHandshakeResend; + + + // we can reuse the _cipherParameters here since the nonce is stored as the byte[] reference we pass in + // so we can update it without creating a new AeadParameters instance + // this might break in the future! (will cause bad data) + byte[] nonce = new byte[NonceSize]; + AeadParameters cipherParametersEncrypt; + AeadParameters cipherParametersDecrypt; + + + /* + * Specifies if we send the first key, then receive ack, then send fin + * Or the opposite if set to false + * + * The client does this, since the fin is not acked explicitly, but by receiving data to decrypt + */ + readonly bool sendsFirst; + + public EncryptedConnection(EncryptionCredentials credentials, + bool isClient, + Action, int> sendAction, + Action, int> receiveAction, + Action readyAction, + Action errorAction, + Func validateRemoteKey = null) + { + this.credentials = credentials; + sendsFirst = isClient; + if (!sendsFirst) + // salt is controlled by the server + hkdfSalt = GenerateSecureBytes(HkdfSaltSize); + send = sendAction; + receive = receiveAction; + ready = readyAction; + error = errorAction; + this.validateRemoteKey = validateRemoteKey; + } + + // Generates a random starting nonce + static byte[] GenerateSecureBytes(int size) + { + byte[] bytes = new byte[size]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + rng.GetBytes(bytes); + + return bytes; + } + + + public void OnReceiveRaw(ArraySegment data, int channel) + { + if (data.Count < 1) + { + error(TransportError.Unexpected, "Received empty packet"); + return; + } + + _tmpReader.SetBuffer(data); + OpCodes opcode = (OpCodes)_tmpReader.ReadByte(); + switch (opcode) + { + case OpCodes.Data: + // first sender ready is implicit when data is received + if (sendsFirst && state == State.WaitingHandshakeReply) + SetReady(); + else if (!IsReady) + error(TransportError.Unexpected, "Unexpected data while not ready."); + + if (_tmpReader.Remaining < Overhead) + { + error(TransportError.Unexpected, "received data packet smaller than metadata size"); + return; + } + + ArraySegment ciphertext = _tmpReader.ReadBytesSegment(_tmpReader.Remaining - NonceSize); + _tmpReader.ReadBytes(ReceiveNonce.Value, NonceSize); + + Profiler.BeginSample("EncryptedConnection.Decrypt"); + ArraySegment plaintext = Decrypt(ciphertext); + Profiler.EndSample(); + if (plaintext.Count == 0) + // error + return; + receive(plaintext, channel); + break; + case OpCodes.HandshakeStart: + if (sendsFirst) + { + error(TransportError.Unexpected, "Received HandshakeStart packet, we don't expect this."); + return; + } + + if (state == State.WaitingHandshakeReply) + // this is fine, packets may arrive out of order + return; + + state = State.WaitingHandshakeReply; + ResetTimeouts(); + CompleteExchange(_tmpReader.ReadBytesSegment(_tmpReader.Remaining), hkdfSalt); + SendHandshakeAndPubKey(OpCodes.HandshakeAck); + break; + case OpCodes.HandshakeAck: + if (!sendsFirst) + { + error(TransportError.Unexpected, "Received HandshakeAck packet, we don't expect this."); + return; + } + + if (IsReady) + // this is fine, packets may arrive out of order + return; + + if (state == State.WaitingHandshakeReply) + // this is fine, packets may arrive out of order + return; + + + state = State.WaitingHandshakeReply; + ResetTimeouts(); + _tmpReader.ReadBytes(TMPRemoteSaltBuffer.Value, HkdfSaltSize); + CompleteExchange(_tmpReader.ReadBytesSegment(_tmpReader.Remaining), TMPRemoteSaltBuffer.Value); + SendHandshakeFin(); + break; + case OpCodes.HandshakeFin: + if (sendsFirst) + { + error(TransportError.Unexpected, "Received HandshakeFin packet, we don't expect this."); + return; + } + + if (IsReady) + // this is fine, packets may arrive out of order + return; + + if (state != State.WaitingHandshakeReply) + { + error(TransportError.Unexpected, + "Received HandshakeFin packet, we didn't expect this yet."); + return; + } + + SetReady(); + + break; + default: + error(TransportError.InvalidReceive, $"Unhandled opcode {(byte)opcode:x}"); + break; + } + } + + void SetReady() + { + // done with credentials, null out the reference + credentials = null; + + state = State.Ready; + ready(); + } + + void ResetTimeouts() + { + handshakeTimeout = 0; + nextHandshakeResend = -1; + } + + public void Send(ArraySegment data, int channel) + { + using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get()) + { + writer.WriteByte((byte)OpCodes.Data); + Profiler.BeginSample("EncryptedConnection.Encrypt"); + ArraySegment encrypted = Encrypt(data); + Profiler.EndSample(); + + if (encrypted.Count == 0) + // error + return; + writer.WriteBytes(encrypted.Array, 0, encrypted.Count); + // write nonce after since Encrypt will update it + writer.WriteBytes(nonce, 0, NonceSize); + send(writer.ToArraySegment(), channel); + } + } + + ArraySegment Encrypt(ArraySegment plaintext) + { + if (plaintext.Count == 0) + // Invalid + return new ArraySegment(); + // Need to make the nonce unique again before encrypting another message + UpdateNonce(); + // Re-initialize the cipher with our cached parameters + Cipher.Value.Init(true, cipherParametersEncrypt); + + // Calculate the expected output size, this should always be input size + mac size + int outSize = Cipher.Value.GetOutputSize(plaintext.Count); +#if UNITY_EDITOR + // expecting the outSize to be input size + MacSize + if (outSize != plaintext.Count + MacSizeBytes) + throw new Exception($"Encrypt: Unexpected output size (Expected {plaintext.Count + MacSizeBytes}, got {outSize}"); +#endif + // Resize the static buffer to fit + byte[] cryptBuffer = TMPCryptBuffer.Value; + EnsureSize(ref cryptBuffer, outSize); + TMPCryptBuffer.Value = cryptBuffer; + + int resultLen; + try + { + // Run the plain text through the cipher, ProcessBytes will only process full blocks + resultLen = + Cipher.Value.ProcessBytes(plaintext.Array, plaintext.Offset, plaintext.Count, cryptBuffer, 0); + // Then run any potentially remaining partial blocks through with DoFinal (and calculate the mac) + resultLen += Cipher.Value.DoFinal(cryptBuffer, resultLen); + } + // catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types + // + catch (Exception e) + { + error(TransportError.Unexpected, $"Unexpected exception while encrypting {e.GetType()}: {e.Message}"); + return new ArraySegment(); + } +#if UNITY_EDITOR + // expecting the result length to match the previously calculated input size + MacSize + if (resultLen != outSize) + throw new Exception($"Encrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})"); +#endif + return new ArraySegment(cryptBuffer, 0, resultLen); + } + + ArraySegment Decrypt(ArraySegment ciphertext) + { + if (ciphertext.Count <= MacSizeBytes) + { + error(TransportError.Unexpected, $"Received too short data packet (min {{MacSizeBytes + 1}}, got {ciphertext.Count})"); + // Invalid + return new ArraySegment(); + } + // Re-initialize the cipher with our cached parameters + Cipher.Value.Init(false, cipherParametersDecrypt); + + // Calculate the expected output size, this should always be input size - mac size + int outSize = Cipher.Value.GetOutputSize(ciphertext.Count); +#if UNITY_EDITOR + // expecting the outSize to be input size - MacSize + if (outSize != ciphertext.Count - MacSizeBytes) + throw new Exception($"Decrypt: Unexpected output size (Expected {ciphertext.Count - MacSizeBytes}, got {outSize}"); +#endif + + byte[] cryptBuffer = TMPCryptBuffer.Value; + EnsureSize(ref cryptBuffer, outSize); + TMPCryptBuffer.Value = cryptBuffer; + + int resultLen; + try + { + // Run the ciphertext through the cipher, ProcessBytes will only process full blocks + resultLen = + Cipher.Value.ProcessBytes(ciphertext.Array, ciphertext.Offset, ciphertext.Count, cryptBuffer, 0); + // Then run any potentially remaining partial blocks through with DoFinal (and calculate/check the mac) + resultLen += Cipher.Value.DoFinal(cryptBuffer, resultLen); + } + // catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types + catch (Exception e) + { + error(TransportError.Unexpected, $"Unexpected exception while decrypting {e.GetType()}: {e.Message}. This usually signifies corrupt data"); + return new ArraySegment(); + } +#if UNITY_EDITOR + // expecting the result length to match the previously calculated input size + MacSize + if (resultLen != outSize) + throw new Exception($"Decrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})"); +#endif + return new ArraySegment(cryptBuffer, 0, resultLen); + } + + void UpdateNonce() + { + // increment the nonce by one + // we need to ensure the nonce is *always* unique and not reused + // easiest way to do this is by simply incrementing it + for (int i = 0; i < NonceSize; i++) + { + nonce[i]++; + if (nonce[i] != 0) + break; + } + } + + static void EnsureSize(ref byte[] buffer, int size) + { + if (buffer.Length < size) + // double buffer to avoid constantly resizing by a few bytes + Array.Resize(ref buffer, Math.Max(size, buffer.Length * 2)); + } + + void SendHandshakeAndPubKey(OpCodes opcode) + { + using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get()) + { + writer.WriteByte((byte)opcode); + if (opcode == OpCodes.HandshakeAck) + writer.WriteBytes(hkdfSalt, 0, HkdfSaltSize); + writer.WriteBytes(credentials.PublicKeySerialized, 0, credentials.PublicKeySerialized.Length); + send(writer.ToArraySegment(), Channels.Unreliable); + } + } + + void SendHandshakeFin() + { + using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get()) + { + writer.WriteByte((byte)OpCodes.HandshakeFin); + send(writer.ToArraySegment(), Channels.Unreliable); + } + } + + void CompleteExchange(ArraySegment remotePubKeyRaw, byte[] salt) + { + AsymmetricKeyParameter remotePubKey; + try + { + remotePubKey = EncryptionCredentials.DeserializePublicKey(remotePubKeyRaw); + } + catch (Exception e) + { + error(TransportError.Unexpected, $"Failed to deserialize public key of remote. {e.GetType()}: {e.Message}"); + return; + } + + if (validateRemoteKey != null) + { + PubKeyInfo info = new PubKeyInfo + { + Fingerprint = EncryptionCredentials.PubKeyFingerprint(remotePubKeyRaw), + Serialized = remotePubKeyRaw, + Key = remotePubKey + }; + if (!validateRemoteKey(info)) + { + error(TransportError.Unexpected, $"Remote public key (fingerprint: {info.Fingerprint}) failed validation. "); + return; + } + } + + // Calculate a common symmetric key from our private key and the remotes public key + // This gives us the same key on the other side, with our public key and their remote + // It's like magic, but with math! + ECDHBasicAgreement ecdh = new ECDHBasicAgreement(); + ecdh.Init(credentials.PrivateKey); + byte[] sharedSecret; + try + { + sharedSecret = ecdh.CalculateAgreement(remotePubKey).ToByteArrayUnsigned(); + } + catch + (Exception e) + { + error(TransportError.Unexpected, $"Failed to calculate the ECDH key exchange. {e.GetType()}: {e.Message}"); + return; + } + + if (salt.Length != HkdfSaltSize) + { + error(TransportError.Unexpected, $"Salt is expected to be {HkdfSaltSize} bytes long, got {salt.Length}."); + return; + } + + Hkdf.Value.Init(new HkdfParameters(sharedSecret, salt, HkdfInfo)); + + // Allocate a buffer for the output key + byte[] keyRaw = new byte[KeyLength]; + + // Generate the output keying material + Hkdf.Value.GenerateBytes(keyRaw, 0, keyRaw.Length); + + KeyParameter key = new KeyParameter(keyRaw); + + // generate a starting nonce + nonce = GenerateSecureBytes(NonceSize); + + // we pass in the nonce array once (as it's stored by reference) so we can cache the AeadParameters instance + // instead of creating a new one each encrypt/decrypt + cipherParametersEncrypt = new AeadParameters(key, MacSizeBits, nonce); + cipherParametersDecrypt = new AeadParameters(key, MacSizeBits, ReceiveNonce.Value); + } + + /** + * non-ready connections need to be ticked for resending key data over unreliable + */ + public void TickNonReady(double time) + { + if (IsReady) + return; + + // Timeout reset + if (handshakeTimeout == 0) + handshakeTimeout = time + DurationTimeout; + else if (time > handshakeTimeout) + { + error?.Invoke(TransportError.Timeout, $"Timed out during {state}, this probably just means the other side went away which is fine."); + return; + } + + // Timeout reset + if (nextHandshakeResend < 0) + { + nextHandshakeResend = time + DurationResend; + return; + } + + if (time < nextHandshakeResend) + // Resend isn't due yet + return; + + nextHandshakeResend = time + DurationResend; + switch (state) + { + case State.WaitingHandshake: + if (sendsFirst) + SendHandshakeAndPubKey(OpCodes.HandshakeStart); + + break; + case State.WaitingHandshakeReply: + if (sendsFirst) + SendHandshakeFin(); + else + SendHandshakeAndPubKey(OpCodes.HandshakeAck); + + break; + case State.Ready: // IsReady is checked above & early-returned + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta new file mode 100644 index 0000000..84c136a --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 28f3ac4ff1d346a895d0b4ff714fb57b +timeCreated: 1708111337 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/EncryptedConnection.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs new file mode 100644 index 0000000..5834366 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs @@ -0,0 +1,119 @@ +using System; +using System.IO; +using Mirror.BouncyCastle.Asn1.Pkcs; +using Mirror.BouncyCastle.Asn1.X509; +using Mirror.BouncyCastle.Crypto; +using Mirror.BouncyCastle.Crypto.Digests; +using Mirror.BouncyCastle.Crypto.Generators; +using Mirror.BouncyCastle.X509; +using Mirror.BouncyCastle.Crypto.Parameters; +using Mirror.BouncyCastle.Pkcs; +using Mirror.BouncyCastle.Security; +using UnityEngine; + +namespace Mirror.Transports.Encryption +{ + public class EncryptionCredentials + { + const int PrivateKeyBits = 256; + // don't actually need to store this currently + // but we'll need to for loading/saving from file maybe? + // public ECPublicKeyParameters PublicKey; + + // The serialized public key, in DER format + public byte[] PublicKeySerialized; + public ECPrivateKeyParameters PrivateKey; + public string PublicKeyFingerprint; + + EncryptionCredentials() {} + + // TODO: load from file + public static EncryptionCredentials Generate() + { + var generator = new ECKeyPairGenerator(); + generator.Init(new KeyGenerationParameters(new SecureRandom(), PrivateKeyBits)); + AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair(); + var serialized = SerializePublicKey((ECPublicKeyParameters)keyPair.Public); + return new EncryptionCredentials + { + // see fields above + // PublicKey = (ECPublicKeyParameters)keyPair.Public, + PublicKeySerialized = serialized, + PublicKeyFingerprint = PubKeyFingerprint(new ArraySegment(serialized)), + PrivateKey = (ECPrivateKeyParameters)keyPair.Private + }; + } + + public static byte[] SerializePublicKey(AsymmetricKeyParameter publicKey) + { + // apparently the best way to transmit this public key over the network is to serialize it as a DER + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(publicKey); + return publicKeyInfo.ToAsn1Object().GetDerEncoded(); + } + + public static AsymmetricKeyParameter DeserializePublicKey(ArraySegment pubKey) => + // And then we do this to deserialize from the DER (from above) + // the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted + // to a byte[] first and then shoved through a MemoryStream + PublicKeyFactory.CreateKey(new MemoryStream(pubKey.Array, pubKey.Offset, pubKey.Count, false)); + + public static byte[] SerializePrivateKey(AsymmetricKeyParameter privateKey) + { + // Serialize privateKey as a DER + PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey); + return privateKeyInfo.ToAsn1Object().GetDerEncoded(); + } + + public static AsymmetricKeyParameter DeserializePrivateKey(ArraySegment privateKey) => + // And then we do this to deserialize from the DER (from above) + // the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted + // to a byte[] first and then shoved through a MemoryStream + PrivateKeyFactory.CreateKey(new MemoryStream(privateKey.Array, privateKey.Offset, privateKey.Count, false)); + + public static string PubKeyFingerprint(ArraySegment publicKeyBytes) + { + Sha256Digest digest = new Sha256Digest(); + byte[] hash = new byte[digest.GetDigestSize()]; + digest.BlockUpdate(publicKeyBytes.Array, publicKeyBytes.Offset, publicKeyBytes.Count); + digest.DoFinal(hash, 0); + + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + + public void SaveToFile(string path) + { + string json = JsonUtility.ToJson(new SerializedPair + { + PublicKeyFingerprint = PublicKeyFingerprint, + PublicKey = Convert.ToBase64String(PublicKeySerialized), + PrivateKey= Convert.ToBase64String(SerializePrivateKey(PrivateKey)) + }); + File.WriteAllText(path, json); + } + + public static EncryptionCredentials LoadFromFile(string path) + { + string json = File.ReadAllText(path); + SerializedPair serializedPair = JsonUtility.FromJson(json); + + byte[] publicKeyBytes = Convert.FromBase64String(serializedPair.PublicKey); + byte[] privateKeyBytes = Convert.FromBase64String(serializedPair.PrivateKey); + + if (serializedPair.PublicKeyFingerprint != PubKeyFingerprint(new ArraySegment(publicKeyBytes))) + throw new Exception("Saved public key fingerprint does not match public key."); + return new EncryptionCredentials + { + PublicKeySerialized = publicKeyBytes, + PublicKeyFingerprint = serializedPair.PublicKeyFingerprint, + PrivateKey = (ECPrivateKeyParameters) DeserializePrivateKey(new ArraySegment(privateKeyBytes)) + }; + } + + class SerializedPair + { + public string PublicKeyFingerprint; + public string PublicKey; + public string PrivateKey; + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta new file mode 100644 index 0000000..16dbc7e --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: af6ae5f74f9548588cba5731643fabaf +timeCreated: 1708139579 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs new file mode 100644 index 0000000..3bb8d1a --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using Mirror.BouncyCastle.Crypto; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Serialization; + +namespace Mirror.Transports.Encryption +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/encryption-transport")] + public class EncryptionTransport : Transport, PortTransport + { + public override bool IsEncrypted => true; + public override string EncryptionCipher => "AES256-GCM"; + [FormerlySerializedAs("inner")] + [HideInInspector] + public Transport Inner; + + public ushort Port + { + get + { + if (Inner is PortTransport portTransport) + return portTransport.Port; + + Debug.LogError($"EncryptionTransport can't get Port because {Inner} is not a PortTransport"); + return 0; + } + set + { + if (Inner is PortTransport portTransport) + { + portTransport.Port = value; + return; + } + Debug.LogError($"EncryptionTransport can't set Port because {Inner} is not a PortTransport"); + } + } + + public enum ValidationMode + { + Off, + List, + Callback + } + + [FormerlySerializedAs("clientValidateServerPubKey")] + [HideInInspector] + public ValidationMode ClientValidateServerPubKey; + [FormerlySerializedAs("clientTrustedPubKeySignatures")] + [HideInInspector] + [Tooltip("List of public key fingerprints the client will accept")] + public string[] ClientTrustedPubKeySignatures; + public Func OnClientValidateServerPubKey; + [FormerlySerializedAs("serverLoadKeyPairFromFile")] + [HideInInspector] + public bool ServerLoadKeyPairFromFile; + [FormerlySerializedAs("serverKeypairPath")] + [HideInInspector] + public string ServerKeypairPath = "./server-keys.json"; + + EncryptedConnection client; + + readonly Dictionary serverConnections = new Dictionary(); + + readonly List serverPendingConnections = + new List(); + + EncryptionCredentials credentials; + public string EncryptionPublicKeyFingerprint => credentials?.PublicKeyFingerprint; + public byte[] EncryptionPublicKey => credentials?.PublicKeySerialized; + + void ServerRemoveFromPending(EncryptedConnection con) + { + for (int i = 0; i < serverPendingConnections.Count; i++) + if (serverPendingConnections[i] == con) + { + // remove by swapping with last + int lastIndex = serverPendingConnections.Count - 1; + serverPendingConnections[i] = serverPendingConnections[lastIndex]; + serverPendingConnections.RemoveAt(lastIndex); + break; + } + } + + void HandleInnerServerDisconnected(int connId) + { + if (serverConnections.TryGetValue(connId, out EncryptedConnection con)) + { + ServerRemoveFromPending(con); + serverConnections.Remove(connId); + } + OnServerDisconnected?.Invoke(connId); + } + + void HandleInnerServerError(int connId, TransportError type, string msg) => OnServerError?.Invoke(connId, type, $"inner: {msg}"); + + void HandleInnerServerDataReceived(int connId, ArraySegment data, int channel) + { + if (serverConnections.TryGetValue(connId, out EncryptedConnection c)) + c.OnReceiveRaw(data, channel); + } + + void HandleInnerServerConnected(int connId) => HandleInnerServerConnected(connId, Inner.ServerGetClientAddress(connId)); + + void HandleInnerServerConnected(int connId, string clientRemoteAddress) + { + Debug.Log($"[EncryptionTransport] New connection #{connId} from {clientRemoteAddress}"); + EncryptedConnection ec = null; + ec = new EncryptedConnection( + credentials, + false, + (segment, channel) => Inner.ServerSend(connId, segment, channel), + (segment, channel) => OnServerDataReceived?.Invoke(connId, segment, channel), + () => + { + Debug.Log($"[EncryptionTransport] Connection #{connId} is ready"); + // ReSharper disable once AccessToModifiedClosure + ServerRemoveFromPending(ec); + OnServerConnectedWithAddress?.Invoke(connId, clientRemoteAddress); + }, + (type, msg) => + { + OnServerError?.Invoke(connId, type, msg); + ServerDisconnect(connId); + }); + serverConnections.Add(connId, ec); + serverPendingConnections.Add(ec); + } + + void HandleInnerClientDisconnected() + { + client = null; + OnClientDisconnected?.Invoke(); + } + + void HandleInnerClientError(TransportError arg1, string arg2) => OnClientError?.Invoke(arg1, $"inner: {arg2}"); + + void HandleInnerClientDataReceived(ArraySegment data, int channel) => client?.OnReceiveRaw(data, channel); + + void HandleInnerClientConnected() => + client = new EncryptedConnection( + credentials, + true, + (segment, channel) => Inner.ClientSend(segment, channel), + (segment, channel) => OnClientDataReceived?.Invoke(segment, channel), + () => + { + OnClientConnected?.Invoke(); + }, + (type, msg) => + { + OnClientError?.Invoke(type, msg); + ClientDisconnect(); + }, + HandleClientValidateServerPubKey); + + bool HandleClientValidateServerPubKey(PubKeyInfo pubKeyInfo) + { + switch (ClientValidateServerPubKey) + { + case ValidationMode.Off: + return true; + case ValidationMode.List: + return Array.IndexOf(ClientTrustedPubKeySignatures, pubKeyInfo.Fingerprint) >= 0; + case ValidationMode.Callback: + return OnClientValidateServerPubKey(pubKeyInfo); + default: + throw new ArgumentOutOfRangeException(); + } + } + + void Awake() => + // check if encryption via hardware acceleration is supported. + // this can be useful to know for low end devices. + // + // hardware acceleration requires netcoreapp3.0 or later: + // https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/AesUtilities.cs#L18 + // because AesEngine_x86 requires System.Runtime.Intrinsics.X86: + // https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/engines/AesEngine_X86.cs + // which Unity does not support yet. + Debug.Log($"EncryptionTransport: IsHardwareAccelerated={AesUtilities.IsHardwareAccelerated}"); + + public override bool Available() => Inner.Available(); + + public override bool ClientConnected() => client != null && client.IsReady; + + public override void ClientConnect(string address) + { + switch (ClientValidateServerPubKey) + { + case ValidationMode.Off: + break; + case ValidationMode.List: + if (ClientTrustedPubKeySignatures == null || ClientTrustedPubKeySignatures.Length == 0) + { + OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to List, but the clientTrustedPubKeySignatures list is empty."); + return; + } + break; + case ValidationMode.Callback: + if (OnClientValidateServerPubKey == null) + { + OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to Callback, but the onClientValidateServerPubKey handler is not set"); + return; + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + credentials = EncryptionCredentials.Generate(); + Inner.OnClientConnected = HandleInnerClientConnected; + Inner.OnClientDataReceived = HandleInnerClientDataReceived; + Inner.OnClientDataSent = (bytes, channel) => OnClientDataSent?.Invoke(bytes, channel); + Inner.OnClientError = HandleInnerClientError; + Inner.OnClientDisconnected = HandleInnerClientDisconnected; + Inner.ClientConnect(address); + } + + public override void ClientSend(ArraySegment segment, int channelId = Channels.Reliable) => + client?.Send(segment, channelId); + + public override void ClientDisconnect() => Inner.ClientDisconnect(); + + public override Uri ServerUri() => Inner.ServerUri(); + + public override bool ServerActive() => Inner.ServerActive(); + + public override void ServerStart() + { + if (ServerLoadKeyPairFromFile) + credentials = EncryptionCredentials.LoadFromFile(ServerKeypairPath); + else + credentials = EncryptionCredentials.Generate(); +#pragma warning disable CS0618 // Type or member is obsolete + Inner.OnServerConnected = HandleInnerServerConnected; +#pragma warning restore CS0618 // Type or member is obsolete + Inner.OnServerConnectedWithAddress = HandleInnerServerConnected; + Inner.OnServerDataReceived = HandleInnerServerDataReceived; + Inner.OnServerDataSent = (connId, bytes, channel) => OnServerDataSent?.Invoke(connId, bytes, channel); + Inner.OnServerError = HandleInnerServerError; + Inner.OnServerDisconnected = HandleInnerServerDisconnected; + Inner.ServerStart(); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId = Channels.Reliable) + { + if (serverConnections.TryGetValue(connectionId, out EncryptedConnection connection) && connection.IsReady) + connection.Send(segment, channelId); + } + + public override void ServerDisconnect(int connectionId) => + // cleanup is done via inners disconnect event + Inner.ServerDisconnect(connectionId); + + public override string ServerGetClientAddress(int connectionId) => Inner.ServerGetClientAddress(connectionId); + + public override void ServerStop() => Inner.ServerStop(); + + public override int GetMaxPacketSize(int channelId = Channels.Reliable) => + Inner.GetMaxPacketSize(channelId) - EncryptedConnection.Overhead; + + public override int GetBatchThreshold(int channelId = Channels.Reliable) => Inner.GetBatchThreshold(channelId) - EncryptedConnection.Overhead; + + public override void Shutdown() => Inner.Shutdown(); + + public override void ClientEarlyUpdate() => Inner.ClientEarlyUpdate(); + + public override void ClientLateUpdate() + { + Inner.ClientLateUpdate(); + Profiler.BeginSample("EncryptionTransport.ServerLateUpdate"); + client?.TickNonReady(NetworkTime.localTime); + Profiler.EndSample(); + } + + public override void ServerEarlyUpdate() => Inner.ServerEarlyUpdate(); + + public override void ServerLateUpdate() + { + Inner.ServerLateUpdate(); + Profiler.BeginSample("EncryptionTransport.ServerLateUpdate"); + // Reverse iteration as entries can be removed while updating + for (int i = serverPendingConnections.Count - 1; i >= 0; i--) + serverPendingConnections[i].TickNonReady(NetworkTime.time); + Profiler.EndSample(); + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta new file mode 100644 index 0000000..3ad4787 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 0aa135acc32a4383ae9a5817f018cb06 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/EncryptionTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/Plugins.meta b/Assets/Mirror/Transports/Encryption/Plugins.meta new file mode 100644 index 0000000..bc110a7 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4048f5ff245dfa34abec0a401364e7c0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle.meta b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle.meta new file mode 100644 index 0000000..e064e80 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 31ff83bf6d2e72542adcbe2c21383f4a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md new file mode 100644 index 0000000..92e7901 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md @@ -0,0 +1,15 @@ +Version with renamed namespaces to avoid conflicts lives here: https://github.com/MirrorNetworking/bc-csharp + +Copyright (c) 2000-2024 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org). +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sub license, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this +permission notice shall be included in all copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.** diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md.meta b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md.meta new file mode 100644 index 0000000..d93eeac --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 2b45a99b5583cda419e1f1ec943fec4b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll new file mode 100644 index 0000000..5838952 Binary files /dev/null and b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll differ diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll.meta b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll.meta new file mode 100644 index 0000000..157989d --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll.meta @@ -0,0 +1,40 @@ +fileFormatVersion: 2 +guid: 03a89f29994a3b44cb3015b3c5ece010 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs new file mode 100644 index 0000000..9d650ec --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs @@ -0,0 +1,12 @@ +using System; +using Mirror.BouncyCastle.Crypto; + +namespace Mirror.Transports.Encryption +{ + public struct PubKeyInfo + { + public string Fingerprint; + public ArraySegment Serialized; + public AsymmetricKeyParameter Key; + } +} diff --git a/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta new file mode 100644 index 0000000..f2ffb1c --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: e1e3744418024c02acf39f44c1d1bd20 +timeCreated: 1708874062 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/PubKeyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs b/Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs new file mode 100644 index 0000000..e9b6eb9 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using kcp2k; +using Mirror.BouncyCastle.Crypto; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Serialization; +using Debug = UnityEngine.Debug; + +namespace Mirror.Transports.Encryption +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/encryption-transport")] + public class ThreadedEncryptionKcpTransport : ThreadedKcpTransport + { + public override bool IsEncrypted => true; + public override string EncryptionCipher => "AES256-GCM"; + public override string ToString() => $"Encrypted {base.ToString()}"; + + public enum ValidationMode + { + Off, + List, + Callback + } + + [HideInInspector] + public ValidationMode ClientValidateServerPubKey; + + [Tooltip("List of public key fingerprints the client will accept")] + [HideInInspector] + public string[] ClientTrustedPubKeySignatures; + /// + /// Called when a client connects to a server + /// ATTENTION: NOT THREAD SAFE. + /// This will be called on the worker thread. + /// + public Func OnClientValidateServerPubKey; + [HideInInspector] + [FormerlySerializedAs("serverLoadKeyPairFromFile")] + public bool ServerLoadKeyPairFromFile; + [HideInInspector] + [FormerlySerializedAs("serverKeypairPath")] + public string ServerKeypairPath = "./server-keys.json"; + + EncryptedConnection encryptedClient; + + readonly Dictionary serverConnections = new Dictionary(); + + readonly List serverPendingConnections = + new List(); + + EncryptionCredentials credentials; + public string EncryptionPublicKeyFingerprint => credentials?.PublicKeyFingerprint; + public byte[] EncryptionPublicKey => credentials?.PublicKeySerialized; + + // Used for threaded time keeping as unitys Time.time is not thread safe + Stopwatch stopwatch = Stopwatch.StartNew(); + + void ServerRemoveFromPending(EncryptedConnection con) + { + for (int i = 0; i < serverPendingConnections.Count; i++) + if (serverPendingConnections[i] == con) + { + // remove by swapping with last + int lastIndex = serverPendingConnections.Count - 1; + serverPendingConnections[i] = serverPendingConnections[lastIndex]; + serverPendingConnections.RemoveAt(lastIndex); + break; + } + } + + void HandleInnerServerDisconnected(int connId) + { + if (serverConnections.TryGetValue(connId, out EncryptedConnection con)) + { + ServerRemoveFromPending(con); + serverConnections.Remove(connId); + } + OnThreadedServerDisconnected(connId); + } + + void HandleInnerServerDataReceived(int connId, ArraySegment data, int channel) + { + if (serverConnections.TryGetValue(connId, out EncryptedConnection c)) + c.OnReceiveRaw(data, channel); + } + + + void HandleInnerServerConnected(int connId, IPEndPoint clientRemoteAddress) + { + Debug.Log($"[ThreadedEncryptionKcpTransport] New connection #{connId} from {clientRemoteAddress}"); + EncryptedConnection ec = null; + ec = new EncryptedConnection( + credentials, + false, + (segment, channel) => + { + server.Send(connId, segment, KcpTransport.ToKcpChannel(channel)); + OnThreadedServerSend(connId, segment,channel); + }, + (segment, channel) => OnThreadedServerReceive(connId, segment, channel), + () => + { + Debug.Log($"[ThreadedEncryptionKcpTransport] Connection #{connId} is ready"); + // ReSharper disable once AccessToModifiedClosure + ServerRemoveFromPending(ec); + OnThreadedServerConnected(connId, clientRemoteAddress); + }, + (type, msg) => + { + OnThreadedServerError(connId, type, msg); + ServerDisconnect(connId); + }); + serverConnections.Add(connId, ec); + serverPendingConnections.Add(ec); + } + + void HandleInnerClientDisconnected() + { + encryptedClient = null; + OnThreadedClientDisconnected(); + } + + void HandleInnerClientDataReceived(ArraySegment data, int channel) => encryptedClient?.OnReceiveRaw(data, channel); + + void HandleInnerClientConnected() => + encryptedClient = new EncryptedConnection( + credentials, + true, + (segment, channel) => + { + client.Send(segment, KcpTransport.ToKcpChannel(channel)); + OnThreadedClientSend(segment, channel); + }, + (segment, channel) => OnThreadedClientReceive(segment, channel), + () => + { + OnThreadedClientConnected(); + }, + (type, msg) => + { + OnThreadedClientError(type, msg); + ClientDisconnect(); + }, + HandleClientValidateServerPubKey); + + bool HandleClientValidateServerPubKey(PubKeyInfo pubKeyInfo) + { + switch (ClientValidateServerPubKey) + { + case ValidationMode.Off: + return true; + case ValidationMode.List: + return Array.IndexOf(ClientTrustedPubKeySignatures, pubKeyInfo.Fingerprint) >= 0; + case ValidationMode.Callback: + return OnClientValidateServerPubKey(pubKeyInfo); + default: + throw new ArgumentOutOfRangeException(); + } + } + + protected override void Awake() + { + base.Awake(); + // client (NonAlloc version is not necessary anymore) + client = new KcpClient( + HandleInnerClientConnected, + (message, channel) => HandleInnerClientDataReceived(message, KcpTransport.FromKcpChannel(channel)), + HandleInnerClientDisconnected, + (error, reason) => OnThreadedClientError(KcpTransport.ToTransportError(error), reason), + config + ); + + // server + server = new KcpServer( + HandleInnerServerConnected, + (connectionId, message, channel) => HandleInnerServerDataReceived(connectionId, message, KcpTransport.FromKcpChannel(channel)), + HandleInnerServerDisconnected, + (connectionId, error, reason) => OnThreadedServerError(connectionId, KcpTransport.ToTransportError(error), reason), + config + ); + // check if encryption via hardware acceleration is supported. + // this can be useful to know for low end devices. + // + // hardware acceleration requires netcoreapp3.0 or later: + // https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/AesUtilities.cs#L18 + // because AesEngine_x86 requires System.Runtime.Intrinsics.X86: + // https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/engines/AesEngine_X86.cs + // which Unity does not support yet. + Debug.Log($"ThreadedEncryptionKcpTransport: IsHardwareAccelerated={AesUtilities.IsHardwareAccelerated}"); + } + + protected override void ThreadedClientConnect(string address) + { + if (!SetupEncryptionForClient()) + return; + base.ThreadedClientConnect(address); + } + + bool SetupEncryptionForClient() + { + + switch (ClientValidateServerPubKey) + { + case ValidationMode.Off: + break; + case ValidationMode.List: + if (ClientTrustedPubKeySignatures == null || ClientTrustedPubKeySignatures.Length == 0) + { + OnThreadedClientError(TransportError.Unexpected, "Validate Server Public Key is set to List, but the clientTrustedPubKeySignatures list is empty."); + return false; + } + break; + case ValidationMode.Callback: + if (OnClientValidateServerPubKey == null) + { + OnThreadedClientError(TransportError.Unexpected, "Validate Server Public Key is set to Callback, but the onClientValidateServerPubKey handler is not set"); + return false; + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + credentials = EncryptionCredentials.Generate(); + return true; + } + + protected override void ThreadedClientConnect(Uri address) + { + if (!SetupEncryptionForClient()) + return; + base.ThreadedClientConnect(address); + } + + protected override void ThreadedClientSend(ArraySegment segment, int channelId) + { + encryptedClient?.Send(segment, channelId); + } + + protected override void ThreadedServerStart() + { + if (ServerLoadKeyPairFromFile) + credentials = EncryptionCredentials.LoadFromFile(ServerKeypairPath); + else + credentials = EncryptionCredentials.Generate(); + base.ThreadedServerStart(); + } + + protected override void ThreadedServerSend(int connectionId, ArraySegment segment, int channelId) + { + if (serverConnections.TryGetValue(connectionId, out EncryptedConnection connection) && connection.IsReady) + connection.Send(segment, channelId); + } + + + public override int GetMaxPacketSize(int channelId = Channels.Reliable) => base.GetMaxPacketSize(channelId) - EncryptedConnection.Overhead; + public override int GetBatchThreshold(int channelId) => base.GetBatchThreshold(channelId) - EncryptedConnection.Overhead; + + protected override void ThreadedClientLateUpdate() + { + base.ThreadedClientLateUpdate(); + Profiler.BeginSample("ThreadedEncryptionKcpTransport.ServerLateUpdate"); + encryptedClient?.TickNonReady(stopwatch.Elapsed.TotalSeconds); + Profiler.EndSample(); + } + + + + protected override void ThreadedServerLateUpdate() + { + base.ThreadedServerLateUpdate(); + Profiler.BeginSample("ThreadedEncryptionKcpTransport.ServerLateUpdate"); + // Reverse iteration as entries can be removed while updating + for (int i = serverPendingConnections.Count - 1; i >= 0; i--) + serverPendingConnections[i].TickNonReady(stopwatch.Elapsed.TotalSeconds); + Profiler.EndSample(); + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs.meta b/Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs.meta new file mode 100644 index 0000000..4f207ab --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 5d3e310924fb49c195391b9699f20809 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP.meta b/Assets/Mirror/Transports/KCP.meta similarity index 77% rename from Assets/Mirror/Runtime/Transports/KCP.meta rename to Assets/Mirror/Transports/KCP.meta index ba9d190..1727b05 100644 --- a/Assets/Mirror/Runtime/Transports/KCP.meta +++ b/Assets/Mirror/Transports/KCP.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 953bb5ec5ab2346a092f58061e01ba65 +guid: ea4ea5d03df6a49449fa679ac2390773 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Transports/KCP/KcpTransport.cs b/Assets/Mirror/Transports/KCP/KcpTransport.cs new file mode 100644 index 0000000..b72e148 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/KcpTransport.cs @@ -0,0 +1,365 @@ +//#if MIRROR <- commented out because MIRROR isn't defined on first import yet +using System; +using System.Linq; +using System.Net; +using Mirror; +using UnityEngine; +using UnityEngine.Serialization; + +namespace kcp2k +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")] + [DisallowMultipleComponent] + public class KcpTransport : Transport, PortTransport + { + // scheme used by this transport + public const string Scheme = "kcp"; + + // common + [Header("Transport Configuration")] + [FormerlySerializedAs("Port")] + public ushort port = 7777; + public ushort Port { get => port; set => port=value; } + [Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")] + public bool DualMode = true; + [Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")] + public bool NoDelay = true; + [Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")] + public uint Interval = 10; + [Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")] + public int Timeout = 10000; + [Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int RecvBufferSize = 1024 * 1027 * 7; + [Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int SendBufferSize = 1024 * 1027 * 7; + + [Header("Advanced")] + [Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")] + public int FastResend = 2; + [Tooltip("KCP congestion window. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")] + /*public*/ bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use. + [Tooltip("KCP window size can be modified to support higher loads. This also increases max message size.")] + public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP window size can be modified to support higher loads.")] + public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")] + public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x. + [Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")] + [FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")] + public bool MaximizeSocketBuffers = true; + + [Header("Allowed Max Message Sizes\nBased on Receive Window Size")] + [Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")] + [ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate + [Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")] + [ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate + + // config is created from the serialized properties above. + // we can expose the config directly in the future. + // for now, let's not break people's old settings. + protected KcpConfig config; + + // use default MTU for this transport. + const int MTU = Kcp.MTU_DEF; + + // server & client + protected KcpServer server; + protected KcpClient client; + + // debugging + [Header("Debug")] + public bool debugLog; + // show statistics in OnGUI + public bool statisticsGUI; + // log statistics for headless servers that can't show them in GUI + public bool statisticsLog; + + // translate Kcp <-> Mirror channels + public static int FromKcpChannel(KcpChannel channel) => + channel == KcpChannel.Reliable ? Channels.Reliable : Channels.Unreliable; + + public static KcpChannel ToKcpChannel(int channel) => + channel == Channels.Reliable ? KcpChannel.Reliable : KcpChannel.Unreliable; + + public static TransportError ToTransportError(ErrorCode error) + { + switch(error) + { + case ErrorCode.DnsResolve: return TransportError.DnsResolve; + case ErrorCode.Timeout: return TransportError.Timeout; + case ErrorCode.Congestion: return TransportError.Congestion; + case ErrorCode.InvalidReceive: return TransportError.InvalidReceive; + case ErrorCode.InvalidSend: return TransportError.InvalidSend; + case ErrorCode.ConnectionClosed: return TransportError.ConnectionClosed; + case ErrorCode.Unexpected: return TransportError.Unexpected; + default: throw new InvalidCastException($"KCP: missing error translation for {error}"); + } + } + + protected virtual void Awake() + { + // logging + // Log.Info should use Debug.Log if enabled, or nothing otherwise + // (don't want to spam the console on headless servers) + if (debugLog) + Log.Info = Debug.Log; + else + Log.Info = _ => {}; + Log.Warning = Debug.LogWarning; + Log.Error = Debug.LogError; + + // create config from serialized settings + config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit); + + // client (NonAlloc version is not necessary anymore) + client = new KcpClient( + () => OnClientConnected.Invoke(), + (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)), + () => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708 + (error, reason) => OnClientError?.Invoke(ToTransportError(error), reason), // may be null during shutdown: https://github.com/MirrorNetworking/Mirror/issues/3876 + config + ); + + // server + server = new KcpServer( + (connectionId, endPoint) => OnServerConnectedWithAddress.Invoke(connectionId, endPoint.PrettyAddress()), + (connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)), + (connectionId) => OnServerDisconnected.Invoke(connectionId), + (connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason), + config + ); + + if (statisticsLog) + InvokeRepeating(nameof(OnLogStatistics), 1, 1); + + Log.Info("KcpTransport initialized!"); + } + + protected virtual void OnValidate() + { + // show max message sizes in inspector for convenience. + // 'config' isn't available in edit mode yet, so use MTU define. + ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize); + UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU); + } + + // all except WebGL + // Do not change this back to using Application.platform + // because that doesn't work in the Editor! + public override bool Available() => +#if UNITY_WEBGL + false; +#else + true; +#endif + + // client + public override bool ClientConnected() => client.connected; + public override void ClientConnect(string address) + { + client.Connect(address, Port); + } + public override void ClientConnect(Uri uri) + { + if (uri.Scheme != Scheme) + throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri)); + + int serverPort = uri.IsDefaultPort ? Port : uri.Port; + client.Connect(uri.Host, (ushort)serverPort); + } + public override void ClientSend(ArraySegment 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.PrettyAddress(); + } + public override void ServerStop() => server.Stop(); + public override void ServerEarlyUpdate() + { + // only process messages while transport is enabled. + // scene change messsages disable it to stop processing. + // (see also: https://github.com/vis2k/Mirror/pull/379) + if (enabled) server.TickIncoming(); + } + // process outgoing in late update + public override void ServerLateUpdate() => server.TickOutgoing(); + + // common + public override void Shutdown() {} + + // max message size + public override int GetMaxPacketSize(int channelId = Channels.Reliable) + { + // switch to kcp channel. + // unreliable or reliable. + // default to reliable just to be sure. + switch (channelId) + { + case Channels.Unreliable: + return KcpPeer.UnreliableMaxMessageSize(config.Mtu); + default: + return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize); + } + } + + // kcp reliable channel max packet size is MTU * WND_RCV + // this allows 144kb messages. but due to head of line blocking, all + // other messages would have to wait until the maxed size one is + // delivered. batching 144kb messages each time would be EXTREMELY slow + // and fill the send queue nearly immediately when using it over the + // network. + // => instead we always use MTU sized batches. + // => people can still send maxed size if needed. + public override int GetBatchThreshold(int channelId) => + KcpPeer.UnreliableMaxMessageSize(config.Mtu); + + // server statistics + // LONG to avoid int overflows with connections.Sum. + // see also: https://github.com/vis2k/Mirror/pull/2777 + public long GetAverageMaxSendRate() => + server.connections.Count > 0 + ? server.connections.Values.Sum(conn => conn.MaxSendRate) / server.connections.Count + : 0; + public long GetAverageMaxReceiveRate() => + server.connections.Count > 0 + ? server.connections.Values.Sum(conn => conn.MaxReceiveRate) / server.connections.Count + : 0; + long GetTotalSendQueue() => + server.connections.Values.Sum(conn => conn.SendQueueCount); + long GetTotalReceiveQueue() => + server.connections.Values.Sum(conn => conn.ReceiveQueueCount); + long GetTotalSendBuffer() => + server.connections.Values.Sum(conn => conn.SendBufferCount); + long GetTotalReceiveBuffer() => + server.connections.Values.Sum(conn => conn.ReceiveBufferCount); + + // PrettyBytes function from DOTSNET + // pretty prints bytes as KB/MB/GB/etc. + // long to support > 2GB + // divides by floats to return "2.5MB" etc. + public static string PrettyBytes(long bytes) + { + // bytes + if (bytes < 1024) + return $"{bytes} B"; + // kilobytes + else if (bytes < 1024L * 1024L) + return $"{(bytes / 1024f):F2} KB"; + // megabytes + else if (bytes < 1024 * 1024L * 1024L) + return $"{(bytes / (1024f * 1024f)):F2} MB"; + // gigabytes + return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB"; + } + + protected virtual void OnGUIStatistics() + { + GUILayout.BeginArea(new Rect(5, 110, 300, 300)); + + if (ServerActive()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("SERVER"); + GUILayout.Label($" connections: {server.connections.Count}"); + GUILayout.Label($" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s"); + GUILayout.Label($" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s"); + GUILayout.Label($" SendQueue: {GetTotalSendQueue()}"); + GUILayout.Label($" ReceiveQueue: {GetTotalReceiveQueue()}"); + GUILayout.Label($" SendBuffer: {GetTotalSendBuffer()}"); + GUILayout.Label($" ReceiveBuffer: {GetTotalReceiveBuffer()}"); + GUILayout.EndVertical(); + } + + if (ClientConnected()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("CLIENT"); + GUILayout.Label($" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s"); + GUILayout.Label($" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s"); + GUILayout.Label($" SendQueue: {client.SendQueueCount}"); + GUILayout.Label($" ReceiveQueue: {client.ReceiveQueueCount}"); + GUILayout.Label($" SendBuffer: {client.SendBufferCount}"); + GUILayout.Label($" ReceiveBuffer: {client.ReceiveBufferCount}"); + GUILayout.EndVertical(); + } + + GUILayout.EndArea(); + } + +// OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + protected virtual void OnGUI() + { + if (statisticsGUI) OnGUIStatistics(); + } +#endif + + protected virtual void OnLogStatistics() + { + if (ServerActive()) + { + string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n"; + log += $" connections: {server.connections.Count}\n"; + log += $" MaxSendRate (avg): {PrettyBytes(GetAverageMaxSendRate())}/s\n"; + log += $" MaxRecvRate (avg): {PrettyBytes(GetAverageMaxReceiveRate())}/s\n"; + log += $" SendQueue: {GetTotalSendQueue()}\n"; + log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n"; + log += $" SendBuffer: {GetTotalSendBuffer()}\n"; + log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n"; + Log.Info(log); + } + + if (ClientConnected()) + { + string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n"; + log += $" MaxSendRate: {PrettyBytes(client.MaxSendRate)}/s\n"; + log += $" MaxRecvRate: {PrettyBytes(client.MaxReceiveRate)}/s\n"; + log += $" SendQueue: {client.SendQueueCount}\n"; + log += $" ReceiveQueue: {client.ReceiveQueueCount}\n"; + log += $" SendBuffer: {client.SendBufferCount}\n"; + log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n"; + Log.Info(log); + } + } + + public override string ToString() => $"KCP [{port}]"; + } +} +//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Transports/KCP/KcpTransport.cs.meta b/Assets/Mirror/Transports/KCP/KcpTransport.cs.meta new file mode 100644 index 0000000..6861e8c --- /dev/null +++ b/Assets/Mirror/Transports/KCP/KcpTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6b0fecffa3f624585964b0d0eb21b18e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/KcpTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs b/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs new file mode 100644 index 0000000..dedf2e8 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs @@ -0,0 +1,327 @@ +// Threaded version of our KCP transport. +// Elevates a few milliseconds of transport computations into a worker thread. +// +//#if MIRROR <- commented out because MIRROR isn't defined on first import yet +using System; +using System.Net; +using Mirror; +using UnityEngine; +using UnityEngine.Serialization; + +namespace kcp2k +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")] + [DisallowMultipleComponent] + public class ThreadedKcpTransport : ThreadedTransport, PortTransport + { + // scheme used by this transport + public const string Scheme = "kcp"; + + // common + [Header("Transport Configuration")] + [FormerlySerializedAs("Port")] + public ushort port = 7777; + public ushort Port { get => port; set => port=value; } + [Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")] + public bool DualMode = true; + [Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")] + public bool NoDelay = true; + [Tooltip("KCP internal update interval. 100ms is KCP default, but a lower interval is recommended to minimize latency and to scale to more networked entities.")] + public uint Interval = 10; + [Tooltip("KCP timeout in milliseconds. Note that KCP sends a ping automatically.")] + public int Timeout = 10000; + [Tooltip("Socket receive buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int RecvBufferSize = 1024 * 1027 * 7; + [Tooltip("Socket send buffer size. Large buffer helps support more connections. Increase operating system socket buffer size limits if needed.")] + public int SendBufferSize = 1024 * 1027 * 7; + + [Header("Advanced")] + [Tooltip("KCP fastresend parameter. Faster resend for the cost of higher bandwidth. 0 in normal mode, 2 in turbo mode.")] + public int FastResend = 2; + [Tooltip("KCP congestion window. Restricts window size to reduce congestion. Results in only 2-3 MTU messages per Flush even on loopback. Best to keept his disabled.")] + /*public*/ bool CongestionWindow = false; // KCP 'NoCongestionWindow' is false by default. here we negate it for ease of use. + [Tooltip("KCP window size can be modified to support higher loads. This also increases max message size.")] + public uint ReceiveWindowSize = 4096; //Kcp.WND_RCV; 128 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP window size can be modified to support higher loads.")] + public uint SendWindowSize = 4096; //Kcp.WND_SND; 32 by default. Mirror sends a lot, so we need a lot more. + [Tooltip("KCP will try to retransmit lost messages up to MaxRetransmit (aka dead_link) before disconnecting.")] + public uint MaxRetransmit = Kcp.DEADLINK * 2; // default prematurely disconnects a lot of people (#3022). use 2x. + [Tooltip("Enable to automatically set client & server send/recv buffers to OS limit. Avoids issues with too small buffers under heavy load, potentially dropping connections. Increase the OS limit if this is still too small.")] + [FormerlySerializedAs("MaximizeSendReceiveBuffersToOSLimit")] + public bool MaximizeSocketBuffers = true; + + [Header("Allowed Max Message Sizes\nBased on Receive Window Size")] + [Tooltip("KCP reliable max message size shown for convenience. Can be changed via ReceiveWindowSize.")] + [ReadOnly] public int ReliableMaxMessageSize = 0; // readonly, displayed from OnValidate + [Tooltip("KCP unreliable channel max message size for convenience. Not changeable.")] + [ReadOnly] public int UnreliableMaxMessageSize = 0; // readonly, displayed from OnValidate + + // config is created from the serialized properties above. + // we can expose the config directly in the future. + // for now, let's not break people's old settings. + protected KcpConfig config; + + // use default MTU for this transport. + const int MTU = Kcp.MTU_DEF; + + // server & client + protected KcpServer server; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD! + protected KcpClient client; // USED IN WORKER THREAD. DON'T TOUCH FROM MAIN THREAD! + + // copy MonoBehaviour.enabled for thread safe access + volatile bool enabledCopy = true; + + // debugging + [Header("Debug")] + public bool debugLog; + // show statistics in OnGUI + public bool statisticsGUI; + // log statistics for headless servers that can't show them in GUI + public bool statisticsLog; + + protected override void Awake() + { + // logging + // Log.Info should use Debug.Log if enabled, or nothing otherwise + // (don't want to spam the console on headless servers) + // THREAD SAFE thanks to ThreadLog.cs + if (debugLog) + Log.Info = Debug.Log; + else + Log.Info = _ => {}; + Log.Warning = Debug.LogWarning; + Log.Error = Debug.LogError; + + // create config from serialized settings + config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MTU, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit); + + // client (NonAlloc version is not necessary anymore) + client = new KcpClient( + OnThreadedClientConnected, + (message, channel) => OnThreadedClientReceive(message, KcpTransport.FromKcpChannel(channel)), + OnThreadedClientDisconnected, + (error, reason) => OnThreadedClientError(KcpTransport.ToTransportError(error), reason), + config + ); + + // server + server = new KcpServer( + OnThreadedServerConnected, + (connectionId, message, channel) => OnThreadedServerReceive(connectionId, message, KcpTransport.FromKcpChannel(channel)), + OnThreadedServerDisconnected, + (connectionId, error, reason) => OnThreadedServerError(connectionId, KcpTransport.ToTransportError(error), reason), + config + ); + + if (statisticsLog) + InvokeRepeating(nameof(OnLogStatistics), 1, 1); + + // call base after creating kcp. + // it'll be used by the created thread immediately. + base.Awake(); + + Log.Info("ThreadedKcpTransport initialized!"); + } + + protected virtual void OnValidate() + { + // show max message sizes in inspector for convenience. + // 'config' isn't available in edit mode yet, so use MTU define. + ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MTU, ReceiveWindowSize); + UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MTU); + } + + // copy MonoBehaviour.enabled for thread safe use + void OnEnable() => enabledCopy = true; + void OnDisable() => enabledCopy = true; + + // all except WebGL + // Do not change this back to using Application.platform + // because that doesn't work in the Editor! + public override bool Available() => +#if UNITY_WEBGL + false; +#else + true; +#endif + + protected override void ThreadedClientConnect(string address) => client.Connect(address, Port); + protected override void ThreadedClientConnect(Uri uri) + { + if (uri.Scheme != Scheme) + throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri)); + + int serverPort = uri.IsDefaultPort ? Port : uri.Port; + client.Connect(uri.Host, (ushort)serverPort); + } + protected override void ThreadedClientSend(ArraySegment segment, int channelId) + { + client.Send(segment, KcpTransport.ToKcpChannel(channelId)); + + // thread safe version for statistics + OnThreadedClientSend(segment, channelId); + } + protected override void ThreadedClientDisconnect() => client.Disconnect(); + // process incoming in early update + protected override void ThreadedClientEarlyUpdate() + { + // only process messages while transport is enabled. + // scene change messsages disable it to stop processing. + // (see also: https://github.com/vis2k/Mirror/pull/379) + // => enabledCopy for thread safe use + if (enabledCopy) client.TickIncoming(); + } + // process outgoing in late update + protected override void ThreadedClientLateUpdate() => client.TickOutgoing(); + + // server thread overrides + public override Uri ServerUri() + { + UriBuilder builder = new UriBuilder(); + builder.Scheme = Scheme; + builder.Host = Dns.GetHostName(); + builder.Port = Port; + return builder.Uri; + } + protected override void ThreadedServerStart() => server.Start(Port); + protected override void ThreadedServerSend(int connectionId, ArraySegment segment, int channelId) + { + server.Send(connectionId, segment, KcpTransport.ToKcpChannel(channelId)); + + // thread safe version for statistics + OnThreadedServerSend(connectionId, segment, channelId); + } + protected override void ThreadedServerDisconnect(int connectionId) => server.Disconnect(connectionId); + /* NOT THREAD SAFE. ThreadedTransport version throws NotImplementedException for this. + public override string ServerGetClientAddress(int connectionId) + { + IPEndPoint endPoint = server.GetClientEndPoint(connectionId); + return endPoint != null + // Map to IPv4 if "IsIPv4MappedToIPv6" + // "::ffff:127.0.0.1" -> "127.0.0.1" + ? (endPoint.Address.IsIPv4MappedToIPv6 + ? endPoint.Address.MapToIPv4().ToString() + : endPoint.Address.ToString()) + : ""; + } + */ + protected override void ThreadedServerStop() => server.Stop(); + protected override void ThreadedServerEarlyUpdate() + { + // only process messages while transport is enabled. + // scene change messsages disable it to stop processing. + // (see also: https://github.com/vis2k/Mirror/pull/379) + // => enabledCopy for thread safe use + if (enabledCopy) server.TickIncoming(); + } + // process outgoing in late update + protected override void ThreadedServerLateUpdate() => server.TickOutgoing(); + + protected override void ThreadedShutdown() {} + + // max message size + public override int GetMaxPacketSize(int channelId = Channels.Reliable) + { + // switch to kcp channel. + // unreliable or reliable. + // default to reliable just to be sure. + switch (channelId) + { + case Channels.Unreliable: + return KcpPeer.UnreliableMaxMessageSize(config.Mtu); + default: + return KcpPeer.ReliableMaxMessageSize(config.Mtu, ReceiveWindowSize); + } + } + + // kcp reliable channel max packet size is MTU * WND_RCV + // this allows 144kb messages. but due to head of line blocking, all + // other messages would have to wait until the maxed size one is + // delivered. batching 144kb messages each time would be EXTREMELY slow + // and fill the send queue nearly immediately when using it over the + // network. + // => instead we always use MTU sized batches. + // => people can still send maxed size if needed. + public override int GetBatchThreshold(int channelId) => + KcpPeer.UnreliableMaxMessageSize(config.Mtu); + + protected virtual void OnGUIStatistics() + { + // TODO not thread safe + /* + GUILayout.BeginArea(new Rect(5, 110, 300, 300)); + + if (ServerActive()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("SERVER"); + GUILayout.Label($" connections: {server.connections.Count}"); + GUILayout.Label($" MaxSendRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s"); + GUILayout.Label($" MaxRecvRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxReceiveRate())}/s"); + GUILayout.Label($" SendQueue: {GetTotalSendQueue()}"); + GUILayout.Label($" ReceiveQueue: {GetTotalReceiveQueue()}"); + GUILayout.Label($" SendBuffer: {GetTotalSendBuffer()}"); + GUILayout.Label($" ReceiveBuffer: {GetTotalReceiveBuffer()}"); + GUILayout.EndVertical(); + } + + if (ClientConnected()) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("CLIENT"); + GUILayout.Label($" MaxSendRate: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s"); + GUILayout.Label($" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s"); + GUILayout.Label($" SendQueue: {client.peer.SendQueueCount}"); + GUILayout.Label($" ReceiveQueue: {client.peer.ReceiveQueueCount}"); + GUILayout.Label($" SendBuffer: {client.peer.SendBufferCount}"); + GUILayout.Label($" ReceiveBuffer: {client.peer.ReceiveBufferCount}"); + GUILayout.EndVertical(); + } + + GUILayout.EndArea(); + */ + } + +// OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + protected virtual void OnGUI() + { + if (statisticsGUI) OnGUIStatistics(); + } +#endif + + protected virtual void OnLogStatistics() + { + // TODO not thread safe + /* + if (ServerActive()) + { + string log = "kcp SERVER @ time: " + NetworkTime.localTime + "\n"; + log += $" connections: {server.connections.Count}\n"; + log += $" MaxSendRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxSendRate())}/s\n"; + log += $" MaxRecvRate (avg): {KcpTransport.PrettyBytes(GetAverageMaxReceiveRate())}/s\n"; + log += $" SendQueue: {GetTotalSendQueue()}\n"; + log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n"; + log += $" SendBuffer: {GetTotalSendBuffer()}\n"; + log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n"; + Log.Info(log); + } + + if (ClientConnected()) + { + string log = "kcp CLIENT @ time: " + NetworkTime.localTime + "\n"; + log += $" MaxSendRate: {KcpTransport.PrettyBytes(client.peer.MaxSendRate)}/s\n"; + log += $" MaxRecvRate: {KcpTransport.PrettyBytes(client.peer.MaxReceiveRate)}/s\n"; + log += $" SendQueue: {client.peer.SendQueueCount}\n"; + log += $" ReceiveQueue: {client.peer.ReceiveQueueCount}\n"; + log += $" SendBuffer: {client.peer.SendBufferCount}\n"; + log += $" ReceiveBuffer: {client.peer.ReceiveBufferCount}\n\n"; + Log.Info(log); + } + */ + } + + public override string ToString() => $"ThreadedKCP {port}"; + } +} +//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta b/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta new file mode 100644 index 0000000..996e096 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: f7e416e0486524f0d9580be7e13388f4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k.meta b/Assets/Mirror/Transports/KCP/kcp2k.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k.meta rename to Assets/Mirror/Transports/KCP/kcp2k.meta diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef b/Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef similarity index 81% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef rename to Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef index 9a90c82..66d2148 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef +++ b/Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef @@ -1,7 +1,8 @@ { "name": "kcp2k", + "rootNamespace": "", "references": [ - "GUID:63c380d6dae6946209ed0832388a657c" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta b/Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta new file mode 100644 index 0000000..284a98d --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 6806a62c384838046a3c66c44f06d75f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE b/Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt similarity index 100% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/LICENSE rename to Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt diff --git a/Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt.meta b/Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt.meta new file mode 100644 index 0000000..f049836 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 9a3e8369060cf4e94ac117603de47aa6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt b/Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt new file mode 100644 index 0000000..43aafcf --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt @@ -0,0 +1,261 @@ +V1.41 [2024-04-28] +- fix: KcpHeader is now parsed safely, handling attackers potentially sending values out of enum range +- fix: KcpClient RawSend may throw ConnectionRefused SocketException when OnDisconnected calls SendDisconnect(), which is fine +- fix: less scary cookie message and better explanation + +V1.40 [2024-01-03] +- added [KCP] to all log messages +- fix: #3704 remove old fix for #2353 which caused log spam and isn't needed anymore since the + original Mirror issue is long gone +- fix: KcpClient.RawSend now returns if socket wasn't created yet +- fix: https://github.com/MirrorNetworking/Mirror/issues/3591 KcpPeer.SendDisconnect now rapid + fires several unreliable messages instead of sending reliable. Fixes disconnect message not + going through if the connection is closed & removed immediately after. + +V1.39 [2023-10-31] +- fix: https://github.com/MirrorNetworking/Mirror/issues/3611 Windows UDP socket exceptions + on server if one of the clients died + +V1.38 [2023-10-29] +- fix: #54 mismatching cookie race condition. cookie is now included in all messages. +- feature: Exposed local end point on KcpClient/Server +- refactor: KcpPeer refactored as abstract class to remove KcpServer initialization workarounds + +V1.37 [2023-07-31] +- fix: #47 KcpServer.Stop now clears connections so they aren't carried over to the next session +- fix: KcpPeer doesn't log 'received unreliable message while not authenticated' anymore. + +V1.36 [2023-06-08] +- fix: #49 KcpPeer.RawInput message size check now considers cookie as well +- kcp.cs cleanups + +V1.35 [2023-04-05] +- fix: KcpClients now need to validate with a secure cookie in order to protect against + UDP spoofing. fixes: + https://github.com/MirrorNetworking/Mirror/issues/3286 + [disclosed by IncludeSec] +- KcpClient/Server: change callbacks to protected so inheriting classes can use them too +- KcpClient/Server: change config visibility to protected + +V1.34 [2023-03-15] +- Send/SendTo/Receive/ReceiveFrom NonBlocking extensions. + to encapsulate WouldBlock allocations, exceptions, etc. + allows for reuse when overwriting KcpServer/Client (i.e. for relays). + +V1.33 [2023-03-14] +- perf: KcpServer/Client RawReceive now call socket.Poll to avoid non-blocking + socket's allocating a new SocketException in case they WouldBlock. + fixes https://github.com/MirrorNetworking/Mirror/issues/3413 +- perf: KcpServer/Client RawSend now call socket.Poll to avoid non-blocking + socket's allocating a new SocketException in case they WouldBlock. + fixes https://github.com/MirrorNetworking/Mirror/issues/3413 + +V1.32 [2023-03-12] +- fix: KcpPeer RawInput now doesn't disconnect in case of random internet noise + +V1.31 [2023-03-05] +- KcpClient: Tick/Incoming/Outgoing can now be overwritten (virtual) +- breaking: KcpClient now takes KcpConfig in constructor instead of in Connect. + cleaner, and prepares for KcpConfig.MTU setting. +- KcpConfig now includes MTU; KcpPeer now works with KcpConfig's MTU, KcpServer/Client + buffers are now created with config's MTU. + +V1.30 [2023-02-20] +- fix: set send/recv buffer sizes directly instead of iterating to find the limit. + fixes: https://github.com/MirrorNetworking/Mirror/issues/3390 +- fix: server & client sockets are now always non-blocking to ensure main thread never + blocks on socket.recv/send. Send() now also handles WouldBlock. +- fix: socket.Receive/From directly with non-blocking sockets and handle WouldBlock, + instead of socket.Poll. faster, more obvious, and fixes Poll() looping forever while + socket is in error state. fixes: https://github.com/MirrorNetworking/Mirror/issues/2733 + +V1.29 [2023-01-28] +- fix: KcpServer.CreateServerSocket now handles NotSupportedException when setting DualMode + https://github.com/MirrorNetworking/Mirror/issues/3358 + +V1.28 [2023-01-28] +- fix: KcpClient.Connect now resolves hostname before creating peer + https://github.com/MirrorNetworking/Mirror/issues/3361 + +V1.27 [2023-01-08] +- KcpClient.Connect: invoke own events directly instead of going through peer, + which calls our own events anyway +- fix: KcpPeer/Client/Server callbacks are readonly and assigned in constructor + to ensure they are safe to use at all times. + fixes https://github.com/MirrorNetworking/Mirror/issues/3337 + +V1.26 [2022-12-22] +- KcpPeer.RawInput: fix compile error in old Unity Mono versions +- fix: KcpServer sets up a new connection's OnError immediately. + fixes KcpPeer throwing NullReferenceException when attempting to call OnError + after authentication errors. +- improved log messages + +V1.25 [2022-12-14] +- breaking: removed where-allocation. use IL2CPP on servers instead. +- breaking: KcpConfig to simplify configuration +- high level cleanups + +V1.24 [2022-12-14] +- KcpClient: fixed NullReferenceException when connection without a server. + added test coverage to ensure this never happens again. + +V1.23 [2022-12-07] +- KcpClient: rawReceiveBuffer exposed +- fix: KcpServer RawSend uses connection.remoteEndPoint instead of the helper + 'newClientEP'. fixes clients receiving the wrong messages meant for others. + https://github.com/MirrorNetworking/Mirror/issues/3296 + +V1.22 [2022-11-30] +- high level refactor, part two. + +V1.21 [2022-11-24] +- high level refactor, part one. + - KcpPeer instead of KcpConnection, KcpClientConnection, KcpServerConnection + - RawSend/Receive can now easily be overwritten in KcpClient/Server. + for non-alloc, relays, etc. + +V1.20 [2022-11-22] +- perf: KcpClient receive allocation was removed entirely. + reduces Mirror benchmark client sided allocations from 4.9 KB / 1.7 KB (non-alloc) to 0B. +- fix: KcpConnection.Disconnect does not check socket.Connected anymore. + UDP sockets don't have a connection. + fixes Disconnects not being sent to clients in netcore. +- KcpConnection.SendReliable: added OnError instead of logs + +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/Transports/KCP/kcp2k/VERSION.txt.meta b/Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt.meta new file mode 100644 index 0000000..af9ae34 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: ed3f2cf1bbf1b4d53a6f2c103d311f71 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/empty.meta b/Assets/Mirror/Transports/KCP/kcp2k/empty.meta new file mode 100644 index 0000000..5f131fc --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/empty.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d9ce2267cb8a4a1c9632025287e8da88 +timeCreated: 1669162433 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs b/Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs new file mode 100644 index 0000000..4623b53 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs @@ -0,0 +1 @@ +// removed 2022-12-13 \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs.meta new file mode 100644 index 0000000..e16d2ee --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 54b8398dcd544c8a93bcad846214cc40 +timeCreated: 1626432191 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel.meta rename to Assets/Mirror/Transports/KCP/kcp2k/highlevel.meta diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs new file mode 100644 index 0000000..e96246a --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs @@ -0,0 +1,75 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; + +namespace kcp2k +{ + public static class Common + { + // 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($"[KCP] Failed to resolve host: {hostname} reason: {exception}"); + addresses = null; + return false; + } + } + + // if connections drop under heavy load, increase to OS limit. + // if still not enough, increase the OS limit. + public static void ConfigureSocketBuffers(Socket socket, int recvBufferSize, int sendBufferSize) + { + // log initial size for comparison. + // remember initial size for log comparison + int initialReceive = socket.ReceiveBufferSize; + int initialSend = socket.SendBufferSize; + + // set to configured size + try + { + socket.ReceiveBufferSize = recvBufferSize; + socket.SendBufferSize = sendBufferSize; + } + catch (SocketException) + { + Log.Warning($"[KCP] failed to set Socket RecvBufSize = {recvBufferSize} SendBufSize = {sendBufferSize}"); + } + + + Log.Info($"[KCP] RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x)"); + } + + // generate a connection hash from IP+Port. + // + // 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. + public static int ConnectionHash(EndPoint endPoint) => + endPoint.GetHashCode(); + + // cookies need to be generated with a secure random generator. + // we don't want them to be deterministic / predictable. + // RNG is cached to avoid runtime allocations. + static readonly RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider(); + static readonly byte[] cryptoRandomBuffer = new byte[4]; + public static uint GenerateCookie() + { + cryptoRandom.GetBytes(cryptoRandomBuffer); + return BitConverter.ToUInt32(cryptoRandomBuffer, 0); + } + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs.meta new file mode 100644 index 0000000..67cc25f --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9ced451c2954435f88cf718bcba020cb +timeCreated: 1669135138 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs rename to Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta new file mode 100644 index 0000000..0a9c6ec --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 3abbeffc1d794f11a45b7fcf110353f5 +timeCreated: 1652320712 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs new file mode 100644 index 0000000..20725a9 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs @@ -0,0 +1,166 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace kcp2k +{ + public static class Extensions + { + // ArraySegment as HexString for convenience + public static string ToHexString(this ArraySegment segment) => + BitConverter.ToString(segment.Array, segment.Offset, segment.Count); + + // non-blocking UDP send. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool SendToNonBlocking(this Socket socket, ArraySegment data, EndPoint remoteEP) + { + try + { + // when using non-blocking sockets, SendTo may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectWrite)) return false; + + // send to the the endpoint. + // do not send to 'newClientEP', as that's always reused. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3296 + socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, remoteEP); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, SendTo may throw WouldBlock. + // in that case, simply drop the message. it's UDP, it's fine. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + + // non-blocking UDP send. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool SendNonBlocking(this Socket socket, ArraySegment data) + { + try + { + // when using non-blocking sockets, SendTo may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectWrite)) return false; + + // SendTo allocates. we used bound Send. + socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, SendTo may throw WouldBlock. + // in that case, simply drop the message. it's UDP, it's fine. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + + // non-blocking UDP receive. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool ReceiveFromNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment data, ref EndPoint remoteEP) + { + data = default; + + try + { + // when using non-blocking sockets, ReceiveFrom may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectRead)) return false; + + // 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 + // + // throws SocketException if datagram was larger than buffer. + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0 + int size = socket.ReceiveFrom(recvBuffer, 0, recvBuffer.Length, SocketFlags.None, ref remoteEP); + data = new ArraySegment(recvBuffer, 0, size); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, Receive throws WouldBlock if there is + // no message to read. that's okay. only log for other errors. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + + // non-blocking UDP receive. + // allows for reuse when overwriting KcpServer/Client (i.e. for relays). + // => wrapped with Poll to avoid WouldBlock allocating new SocketException. + // => wrapped with try-catch to ignore WouldBlock exception. + // make sure to set socket.Blocking = false before using this! + public static bool ReceiveNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment data) + { + data = default; + + try + { + // when using non-blocking sockets, ReceiveFrom may return WouldBlock. + // in C#, WouldBlock throws a SocketException, which is expected. + // unfortunately, creating the SocketException allocates in C#. + // let's poll first to avoid the WouldBlock allocation. + // note that this entirely to avoid allocations. + // non-blocking UDP doesn't need Poll in other languages. + // and the code still works without the Poll call. + if (!socket.Poll(0, SelectMode.SelectRead)) return false; + + // ReceiveFrom allocates. we used bound Receive. + // returns amount of bytes written into buffer. + // throws SocketException if datagram was larger than buffer. + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0 + // + // throws SocketException if datagram was larger than buffer. + // https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0 + int size = socket.Receive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None); + data = new ArraySegment(recvBuffer, 0, size); + return true; + } + catch (SocketException e) + { + // for non-blocking sockets, Receive throws WouldBlock if there is + // no message to read. that's okay. only log for other errors. + if (e.SocketErrorCode == SocketError.WouldBlock) return false; + + // otherwise it's a real socket error. throw it. + throw; + } + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta new file mode 100644 index 0000000..2b0422f --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: c0649195e5ba4fcf8e0e1231fee7d5f6 +timeCreated: 1641701011 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs similarity index 78% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs rename to Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs index ccb19ba..c085e17 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs @@ -4,7 +4,7 @@ namespace kcp2k public enum KcpChannel : byte { // don't react on 0x00. might help to filter out random noise. - Reliable = 0x01, - Unreliable = 0x02 + Reliable = 1, + Unreliable = 2 } } \ No newline at end of file diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta new file mode 100644 index 0000000..7d574e6 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9e852b2532fb248d19715cfebe371db3 +timeCreated: 1610081248 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs new file mode 100644 index 0000000..827cff0 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs @@ -0,0 +1,292 @@ +// kcp client logic abstracted into a class. +// for use in Mirror, DOTSNET, testing, etc. +using System; +using System.Net; +using System.Net.Sockets; + +namespace kcp2k +{ + public class KcpClient : KcpPeer + { + // IO + protected Socket socket; + public EndPoint remoteEndPoint; + + // expose local endpoint for users / relays / nat traversal etc. + public EndPoint LocalEndPoint => socket?.LocalEndPoint; + + // config + protected readonly KcpConfig config; + + // 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! + // => protected because someone may overwrite RawReceive but still wants + // to reuse the buffer. + protected readonly byte[] rawReceiveBuffer; + + // callbacks + // even for errors, to allow liraries to show popups etc. + // instead of logging directly. + // (string instead of Exception for ease of use and to avoid user panic) + // + // events are readonly, set in constructor. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected readonly Action OnConnectedCallback; + protected readonly Action, KcpChannel> OnDataCallback; + protected readonly Action OnDisconnectedCallback; + protected readonly Action OnErrorCallback; + + // state + bool active = false; // active between when connect() and disconnect() are called + public bool connected; + + public KcpClient(Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + : base(config, 0) // client has no cookie yet + { + // initialize callbacks first to ensure they can be used safely. + OnConnectedCallback = OnConnected; + OnDataCallback = OnData; + OnDisconnectedCallback = OnDisconnected; + OnErrorCallback = OnError; + this.config = config; + + // create mtu sized receive buffer + rawReceiveBuffer = new byte[config.Mtu]; + } + + // callbacks /////////////////////////////////////////////////////////// + // some callbacks need to wrapped with some extra logic + protected override void OnAuthenticated() + { + Log.Info($"[KCP] Client: OnConnected"); + connected = true; + OnConnectedCallback(); + } + + protected override void OnData(ArraySegment message, KcpChannel channel) => + OnDataCallback(message, channel); + + protected override void OnError(ErrorCode error, string message) => + OnErrorCallback(error, message); + + protected override void OnDisconnected() + { + Log.Info($"[KCP] Client: OnDisconnected"); + connected = false; + socket?.Close(); + socket = null; + remoteEndPoint = null; + OnDisconnectedCallback(); + active = false; + } + + //////////////////////////////////////////////////////////////////////// + public void Connect(string address, ushort port) + { + if (connected) + { + Log.Warning("[KCP] Client: already connected!"); + return; + } + + // resolve host name before creating peer. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3361 + if (!Common.ResolveHostname(address, out IPAddress[] addresses)) + { + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}"); + OnDisconnectedCallback(); + return; + } + + // create fresh peer for each new session + // client doesn't need secure cookie. + Reset(config); + + Log.Info($"[KCP] Client: connect to {address}:{port}"); + + // create socket + remoteEndPoint = new IPEndPoint(addresses[0], port); + socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + active = true; + + // recv & send are called from main thread. + // need to ensure this never blocks. + // even a 1ms block per connection would stop us from scaling. + socket.Blocking = false; + + // configure buffer sizes + Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize); + + // bind to endpoint so we can use send/recv instead of sendto/recvfrom. + socket.Connect(remoteEndPoint); + + // immediately send a hello message to the server. + // server will call OnMessage and add the new connection. + // note that this still has cookie=0 until we receive the server's hello. + SendHello(); + } + + // io - input. + // virtual so it may be modified for relays, etc. + // call this while it returns true, to process all messages this tick. + // returned ArraySegment is valid until next call to RawReceive. + protected virtual bool RawReceive(out ArraySegment segment) + { + segment = default; + if (socket == null) return false; + + try + { + return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment); + } + // for non-blocking sockets, Receive throws WouldBlock if there is + // no message to read. that's okay. only log for other errors. + catch (SocketException e) + { + // 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. + // for example, his can happen when connecting without a server. + // see test: ConnectWithoutServer(). + Log.Info($"[KCP] Client.RawReceive: looks like the other end has closed the connection. This is fine: {e}"); + base.Disconnect(); + return false; + } + } + + // io - output. + // virtual so it may be modified for relays, etc. + protected override void RawSend(ArraySegment data) + { + // only if socket was connected / created yet. + // users may call send functions without having connected, causing NRE. + if (socket == null) return; + + try + { + socket.SendNonBlocking(data); + } + catch (SocketException e) + { + // SendDisconnect() sometimes gets a SocketException with + // 'Connection Refused' if the other end already closed. + // this is not an 'error', it's expected to happen. + // but connections should never just end silently. + // at least log a message for easier debugging. + Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}"); + // base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws + + } + } + + public void Send(ArraySegment segment, KcpChannel channel) + { + if (!connected) + { + Log.Warning("[KCP] Client: can't send because not connected!"); + return; + } + + SendData(segment, channel); + } + + // insert raw IO. usually from socket.Receive. + // offset is useful for relays, where we may parse a header and then + // feed the rest to kcp. + public void RawInput(ArraySegment segment) + { + // ensure valid size: at least 1 byte for channel + 4 bytes for cookie + if (segment.Count <= 5) return; + + // parse channel + // byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions + byte channel = segment.Array[segment.Offset + 0]; + + // server messages always contain the security cookie. + // parse it, assign if not assigned, warn if suddenly different. + Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie); + if (messageCookie == 0) + { + Log.Error($"[KCP] Client: received message with cookie=0, this should never happen. Server should always include the security cookie."); + } + + if (cookie == 0) + { + cookie = messageCookie; + Log.Info($"[KCP] Client: received initial cookie: {cookie}"); + } + else if (cookie != messageCookie) + { + Log.Warning($"[KCP] Client: dropping message with mismatching cookie: {messageCookie} expected: {cookie}."); + return; + } + + // parse message + ArraySegment message = new ArraySegment(segment.Array, segment.Offset + 1+4, segment.Count - 1-4); + + switch (channel) + { + case (byte)KcpChannel.Reliable: + { + OnRawInputReliable(message); + break; + } + case (byte)KcpChannel.Unreliable: + { + OnRawInputUnreliable(message); + break; + } + default: + { + // invalid channel indicates random internet noise. + // servers may receive random UDP data. + // just ignore it, but log for easier debugging. + Log.Warning($"[KCP] Client: invalid channel header: {channel}, likely internet noise"); + break; + } + } + } + + // process incoming messages. should be called before updating the world. + // virtual because relay may need to inject their own ping or similar. + public override 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) + if (active) + { + while (RawReceive(out ArraySegment segment)) + RawInput(segment); + } + + // RawReceive may have disconnected peer. active check again. + if (active) base.TickIncoming(); + } + + // process outgoing messages. should be called after updating the world. + // virtual because relay may need to inject their own ping or similar. + public override void TickOutgoing() + { + // process outgoing while active + if (active) base.TickOutgoing(); + } + + // process incoming and outgoing for convenience + // => ideally call ProcessIncoming() before updating the world and + // ProcessOutgoing() after updating the world for minimum latency + public virtual void Tick() + { + TickIncoming(); + TickOutgoing(); + } + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta new file mode 100644 index 0000000..d3d255c --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 6aa069a28ed24fedb533c102d9742b36 +timeCreated: 1603786960 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs new file mode 100644 index 0000000..382d06b --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs @@ -0,0 +1,97 @@ +// common config struct, instead of passing 10 parameters manually every time. +using System; + +namespace kcp2k +{ + // [Serializable] to show it in Unity inspector. + // 'class' so we can set defaults easily. + [Serializable] + public class KcpConfig + { + // socket configuration //////////////////////////////////////////////// + // DualMode uses both IPv6 and IPv4. not all platforms support it. + // (Nintendo Switch, etc.) + public bool DualMode; + + // UDP servers use only one socket. + // maximize buffer to handle as many connections as possible. + // + // M1 mac pro: + // recv buffer default: 786896 (771 KB) + // send buffer default: 9216 (9 KB) + // max configurable: ~7 MB + public int RecvBufferSize; + public int SendBufferSize; + + // kcp configuration /////////////////////////////////////////////////// + // configurable MTU in case kcp sits on top of other abstractions like + // encrypted transports, relays, etc. + public int Mtu; + + // 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 congestion window heavily limits messages flushed per update. + // congestion window may actually be broken in kcp: + // - sending max sized message @ M1 mac flushes 2-3 messages per update + // - even with super large send/recv window, it requires thousands of + // update calls + // best to leave this disabled, as it may significantly increase latency. + 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; + + // constructor ///////////////////////////////////////////////////////// + // constructor with defaults for convenience. + // makes it easy to define "new KcpConfig(DualMode=false)" etc. + public KcpConfig( + bool DualMode = true, + int RecvBufferSize = 1024 * 1024 * 7, + int SendBufferSize = 1024 * 1024 * 7, + int Mtu = Kcp.MTU_DEF, + bool NoDelay = true, + uint Interval = 10, + int FastResend = 0, + bool CongestionWindow = false, + uint SendWindowSize = Kcp.WND_SND, + uint ReceiveWindowSize = Kcp.WND_RCV, + int Timeout = KcpPeer.DEFAULT_TIMEOUT, + uint MaxRetransmits = Kcp.DEADLINK) + { + this.DualMode = DualMode; + this.RecvBufferSize = RecvBufferSize; + this.SendBufferSize = SendBufferSize; + this.Mtu = Mtu; + this.NoDelay = NoDelay; + this.Interval = Interval; + this.FastResend = FastResend; + this.CongestionWindow = CongestionWindow; + this.SendWindowSize = SendWindowSize; + this.ReceiveWindowSize = ReceiveWindowSize; + this.Timeout = Timeout; + this.MaxRetransmits = MaxRetransmits; + } + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs.meta new file mode 100644 index 0000000..444a7fe --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 99692f99c45c4b47b0500e7abbfd12da +timeCreated: 1670946969 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs new file mode 100644 index 0000000..13d198e --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs @@ -0,0 +1,57 @@ +using System; + +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 KcpHeaderReliable : byte + { + // don't react on 0x00. might help to filter out random noise. + Hello = 1, + // ping goes over reliable & KcpHeader for now. could go over unreliable + // 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 = 2, + Data = 3, + } + + public enum KcpHeaderUnreliable : byte + { + // users may send unreliable messages + Data = 4, + // disconnect always goes through rapid fire unreliable (glenn fielder) + Disconnect = 5, + } + + // save convert the enums from/to byte. + // attackers may attempt to send invalid values, so '255' may not convert. + public static class KcpHeader + { + public static bool ParseReliable(byte value, out KcpHeaderReliable header) + { + if (Enum.IsDefined(typeof(KcpHeaderReliable), value)) + { + header = (KcpHeaderReliable)value; + return true; + } + + header = KcpHeaderReliable.Ping; // any default + return false; + } + + public static bool ParseUnreliable(byte value, out KcpHeaderUnreliable header) + { + if (Enum.IsDefined(typeof(KcpHeaderUnreliable), value)) + { + header = (KcpHeaderUnreliable)value; + return true; + } + + header = KcpHeaderUnreliable.Disconnect; // any default + return false; + } + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta new file mode 100644 index 0000000..f7178c0 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 91b5edac31224a49bd76f960ae018942 +timeCreated: 1610081248 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs new file mode 100644 index 0000000..cbf74ef --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs @@ -0,0 +1,791 @@ +// Kcp Peer, similar to UDP Peer but wrapped with reliability, channels, +// timeouts, authentication, state, etc. +// +// still IO agnostic to work with udp, nonalloc, relays, native, etc. +using System; +using System.Diagnostics; +using System.Net.Sockets; + +namespace kcp2k +{ + public abstract class KcpPeer + { + // kcp reliability algorithm + internal Kcp kcp; + + // security cookie to prevent UDP spoofing. + // credits to IncludeSec for disclosing the issue. + // + // server passes the expected cookie to the client's KcpPeer. + // KcpPeer sends cookie to the connected client. + // KcpPeer only accepts packets which contain the cookie. + // => cookie can be a random number, but it needs to be cryptographically + // secure random that can't be easily predicted. + // => cookie can be hash(ip, port) BUT only if salted to be not predictable + internal uint cookie; + + // state: connected as soon as we create the peer. + // leftover from KcpConnection. remove it after refactoring later. + protected KcpState state = KcpState.Connected; + + // If we don't receive anything these many milliseconds + // then consider us disconnected + public const int DEFAULT_TIMEOUT = 10000; + public int timeout; + uint lastReceiveTime; + + // internal time. + // StopWatch offers ElapsedMilliSeconds and should be more precise than + // Unity's time.deltaTime over long periods. + readonly Stopwatch watch = new Stopwatch(); + + // 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 + readonly 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 + readonly byte[] kcpSendBuffer;// = new byte[1 + ReliableMaxMessageSize]; + + // raw send buffer is exactly MTU. + readonly byte[] rawSendBuffer; + + // 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; + + // we need to subtract the channel and cookie bytes from every + // MaxMessageSize calculation. + // we also need to tell kcp to use MTU-1 to leave space for the byte. + public const int CHANNEL_HEADER_SIZE = 1; + public const int COOKIE_HEADER_SIZE = 4; + public const int METADATA_SIZE = CHANNEL_HEADER_SIZE + COOKIE_HEADER_SIZE; + + // 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(int mtu, uint rcv_wnd) => + (mtu - Kcp.OVERHEAD - METADATA_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(int mtu, uint rcv_wnd) => + ReliableMaxMessageSize_Unconstrained(mtu, Math.Min(rcv_wnd, Kcp.FRG_MAX)); + + // unreliable max message size is simply MTU - channel header - kcp header + public static int UnreliableMaxMessageSize(int mtu) => + mtu - METADATA_SIZE - 1; + + // 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; + + // calculate max message sizes based on mtu and wnd only once + public readonly int unreliableMax; + public readonly int reliableMax; + + // 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 KcpPeer(KcpConfig config, uint cookie) + { + // initialize variable state in extra function so we can reuse it + // when reconnecting to reset state + Reset(config); + + // set the cookie after resetting state so it's not overwritten again. + // with log message for debugging in case of cookie issues. + this.cookie = cookie; + Log.Info($"[KCP] {GetType()}: created with cookie={cookie}"); + + // create mtu sized send buffer + rawSendBuffer = new byte[config.Mtu]; + + // calculate max message sizes once + unreliableMax = UnreliableMaxMessageSize(config.Mtu); + reliableMax = ReliableMaxMessageSize(config.Mtu, config.ReceiveWindowSize); + + // create message buffers AFTER window size is set + // see comments on buffer definition for the "+1" part + kcpMessageBuffer = new byte[1 + reliableMax]; + kcpSendBuffer = new byte[1 + reliableMax]; + } + + // Reset all state once. + // useful for KcpClient to reconned with a fresh kcp state. + protected void Reset(KcpConfig config) + { + // reset state + cookie = 0; + state = KcpState.Connected; + lastReceiveTime = 0; + lastPingTime = 0; + watch.Restart(); // start at 0 each time + + // 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(config.NoDelay ? 1u : 0u, config.Interval, config.FastResend, !config.CongestionWindow); + kcp.SetWindowSize(config.SendWindowSize, config.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((uint)config.Mtu - METADATA_SIZE); + + // set maximum retransmits (aka dead_link) + kcp.dead_link = config.MaxRetransmits; + timeout = config.Timeout; + } + + // callbacks /////////////////////////////////////////////////////////// + // events are abstract, guaranteed to be implemented. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected abstract void OnAuthenticated(); + protected abstract void OnData(ArraySegment message, KcpChannel channel); + protected abstract void 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) + protected abstract void OnError(ErrorCode error, string message); + protected abstract void RawSend(ArraySegment data); + + //////////////////////////////////////////////////////////////////////// + + 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. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Timeout, $"{GetType()}: 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. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Timeout, $"{GetType()}: 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. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Congestion, + $"{GetType()}: 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 KcpHeaderReliable header, out ArraySegment message) + { + message = default; + header = KcpHeaderReliable.Ping; + + int msgSize = kcp.PeekSize(); + if (msgSize <= 0) return false; + + // only allow receiving up to buffer sized messages. + // otherwise we would get BlockCopy ArgumentException anyway. + if (msgSize > kcpMessageBuffer.Length) + { + // we don't allow sending messages > Max, so this must be an + // attacker. let's disconnect to avoid allocation attacks etc. + // pass error to user callback. no need to log it manually. + OnError(ErrorCode.InvalidReceive, $"{GetType()}: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection."); + Disconnect(); + return false; + } + + // receive from kcp + int received = kcp.Receive(kcpMessageBuffer, msgSize); + if (received < 0) + { + // if receive failed, close everything + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed with error={received}. closing connection."); + Disconnect(); + return false; + } + + // safely extract header. attackers may send values out of enum range. + byte headerByte = kcpMessageBuffer[0]; + if (!KcpHeader.ParseReliable(headerByte, out header)) + { + OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderReliable)}."); + Disconnect(); + return false; + } + + // extract content without header + message = new ArraySegment(kcpMessageBuffer, 1, msgSize - 1); + lastReceiveTime = (uint)watch.ElapsedMilliseconds; + return true; + } + + void TickIncoming_Connected(uint time) + { + // detect common events & ping + HandleTimeout(time); + HandleDeadLink(); + HandlePing(time); + HandleChoked(); + + // any reliable kcp message received? + if (ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment message)) + { + // message type FSM. no default so we never miss a case. + switch (header) + { + case KcpHeaderReliable.Hello: + { + // we were waiting for a Hello message. + // it proves that the other end speaks our protocol. + + // log with previously parsed cookie + Log.Info($"[KCP] {GetType()}: received hello with cookie={cookie}"); + state = KcpState.Authenticated; + OnAuthenticated(); + break; + } + case KcpHeaderReliable.Ping: + { + // ping keeps kcp from timing out. do nothing. + break; + } + case KcpHeaderReliable.Data: + { + // everything else is not allowed during handshake! + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidReceive, $"[KCP] {GetType()}: 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 KcpHeaderReliable header, out ArraySegment message)) + { + // message type FSM. no default so we never miss a case. + switch (header) + { + case KcpHeaderReliable.Hello: + { + // should never receive another hello after auth + // GetType() shows Server/ClientConn instead of just Connection. + Log.Warning($"{GetType()}: received invalid header {header} while Authenticated. Disconnecting the connection."); + Disconnect(); + break; + } + case KcpHeaderReliable.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(message, KcpChannel.Reliable); + } + // empty data = attacker, or something went wrong + else + { + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidReceive, $"{GetType()}: received empty Data message while Authenticated. Disconnecting the connection."); + Disconnect(); + } + break; + } + case KcpHeaderReliable.Ping: + { + // ping keeps kcp from timing out. do nothing. + break; + } + } + } + } + + public virtual void TickIncoming() + { + uint time = (uint)watch.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; + } + } + } + // TODO KcpConnection is IO agnostic. move this to outside later. + catch (SocketException exception) + { + // this is ok, the connection was closed + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: 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. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (Exception exception) + { + // unexpected + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected Exception: {exception}"); + Disconnect(); + } + } + + public virtual void TickOutgoing() + { + uint time = (uint)watch.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; + } + } + } + // TODO KcpConnection is IO agnostic. move this to outside later. + catch (SocketException exception) + { + // this is ok, the connection was closed + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: 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. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine."); + Disconnect(); + } + catch (Exception exception) + { + // unexpected + // pass error to user callback. no need to log it manually. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected exception: {exception}"); + Disconnect(); + } + } + + protected void OnRawInputReliable(ArraySegment message) + { + // input into kcp, but skip channel byte + int input = kcp.Input(message.Array, message.Offset, message.Count); + if (input != 0) + { + // GetType() shows Server/ClientConn instead of just Connection. + Log.Warning($"[KCP] {GetType()}: Input failed with error={input} for buffer with length={message.Count - 1}"); + } + } + + protected void OnRawInputUnreliable(ArraySegment message) + { + // need at least one byte for the KcpHeader enum + if (message.Count < 1) return; + + // safely extract header. attackers may send values out of enum range. + byte headerByte = message.Array[message.Offset + 0]; + if (!KcpHeader.ParseUnreliable(headerByte, out KcpHeaderUnreliable header)) + { + OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderUnreliable)}."); + Disconnect(); + return; + } + + // subtract header from message content + // (above we already ensure it's at least 1 byte long) + message = new ArraySegment(message.Array, message.Offset + 1, message.Count - 1); + + switch (header) + { + case KcpHeaderUnreliable.Data: + { + // 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) + { + OnData(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)watch.ElapsedMilliseconds; + } + else + { + // it's common to receive unreliable messages before being + // authenticated, for example: + // - random internet noise + // - game server may send an unreliable message after authenticating, + // and the unreliable message arrives on the client before the + // 'auth_ok' message. this can be avoided by sending a final + // 'ready' message after being authenticated, but this would + // add another 'round trip time' of latency to the handshake. + // + // it's best to simply ignore invalid unreliable messages here. + // Log.Info($"{GetType()}: received unreliable message while not authenticated."); + } + break; + } + case KcpHeaderUnreliable.Disconnect: + { + // GetType() shows Server/ClientConn instead of just Connection. + Log.Info($"[KCP] {GetType()}: received disconnect message"); + Disconnect(); + break; + } + } + } + + // raw send called by kcp + void RawSendReliable(byte[] data, int length) + { + // write channel header + // from 0, with 1 byte + rawSendBuffer[0] = (byte)KcpChannel.Reliable; + + // write handshake cookie to protect against UDP spoofing. + // from 1, with 4 bytes + Utils.Encode32U(rawSendBuffer, 1, cookie); // allocation free + + // write data + // from 5, with N bytes + Buffer.BlockCopy(data, 0, rawSendBuffer, 1+4, length); + + // IO send + ArraySegment segment = new ArraySegment(rawSendBuffer, 0, length + 1+4); + RawSend(segment); + } + + void SendReliable(KcpHeaderReliable header, ArraySegment content) + { + // 1 byte header + content needs to fit into send buffer + if (1 + content.Count > kcpSendBuffer.Length) // TODO + { + // otherwise content is larger than MaxMessageSize. let user know! + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidSend, $"{GetType()}: Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={reliableMax}"); + return; + } + + // write channel header + kcpSendBuffer[0] = (byte)header; + + // write data (if any) + 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) + { + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidSend, $"{GetType()}: Send failed with error={sent} for content with length={content.Count}"); + } + } + + void SendUnreliable(KcpHeaderUnreliable header, ArraySegment content) + { + // message size needs to be <= unreliable max size + if (content.Count > unreliableMax) + { + // otherwise content is larger than MaxMessageSize. let user know! + // GetType() shows Server/ClientConn instead of just Connection. + Log.Error($"[KCP] {GetType()}: Failed to send unreliable message of size {content.Count} because it's larger than UnreliableMaxMessageSize={unreliableMax}"); + return; + } + + // write channel header + // from 0, with 1 byte + rawSendBuffer[0] = (byte)KcpChannel.Unreliable; + + // write handshake cookie to protect against UDP spoofing. + // from 1, with 4 bytes + Utils.Encode32U(rawSendBuffer, 1, cookie); // allocation free + + // write kcp header + rawSendBuffer[5] = (byte)header; + + // write data (if any) + // from 6, with N bytes + if (content.Count > 0) + Buffer.BlockCopy(content.Array, content.Offset, rawSendBuffer, 1 + 4 + 1, content.Count); + + // IO send + ArraySegment segment = new ArraySegment(rawSendBuffer, 0, content.Count + 1 + 4 + 1); + RawSend(segment); + } + + // 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 SendHello() + { + // send an empty message with 'Hello' header. + // cookie is automatically included in all messages. + + // GetType() shows Server/ClientConn instead of just Connection. + Log.Info($"[KCP] {GetType()}: sending handshake to other end with cookie={cookie}"); + SendReliable(KcpHeaderReliable.Hello, 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. + // GetType() shows Server/ClientConn instead of just Connection. + OnError(ErrorCode.InvalidSend, $"{GetType()}: tried sending empty message. This should never happen. Disconnecting."); + Disconnect(); + return; + } + + switch (channel) + { + case KcpChannel.Reliable: + SendReliable(KcpHeaderReliable.Data, data); + break; + case KcpChannel.Unreliable: + SendUnreliable(KcpHeaderUnreliable.Data, data); + break; + } + } + + // ping goes through kcp to keep it from timing out, so it goes over the + // reliable channel. + void SendPing() => SendReliable(KcpHeaderReliable.Ping, default); + + // send disconnect message + void SendDisconnect() + { + // sending over reliable to ensure delivery seems like a good idea: + // but if we close the connection immediately, it often doesn't get + // fully delivered: https://github.com/MirrorNetworking/Mirror/issues/3591 + // SendReliable(KcpHeader.Disconnect, default); + // + // instead, rapid fire a few unreliable messages. + // they are sent immediately even if we close the connection after. + // this way we don't need to keep the connection alive for a while. + // (glenn fiedler method) + for (int i = 0; i < 5; ++i) + SendUnreliable(KcpHeaderUnreliable.Disconnect, default); + } + + // disconnect this connection + public virtual void Disconnect() + { + // only if not disconnected yet + if (state == KcpState.Disconnected) + return; + + // send a disconnect message + try + { + SendDisconnect(); + } + // TODO KcpConnection is IO agnostic. move this to outside later. + 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 + // GetType() shows Server/ClientConn instead of just Connection. + Log.Info($"[KCP] {GetType()}: Disconnected."); + state = KcpState.Disconnected; + OnDisconnected(); + } + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs.meta new file mode 100644 index 0000000..d35440d --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 3915c7c62b72d4dc2a9e4e76c94fc484 +timeCreated: 1602600432 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs new file mode 100644 index 0000000..25648cf --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs @@ -0,0 +1,412 @@ +// 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; +using System.Runtime.InteropServices; + +namespace kcp2k +{ + public class KcpServer + { + // callbacks + // even for errors, to allow liraries to show popups etc. + // instead of logging directly. + // (string instead of Exception for ease of use and to avoid user panic) + // + // events are readonly, set in constructor. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected readonly Action OnConnected; // connectionId, address + protected readonly Action, KcpChannel> OnData; + protected readonly Action OnDisconnected; + protected readonly Action OnError; + + // configuration + protected readonly KcpConfig config; + + // state + protected Socket socket; + EndPoint newClientEP; + + // expose local endpoint for users / relays / nat traversal etc. + public EndPoint LocalEndPoint => socket?.LocalEndPoint; + + // 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! + protected readonly byte[] rawReceiveBuffer; + + // connections where connectionId is EndPoint.GetHashCode + public Dictionary connections = + new Dictionary(); + + public KcpServer(Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + { + // initialize callbacks first to ensure they can be used safely. + this.OnConnected = OnConnected; + this.OnData = OnData; + this.OnDisconnected = OnDisconnected; + this.OnError = OnError; + this.config = config; + + // create mtu sized receive buffer + rawReceiveBuffer = new byte[config.Mtu]; + + // create newClientEP either IPv4 or IPv6 + newClientEP = config.DualMode + ? new IPEndPoint(IPAddress.IPv6Any, 0) + : new IPEndPoint(IPAddress.Any, 0); + } + + public virtual bool IsActive() => socket != null; + + static Socket CreateServerSocket(bool DualMode, ushort port) + { + if (DualMode) + { + // IPv6 socket with DualMode @ "::" : port + Socket socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + + // enabling DualMode may throw: + // https://learn.microsoft.com/en-us/dotnet/api/System.Net.Sockets.Socket.DualMode?view=net-7.0 + // attempt it, otherwise log but continue + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3358 + try + { + socket.DualMode = true; + } + catch (NotSupportedException e) + { + Log.Warning($"[KCP] Failed to set Dual Mode, continuing with IPv6 without Dual Mode. Error: {e}"); + } + + // for windows sockets, there's a rare issue where when using + // a server socket with multiple clients, if one of the clients + // is closed, the single server socket throws exceptions when + // sending/receiving. even if the socket is made for N clients. + // + // this actually happened to one of our users: + // https://github.com/MirrorNetworking/Mirror/issues/3611 + // + // here's the in-depth explanation & solution: + // + // "As you may be aware, if a host receives a packet for a UDP + // port that is not currently bound, it may send back an ICMP + // "Port Unreachable" message. Whether or not it does this is + // dependent on the firewall, private/public settings, etc. + // On localhost, however, it will pretty much always send this + // packet back. + // + // Now, on Windows (and only on Windows), by default, a received + // ICMP Port Unreachable message will close the UDP socket that + // sent it; hence, the next time you try to receive on the + // socket, it will throw an exception because the socket has + // been closed by the OS. + // + // Obviously, this causes a headache in the multi-client, + // single-server socket set-up you have here, but luckily there + // is a fix: + // + // You need to utilise the not-often-required SIO_UDP_CONNRESET + // Winsock control code, which turns off this built-in behaviour + // of automatically closing the socket. + // + // Note that this ioctl code is only supported on Windows + // (XP and later), not on Linux, since it is provided by the + // Winsock extensions. Of course, since the described behavior + // is only the default behavior on Windows, this omission is not + // a major loss. If you are attempting to create a + // cross-platform library, you should cordon this off as + // Windows-specific code." + // https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + const uint IOC_IN = 0x80000000U; + const uint IOC_VENDOR = 0x18000000U; + const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12)); + socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null); + } + + socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); + return socket; + } + else + { + // IPv4 socket @ "0.0.0.0" : port + Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.Bind(new IPEndPoint(IPAddress.Any, port)); + return socket; + } + } + + public virtual void Start(ushort port) + { + // only start once + if (socket != null) + { + Log.Warning("[KCP] Server: already started!"); + return; + } + + // listen + socket = CreateServerSocket(config.DualMode, port); + + // recv & send are called from main thread. + // need to ensure this never blocks. + // even a 1ms block per connection would stop us from scaling. + socket.Blocking = false; + + // configure buffer sizes + Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize); + } + + 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.remoteEndPoint as IPEndPoint; + } + return null; + } + + // io - input. + // virtual so it may be modified for relays, nonalloc workaround, etc. + // https://github.com/vis2k/where-allocation + // bool return because not all receives may be valid. + // for example, relay may expect a certain header. + protected virtual bool RawReceiveFrom(out ArraySegment segment, out int connectionId) + { + segment = default; + connectionId = 0; + if (socket == null) return false; + + try + { + if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP)) + { + // set connectionId to hash from endpoint + connectionId = Common.ConnectionHash(newClientEP); + return true; + } + } + catch (SocketException e) + { + // NOTE: SocketException is not a subclass of IOException. + // 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] Server: ReceiveFrom failed: {e}"); + } + + return false; + } + + // io - out. + // virtual so it may be modified for relays, nonalloc workaround, etc. + // relays may need to prefix connId (and remoteEndPoint would be same for all) + protected virtual void RawSend(int connectionId, ArraySegment data) + { + // get the connection's endpoint + if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) + { + Log.Warning($"[KCP] Server: RawSend invalid connectionId={connectionId}"); + return; + } + + try + { + socket.SendToNonBlocking(data, connection.remoteEndPoint); + } + catch (SocketException e) + { + Log.Error($"[KCP] Server: SendTo failed: {e}"); + } + } + + protected virtual KcpServerConnection CreateConnection(int connectionId) + { + // generate a random cookie for this connection to avoid UDP spoofing. + // needs to be random, but without allocations to avoid GC. + uint cookie = Common.GenerateCookie(); + + // create empty connection without peer first. + // we need it to set up peer callbacks. + // afterwards we assign the peer. + // events need to be wrapped with connectionIds + KcpServerConnection connection = new KcpServerConnection( + OnConnectedCallback, + (message, channel) => OnData(connectionId, message, channel), + OnDisconnectedCallback, + (error, reason) => OnError(connectionId, error, reason), + (data) => RawSend(connectionId, data), + config, + cookie, + newClientEP); + + return connection; + + // setup authenticated event that also adds to connections + void OnConnectedCallback(KcpServerConnection conn) + { + // add to connections dict after being authenticated. + connections.Add(connectionId, conn); + 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 + + // finally, call mirror OnConnected event + Log.Info($"[KCP] Server: OnConnected({connectionId})"); + IPEndPoint endPoint = conn.remoteEndPoint as IPEndPoint; + OnConnected(connectionId, endPoint); + } + + void OnDisconnectedCallback() + { + // 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] Server: OnDisconnected({connectionId})"); + OnDisconnected(connectionId); + } + } + + // receive + add + process once. + // best to call this as long as there is more data to receive. + void ProcessMessage(ArraySegment segment, int connectionId) + { + //Log.Info($"[KCP] server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); + + // 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(connectionId); + + // 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. + + + // 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(segment); + 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(segment); + } + } + + // process incoming messages. should be called before updating the world. + // virtual because relay may need to inject their own ping or similar. + readonly HashSet connectionsToRemove = new HashSet(); + public virtual void TickIncoming() + { + // input all received messages into kcp + while (RawReceiveFrom(out ArraySegment segment, out int connectionId)) + { + ProcessMessage(segment, connectionId); + } + + // 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. + // virtual because relay may need to inject their own ping or similar. + public virtual 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 virtual void Tick() + { + TickIncoming(); + TickOutgoing(); + } + + public virtual void Stop() + { + // need to clear connections, otherwise they are in next session. + // fixes https://github.com/vis2k/kcp2k/pull/47 + connections.Clear(); + socket?.Close(); + socket = null; + } + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta new file mode 100644 index 0000000..6069122 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9759159c6589494a9037f5e130a867ed +timeCreated: 1603787747 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs new file mode 100644 index 0000000..2e06b9b --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs @@ -0,0 +1,126 @@ +// server needs to store a separate KcpPeer for each connection. +// as well as remoteEndPoint so we know where to send data to. +using System; +using System.Net; + +namespace kcp2k +{ + public class KcpServerConnection : KcpPeer + { + public readonly EndPoint remoteEndPoint; + + // callbacks + // even for errors, to allow liraries to show popups etc. + // instead of logging directly. + // (string instead of Exception for ease of use and to avoid user panic) + // + // events are readonly, set in constructor. + // this ensures they are always initialized when used. + // fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more + protected readonly Action OnConnectedCallback; + protected readonly Action, KcpChannel> OnDataCallback; + protected readonly Action OnDisconnectedCallback; + protected readonly Action OnErrorCallback; + protected readonly Action> RawSendCallback; + + public KcpServerConnection( + Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + Action> OnRawSend, + KcpConfig config, + uint cookie, + EndPoint remoteEndPoint) + : base(config, cookie) + { + OnConnectedCallback = OnConnected; + OnDataCallback = OnData; + OnDisconnectedCallback = OnDisconnected; + OnErrorCallback = OnError; + RawSendCallback = OnRawSend; + + this.remoteEndPoint = remoteEndPoint; + } + + // callbacks /////////////////////////////////////////////////////////// + protected override void OnAuthenticated() + { + // once we receive the first client hello, + // immediately reply with hello so the client knows the security cookie. + SendHello(); + OnConnectedCallback(this); + } + + protected override void OnData(ArraySegment message, KcpChannel channel) => + OnDataCallback(message, channel); + + protected override void OnDisconnected() => + OnDisconnectedCallback(); + + protected override void OnError(ErrorCode error, string message) => + OnErrorCallback(error, message); + + protected override void RawSend(ArraySegment data) => + RawSendCallback(data); + //////////////////////////////////////////////////////////////////////// + + // insert raw IO. usually from socket.Receive. + // offset is useful for relays, where we may parse a header and then + // feed the rest to kcp. + public void RawInput(ArraySegment segment) + { + // ensure valid size: at least 1 byte for channel + 4 bytes for cookie + if (segment.Count <= 5) return; + + // parse channel + // byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions + byte channel = segment.Array[segment.Offset + 0]; + + // all server->client messages include the server's security cookie. + // all client->server messages except for the initial 'hello' include it too. + // parse the cookie and make sure it matches (except for initial hello). + Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie); + + // security: messages after authentication are expected to contain the cookie. + // this protects against UDP spoofing. + // simply drop the message if the cookie doesn't match. + if (state == KcpState.Authenticated) + { + if (messageCookie != cookie) + { + // Info is enough, don't scare users. + // => this can happen for malicious messages + // => it can also happen if client's Hello message was retransmitted multiple times, which is totally normal. + Log.Info($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} from {remoteEndPoint} expected: {cookie} state: {state}. This can happen if the client's Hello message was transmitted multiple times, or if an attacker attempted UDP spoofing."); + return; + } + } + + // parse message + ArraySegment message = new ArraySegment(segment.Array, segment.Offset + 1+4, segment.Count - 1-4); + + switch (channel) + { + case (byte)KcpChannel.Reliable: + { + OnRawInputReliable(message); + break; + } + case (byte)KcpChannel.Unreliable: + { + OnRawInputUnreliable(message); + break; + } + default: + { + // invalid channel indicates random internet noise. + // servers may receive random UDP data. + // just ignore it, but log for easier debugging. + Log.Warning($"[KCP] ServerConnection: invalid channel header: {channel} from {remoteEndPoint}, likely internet noise"); + break; + } + } + } + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta new file mode 100644 index 0000000..5c04874 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 80a9b1ce9a6f14abeb32bfa9921d097b +timeCreated: 1602601483 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs new file mode 100644 index 0000000..9d01d64 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs @@ -0,0 +1,4 @@ +namespace kcp2k +{ + public enum KcpState { Connected, Authenticated, Disconnected } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs.meta new file mode 100644 index 0000000..1ee788d --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 81a02c141a88d45d4a2f5ef68c6da75f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs new file mode 100644 index 0000000..c28d8b8 --- /dev/null +++ b/Assets/Mirror/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/Transports/KCP/kcp2k/highlevel/Log.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs.meta new file mode 100644 index 0000000..1db1482 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7b5e1de98d6d84c3793a61cf7d8da9a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp.meta b/Assets/Mirror/Transports/KCP/kcp2k/kcp.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp.meta rename to Assets/Mirror/Transports/KCP/kcp2k/kcp.meta diff --git a/Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs b/Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs new file mode 100644 index 0000000..820f451 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs @@ -0,0 +1,8 @@ +namespace kcp2k +{ + internal struct AckItem + { + internal uint serialNumber; + internal uint timestamp; + } +} diff --git a/Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs.meta new file mode 100644 index 0000000..d9b4836 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 71f47cb11125d429e84e188a150f3ae5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs b/Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs rename to Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs diff --git a/Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta new file mode 100644 index 0000000..c95f88a --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: aec6a15ac7bd43129317ea1f01f19782 +timeCreated: 1602665988 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs similarity index 79% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs rename to Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs index dff49e1..851faaa 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Kcp.cs +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs @@ -17,7 +17,7 @@ public class Kcp 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 CMD_WINS = 84; // cmd: window size (tell/insert) 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 @@ -34,20 +34,14 @@ public class Kcp 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 snd_nxt; // forever growing send counter for sequence numbers + internal uint rcv_nxt; // forever growing receive counter for sequence numbers 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) @@ -59,11 +53,11 @@ internal struct AckItem internal uint cwnd; // congestion window internal uint probe; internal uint interval; - internal uint ts_flush; + internal uint ts_flush; // last flush timestamp in milliseconds 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 ts_probe; // probe timestamp internal uint probe_wait; internal uint dead_link; // maximum amount of 'xmit' retransmissions until a segment is considered lost internal uint incr; @@ -71,7 +65,7 @@ internal struct AckItem internal int fastresend; internal int fastlimit; - internal bool nocwnd; // no congestion window + internal bool nocwnd; // congestion control, negated. heavily restricts send/recv window sizes. 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. @@ -82,11 +76,13 @@ internal struct AckItem internal readonly List rcv_buf = new List(16); // receive buffer internal readonly List acklist = new List(16); + // memory buffer + // size depends on MTU. + // MTU can be changed at runtime, which resizes the buffer. 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; + // output function of type + readonly Action output; // segment pool to avoid allocations in C#. // this is not part of the original C code. @@ -104,18 +100,18 @@ internal struct AckItem // from the same connection. public Kcp(uint conv, Action output) { - this.conv = conv; + 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_rto = RTO_DEF; rx_minrto = RTO_MIN; - interval = INTERVAL; - ts_flush = INTERVAL; - ssthresh = THRESH_INIT; + interval = INTERVAL; + ts_flush = INTERVAL; + ssthresh = THRESH_INIT; fastlimit = FASTACK_LIMIT; dead_link = DEADLINK; buffer = new byte[(mtu + OVERHEAD) * 3]; @@ -131,6 +127,18 @@ public Kcp(uint conv, Action output) // this way we'll never miss it anywhere. void SegmentDelete(Segment seg) => SegmentPool.Return(seg); + // calculate how many packets are waiting to be sent + public int WaitSnd => snd_buf.Count + snd_queue.Count; + + // ikcp_wnd_unused + // returns the remaining space in receive window (rcv_wnd - rcv_queue) + internal uint WndUnused() + { + if (rcv_queue.Count < rcv_wnd) + return rcv_wnd - (uint)rcv_queue.Count; + return 0; + } + // ikcp_recv // receive data from kcp state machine // returns number of bytes read. @@ -176,6 +184,7 @@ public int Receive(byte[] buffer, int len) // entry. this is fine because we remove it in ANY case. Segment seg = rcv_queue.Dequeue(); + // copy segment data into our buffer Buffer.BlockCopy(seg.data.GetBuffer(), 0, buffer, offset, (int)seg.data.Position); offset += (int)seg.data.Position; @@ -205,6 +214,7 @@ public int Receive(byte[] buffer, int len) ++removed; // add rcv_queue.Enqueue(seg); + // increase sequence number for next segment rcv_nxt++; } else @@ -226,18 +236,33 @@ public int Receive(byte[] buffer, int len) } // ikcp_peeksize - // check the size of next message in the recv queue + // check the size of next message in the recv queue. + // returns -1 if there is no message, or if the message is still incomplete. public int PeekSize() { int length = 0; + // empty queue? if (rcv_queue.Count == 0) return -1; + // peek the first segment Segment seq = rcv_queue.Peek(); + + // seg.frg is 0 if the message requires no fragmentation. + // in that case, the segment's size is the final message size. if (seq.frg == 0) return (int)seq.data.Position; + // check if all fragment parts were received yet. + // seg.frg is the n-th fragment, but in reverse. + // this way the first received segment tells us how many fragments there are for the message. + // for example, if a message contains 3 segments: + // first segment: .frg is 2 (index in reverse) + // second segment: .frg is 1 (index in reverse) + // third segment: .frg is 0 (index in reverse) if (rcv_queue.Count < seq.frg + 1) return -1; + // recv_queue contains all the fragments necessary to reconstruct the message. + // sum all fragment's sizes to get the full message size. foreach (Segment seg in rcv_queue) { length += (int)seg.data.Position; @@ -248,7 +273,7 @@ public int PeekSize() } // ikcp_send - // sends byte[] to the other end. + // splits message into MTU sized fragments, adds them to snd_queue. public int Send(byte[] buffer, int offset, int len) { // fragment count @@ -266,7 +291,7 @@ public int Send(byte[] buffer, int offset, int len) // 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. + // this is difficult 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."); @@ -290,7 +315,11 @@ public int Send(byte[] buffer, int offset, int len) seg.data.Write(buffer, offset, size); } // seg.len = size: WriteBytes sets segment.Position! - seg.frg = (byte)(count - i - 1); + + // set fragment number. + // if the message requires no fragmentation, then + // seg.frg becomes 1-0-1 = 0 + seg.frg = (uint)(count - i - 1); snd_queue.Enqueue(seg); offset += size; len -= size; @@ -313,7 +342,7 @@ void UpdateAck(int rtt) // round trip time int delta = rtt - rx_srtt; if (delta < 0) delta = -delta; rx_rttval = (3 * rx_rttval + delta) / 4; - rx_srtt = (7 * rx_srtt + rtt) / 8; + 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); @@ -344,9 +373,11 @@ internal void ParseAck(uint sn) // for-int so we can erase while iterating for (int i = 0; i < snd_buf.Count; ++i) { + // is this the segment? Segment seg = snd_buf[i]; if (sn == seg.sn) { + // remove and return snd_buf.RemoveAt(i); SegmentDelete(seg); break; @@ -359,12 +390,14 @@ internal void ParseAck(uint sn) } // ikcp_parse_una - void ParseUna(uint una) + // removes all unacknowledged segments with sequence numbers < una from send buffer + internal void ParseUna(uint una) { int removed = 0; foreach (Segment seg in snd_buf) { - if (Utils.TimeDiff(una, seg.sn) > 0) + // if (Utils.TimeDiff(una, seg.sn) > 0) + if (seg.sn < una) { // can't remove while iterating. remember how many to remove // and do it after the loop. @@ -380,14 +413,23 @@ void ParseUna(uint una) } // ikcp_parse_fastack - void ParseFastack(uint sn, uint ts) + internal void ParseFastack(uint sn, uint ts) // serial number, timestamp { - if (Utils.TimeDiff(sn, snd_una) < 0 || Utils.TimeDiff(sn, snd_nxt) >= 0) + // sn needs to be between snd_una and snd_nxt + // if !(snd_una <= sn && sn < snd_nxt) return; + + // if (Utils.TimeDiff(sn, snd_una) < 0) + if (sn < snd_una) + return; + + // if (Utils.TimeDiff(sn, snd_nxt) >= 0) + if (sn >= snd_nxt) return; foreach (Segment seg in snd_buf) { - if (Utils.TimeDiff(sn, seg.sn) < 0) + // if (Utils.TimeDiff(sn, seg.sn) < 0) + if (sn < seg.sn) { break; } @@ -405,7 +447,7 @@ void ParseFastack(uint sn, uint ts) // ikcp_ack_push // appends an ack. - void AckPush(uint sn, uint ts) + void AckPush(uint sn, uint ts) // serial number, timestamp { acklist.Add(new AckItem{ serialNumber = sn, timestamp = ts }); } @@ -423,7 +465,7 @@ void ParseData(Segment newseg) } InsertSegmentInReceiveBuffer(newseg); - MoveReceiveBufferDataToReceiveQueue(); + MoveReceiveBufferReadySegmentsToQueue(); } // inserts the segment into rcv_buf, ordered by seg.sn. @@ -438,6 +480,7 @@ internal void InsertSegmentInReceiveBuffer(Segment newseg) bool repeat = false; // 'duplicate' // original C iterates backwards, so we need to do that as well. + // note if rcv_buf.Count == 0, i becomes -1 and no looping happens. int i; for (i = rcv_buf.Count - 1; i >= 0; i--) { @@ -467,18 +510,24 @@ internal void InsertSegmentInReceiveBuffer(Segment newseg) } } - // move available data from rcv_buf -> rcv_queue - void MoveReceiveBufferDataToReceiveQueue() + // move ready segments from rcv_buf -> rcv_queue. + // moves only the ready segments which are in rcv_nxt sequence order. + // some may still be missing an inserted later. + void MoveReceiveBufferReadySegmentsToQueue() { int removed = 0; foreach (Segment seg in rcv_buf) { + // move segments while they are in 'rcv_nxt' sequence order. + // some may still be missing and inserted later, in this case it stops immediately + // because segments always need to be received in the exact sequence order. 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); + // increase sequence number for next segment rcv_nxt++; } else @@ -504,40 +553,32 @@ public int Input(byte[] data, int offset, int size) 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_); + offset += Utils.Decode32U(data, offset, out uint conv_); if (conv_ != conv) return -1; - offset += Utils.Decode8u(data, offset, ref cmd); + offset += Utils.Decode8u(data, offset, out 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.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 + offset += Utils.Decode8u(data, offset, out byte frg); + offset += Utils.Decode16U(data, offset, out ushort wnd); + offset += Utils.Decode32U(data, offset, out uint ts); + offset += Utils.Decode32U(data, offset, out uint sn); + offset += Utils.Decode32U(data, offset, out uint una); + offset += Utils.Decode32U(data, offset, out uint len); + + // reduce remaining size by what was read 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; + // validate command type if (cmd != CMD_PUSH && cmd != CMD_ACK && cmd != CMD_WASK && cmd != CMD_WINS) return -3; @@ -589,8 +630,8 @@ public int Input(byte[] data, int offset, int size) seg.cmd = cmd; seg.frg = frg; seg.wnd = wnd; - seg.ts = ts; - seg.sn = sn; + seg.ts = ts; + seg.sn = sn; seg.una = una; if (len > 0) { @@ -654,47 +695,45 @@ public int Input(byte[] data, int offset, int size) return 0; } - // ikcp_wnd_unused - uint WndUnused() + // flush helper function + void MakeSpace(ref int size, int space) { - if (rcv_queue.Count < rcv_wnd) - return rcv_wnd - (uint)rcv_queue.Count; - return 0; + if (size + space > mtu) + { + output(buffer, size); + size = 0; + } } - // ikcp_flush - // flush remain ack segments - public void Flush() + // flush helper function + void FlushBuffer(int size) { - int offset = 0; // buffer ptr in original C - bool lost = false; // lost segments - - // helper functions - void MakeSpace(int space) + // flush buffer up to 'offset' (<= MTU) + if (size > 0) { - if (offset + space > mtu) - { - output(buffer, offset); - offset = 0; - } + output(buffer, size); } + } - void FlushBuffer() - { - if (offset > 0) - { - output(buffer, offset); - } - } + // ikcp_flush + // flush remain ack segments. + // flush may output multiple <= MTU messages from MakeSpace / FlushBuffer. + // the amount of messages depends on the sliding window. + // configured by send/receive window sizes + congestion control. + // with congestion control, the window will be extremely small(!). + public void Flush() + { + int size = 0; // amount of bytes to flush. 'buffer ptr' in C. + bool lost = false; // lost segments - // 'ikcp_update' haven't been called. + // update needs to be called before flushing 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. + // used. that's fine in C, but in C# our segment is a 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; @@ -704,13 +743,12 @@ void FlushBuffer() // flush acknowledges foreach (AckItem ack in acklist) { - MakeSpace(OVERHEAD); + MakeSpace(ref size, 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); + size += seg.Encode(buffer, size); } - acklist.Clear(); // probe window size (if remote window size equals zero) @@ -745,31 +783,37 @@ void FlushBuffer() if ((probe & ASK_SEND) != 0) { seg.cmd = CMD_WASK; - MakeSpace(OVERHEAD); - offset += seg.Encode(buffer, offset); + MakeSpace(ref size, OVERHEAD); + size += seg.Encode(buffer, size); } // flush window probing commands if ((probe & ASK_TELL) != 0) { seg.cmd = CMD_WINS; - MakeSpace(OVERHEAD); - offset += seg.Encode(buffer, offset); + MakeSpace(ref size, OVERHEAD); + size += seg.Encode(buffer, size); } probe = 0; - // calculate window size + // calculate the window size which is currently safe to send. + // it's send window, or remote window, whatever is smaller. + // for our max 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 + // double negative: if congestion window is enabled: + // limit window size to 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) + // note this may heavily limit window sizes. + // for our max message size test with super large windows of 32k, + // 'congestion window' limits it down from 32.000 to 2. + if (!nocwnd) cwnd_ = Math.Min(cwnd, cwnd_); + + // move cwnd_ 'window size' messages from snd_queue to snd_buf + // '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; @@ -780,7 +824,8 @@ void FlushBuffer() newseg.cmd = CMD_PUSH; newseg.wnd = seg.wnd; newseg.ts = current; - newseg.sn = snd_nxt++; + newseg.sn = snd_nxt; + snd_nxt += 1; // increase sequence number for next segment newseg.una = rcv_nxt; newseg.resendts = current; newseg.rto = rx_rto; @@ -798,6 +843,7 @@ void FlushBuffer() foreach (Segment segment in snd_buf) { bool needsend = false; + // initial transmit if (segment.xmit == 0) { @@ -844,14 +890,14 @@ void FlushBuffer() segment.una = rcv_nxt; int need = OVERHEAD + (int)segment.data.Position; - MakeSpace(need); + MakeSpace(ref size, need); - offset += segment.Encode(buffer, offset); + size += segment.Encode(buffer, size); if (segment.data.Position > 0) { - Buffer.BlockCopy(segment.data.GetBuffer(), 0, buffer, offset, (int)segment.data.Position); - offset += (int)segment.data.Position; + Buffer.BlockCopy(segment.data.GetBuffer(), 0, buffer, size, (int)segment.data.Position); + size += (int)segment.data.Position; } // dead link happens if a message was resent N times, but an @@ -868,8 +914,8 @@ void FlushBuffer() // done with it. SegmentDelete(seg); - // flash remain segments - FlushBuffer(); + // flush remaining segments + FlushBuffer(size); // update ssthresh // rate halving, https://tools.ietf.org/html/rfc6937 @@ -907,28 +953,40 @@ void FlushBuffer() // // 'current' - current timestamp in millisec. pass it to Kcp so that // Kcp doesn't have to do any stopwatch/deltaTime/etc. code + // + // time as uint, likely to minimize bandwidth. + // uint.max = 4294967295 ms = 1193 hours = 49 days public void Update(uint currentTimeMilliSeconds) { current = currentTimeMilliSeconds; + // not updated yet? then set updated and last flush time. if (!updated) { updated = true; ts_flush = current; } + // slap is time since last flush in milliseconds int slap = Utils.TimeDiff(current, ts_flush); + // hard limit: if 10s elapsed, always flush no matter what if (slap >= 10000 || slap < -10000) { ts_flush = current; slap = 0; } + // last flush is increased by 'interval' each time. + // so slap >= is a strange way to check if interval has elapsed yet. if (slap >= 0) { + // increase last flush time by one interval ts_flush += interval; - if (Utils.TimeDiff(current, ts_flush) >= 0) + + // if last flush is still behind, increase it to current + interval + // if (Utils.TimeDiff(current, ts_flush) >= 0) // original kcp.c + if (current >= ts_flush) // less confusing { ts_flush = current + interval; } @@ -948,7 +1006,7 @@ public void Update(uint currentTimeMilliSeconds) public uint Check(uint current_) { uint ts_flush_ = ts_flush; - int tm_flush = 0x7fffffff; + // int tm_flush = 0x7fffffff; original kcp: useless assignment int tm_packet = 0x7fffffff; if (!updated) @@ -967,7 +1025,7 @@ public uint Check(uint current_) return current_; } - tm_flush = Utils.TimeDiff(ts_flush_, current_); + int tm_flush = Utils.TimeDiff(ts_flush_, current_); foreach (Segment seg in snd_buf) { @@ -1000,8 +1058,9 @@ public void SetMtu(uint mtu) // ikcp_interval public void SetInterval(uint interval) { - if (interval > 5000) interval = 5000; - else if (interval < 10) interval = 10; + // clamp interval between 10 and 5000 + if (interval > 5000) interval = 5000; + else if (interval < 10) interval = 10; this.interval = interval; } @@ -1027,6 +1086,7 @@ public void SetNoDelay(uint nodelay, uint interval = INTERVAL, int resend = 0, b if (interval >= 0) { + // clamp interval between 10 and 5000 if (interval > 5000) interval = 5000; else if (interval < 10) interval = 10; this.interval = interval; diff --git a/Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs.meta new file mode 100644 index 0000000..3c4377b --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a59b1cae10a334faf807432ab472f212 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/kcp/Kcp.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Pool.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/Pool.cs rename to Assets/Mirror/Transports/KCP/kcp2k/kcp/Pool.cs diff --git a/Assets/Mirror/Transports/KCP/kcp2k/kcp/Pool.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Pool.cs.meta new file mode 100644 index 0000000..b945fa1 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Pool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 35c07818fc4784bb4ba472c8e5029002 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/kcp/Pool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs new file mode 100644 index 0000000..d7e4131 --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs @@ -0,0 +1,78 @@ +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. + // fragment (sent as 1 byte). + // 0 if unfragmented, otherwise fragment numbers in reverse: N,..,32,1,0 + // this way the first received segment tells us how many fragments there are. + internal uint frg; + internal uint wnd; // window size that the receive can currently receive + internal uint ts; // timestamp + internal uint sn; // sequence 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. + // -> default MTU as initial capacity to avoid most runtime resizing/allocations + // + // .data is only used for Encode(), which always fits it into a buffer. + // the buffer is always Kcp.buffer. Kcp ctor creates the buffer of size: + // (mtu + OVERHEAD) * 3 bytes. + // in other words, Encode only ever writes up to the above amount of bytes. + internal MemoryStream data = new MemoryStream(Kcp.MTU_DEF); + + // ikcp_encode_seg + // encode a segment into buffer. + // buffer is always Kcp.buffer. Kcp ctor creates the buffer of size: + // (mtu + OVERHEAD) * 3 bytes. + // in other words, Encode only ever writes up to the above amount of bytes. + internal int Encode(byte[] ptr, int offset) + { + int previousPosition = 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); + + int written = offset - previousPosition; + return written; + } + + // 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/Transports/KCP/kcp2k/kcp/Segment.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs.meta new file mode 100644 index 0000000..a60a91c --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fc58706a05dd3442c8fde858d5266855 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/kcp/Segment.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/KCP/kcp2k/kcp/Utils.cs b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Utils.cs new file mode 100644 index 0000000..2cb7462 --- /dev/null +++ b/Assets/Mirror/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 value) + { + p[0 + offset] = value; + return 1; + } + + // decode 8 bits unsigned int + public static int Decode8u(byte[] p, int offset, out byte value) + { + value = p[0 + offset]; + return 1; + } + + // encode 16 bits unsigned int (lsb) + public static int Encode16U(byte[] p, int offset, ushort value) + { + p[0 + offset] = (byte)(value >> 0); + p[1 + offset] = (byte)(value >> 8); + return 2; + } + + // decode 16 bits unsigned int (lsb) + public static int Decode16U(byte[] p, int offset, out ushort value) + { + ushort result = 0; + result |= p[0 + offset]; + result |= (ushort)(p[1 + offset] << 8); + value = result; + return 2; + } + + // encode 32 bits unsigned int (lsb) + public static int Encode32U(byte[] p, int offset, uint value) + { + p[0 + offset] = (byte)(value >> 0); + p[1 + offset] = (byte)(value >> 8); + p[2 + offset] = (byte)(value >> 16); + p[3 + offset] = (byte)(value >> 24); + return 4; + } + + // decode 32 bits unsigned int (lsb) + public static int Decode32U(byte[] p, int offset, out uint value) + { + 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); + value = 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/Transports/KCP/kcp2k/kcp/Utils.cs.meta b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Utils.cs.meta new file mode 100644 index 0000000..19603af --- /dev/null +++ b/Assets/Mirror/Transports/KCP/kcp2k/kcp/Utils.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ef959eb716205bd48b050f010a9a35ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/KCP/kcp2k/kcp/Utils.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Latency.meta b/Assets/Mirror/Transports/Latency.meta new file mode 100644 index 0000000..07d138e --- /dev/null +++ b/Assets/Mirror/Transports/Latency.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 397bb578e2bb049ebac1e29effa9d298 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Latency/LatencySimulation.cs b/Assets/Mirror/Transports/Latency/LatencySimulation.cs new file mode 100644 index 0000000..e5dabd0 --- /dev/null +++ b/Assets/Mirror/Transports/Latency/LatencySimulation.cs @@ -0,0 +1,319 @@ +// 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; +using UnityEngine.Serialization; + +namespace Mirror +{ + struct QueuedMessage + { + public int connectionId; + public byte[] bytes; + public double time; + public int channelId; + + public QueuedMessage(int connectionId, byte[] bytes, double time, int channelId) + { + this.connectionId = connectionId; + this.bytes = bytes; + this.time = time; + this.channelId = channelId; + } + } + + [HelpURL("https://mirror-networking.gitbook.io/docs/transports/latency-simulaton-transport")] + [DisallowMultipleComponent] + public class LatencySimulation : Transport, PortTransport + { + public Transport wrap; + + // implement PortTransport in case the underlying Tranpsport is a PortTransport too. + // otherwise gameplay code like 'if Transport is PortTransport' would completely break with Latency Simulation. + public ushort Port + { + get + { + if (wrap is PortTransport port) + return port.Port; + + Debug.LogWarning($"LatencySimulation: attempted to get Port but {wrap} is not a PortTransport."); + return 0; + } + set + { + if (wrap is PortTransport port) + { + port.Port = value; + return; + } + + Debug.LogWarning($"LatencySimulation: attempted to set Port but {wrap} is not a PortTransport."); + } + } + + [Header("Common")] + // latency always needs to be applied to both channels! + // fixes a bug in prediction where predictedTime would have no latency, but [Command]s would have 100ms latency resulting in heavy, hard to debug jittering! + // in real world, all UDP channels go over the same socket connection with the same latency. + [Tooltip("Latency in milliseconds (1000 = 1 second). Always applied to both reliable and unreliable, otherwise unreliable NetworkTime may be behind reliable [SyncVars/Commands/Rpcs] or vice versa!")] + [Range(0, 10000)] public float latency = 100; + + [Tooltip("Jitter latency via perlin(Time * jitterSpeed) * jitter")] + [FormerlySerializedAs("latencySpikeMultiplier")] + [Range(0, 1)] public float jitter = 0.02f; + + [Tooltip("Jitter latency via perlin(Time * jitterSpeed) * jitter")] + [FormerlySerializedAs("latencySpikeSpeedMultiplier")] + public float jitterSpeed = 1; + + [Header("Reliable Messages")] + // 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 %\n2% recommended for long term play testing, upto 5% for short bursts.\nAnything higher, or for a prolonged amount of time, suggests user has a connection fault.")] + [Range(0, 100)] public float unreliableLoss = 2; + + [Tooltip("Scramble % of unreliable messages, just like over the real network. Mirror unreliable is unordered.")] + [Range(0, 100)] public float unreliableScramble = 2; + + // message queues + // list so we can insert randomly (scramble) + readonly List clientToServer = new List(); + readonly List serverToClient = 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! + readonly System.Random random = new System.Random(); + + public void Awake() + { + if (wrap == null) + throw new Exception("LatencySimulationTransport 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. +#if !UNITY_2020_3_OR_NEWER + float spike = Noise((float)NetworkTime.localTime * jitterSpeed) * jitter; +#else + float spike = Noise((float)Time.unscaledTimeAsDouble * jitterSpeed) * jitter; +#endif + + // base latency + switch (channeldId) + { + case Channels.Reliable: + return latency/1000 + spike; + case Channels.Unreliable: + return latency/1000 + 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 messageQueue) + { + // 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); + + // simulate latency +#if !UNITY_2020_3_OR_NEWER + double sendTime = NetworkTime.localTime + latency; +#else + double sendTime = Time.unscaledTimeAsDouble + latency; +#endif + + // construct message + QueuedMessage message = new QueuedMessage + ( + connectionId, + bytes, + sendTime, + channelId + ); + + // drop & scramble can only be simulated on Unreliable channel. + if (channelId == Channels.Unreliable) + { + // simulate drop + bool drop = random.NextDouble() < unreliableLoss/100; + if (!drop) + { + // simulate scramble (Random.Next is < max, so +1) + bool scramble = random.NextDouble() < unreliableScramble/100; + int last = messageQueue.Count; + int index = scramble ? random.Next(0, last + 1) : last; + + // simulate latency + messageQueue.Insert(index, message); + } + } + // any other channel may be relialbe / sequenced / ordered / etc. + // in that case we only simulate latency (above) + else + { + messageQueue.Add(message); + } + } + + public override bool Available() => wrap.Available(); + + public override void ClientConnect(string address) + { + wrap.OnClientConnected = OnClientConnected; + wrap.OnClientDataReceived = OnClientDataReceived; + wrap.OnClientError = OnClientError; + wrap.OnClientTransportException = OnClientTransportException; + wrap.OnClientDisconnected = OnClientDisconnected; + wrap.ClientConnect(address); + } + + public override void ClientConnect(Uri uri) + { + wrap.OnClientConnected = OnClientConnected; + wrap.OnClientDataReceived = OnClientDataReceived; + wrap.OnClientError = OnClientError; + wrap.OnClientTransportException = OnClientTransportException; + wrap.OnClientDisconnected = OnClientDisconnected; + wrap.ClientConnect(uri); + } + + public override bool ClientConnected() => wrap.ClientConnected(); + + public override void ClientDisconnect() + { + wrap.ClientDisconnect(); + clientToServer.Clear(); + } + + public override void ClientSend(ArraySegment segment, int channelId) + { + float latency = SimulateLatency(channelId); + SimulateSend(0, segment, channelId, latency, clientToServer); + } + + 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, serverToClient); + } + + public override void ServerStart() + { +#pragma warning disable CS0618 // Type or member is obsolete + wrap.OnServerConnected = OnServerConnected; +#pragma warning restore CS0618 // Type or member is obsolete + wrap.OnServerConnectedWithAddress = OnServerConnectedWithAddress; + wrap.OnServerDataReceived = OnServerDataReceived; + wrap.OnServerError = OnServerError; + wrap.OnServerTransportException = OnServerTransportException; + wrap.OnServerDisconnected = OnServerDisconnected; + wrap.ServerStart(); + } + + public override void ServerStop() + { + wrap.ServerStop(); + serverToClient.Clear(); + } + + public override void ClientEarlyUpdate() => wrap.ClientEarlyUpdate(); + public override void ServerEarlyUpdate() => wrap.ServerEarlyUpdate(); + public override void ClientLateUpdate() + { + // flush messages after latency. + // need to iterate all, since queue isn't a sortedlist. + for (int i = 0; i < clientToServer.Count; ++i) + { + // message ready to be sent? + QueuedMessage message = clientToServer[i]; +#if !UNITY_2020_3_OR_NEWER + if (message.time <= NetworkTime.localTime) +#else + if (message.time <= Time.unscaledTimeAsDouble) +#endif + { + // send and eat + wrap.ClientSend(new ArraySegment(message.bytes), message.channelId); + clientToServer.RemoveAt(i); + --i; + } + } + + // update wrapped transport too + wrap.ClientLateUpdate(); + } + public override void ServerLateUpdate() + { + // flush messages after latency. + // need to iterate all, since queue isn't a sortedlist. + for (int i = 0; i < serverToClient.Count; ++i) + { + // message ready to be sent? + QueuedMessage message = serverToClient[i]; +#if !UNITY_2020_3_OR_NEWER + if (message.time <= NetworkTime.localTime) +#else + if (message.time <= Time.unscaledTimeAsDouble) +#endif + { + // send and eat + wrap.ServerSend(message.connectionId, new ArraySegment(message.bytes), message.channelId); + serverToClient.RemoveAt(i); + --i; + } + } + + // 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/Transports/Latency/LatencySimulation.cs.meta b/Assets/Mirror/Transports/Latency/LatencySimulation.cs.meta new file mode 100644 index 0000000..34551ed --- /dev/null +++ b/Assets/Mirror/Transports/Latency/LatencySimulation.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 96b149f511061407fb54895c057b7736 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Latency/LatencySimulation.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Middleware.meta b/Assets/Mirror/Transports/Middleware.meta new file mode 100644 index 0000000..3038832 --- /dev/null +++ b/Assets/Mirror/Transports/Middleware.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 69344c174fdaf432e9ff0cdaf2e2ba21 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs b/Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs similarity index 88% rename from Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs rename to Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs index 7dd934a..ee18240 100644 --- a/Assets/Mirror/Runtime/Transports/MiddlewareTransport.cs +++ b/Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs @@ -26,6 +26,7 @@ public override void ClientConnect(string address) inner.OnClientDataReceived = OnClientDataReceived; inner.OnClientDisconnected = OnClientDisconnected; inner.OnClientError = OnClientError; + inner.OnClientTransportException = OnClientTransportException; inner.ClientConnect(address); } @@ -41,10 +42,14 @@ public override void ClientConnect(string address) public override bool ServerActive() => inner.ServerActive(); public override void ServerStart() { +#pragma warning disable CS0618 // Type or member is obsolete inner.OnServerConnected = OnServerConnected; +#pragma warning restore CS0618 // Type or member is obsolete + inner.OnServerConnectedWithAddress = OnServerConnectedWithAddress; inner.OnServerDataReceived = OnServerDataReceived; inner.OnServerDisconnected = OnServerDisconnected; inner.OnServerError = OnServerError; + inner.OnServerTransportException = OnServerTransportException; inner.ServerStart(); } diff --git a/Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs.meta b/Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs.meta new file mode 100644 index 0000000..48449c2 --- /dev/null +++ b/Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 46f20ede74658e147a1af57172710de2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Middleware/MiddlewareTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Mirror.Transports.asmdef b/Assets/Mirror/Transports/Mirror.Transports.asmdef new file mode 100644 index 0000000..3a67a0a --- /dev/null +++ b/Assets/Mirror/Transports/Mirror.Transports.asmdef @@ -0,0 +1,19 @@ +{ + "name": "Mirror.Transports", + "rootNamespace": "", + "references": [ + "GUID:30817c1a0e6d646d99c048fc403f5979", + "GUID:6806a62c384838046a3c66c44f06d75f", + "GUID:725ee7191c021de4dbf9269590ded755", + "GUID:3b5390adca4e2bb4791cb930316d6f3e" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/Mirror.Transports.asmdef.meta b/Assets/Mirror/Transports/Mirror.Transports.asmdef.meta new file mode 100644 index 0000000..20026c8 --- /dev/null +++ b/Assets/Mirror/Transports/Mirror.Transports.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 627104647b9c04b4ebb8978a92ecac63 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Mirror.Transports.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Multiplex.meta b/Assets/Mirror/Transports/Multiplex.meta new file mode 100644 index 0000000..59c257e --- /dev/null +++ b/Assets/Mirror/Transports/Multiplex.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 28d5150129fea43048bbdd23ea6e2446 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs b/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs new file mode 100644 index 0000000..65fe710 --- /dev/null +++ b/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Generic; +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, PortTransport + { + public Transport[] transports; + + Transport available; + + // underlying transport connectionId to multiplexed connectionId lookup. + // + // originally we used a formula to map the connectionId: + // connectionId * transportAmount + transportId + // + // 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, ...] + // + // however, some transports like kcp may give very large connectionIds. + // if they are near int.max, then "* transprotAmount + transportIndex" + // will overflow, resulting in connIds which can't be projected back. + // https://github.com/vis2k/Mirror/issues/3280 + // + // instead, use a simple lookup with 0-indexed ids. + // with initial capacity to avoid runtime allocations. + + // (original connectionId, transport#) to multiplexed connectionId + readonly Dictionary, int> originalToMultiplexedId = + new Dictionary, int>(100); + + // multiplexed connectionId to (original connectionId, transport#) + readonly Dictionary> multiplexedToOriginalId = + new Dictionary>(100); + + // next multiplexed id counter. start at 1 because 0 is reserved for host. + int nextMultiplexedId = 1; + + // prevent log flood from OnGUI or similar per-frame updates + bool alreadyWarned; + + public ushort Port + { + get + { + foreach (Transport transport in transports) + if (transport.Available() && transport is PortTransport portTransport) + return portTransport.Port; + + return 0; + } + set + { + if (Utils.IsHeadless() && !alreadyWarned) + { + // prevent log flood from OnGUI or similar per-frame updates + alreadyWarned = true; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Multiplexer] Server cannot set the same listen port for all transports! Set them directly instead."); + Console.ResetColor(); + } + else + { + // We can't set the same port for all transports because + // listen ports have to be different for each transport + // so we just set the first available one. + // This depends on the selected build platform. + foreach (Transport transport in transports) + if (transport.Available() && transport is PortTransport portTransport) + { + portTransport.Port = value; + break; + } + } + } + } + + // add to bidirection lookup. returns the multiplexed connectionId. + public int AddToLookup(int originalConnectionId, int transportIndex) + { + // add to both + KeyValuePair pair = new KeyValuePair(originalConnectionId, transportIndex); + int multiplexedId = nextMultiplexedId++; + + originalToMultiplexedId[pair] = multiplexedId; + multiplexedToOriginalId[multiplexedId] = pair; + + return multiplexedId; + } + + public void RemoveFromLookup(int originalConnectionId, int transportIndex) + { + // remove from both + KeyValuePair pair = new KeyValuePair(originalConnectionId, transportIndex); + if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId)) + { + originalToMultiplexedId.Remove(pair); + multiplexedToOriginalId.Remove(multiplexedId); + } + } + + public bool OriginalId(int multiplexId, out int originalConnectionId, out int transportIndex) + { + if (!multiplexedToOriginalId.ContainsKey(multiplexId)) + { + originalConnectionId = 0; + transportIndex = 0; + return false; + } + + KeyValuePair pair = multiplexedToOriginalId[multiplexId]; + originalConnectionId = pair.Key; + transportIndex = pair.Value; + return true; + } + + public int MultiplexId(int originalConnectionId, int transportIndex) + { + KeyValuePair pair = new KeyValuePair(originalConnectionId, transportIndex); + if (originalToMultiplexedId.TryGetValue(pair, out int multiplexedId)) + return multiplexedId; + else + return 0; + } + + //////////////////////////////////////////////////////////////////////// + + public void Awake() + { + if (transports == null || transports.Length == 0) + { + Debug.LogError("[Multiplexer] 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.OnClientTransportException = OnClientTransportException; + transport.OnClientDisconnected = OnClientDisconnected; + transport.ClientConnect(address); + return; + } + } + throw new ArgumentException("[Multiplexer] 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.OnClientTransportException = OnClientTransportException; + 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("[Multiplexer] 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 + void AddServerCallbacks() + { + // all underlying transports should call the multiplex transport's 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 transportIndex = i; + Transport transport = transports[i]; + +#pragma warning disable CS0618 // Type or member is obsolete + transport.OnServerConnected = (originalConnectionId => + { + // invoke Multiplex event with multiplexed connectionId + int multiplexedId = AddToLookup(originalConnectionId, transportIndex); + OnServerConnected.Invoke(multiplexedId); + }); +#pragma warning restore CS0618 // Type or member is obsolete + + transport.OnServerConnectedWithAddress = (originalConnectionId, address) => + { + // invoke Multiplex event with multiplexed connectionId + int multiplexedId = AddToLookup(originalConnectionId, transportIndex); + OnServerConnectedWithAddress.Invoke(multiplexedId, address); + }; + + transport.OnServerDataReceived = (originalConnectionId, data, channel) => + { + // invoke Multiplex event with multiplexed connectionId + int multiplexedId = MultiplexId(originalConnectionId, transportIndex); + if (multiplexedId == 0) + { + if (Utils.IsHeadless()) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + Console.ResetColor(); + } + else + Debug.LogWarning($"[Multiplexer] Received data for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + + return; + } + OnServerDataReceived.Invoke(multiplexedId, data, channel); + }; + + transport.OnServerError = (originalConnectionId, error, reason) => + { + // invoke Multiplex event with multiplexed connectionId + int multiplexedId = MultiplexId(originalConnectionId, transportIndex); + if (multiplexedId == 0) + { + if (Utils.IsHeadless()) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + Console.ResetColor(); + } + else + Debug.LogError($"[Multiplexer] Received error for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + + return; + } + OnServerError.Invoke(multiplexedId, error, reason); + }; + + transport.OnServerTransportException = (originalConnectionId, exception) => + { + // invoke Multiplex event with multiplexed connectionId + int multiplexedId = MultiplexId(originalConnectionId, transportIndex); + OnServerTransportException.Invoke(multiplexedId, exception); + }; + + transport.OnServerDisconnected = originalConnectionId => + { + // invoke Multiplex event with multiplexed connectionId + int multiplexedId = MultiplexId(originalConnectionId, transportIndex); + if (multiplexedId == 0) + { + if (Utils.IsHeadless()) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + Console.ResetColor(); + } + else + Debug.LogWarning($"[Multiplexer] Received disconnect for unknown connectionId={originalConnectionId} on transport={transportIndex}"); + + return; + } + OnServerDisconnected.Invoke(multiplexedId); + RemoveFromLookup(originalConnectionId, transportIndex); + }; + } + } + + // for now returns the first uri, + // should we return all available uris? + public override Uri ServerUri() => + 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) + { + // convert multiplexed connectionId to original id & transport index + if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex)) + return transports[transportIndex].ServerGetClientAddress(originalConnectionId); + else + return ""; + } + + public override void ServerDisconnect(int connectionId) + { + // convert multiplexed connectionId to original id & transport index + if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex)) + transports[transportIndex].ServerDisconnect(originalConnectionId); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + // convert multiplexed connectionId to original transport + connId + if (OriginalId(connectionId, out int originalConnectionId, out int transportIndex)) + transports[transportIndex].ServerSend(originalConnectionId, segment, channelId); + } + + public override void ServerStart() + { + AddServerCallbacks(); + + foreach (Transport transport in transports) + { + transport.ServerStart(); + + if (transport is PortTransport portTransport) + { + if (Utils.IsHeadless()) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}"); + Console.ResetColor(); + } + else + { + Debug.Log($"[Multiplexer]: Server listening on port {portTransport.Port} with {transport}"); + } + } + } + } + + 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(); + builder.Append("Multiplexer:"); + + foreach (Transport transport in transports) + builder.Append($" {transport}"); + + return builder.ToString().Trim(); + } + } +} diff --git a/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs.meta b/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs.meta new file mode 100644 index 0000000..c5445a1 --- /dev/null +++ b/Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 929e3234c7db540b899f00183fc2b1fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport.meta b/Assets/Mirror/Transports/SimpleWeb.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport.meta rename to Assets/Mirror/Transports/SimpleWeb.meta diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/.cert.example.Json b/Assets/Mirror/Transports/SimpleWeb/.cert.example.Json similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/.cert.example.Json rename to Assets/Mirror/Transports/SimpleWeb/.cert.example.Json diff --git a/Assets/Mirror/Transports/SimpleWeb/.cert.example.Json.meta b/Assets/Mirror/Transports/SimpleWeb/.cert.example.Json.meta new file mode 100644 index 0000000..6d3134b --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/.cert.example.Json.meta @@ -0,0 +1,9 @@ +guid: 4DD9F6E6E712911717200F85FA16CCAD +fileFormatVersion: 2 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/.cert.example.Json + uploadId: 736421 diff --git a/Assets/Mirror/Transports/SimpleWeb/Editor.meta b/Assets/Mirror/Transports/SimpleWeb/Editor.meta new file mode 100644 index 0000000..0ffdb34 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a7a134b4d9eef45239a2d7caf7f52c3e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs b/Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs new file mode 100644 index 0000000..9857dda --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs @@ -0,0 +1,71 @@ +using UnityEditor; +using UnityEngine; + +namespace Mirror.SimpleWeb.Editor +{ +#if UNITY_EDITOR + [CustomPropertyDrawer(typeof(ClientWebsocketSettings))] + public class ClientWebsocketSettingsDrawer : PropertyDrawer + { + readonly string websocketPortOptionName = nameof(ClientWebsocketSettings.ClientPortOption); + readonly string customPortName = nameof(ClientWebsocketSettings.CustomClientPort); + readonly GUIContent portOptionLabel = new GUIContent("Client Port Option", + "Specify what port the client websocket connection uses (default same as server port)"); + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + property.isExpanded = true; + return SumPropertyHeights(property, websocketPortOptionName, customPortName); + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + DrawPortSettings(position, property); + } + + void DrawPortSettings(Rect position, SerializedProperty property) + { + SerializedProperty portOptionProp = property.FindPropertyRelative(websocketPortOptionName); + SerializedProperty portProp = property.FindPropertyRelative(customPortName); + float portOptionHeight = EditorGUI.GetPropertyHeight(portOptionProp); + float portHeight = EditorGUI.GetPropertyHeight(portProp); + float spacing = EditorGUIUtility.standardVerticalSpacing; + bool wasEnabled = GUI.enabled; + + position.height = portOptionHeight; + + EditorGUI.PropertyField(position, portOptionProp, portOptionLabel); + position.y += spacing + portOptionHeight; + position.height = portHeight; + + WebsocketPortOption portOption = (WebsocketPortOption)portOptionProp.enumValueIndex; + if (portOption == WebsocketPortOption.MatchWebpageProtocol || portOption == WebsocketPortOption.DefaultSameAsServer) + { + int port = 0; + if (property.serializedObject.targetObject is SimpleWebTransport swt) + if (portOption == WebsocketPortOption.MatchWebpageProtocol) + port = swt.clientUseWss ? 443 : 80; + else + port = swt.port; + + GUI.enabled = false; + EditorGUI.IntField(position, new GUIContent("Client Port"), port); + GUI.enabled = wasEnabled; + } + else + EditorGUI.PropertyField(position, portProp); + + position.y += spacing + portHeight; + } + + float SumPropertyHeights(SerializedProperty property, params string[] propertyNames) + { + float totalHeight = 0; + foreach (var name in propertyNames) + totalHeight += EditorGUI.GetPropertyHeight(property.FindPropertyRelative(name)) + EditorGUIUtility.standardVerticalSpacing; + + return totalHeight; + } + } +#endif +} diff --git a/Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs.meta b/Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs.meta new file mode 100644 index 0000000..525cdd2 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 2d0e5b00ac8e45c99e68ad95cf843f80 +timeCreated: 1700081340 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta new file mode 100644 index 0000000..c2852a8 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8ae237a052b29fc4b8d000f48e545bb7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs new file mode 100644 index 0000000..c0df7fe --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyVersion("1.6.0")] + +[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Runtime")] +[assembly: InternalsVisibleTo("SimpleWebTransport.Tests.Editor")] diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs.meta new file mode 100644 index 0000000..04630ce --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ee9e76201f7665244bd6ab8ea343a83f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md.meta new file mode 100644 index 0000000..1bd6d30 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: b0ef23ac1c6a62546bbad5529b3bfdad +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client.meta rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client.meta diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs new file mode 100644 index 0000000..50ce331 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs @@ -0,0 +1,17 @@ +using System; + +namespace Mirror.SimpleWeb +{ + [Serializable] + public struct ClientWebsocketSettings + { + public WebsocketPortOption ClientPortOption; + public ushort CustomClientPort; + } + public enum WebsocketPortOption + { + DefaultSameAsServer, + MatchWebpageProtocol, + SpecifyPort + } +} diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs.meta new file mode 100644 index 0000000..dda5159 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 20ed869c5ba54a56b750ac9e82be069f +timeCreated: 1700425326 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs similarity index 95% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs index 3569af3..0eef27b 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/SimpleWebClient.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs @@ -11,12 +11,32 @@ public enum ClientState Connected = 2, Disconnecting = 3, } + /// /// Client used to control websockets /// Base class used by WebSocketClientWebGl and WebSocketClientStandAlone /// public abstract class SimpleWebClient { + readonly int maxMessagesPerTick; + + protected ClientState state; + protected readonly int maxMessageSize; + protected readonly BufferPool bufferPool; + + public readonly ConcurrentQueue receiveQueue = new ConcurrentQueue(); + + public ClientState ConnectionState => state; + + public event Action onConnect; + public event Action onDisconnect; + public event Action> onData; + public event Action onError; + + public abstract void Connect(Uri serverAddress); + public abstract void Disconnect(); + public abstract void Send(ArraySegment segment); + public static SimpleWebClient Create(int maxMessageSize, int maxMessagesPerTick, TcpConfig tcpConfig) { #if UNITY_WEBGL && !UNITY_EDITOR @@ -26,13 +46,6 @@ public static SimpleWebClient Create(int maxMessageSize, int maxMessagesPerTick, #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; @@ -40,13 +53,6 @@ protected SimpleWebClient(int maxMessageSize, int 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 /// @@ -90,10 +96,8 @@ public void ProcessMessageQueue(MonoBehaviour behaviour) break; } } + if (receiveQueue.Count > 0) + Log.Warn("[SWT-SimpleWebClient]: ProcessMessageQueue has {0} remaining.", receiveQueue.Count); } - - public abstract void Connect(Uri serverAddress); - public abstract void Disconnect(); - public abstract void Send(ArraySegment segment); } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs.meta new file mode 100644 index 0000000..ca3e322 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 13131761a0bf5a64dadeccd700fe26e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/SimpleWebClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone.meta rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone.meta diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs new file mode 100644 index 0000000..b1cffe8 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs @@ -0,0 +1,88 @@ +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("[SWT-ClientHandshake]: Handshake Hashing {0}", Encoding.ASCII.GetString(keySumBytes)); + + // SHA-1 is the websocket standard: + // https://www.rfc-editor.org/rfc/rfc6455 + // we should follow the standard, even though SHA1 is considered weak: + // https://stackoverflow.com/questions/38038841/why-is-sha-1-considered-insecure + 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("[SWT-ClientHandshake]: Connection closed before handshake"); + return false; + } + + string responseString = Encoding.ASCII.GetString(responseBuffer, 0, lengthOrNull.Value); + Log.Verbose("[SWT-ClientHandshake]: Handshake Response {0}", responseString); + + string acceptHeader = "Sec-WebSocket-Accept: "; + int startIndex = responseString.IndexOf(acceptHeader, StringComparison.InvariantCultureIgnoreCase); + + if (startIndex < 0) + { + Log.Error("[SWT-ClientHandshake]: Unexpected Handshake Response {0}", responseString); + return false; + } + + startIndex += acceptHeader.Length; + int endIndex = responseString.IndexOf("\r\n", startIndex); + string responseKey = responseString.Substring(startIndex, endIndex - startIndex); + + if (responseKey != expectedResponse) + { + Log.Error("[SWT-ClientHandshake]: Response key incorrect\nExpected:{0}\nResponse:{1}\nThis can happen if Websocket Protocol is not installed in Windows Server Roles.", expectedResponse, responseKey); + return false; + } + + return true; + } + catch (Exception e) + { + Log.Exception(e); + return false; + } + } + } +} diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs.meta new file mode 100644 index 0000000..4ebf03b --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3ffdcabc9e28f764a94fc4efc82d3e8b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientHandshake.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs similarity index 87% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs index be93f6c..7a6696e 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/ClientSslHelper.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs @@ -11,7 +11,7 @@ internal class ClientSslHelper internal bool TryCreateStream(Connection conn, Uri uri) { NetworkStream stream = conn.client.GetStream(); - if (uri.Scheme != "wss") + if (uri.Scheme != "wss" && uri.Scheme != "https") { conn.stream = stream; return true; @@ -24,7 +24,7 @@ internal bool TryCreateStream(Connection conn, Uri uri) } catch (Exception e) { - Log.Error($"Create SSLStream Failed: {e}", false); + Log.Error("[SWT-ClientSslHelper]: Create SSLStream Failed: {0}\n{1}\n\n", e.Message, e.StackTrace); return false; } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs.meta new file mode 100644 index 0000000..d7be536 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 46055a75559a79849a750f39a766db61 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/ClientSslHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs similarity index 88% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs index 3414afb..e4c43d6 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/StandAlone/WebSocketClientStandAlone.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs @@ -60,7 +60,7 @@ void ConnectAndReceiveLoop(Uri serverAddress) bool success = sslHelper.TryCreateStream(conn, serverAddress); if (!success) { - Log.Warn("Failed to create Stream"); + Log.Warn("[SWT-WebSocketClientStandAlone]: Failed to create Stream with {0}", serverAddress); conn.Dispose(); return; } @@ -68,12 +68,12 @@ void ConnectAndReceiveLoop(Uri serverAddress) success = handshake.TryHandshake(conn, serverAddress); if (!success) { - Log.Warn("Failed Handshake"); + Log.Warn("[SWT-WebSocketClientStandAlone]: Failed Handshake with {0}", serverAddress); conn.Dispose(); return; } - Log.Info("HandShake Successful"); + Log.Info("[SWT-WebSocketClientStandAlone]: HandShake Successful with {0}", serverAddress); state = ClientState.Connected; @@ -101,7 +101,7 @@ void ConnectAndReceiveLoop(Uri serverAddress) ReceiveLoop.Loop(config); } catch (ThreadInterruptedException e) { Log.InfoException(e); } - catch (ThreadAbortException e) { Log.InfoException(e); } + catch (ThreadAbortException) { Log.Error("[SWT-WebSocketClientStandAlone]: Thread Abort Exception"); } catch (Exception e) { Log.Exception(e); } finally { @@ -120,15 +120,12 @@ void AfterConnectionDisposed(Connection conn) public override void Disconnect() { state = ClientState.Disconnecting; - Log.Info("Disconnect Called"); + Log.Verbose("[SWT-WebSocketClientStandAlone]: Disconnect Called"); + if (conn == null) - { state = ClientState.NotConnected; - } else - { conn?.Dispose(); - } } public override void Send(ArraySegment segment) diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs.meta new file mode 100644 index 0000000..8763cb2 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 05a9c87dea309e241a9185e5aa0d72ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/StandAlone/WebSocketClientStandAlone.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl.meta rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl.meta diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/SimpleWebJSLib.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/SimpleWebJSLib.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/SimpleWebJSLib.cs diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/SimpleWebJSLib.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/SimpleWebJSLib.cs.meta new file mode 100644 index 0000000..1a67ae2 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/SimpleWebJSLib.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 97b96a0b65c104443977473323c2ff35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/SimpleWebJSLib.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs similarity index 83% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs index ece94d6..d56435a 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/WebSocketClientWebGl.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs @@ -1,14 +1,40 @@ using System; using System.Collections.Generic; -using System.Linq; using AOT; namespace Mirror.SimpleWeb { +#if !UNITY_2021_3_OR_NEWER + + // Unity 2019 doesn't have ArraySegment.ToArray() yet. + public static class Extensions + { + public static byte[] ToArray(this ArraySegment segment) + { + byte[] array = new byte[segment.Count]; + Array.Copy(segment.Array, segment.Offset, array, 0, segment.Count); + return array; + } + } + +#endif + public class WebSocketClientWebGl : SimpleWebClient { static readonly Dictionary instances = new Dictionary(); + [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(); + /// /// key for instances sent between c# and js /// @@ -23,6 +49,8 @@ public class WebSocketClientWebGl : SimpleWebClient /// Queue ConnectingSendQueue; + public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index); + internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick) { #if !UNITY_WEBGL || UNITY_EDITOR @@ -30,8 +58,6 @@ internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base #endif } - public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index); - public override void Connect(Uri serverAddress) { index = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback); @@ -50,7 +76,7 @@ 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}"); + Log.Error("[SWT-WebSocketClientWebGl]: Cant send message with length {0} because it is over the max size of {1}", segment.Count, maxMessageSize); return; } @@ -58,10 +84,9 @@ public override void Send(ArraySegment segment) { SimpleWebJSLib.Send(index, segment.Array, segment.Offset, segment.Count); } - else + else if (ConnectingSendQueue == null) { - if (ConnectingSendQueue == null) - ConnectingSendQueue = new Queue(); + ConnectingSendQueue = new Queue(); ConnectingSendQueue.Enqueue(segment.ToArray()); } } @@ -78,6 +103,7 @@ void onOpen() byte[] next = ConnectingSendQueue.Dequeue(); SimpleWebJSLib.Send(index, next, 0, next.Length); } + ConnectingSendQueue = null; } } @@ -102,7 +128,7 @@ void onMessage(IntPtr bufferPtr, int count) } catch (Exception e) { - Log.Error($"onData {e.GetType()}: {e.Message}\n{e.StackTrace}"); + Log.Error("[SWT-WebSocketClientWebGl]: onMessage {0}: {1}\n{2}", e.GetType(), e.Message, e.StackTrace); receiveQueue.Enqueue(new Message(e)); } } @@ -112,17 +138,5 @@ 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/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs.meta new file mode 100644 index 0000000..f285822 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 015c5b1915fd1a64cbe36444d16b2f7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/WebSocketClientWebGl.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin.meta rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin.meta diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib similarity index 75% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib index 02e6b93..f0ab1ac 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib @@ -1,62 +1,69 @@ // this will create a global object -const SimpleWeb = { +const SimpleWeb = +{ webSockets: [], next: 1, - GetWebSocket: function (index) { + GetWebSocket: function (index) + { return SimpleWeb.webSockets[index] }, - AddNextSocket: function (webSocket) { + AddNextSocket: function (webSocket) + { var index = SimpleWeb.next; SimpleWeb.next++; SimpleWeb.webSockets[index] = webSocket; return index; }, - RemoveSocket: function (index) { + RemoveSocket: function (index) + { SimpleWeb.webSockets[index] = undefined; }, }; -function IsConnected(index) { +function IsConnected(index) +{ var webSocket = SimpleWeb.GetWebSocket(index); - if (webSocket) { + if (webSocket) return webSocket.readyState === webSocket.OPEN; - } - else { + else return false; - } } -function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) { +function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackPtr, errorCallbackPtr) +{ // fix for unity 2021 because unity bug in .jslib - if (typeof Runtime === "undefined") { + 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 - } + var Runtime = { dynCall: dynCall } } const address = UTF8ToString(addressPtr); console.log("Connecting to " + address); + // Create webSocket connection. - webSocket = new WebSocket(address); + var webSocket = new WebSocket(address); webSocket.binaryType = 'arraybuffer'; + const index = SimpleWeb.AddNextSocket(webSocket); // Connection opened - webSocket.addEventListener('open', function (event) { + webSocket.onopen = function(event) + { console.log("Connected to " + address); Runtime.dynCall('vi', openCallbackPtr, [index]); - }); - webSocket.addEventListener('close', function (event) { + }; + + webSocket.onclose = function(event) + { console.log("Disconnected from " + address); Runtime.dynCall('vi', closeCallBackPtr, [index]); - }); + }; - // Listen for messages - webSocket.addEventListener('message', function (event) { + webSocket.onmessage = function(event) + { if (event.data instanceof ArrayBuffer) { - // TODO dont alloc each time var array = new Uint8Array(event.data); var arrayLength = array.length; @@ -67,32 +74,33 @@ function Connect(addressPtr, openCallbackPtr, closeCallBackPtr, messageCallbackP Runtime.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]); _free(bufferPtr); } - else { + else + { console.error("message type not supported") } - }); + }; - webSocket.addEventListener('error', function (event) { + webSocket.onerror = function(event) + { console.error('Socket Error', event); - Runtime.dynCall('vi', errorCallbackPtr, [index]); - }); + }; return index; } function Disconnect(index) { var webSocket = SimpleWeb.GetWebSocket(index); - if (webSocket) { + 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) { + if (webSocket) + { const start = arrayPtr + offset; const end = start + length; const data = HEAPU8.buffer.slice(start, end); @@ -102,13 +110,14 @@ function Send(index, arrayPtr, offset, length) { return false; } - -const SimpleWebLib = { +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/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib.meta similarity index 76% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib.meta rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib.meta index cc1319e..ab1faeb 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Client/Webgl/plugin/SimpleWeb.jslib.meta +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib.meta @@ -35,3 +35,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/Webgl/plugin/SimpleWeb.jslib + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common.meta rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common.meta diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs similarity index 81% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs index 4262feb..7b9fed2 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/BufferPool.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs @@ -60,10 +60,10 @@ 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)); + 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); } @@ -75,7 +75,8 @@ public void CopyFrom(ArraySegment segment) 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)); + 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); @@ -83,24 +84,20 @@ public void CopyFrom(byte[] source, int offset, int 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)); + 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); - } + public ArraySegment ToSegment() => 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"); - } + Log.Error("[SWT-ArrayBuffer]: Buffer that was returned had an array of the wrong size"); } } @@ -124,12 +121,10 @@ public ArrayBuffer Take() { IncrementCreated(); if (buffers.TryDequeue(out ArrayBuffer buffer)) - { return buffer; - } else { - Log.Verbose($"BufferBucket({arraySize}) create new"); + Log.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) create new"); return new ArrayBuffer(this, arraySize); } } @@ -145,13 +140,14 @@ public void Return(ArrayBuffer buffer) void IncrementCreated() { int next = Interlocked.Increment(ref _current); - Log.Verbose($"BufferBucket({arraySize}) count:{next}"); + Log.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) count:{next}"); } + [Conditional("DEBUG")] void DecrementCreated() { int next = Interlocked.Decrement(ref _current); - Log.Verbose($"BufferBucket({arraySize}) count:{next}"); + Log.Flood($"[SWT-BufferBucket]: BufferBucket({arraySize}) count:{next}"); } } @@ -186,17 +182,13 @@ public BufferPool(int bucketCount, int smallest, int largest) 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); @@ -208,16 +200,15 @@ public BufferPool(int bucketCount, int smallest, int largest) buckets[i] = new BufferBucket((int)Math.Ceiling(size)); } - Validate(); // Example - // 5 count + // 5 count // 20 smallest // 16400 largest // 3.0 log 20 - // 9.7 log 16400 + // 9.7 log 16400 // 6.7 range 9.7 - 3 // 1.675 each 6.7 / (5-1) @@ -235,31 +226,24 @@ public BufferPool(int bucketCount, int smallest, int largest) void Validate() { if (buckets[0].arraySize != smallest) - { - Log.Error($"BufferPool Failed to create bucket for smallest. bucket:{buckets[0].arraySize} smallest{smallest}"); - } + Log.Error("[SWT-BufferPool]: BufferPool Failed to create bucket for smallest. bucket:{0} smallest:{1}", buckets[0].arraySize, 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}"); - } + Log.Error("[SWT-BufferPool]: BufferPool Failed to create bucket for largest. bucket:{0} smallest:{1}", largestBucket, largest); } public ArrayBuffer Take(int size) { - if (size > largest) { throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); } + if (size > largest) + throw new ArgumentException($"Size ({size}) is greater than 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})"); + throw new ArgumentException($"Size ({size}) is greater than largest ({largest})"); } } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs.meta new file mode 100644 index 0000000..969cd5b --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 94ae50f3ec35667469b861b12cd72f92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/BufferPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs new file mode 100644 index 0000000..6cbe174 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Mirror.SimpleWeb +{ + internal sealed class Connection : IDisposable + { + readonly object disposedLock = new object(); + + public const int IdNotSet = -1; + public TcpClient client; + public int connId = IdNotSet; + + /// + /// Connect request, sent from client to start handshake + /// Only valid on server + /// + public Request request; + /// + /// RemoteEndpoint address or address from request header + /// Only valid on server + /// + public string remoteAddress; + + 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("[SWT-Connection]: Dispose {0}", ToString()); + + // check hasDisposed first to stop ThreadInterruptedException on lock + if (hasDisposed) return; + + Log.Verbose("[SWT-Connection]: Connection Close: {0}", 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() + { + // remoteAddress isn't set until after handshake + if (hasDisposed) + return $"[Conn:{connId}, Disposed]"; + else if (!string.IsNullOrWhiteSpace(remoteAddress)) + return $"[Conn:{connId}, endPoint:{remoteAddress}]"; + else + try + { + EndPoint endpoint = client?.Client?.RemoteEndPoint; + return $"[Conn:{connId}, endPoint:{endpoint}]"; + } + catch (SocketException) + { + return $"[Conn:{connId}, endPoint:n/a]"; + } + } + + /// + /// Gets the address based on the and RemoteEndPoint + /// Called after ServerHandShake is accepted + /// + internal string CalculateAddress() + { + if (request.Headers.TryGetValue("X-Forwarded-For", out string forwardFor)) + { + string actualClientIP = forwardFor.ToString().Split(',').First(); + // Remove the port number from the address + return actualClientIP.Split(':').First(); + } + else + { + IPEndPoint ipEndPoint = (IPEndPoint)client.Client.RemoteEndPoint; + IPAddress ipAddress = ipEndPoint.Address; + if (ipAddress.IsIPv4MappedToIPv6) + ipAddress = ipAddress.MapToIPv4(); + + return ipAddress.ToString(); + } + } + } +} diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs.meta new file mode 100644 index 0000000..24dd794 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a13073c2b49d39943888df45174851bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Connection.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs similarity index 99% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs index 3aa16c3..95d8cbb 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Constants.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs @@ -59,7 +59,6 @@ internal static class Constants /// public const int UlongPayloadLength = 127; - /// /// Guid used for WebSocket Protocol /// diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs.meta new file mode 100644 index 0000000..4bfe4b7 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 85d110a089d6ad348abf2d073ebce7cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Constants.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/EventType.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/EventType.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/EventType.cs diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/EventType.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/EventType.cs.meta new file mode 100644 index 0000000..60addeb --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/EventType.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 2d9cd7d2b5229ab42a12e82ae17d0347 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/EventType.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs new file mode 100644 index 0000000..fe0c4ec --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs @@ -0,0 +1,270 @@ +using System; +using UnityEngine; +using Conditional = System.Diagnostics.ConditionalAttribute; + +namespace Mirror.SimpleWeb +{ + public static class Log + { + // The.NET console color names map to the following approximate CSS color names: + + // Black: Black + // Blue: Blue + // Cyan: Aqua or Cyan + // DarkBlue: DarkBlue + // DarkCyan: DarkCyan + // DarkGray: DarkGray + // DarkGreen: DarkGreen + // DarkMagenta: DarkMagenta + // DarkRed: DarkRed + // DarkYellow: DarkOrange or DarkGoldenRod + // Gray: Gray + // Green: Green + // Magenta: Magenta + // Red: Red + // White: White + // Yellow: Yellow + + // We can't use colors that are close to white or black because + // they won't show up well in the server console or browser console + + public enum Levels + { + Flood, + Verbose, + Info, + Warn, + Error, + None + } + + public static ILogger logger = Debug.unityLogger; + public static Levels minLogLevel = Levels.None; + + /// + /// Logs all exceptions to console + /// + /// Exception to log + public static void Exception(Exception e) + { +#if UNITY_SERVER || UNITY_WEBGL + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"[SWT:Exception] {e.GetType().Name}: {e.Message}\n{e.StackTrace}\n\n"); + Console.ResetColor(); +#else + logger.Log(LogType.Exception, $"[SWT:Exception] {e.GetType().Name}: {e.Message}\n{e.StackTrace}\n\n"); +#endif + } + + /// + /// Logs flood to console if minLogLevel is set to Flood or lower + /// + /// Message text to log + [Conditional("DEBUG")] + public static void Flood(string msg) + { + if (minLogLevel > Levels.Flood) return; + +#if UNITY_SERVER || UNITY_WEBGL + Console.ForegroundColor = ConsoleColor.Gray; + logger.Log(LogType.Log, msg.Trim()); + Console.ResetColor(); +#else + logger.Log(LogType.Log, msg.Trim()); +#endif + } + + /// + /// Logs buffer to console if minLogLevel is set to Flood or lower + /// Debug mode requrired, e.g. Unity Editor of Develpment Build + /// + /// Source of the log message + /// Byte array to be logged + /// starting point of byte array + /// number of bytes to read + [Conditional("DEBUG")] + public static void DumpBuffer(string label, byte[] buffer, int offset, int length) + { + if (minLogLevel > Levels.Flood) return; + +#if UNITY_SERVER || UNITY_WEBGL + Console.ForegroundColor = ConsoleColor.DarkBlue; + logger.Log(LogType.Log, $"{label}: {BufferToString(buffer, offset, length)}"); + Console.ResetColor(); +#else + logger.Log(LogType.Log, $"{label}: {BufferToString(buffer, offset, length)}"); +#endif + } + + /// + /// Logs buffer to console if minLogLevel is set to Flood or lower + /// Debug mode requrired, e.g. Unity Editor of Develpment Build + /// + /// Source of the log message + /// ArrayBuffer to show details for + [Conditional("DEBUG")] + public static void DumpBuffer(string label, ArrayBuffer arrayBuffer) + { + if (minLogLevel > Levels.Flood) return; + +#if UNITY_SERVER || UNITY_WEBGL + Console.ForegroundColor = ConsoleColor.DarkBlue; + logger.Log(LogType.Log, $"{label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}"); + Console.ResetColor(); +#else + logger.Log(LogType.Log, $"{label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}"); +#endif + } + + /// + /// Logs verbose to console if minLogLevel is set to Verbose or lower + /// + /// Message text to log + public static void Verbose(string msg) + { + if (minLogLevel > Levels.Verbose) return; + +#if DEBUG + // Debug builds and Unity Editor + logger.Log(LogType.Log, msg.Trim()); +#else + // Server or WebGL + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(msg.Trim()); + Console.ResetColor(); +#endif + } + + public static void Verbose(string msg, T arg1) + { + if (minLogLevel > Levels.Verbose) return; + Verbose(String.Format(msg, arg1)); + } + + public static void Verbose(string msg, T1 arg1, T2 arg2) + { + if (minLogLevel > Levels.Verbose) return; + Verbose(String.Format(msg, arg1, arg2)); + } + + /// + /// Logs info to console if minLogLevel is set to Info or lower + /// + /// Message text to log + /// Default Cyan works in server and browser consoles + static void Info(string msg, ConsoleColor consoleColor = ConsoleColor.Cyan) + { +#if DEBUG + // Debug builds and Unity Editor + logger.Log(LogType.Log, msg.Trim()); +#else + // Server or WebGL + Console.ForegroundColor = consoleColor; + Console.WriteLine(msg.Trim()); + Console.ResetColor(); +#endif + } + + public static void Info(string msg, T arg1, ConsoleColor consoleColor = ConsoleColor.Cyan) + { + if (minLogLevel > Levels.Info) return; + Info(String.Format(msg, arg1), consoleColor); + } + + public static void Info(string msg, T1 arg1, T2 arg2, ConsoleColor consoleColor = ConsoleColor.Cyan) + { + if (minLogLevel > Levels.Info) return; + Info(String.Format(msg, arg1, arg2), consoleColor); + } + + /// + /// Logs info to console if minLogLevel is set to Info or lower + /// + /// Exception to log + public static void InfoException(Exception e) + { + if (minLogLevel > Levels.Info) return; + +#if DEBUG + // Debug builds and Unity Editor + logger.Log(LogType.Exception, e.Message); +#else + // Server or WebGL + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine(e.Message); + Console.ResetColor(); +#endif + } + + /// + /// Logs info to console if minLogLevel is set to Warn or lower + /// + /// Message text to log + public static void Warn(string msg) + { + if (minLogLevel > Levels.Warn) return; + +#if DEBUG + // Debug builds and Unity Editor + logger.Log(LogType.Warning, msg.Trim()); +#else + // Server or WebGL + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(msg.Trim()); + Console.ResetColor(); +#endif + } + + public static void Warn(string msg, T arg1) + { + if (minLogLevel > Levels.Warn) return; + Warn(String.Format(msg, arg1)); + } + + /// + /// Logs info to console if minLogLevel is set to Error or lower + /// + /// Message text to log + public static void Error(string msg) + { + if (minLogLevel > Levels.Error) return; + +#if DEBUG + // Debug builds and Unity Editor + logger.Log(LogType.Error, msg.Trim()); +#else + // Server or WebGL + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(msg.Trim()); + Console.ResetColor(); +#endif + } + + public static void Error(string msg, T arg1) + { + if (minLogLevel > Levels.Error) return; + Error(String.Format(msg, arg1)); + } + + public static void Error(string msg, T1 arg1, T2 arg2) + { + if (minLogLevel > Levels.Error) return; + Error(String.Format(msg, arg1, arg2)); + } + + public static void Error(string msg, T1 arg1, T2 arg2, T3 arg3) + { + if (minLogLevel > Levels.Error) return; + Error(String.Format(msg, arg1, arg2, arg3)); + } + + /// + /// Returns a string representation of the byte array starting from offset for length bytes + /// + /// Byte array to read + /// starting point in the byte array + /// number of bytes to read from offset + /// + public static string BufferToString(byte[] buffer, int offset = 0, int? length = null) => BitConverter.ToString(buffer, offset, length ?? buffer.Length); + } +} diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs.meta new file mode 100644 index 0000000..b2ec7c8 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3cf1521098e04f74fbea0fe2aa0439f8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Message.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Message.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Message.cs diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Message.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Message.cs.meta new file mode 100644 index 0000000..e1e48a7 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Message.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: f5d05d71b09d2714b96ffe80bc3d2a77 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Message.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs similarity index 92% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs index 59c9326..e3269f3 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/MessageProcessor.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs @@ -13,29 +13,21 @@ public static class MessageProcessor 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; - } + public static int GetOpcode(byte[] buffer) => buffer[0] & 0b0000_1111; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetPayloadLength(byte[] buffer) - { - byte lenByte = FirstLengthByte(buffer); - return GetMessageLength(buffer, 0, lenByte); - } + public static int GetPayloadLength(byte[] buffer) => GetMessageLength(buffer, 0, FirstLengthByte(buffer)); /// /// Has full message been sent @@ -43,10 +35,7 @@ public static int GetPayloadLength(byte[] buffer) /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Finished(byte[] buffer) - { - return (buffer[0] & 0b1000_0000) != 0; - } + public static bool Finished(byte[] buffer) => (buffer[0] & 0b1000_0000) != 0; public static void ValidateHeader(byte[] buffer, int maxLength, bool expectMask, bool opCodeContinuation = false) { @@ -114,9 +103,8 @@ static int GetMessageLength(byte[] buffer, int offset, byte lenByte) 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 @@ -130,9 +118,7 @@ static int GetMessageLength(byte[] buffer, int offset, byte lenByte) static void ThrowIfMaskNotExpected(bool hasMask, bool expectMask) { if (hasMask != expectMask) - { throw new InvalidDataException($"Message expected mask to be {expectMask} but was {hasMask}"); - } } /// @@ -174,9 +160,7 @@ static void ThrowIfBadOpCode(int opcode, bool finished, bool opCodeContinuation) static void ThrowIfLengthZero(int msglen) { if (msglen == 0) - { throw new InvalidDataException("Message length was zero"); - } } /// @@ -185,9 +169,7 @@ static void ThrowIfLengthZero(int msglen) 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/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs.meta new file mode 100644 index 0000000..a81315e --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4c1f218a2b16ca846aaf23260078e549 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/MessageProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs similarity index 88% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs index 74cbf2d..a56edfd 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReadHelper.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs @@ -20,9 +20,8 @@ public static int Read(Stream stream, byte[] outBuffer, int outOffset, int lengt { int read = stream.Read(outBuffer, outOffset + received, length - received); if (read == 0) - { - throw new ReadHelperException("returned 0"); - } + throw new ReadHelperException("[SWT-ReadHelper]: Read returned 0"); + received += read; } } @@ -36,9 +35,7 @@ public static int Read(Stream stream, byte[] outBuffer, int outOffset, int lengt } if (received != length) - { - throw new ReadHelperException("returned not equal to length"); - } + throw new ReadHelperException("[SWT-ReadHelper]: received not equal to length"); return outOffset + received; } @@ -83,7 +80,7 @@ public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int l if (read >= maxLength) { - Log.Error("SafeReadTillMatch exceeded maxLength"); + Log.Error("[SWT-ReadHelper]: SafeReadTillMatch exceeded maxLength"); return null; } @@ -96,15 +93,11 @@ public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int l 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) @@ -125,8 +118,6 @@ public class ReadHelperException : Exception { public ReadHelperException(string message) : base(message) { } - protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } + protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs.meta new file mode 100644 index 0000000..9be22a9 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 9f4fa5d324e708c46a55810a97de75bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReadHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs similarity index 86% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs index 952592c..b8d1929 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/ReceiveLoop.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs @@ -60,11 +60,9 @@ public static void Loop(Config config) TcpClient client = conn.client; while (client.Connected) - { ReadOneMessage(config, readBuffer); - } - Log.Info($"{conn} Not Connected"); + Log.Verbose("[SWT-ReceiveLoop]: {0} Not Connected", conn); } catch (Exception) { @@ -74,27 +72,24 @@ public static void Loop(Config config) } } catch (ThreadInterruptedException e) { Log.InfoException(e); } - catch (ThreadAbortException e) { Log.InfoException(e); } + catch (ThreadAbortException) { Log.Error("[SWT-ReceiveLoop]: Thread Abort Exception"); } catch (ObjectDisposedException e) { Log.InfoException(e); } - catch (ReadHelperException 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); + Log.Warn("[SWT-ReceiveLoop]: ReceiveLoop SocketException\n{0}", e.Message); queue.Enqueue(new Message(conn.connId, e)); } catch (IOException e) { // this could happen if client disconnects - Log.Warn($"ReceiveLoop IOException\n{e.Message}", false); + Log.Warn("[SWT-ReceiveLoop]: ReceiveLoop IOException\n{0}", e.Message); queue.Enqueue(new Message(conn.connId, e)); } catch (InvalidDataException e) { - Log.Error($"Invalid data from {conn}: {e.Message}"); + Log.Error("[SWT-ReceiveLoop]: Invalid data from {0}\n{1}\n{2}\n\n", conn, e.Message, e.StackTrace); queue.Enqueue(new Message(conn.connId, e)); } catch (Exception e) @@ -105,7 +100,6 @@ public static void Loop(Config config) finally { Profiler.EndThreadProfiling(); - conn.Dispose(); } } @@ -151,7 +145,6 @@ static void ReadOneMessage(Config config, byte[] buffer) MessageProcessor.ThrowIfMsgLengthTooLong(totalSize, maxMessageSize); } - ArrayBuffer msg = bufferPool.Take(totalSize); msg.count = 0; while (fragments.Count > 0) @@ -165,7 +158,7 @@ static void ReadOneMessage(Config config, byte[] buffer) } // dump after mask off - Log.DumpBuffer($"Message", msg); + Log.DumpBuffer($"[SWT-ReceiveLoop]: Message", msg); queue.Enqueue(new Message(conn.connId, msg)); } @@ -180,31 +173,25 @@ static Header ReadHeader(Config config, byte[] buffer, bool opCodeContinuation = // read 2 header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.HeaderMinSize); // log after first blocking call - Log.Verbose($"Message From {conn}"); + Log.Flood($"[SWT-ReceiveLoop]: 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); + Log.DumpBuffer($"[SWT-ReceiveLoop]: 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}"); + Log.Flood($"[SWT-ReceiveLoop]: Header ln:{header.payloadLength} op:{header.opcode} mask:{expectMask}"); return header; } @@ -216,7 +203,7 @@ static void HandleArrayMessage(Config config, byte[] buffer, int msgOffset, int ArrayBuffer arrayBuffer = CopyMessageToBuffer(bufferPool, expectMask, buffer, msgOffset, payloadLength); // dump after mask off - Log.DumpBuffer($"Message", arrayBuffer); + Log.DumpBuffer($"[SWT-ReceiveLoop]: Message", arrayBuffer); queue.Enqueue(new Message(conn.connId, arrayBuffer)); } @@ -232,9 +219,7 @@ static ArrayBuffer CopyMessageToBuffer(BufferPool bufferPool, bool expectMask, b MessageProcessor.ToggleMask(buffer, msgOffset, arrayBuffer, payloadLength, buffer, maskOffset); } else - { arrayBuffer.CopyFrom(buffer, msgOffset, payloadLength); - } return arrayBuffer; } @@ -250,21 +235,16 @@ static void HandleCloseMessage(Config config, byte[] buffer, int msgOffset, int } // dump after mask off - Log.DumpBuffer($"Message", buffer, msgOffset, payloadLength); - - Log.Info($"Close: {GetCloseCode(buffer, msgOffset)} message:{GetCloseMessage(buffer, msgOffset, payloadLength)}"); + Log.DumpBuffer($"[SWT-ReceiveLoop]: Message", buffer, msgOffset, payloadLength); + Log.Verbose("[SWT-ReceiveLoop]: Close: {0} message:{1}", GetCloseCode(buffer, msgOffset), GetCloseMessage(buffer, msgOffset, payloadLength)); conn.Dispose(); } static string GetCloseMessage(byte[] buffer, int msgOffset, int payloadLength) - { - return Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2); - } + => Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2); static int GetCloseCode(byte[] buffer, int msgOffset) - { - return buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1]; - } + => buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1]; } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs.meta new file mode 100644 index 0000000..43032d9 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a26c2815f58431c4a98c158c8b655ffd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/ReceiveLoop.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs new file mode 100644 index 0000000..c062faa --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Mirror.SimpleWeb +{ + /// + /// Represents a client's request to the Websockets server, which is the first message from the client. + /// + public class Request + { + static readonly char[] lineSplitChars = new char[] { '\r', '\n' }; + static readonly char[] headerSplitChars = new char[] { ':' }; + public string RequestLine; + public Dictionary Headers = new Dictionary(); + + public Request(string message) + { + string[] all = message.Split(lineSplitChars, StringSplitOptions.RemoveEmptyEntries); + RequestLine = all.First(); + Headers = all.Skip(1) + .Select(header => header.Split(headerSplitChars, 2, StringSplitOptions.RemoveEmptyEntries)) + .ToDictionary(split => split[0].Trim(), split => split[1].Trim()); + } + } +} diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs.meta new file mode 100644 index 0000000..7ad37bf --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 50b41ad63d4956a42a073bad5158fe09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs similarity index 93% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs index 6dc1b1b..d701aad 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/SendLoop.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs @@ -59,9 +59,8 @@ public static void Loop(Config config) conn.sendPending.Wait(); // wait for 1ms for mirror to send other messages if (SendLoopConfig.sleepBeforeSend) - { Thread.Sleep(1); - } + conn.sendPending.Reset(); if (SendLoopConfig.batchSend) @@ -72,7 +71,7 @@ public static void Loop(Config config) // check if connected before sending message if (!client.Connected) { - Log.Info($"SendLoop {conn} not connected"); + Log.Verbose("[SWT-SendLoop]: SendLoop {0} not connected", conn); msg.Release(); return; } @@ -102,7 +101,7 @@ public static void Loop(Config config) // check if connected before sending message if (!client.Connected) { - Log.Info($"SendLoop {conn} not connected"); + Log.Verbose("[SWT-SendLoop]: SendLoop {0} not connected", conn); msg.Release(); return; } @@ -114,14 +113,11 @@ public static void Loop(Config config) } } - Log.Info($"{conn} Not Connected"); + Log.Verbose("[SWT-SendLoop]: {0} Not Connected", conn); } catch (ThreadInterruptedException e) { Log.InfoException(e); } - catch (ThreadAbortException e) { Log.InfoException(e); } - catch (Exception e) - { - Log.Exception(e); - } + catch (ThreadAbortException) { Log.Error("[SWT-SendLoop]: Thread Abort Exception"); } + catch (Exception e) { Log.Exception(e); } finally { Profiler.EndThreadProfiling(); @@ -145,7 +141,7 @@ static int SendMessage(byte[] buffer, int startOffset, ArrayBuffer msg, bool set offset += msgLength; // dump before mask on - Log.DumpBuffer("Send", buffer, startOffset, offset); + Log.DumpBuffer("[SWT-SendLoop]: Send", buffer, startOffset, offset); if (setMask) { @@ -194,9 +190,7 @@ public static int WriteHeader(byte[] buffer, int startOffset, int msgLength, boo } if (setMask) - { buffer[startOffset + 1] |= 0b1000_0000; - } return sendLength + startOffset; } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs.meta new file mode 100644 index 0000000..ebeb07f --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: f87dd81736d9c824db67f808ac71841d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/SendLoop.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/TcpConfig.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/TcpConfig.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/TcpConfig.cs diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/TcpConfig.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/TcpConfig.cs.meta new file mode 100644 index 0000000..03014bd --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/TcpConfig.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 81ac8d35f28fab14b9edda5cd9d4fc86 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/TcpConfig.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Utils.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs.meta new file mode 100644 index 0000000..ee139a3 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4643ffb4cb0562847b1ae925d07e15b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Utils.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE.meta new file mode 100644 index 0000000..0a1d659 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 0a0cf751b4a201242ac60b4adbc54657 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt.meta new file mode 100644 index 0000000..c4d049f --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 0e3971d5783109f4d9ce93c7a689d701 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server.meta rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server.meta diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs new file mode 100644 index 0000000..c80730c --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs @@ -0,0 +1,156 @@ +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 = "\r\nSec-WebSocket-Key: "; + // this isn't an official max, just a reasonable size for a websocket handshake + readonly int maxHttpHeaderSize = 3000; + + // SHA-1 is the websocket standard: + // https://www.rfc-editor.org/rfc/rfc6455 + // we should follow the standard, even though SHA1 is considered weak: + // https://stackoverflow.com/questions/38038841/why-is-sha-1-considered-insecure + 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("[SWT-ServerHandshake]: First bytes from client was not 'GET' for handshake, instead was {0}", Log.BufferToString(getHeader.array, 0, GetSize)); + return false; + } + } + + string msg = ReadToEndForHandshake(stream); + + if (string.IsNullOrEmpty(msg)) + return false; + + try + { + AcceptHandshake(stream, msg); + + conn.request = new Request(msg); + conn.remoteAddress = conn.CalculateAddress(); + Log.Info($"[SWT-ServerHandshake]: A client connected from {0}", conn); + + 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); + // GET isn't in the bytes we read here, so we need to add it back + msg = $"GET{msg}"; + Log.Verbose("[SWT-ServerHandshake]: Client Handshake Message:\r\n{0}", 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, StringComparison.InvariantCultureIgnoreCase) + KeyHeaderString.Length; + + Log.Verbose("[SWT-ServerHandshake]: Handshake Key: {0}", 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("[SWT-ServerHandshake]: Handshake Hashing {0}", 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("[SWT-ServerHandshake]: Handshake Response length {0}, IsExpected {1}", message.Length, message.Length == ResponseLength); + Encoding.ASCII.GetBytes(message, 0, ResponseLength, responseBuffer, 0); + } + } +} diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs.meta new file mode 100644 index 0000000..857acbb --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6268509ac4fb48141b9944c03295da11 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerHandshake.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs similarity index 83% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs index de6c022..b2a959d 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/ServerSslHelper.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs @@ -31,7 +31,10 @@ public ServerSslHelper(SslConfig sslConfig) { config = sslConfig; if (config.enabled) + { certificate = new X509Certificate2(config.certPath, config.certPassword); + Log.Info($"[SWT-ServerSslHelper]: SSL Certificate {0} loaded with expiration of {1}", certificate.Subject, certificate.GetExpirationDateString()); + } } internal bool TryCreateStream(Connection conn) @@ -46,7 +49,7 @@ internal bool TryCreateStream(Connection conn) } catch (Exception e) { - Log.Error($"Create SSLStream Failed: {e}", false); + Log.Error("[SWT-ServerSslHelper]: Create SSLStream Failed: {0}", e.Message); return false; } } @@ -65,10 +68,7 @@ Stream CreateStream(NetworkStream stream) return sslStream; } - bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - // always accept client - return true; - } + // always accept client + bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => true; } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs.meta new file mode 100644 index 0000000..8824406 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 11061fee528ebdd43817a275b1e4a317 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/ServerSslHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs similarity index 85% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs index 05cf996..9e4edc3 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/SimpleWebServer.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs @@ -6,28 +6,26 @@ namespace Mirror.SimpleWeb { public class SimpleWebServer { - readonly int maxMessagesPerTick; + public event Action onConnect; + public event Action onDisconnect; + public event Action> onData; + public event Action onError; + readonly int maxMessagesPerTick; readonly WebSocketServer server; readonly BufferPool bufferPool; + public bool Active { get; private set; } + 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); @@ -48,28 +46,21 @@ public void SendAll(List connectionIds, ArraySegment source) // 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 bool KickClient(int connectionId) => server.CloseConnection(connectionId); - public string GetClientAddress(int connectionId) - { - return server.GetClientAddress(connectionId); - } + public string GetClientAddress(int connectionId) => server.GetClientAddress(connectionId); + + public Request GetClientRequest(int connectionId) => server.GetClientRequest(connectionId); /// /// Processes all new messages @@ -100,7 +91,7 @@ public void ProcessMessageQueue(MonoBehaviour behaviour) switch (next.type) { case EventType.Connected: - onConnect?.Invoke(next.connId); + onConnect?.Invoke(next.connId, GetClientAddress(next.connId)); break; case EventType.Data: onData?.Invoke(next.connId, next.data.ToSegment()); @@ -114,6 +105,11 @@ public void ProcessMessageQueue(MonoBehaviour behaviour) break; } } + + if (server.receiveQueue.Count > 0) + { + Log.Warn("[SWT-SimpleWebServer]: ProcessMessageQueue has {0} remaining.", server.receiveQueue.Count); + } } } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs.meta new file mode 100644 index 0000000..0f894ce --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: bd51d7896f55a5e48b41a4b526562b0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/SimpleWebServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs similarity index 77% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs index da4f402..33a333d 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Server/WebSocketServer.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs @@ -21,7 +21,6 @@ public class WebSocketServer readonly BufferPool bufferPool; readonly ConcurrentDictionary connections = new ConcurrentDictionary(); - int _idCounter = 0; public WebSocketServer(TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig, BufferPool bufferPool) @@ -38,7 +37,7 @@ public void Listen(int port) listener = TcpListener.Create(port); listener.Start(); - Log.Info($"Server has started on port {port}"); + Log.Verbose("[SWT-WebSocketServer]: Server Started on {0}", port); acceptThread = new Thread(acceptLoop); acceptThread.IsBackground = true; @@ -54,13 +53,12 @@ public void Stop() listener?.Stop(); acceptThread = null; - Log.Info("Server stopped, Closing all connections..."); + Log.Verbose("[SWT-WebSocketServer]: Server stopped...closing all connections."); + // make copy so that foreach doesn't break if values are removed Connection[] connectionsCopy = connections.Values.ToArray(); foreach (Connection conn in connectionsCopy) - { conn.Dispose(); - } connections.Clear(); } @@ -76,12 +74,11 @@ void acceptLoop() 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}"); + Log.Verbose("[SWT-WebSocketServer]: A client connected from {0}", conn); // handshake needs its own thread as it needs to wait for message from client Thread receiveThread = new Thread(() => HandshakeAndReceiveLoop(conn)); @@ -100,7 +97,7 @@ void acceptLoop() } } catch (ThreadInterruptedException e) { Log.InfoException(e); } - catch (ThreadAbortException e) { Log.InfoException(e); } + catch (ThreadAbortException) { Log.Error("[SWT-WebSocketServer]: Thread Abort Exception"); } catch (Exception e) { Log.Exception(e); } } @@ -111,7 +108,7 @@ void HandshakeAndReceiveLoop(Connection conn) bool success = sslHelper.TryCreateStream(conn); if (!success) { - Log.Error($"Failed to create SSL Stream {conn}"); + Log.Warn("[SWT-WebSocketServer]: Failed to create SSL Stream {0}", conn); conn.Dispose(); return; } @@ -119,12 +116,10 @@ void HandshakeAndReceiveLoop(Connection conn) success = handShake.TryHandshake(conn); if (success) - { - Log.Info($"Sent Handshake {conn}"); - } + Log.Verbose("[SWT-WebSocketServer]: Sent Handshake {0}, false", conn); else { - Log.Error($"Handshake Failed {conn}"); + Log.Warn("[SWT-WebSocketServer]: Handshake Failed {0}", conn); conn.Dispose(); return; } @@ -132,7 +127,7 @@ void HandshakeAndReceiveLoop(Connection conn) // check if Stop has been called since accepting this client if (serverStopped) { - Log.Info("Server stops after successful handshake"); + Log.Warn("[SWT-WebSocketServer]: Server stopped after successful handshake"); return; } @@ -153,7 +148,7 @@ void HandshakeAndReceiveLoop(Connection conn) conn.sendThread = sendThread; sendThread.IsBackground = true; - sendThread.Name = $"SendLoop {conn.connId}"; + sendThread.Name = $"SendThread {conn.connId}"; sendThread.Start(); ReceiveLoop.Config receiveConfig = new ReceiveLoop.Config( @@ -166,7 +161,7 @@ void HandshakeAndReceiveLoop(Connection conn) ReceiveLoop.Loop(receiveConfig); } catch (ThreadInterruptedException e) { Log.InfoException(e); } - catch (ThreadAbortException e) { Log.InfoException(e); } + catch (ThreadAbortException) { Log.Error("[SWT-WebSocketServer]: Thread Abort Exception"); } catch (Exception e) { Log.Exception(e); } finally { @@ -192,38 +187,44 @@ public void Send(int id, ArrayBuffer buffer) conn.sendPending.Set(); } else - { - Log.Warn($"Cant send message to {id} because connection was not found in dictionary. Maybe it disconnected."); - } + Log.Warn("[SWT-WebSocketServer]: Cannot send message to {0} because connection was not found in dictionary. Maybe it disconnected.", id); } public bool CloseConnection(int id) { if (connections.TryGetValue(id, out Connection conn)) { - Log.Info($"Kicking connection {id}"); + Log.Info($"[SWT-WebSocketServer]: Disconnecting connection {0}", id); conn.Dispose(); return true; } else { - Log.Warn($"Failed to kick {id} because id not found"); - + Log.Warn("[SWT-WebSocketServer]: Failed to kick {0} because id not found.", id); return false; } } public string GetClientAddress(int id) { - if (connections.TryGetValue(id, out Connection conn)) + if (!connections.TryGetValue(id, out Connection conn)) { - return conn.client.Client.RemoteEndPoint.ToString(); + Log.Warn("[SWT-WebSocketServer]: Cannot get address of connection {0} because connection was not found in dictionary.", id); + return null; } - else + + return conn.remoteAddress; + } + + public Request GetClientRequest(int id) + { + if (!connections.TryGetValue(id, out Connection conn)) { - Log.Error($"Cant get address of connection {id} because connection was not found in dictionary"); + Log.Warn("[SWT-WebSocketServer]: Cannot get request of connection {0} because connection was not found in dictionary.", id); return null; } + + return conn.request; } } } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs.meta new file mode 100644 index 0000000..ced162a --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 5c434db044777d2439bae5a57d4e8ee7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Server/WebSocketServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef new file mode 100644 index 0000000..dfb91ea --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef @@ -0,0 +1,14 @@ +{ + "name": "SimpleWebTransport", + "rootNamespace": "", + "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/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef.meta new file mode 100644 index 0000000..a971bf8 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 3b5390adca4e2bb4791cb930316d6f3e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs similarity index 93% rename from Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs rename to Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs index 4baf8c5..9996793 100644 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SslConfigLoader.cs +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs @@ -36,15 +36,12 @@ internal static Cert LoadCertJson(string 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 + // password can also be empty if (string.IsNullOrEmpty(cert.password)) - { - // password can be empty cert.password = string.Empty; - } return cert; } diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs.meta new file mode 100644 index 0000000..f4fbd1c --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: dfdb6b97a48a48b498e563e857342da1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SslConfigLoader.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs new file mode 100644 index 0000000..9f25028 --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs @@ -0,0 +1,389 @@ +using System; +using System.Net; +using System.Security.Authentication; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Mirror.SimpleWeb +{ + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/websockets-transport")] + public class SimpleWebTransport : Transport, PortTransport + { + public const string NormalScheme = "ws"; + public const string SecureScheme = "wss"; + + [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; + + [FormerlySerializedAs("handshakeMaxSize")] + [Tooltip("Max size for http header send as handshake for websockets")] + public int maxHandshakeSize = 16 * 1024; + + [FormerlySerializedAs("serverMaxMessagesPerTick")] + [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 serverMaxMsgsPerTick = 10000; + + [FormerlySerializedAs("clientMaxMessagesPerTick")] + [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 clientMaxMsgsPerTick = 1000; + + [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("disables nagle algorithm. lowers CPU% and latency but increases bandwidth")] + public bool noDelay = true; + + [Header("Obsolete SSL settings")] + + [Tooltip("Requires wss connections on server, only to be used with SSL cert.json, never with reverse proxy.\nNOTE: if sslEnabled is true clientUseWss is forced true, even if not checked.")] + public bool sslEnabled; + + [Tooltip("Protocols that SSL certificate is created to support.")] + public SslProtocols sslProtocols = SslProtocols.Tls12; + + [Tooltip("Path to json file that contains path to cert and its password\nUse Json file so that cert password is not included in client builds\nSee Assets/Mirror/Transports/.cert.example.Json")] + public string sslCertJson = "./cert.json"; + + [Header("Server settings")] + + [Tooltip("Port to use for server")] + public ushort port = 27777; + public ushort Port + { + get + { +#if UNITY_WEBGL + if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort) + return clientWebsocketSettings.CustomClientPort; + else + return port; +#else + return port; +#endif + } + set + { +#if UNITY_WEBGL + if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort) + clientWebsocketSettings.CustomClientPort = value; + else + port = value; +#else + port = value; +#endif + } + } + + [Tooltip("Groups messages in queue before calling Stream.Send")] + public bool batchSend = true; + + [Tooltip("Waits for 1ms before grouping and sending messages.\n" + + "This gives time for mirror to finish adding message to queue so that less groups need to be made.\n" + + "If WaitBeforeSend is true then BatchSend Will also be set to true")] + public bool waitBeforeSend = true; + + [Header("Client settings")] + + [Tooltip("Sets connect scheme to wss. Useful when client needs to connect using wss when TLS is outside of transport.\nNOTE: if sslEnabled is true clientUseWss is also true")] + public bool clientUseWss; + public ClientWebsocketSettings clientWebsocketSettings = new ClientWebsocketSettings { ClientPortOption = WebsocketPortOption.DefaultSameAsServer, CustomClientPort = 7777 }; + + [Header("Logging")] + + [Tooltip("Choose minimum severity level for logging\nFlood level requires Debug build")] + [SerializeField] Log.Levels minimumLogLevel = Log.Levels.Warn; + + /// + /// Gets _logLevels field + /// Sets _logLevels and Log.level fields + /// + public Log.Levels LogLevels + { + get => minimumLogLevel; + set + { + minimumLogLevel = value; + Log.minLogLevel = minimumLogLevel; + } + } + + SimpleWebClient client; + SimpleWebServer server; + + TcpConfig TcpConfig => new TcpConfig(noDelay, sendTimeout, receiveTimeout); + + void Awake() + { + Log.minLogLevel = minimumLogLevel; + } + + public override string ToString() => $"SWT [{port}]"; + + void OnValidate() + { + Log.minLogLevel = minimumLogLevel; + } + + public override bool Available() => true; + + public override int GetMaxPacketSize(int channelId = 0) => maxMessageSize; + + public override void Shutdown() + { + client?.Disconnect(); + client = null; + server?.Stop(); + server = null; + } + + #region Client + + string GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme; + + public override bool IsEncrypted => ClientConnected() && (clientUseWss || sslEnabled) || ServerActive() && sslEnabled; + + // Not technically correct, but there's no good way to get the actual cipher, especially in browser + // When using reverse proxy, connection between proxy and server is not encrypted. + public override string EncryptionCipher => "TLS"; + + 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) + { + UriBuilder builder = new UriBuilder + { + Scheme = GetClientScheme(), + Host = hostname, + }; + + switch (clientWebsocketSettings.ClientPortOption) + { + case WebsocketPortOption.SpecifyPort: + builder.Port = clientWebsocketSettings.CustomClientPort; + break; + case WebsocketPortOption.MatchWebpageProtocol: + // not including a port in the builder allows the webpage to drive the port + // https://github.com/MirrorNetworking/Mirror/pull/3477 + break; + default: // default case handles ClientWebsocketPortOption.DefaultSameAsServerPort + builder.Port = port; + break; + } + + ClientConnect(builder.Uri); + } + + public override void ClientConnect(Uri uri) + { + // connecting or connected + if (ClientConnected()) + { + Log.Warn("[SWT-ClientConnect]: Already Connected"); + return; + } + + client = SimpleWebClient.Create(maxMessageSize, clientMaxMsgsPerTick, 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); + + // We will not invoke OnClientError if minLogLevel is set to None + // We only send the full exception if minLogLevel is set to Verbose + switch (Log.minLogLevel) + { + case Log.Levels.Flood: + case Log.Levels.Verbose: + client.onError += (Exception e) => + { + OnClientError.Invoke(TransportError.Unexpected, e.ToString()); + ClientDisconnect(); + }; + break; + case Log.Levels.Info: + case Log.Levels.Warn: + case Log.Levels.Error: + client.onError += (Exception e) => + { + OnClientError.Invoke(TransportError.Unexpected, e.Message); + ClientDisconnect(); + }; + break; + } + + client.Connect(uri); + } + + 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()) + { + Log.Error("[SWT-ClientSend]: Not Connected"); + return; + } + + if (segment.Count > maxMessageSize) + { + Log.Error("[SWT-ClientSend]: Message greater than max size"); + return; + } + + if (segment.Count == 0) + { + Log.Error("[SWT-ClientSend]: 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 + + string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme; + + public override Uri ServerUri() + { + UriBuilder builder = new UriBuilder + { + Scheme = GetServerScheme(), + Host = Dns.GetHostName(), + Port = port + }; + return builder.Uri; + } + + public override bool ServerActive() + { + return server != null && server.Active; + } + + public override void ServerStart() + { + if (ServerActive()) + Log.Warn("[SWT-ServerStart]: Server Already Started"); + + SslConfig config = SslConfigLoader.Load(sslEnabled, sslCertJson, sslProtocols); + server = new SimpleWebServer(serverMaxMsgsPerTick, TcpConfig, maxMessageSize, maxHandshakeSize, config); + + server.onConnect += OnServerConnectedWithAddress.Invoke; + server.onDisconnect += OnServerDisconnected.Invoke; + server.onData += (int connId, ArraySegment data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable); + + // We will not invoke OnServerError if minLogLevel is set to None + // We only send the full exception if minLogLevel is set to Verbose + switch (Log.minLogLevel) + { + case Log.Levels.Flood: + case Log.Levels.Verbose: + server.onError += (connId, exception) => + { + OnServerError(connId, TransportError.Unexpected, exception.ToString()); + ServerDisconnect(connId); + }; + break; + case Log.Levels.Info: + case Log.Levels.Warn: + case Log.Levels.Error: + server.onError += (connId, exception) => + { + OnServerError(connId, TransportError.Unexpected, exception.Message); + ServerDisconnect(connId); + }; + break; + } + + SendLoopConfig.batchSend = batchSend || waitBeforeSend; + SendLoopConfig.sleepBeforeSend = waitBeforeSend; + + server.Start(port); + } + + public override void ServerStop() + { + if (ServerActive()) + { + server.Stop(); + server = null; + } + } + + public override void ServerDisconnect(int connectionId) + { + if (ServerActive()) + server.KickClient(connectionId); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId) + { + if (!ServerActive()) + { + Log.Error("[SWT-ServerSend]: Server Not Active"); + return; + } + + if (segment.Count > maxMessageSize) + { + Log.Error("[SWT-ServerSend]: Message greater than max size"); + return; + } + + if (segment.Count == 0) + { + Log.Error("[SWT-ServerSend]: 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) => server.GetClientAddress(connectionId); + + public Request ServerGetClientRequest(int connectionId) => server.GetClientRequest(connectionId); + + // messages should always be processed in early update + public override void ServerEarlyUpdate() + { + server?.ProcessMessageQueue(this); + } + + #endregion + } +} diff --git a/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs.meta b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs.meta new file mode 100644 index 0000000..b4d088a --- /dev/null +++ b/Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 0110f245bfcfc7d459681f7bd9ebc590 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/SimpleWeb/SimpleWebTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy.meta b/Assets/Mirror/Transports/Telepathy.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy.meta rename to Assets/Mirror/Transports/Telepathy.meta diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy.meta b/Assets/Mirror/Transports/Telepathy/Telepathy.meta similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy.meta rename to Assets/Mirror/Transports/Telepathy/Telepathy.meta diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/Client.cs similarity index 98% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/Client.cs index 73e775c..b2a1c85 100644 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Client.cs +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Client.cs @@ -154,10 +154,6 @@ static void ReceiveThreadFunction(ClientConnectionState state, string ip, int po // 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) { @@ -177,7 +173,10 @@ static void ReceiveThreadFunction(ClientConnectionState state, string ip, int po // something went wrong. probably important. Log.Error("Client Recv Exception: " + 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); + // sendthread might be waiting on ManualResetEvent, // so let's make sure to end it if the connection // closed. diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/Client.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/Client.cs.meta new file mode 100644 index 0000000..4cd6ede --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Client.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a5b95294cc4ec4b15aacba57531c7985 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/Client.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/Common.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Common.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/Common.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/Common.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/Common.cs.meta new file mode 100644 index 0000000..3998c20 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Common.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c4d56322cf0e248a89103c002a505dab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/Common.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/ConnectionState.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ConnectionState.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/ConnectionState.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/ConnectionState.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/ConnectionState.cs.meta new file mode 100644 index 0000000..16706ec --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/ConnectionState.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: af95e2b6f6343411aa8bdf871abd7b1b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/ConnectionState.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/EventType.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/EventType.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/EventType.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/EventType.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/EventType.cs.meta new file mode 100644 index 0000000..52a6b69 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/EventType.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 49f1a330755814803be5f27f493e1910 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/EventType.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE b/Assets/Mirror/Transports/Telepathy/Telepathy/LICENSE similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE rename to Assets/Mirror/Transports/Telepathy/Telepathy/LICENSE diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/LICENSE.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/LICENSE.meta new file mode 100644 index 0000000..ec2558a --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/LICENSE.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 0ba11103b95fd4721bffbb08440d5b8e +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/LICENSE + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/Log.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Log.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/Log.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/Log.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/Log.cs.meta new file mode 100644 index 0000000..cc4dc70 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Log.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 0a123d054bef34d059057ac2ce936605 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/Log.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta new file mode 100644 index 0000000..fc67b9b --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 010a208972a9a4e0cb0e7c18a60b4494 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta new file mode 100644 index 0000000..f25085e --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: d490021c2e6a64374bc88168cec75c70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta new file mode 100644 index 0000000..d8b5bec --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7a8076c43fa8d4d45831adae232d4d3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/Pool.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Pool.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/Pool.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/Pool.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/Pool.cs.meta new file mode 100644 index 0000000..393fc68 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Pool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6d3e530f6872642ec81e9b8b76277c93 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/Pool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/Server.cs similarity index 93% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/Server.cs index 0b4ada7..7186618 100644 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Server.cs +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Server.cs @@ -11,7 +11,7 @@ public class Server : Common { // events to hook into // => OnData uses ArraySegment for allocation free receives later - public Action OnConnected; + public Action OnConnected; public Action> OnData; public Action OnDisconnected; @@ -86,9 +86,9 @@ void Listen(int port) 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). + // 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 // @@ -318,12 +318,35 @@ public bool Send(int connectionId, ArraySegment message) // 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)) + try + { + // find the connection + if (clients.TryGetValue(connectionId, out ConnectionState connection)) + { + return ((IPEndPoint)connection.client.Client.RemoteEndPoint).Address.ToString(); + } + return ""; + } + 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"; + } + catch (ObjectDisposedException) + { + return "Disposed"; + } + catch (Exception) { - return ((IPEndPoint)connection.client.Client.RemoteEndPoint).Address.ToString(); + return ""; } - return ""; } // disconnect (kick) a client @@ -373,7 +396,7 @@ public int Tick(int processLimit, Func checkEnabled = null) switch (eventType) { case EventType.Connected: - OnConnected?.Invoke(connectionId); + OnConnected?.Invoke(connectionId, GetClientAddress(connectionId)); break; case EventType.Data: OnData?.Invoke(connectionId, message); diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/Server.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/Server.cs.meta new file mode 100644 index 0000000..3c159d0 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Server.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fb98a16841ccc4338a7e0b4e59136563 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/Server.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef b/Assets/Mirror/Transports/Telepathy/Telepathy/Telepathy.asmdef similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef rename to Assets/Mirror/Transports/Telepathy/Telepathy/Telepathy.asmdef diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta new file mode 100644 index 0000000..ebb4647 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 725ee7191c021de4dbf9269590ded755 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/Telepathy.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/ThreadFunctions.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/ThreadFunctions.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/ThreadFunctions.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta new file mode 100644 index 0000000..4016e07 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: d01598bf851164dc48a24c26913460b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/ThreadFunctions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs b/Assets/Mirror/Transports/Telepathy/Telepathy/Utils.cs similarity index 100% rename from Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Utils.cs rename to Assets/Mirror/Transports/Telepathy/Telepathy/Utils.cs diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/Utils.cs.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/Utils.cs.meta new file mode 100644 index 0000000..7d9ed75 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/Utils.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 951d08c05297f4b3e8feb5bfcab86531 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/Utils.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Telepathy/Telepathy/VERSION b/Assets/Mirror/Transports/Telepathy/Telepathy/VERSION new file mode 100644 index 0000000..a0e7ded --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/VERSION @@ -0,0 +1,65 @@ +V1.9 [2023-11-10] +- fix: Always enqueue Disconnected event (imer) + +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/Transports/Telepathy/Telepathy/VERSION.meta b/Assets/Mirror/Transports/Telepathy/Telepathy/VERSION.meta new file mode 100644 index 0000000..a6bb937 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/Telepathy/VERSION.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: d942af06608be434dbeeaa58207d20bd +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/Telepathy/VERSION + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs b/Assets/Mirror/Transports/Telepathy/TelepathyTransport.cs similarity index 81% rename from Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs rename to Assets/Mirror/Transports/Telepathy/TelepathyTransport.cs index 5e0bc05..bbb80b4 100644 --- a/Assets/Mirror/Runtime/Transports/Telepathy/TelepathyTransport.cs +++ b/Assets/Mirror/Transports/Telepathy/TelepathyTransport.cs @@ -9,13 +9,14 @@ namespace Mirror { [HelpURL("https://github.com/vis2k/Telepathy/blob/master/README.md")] [DisallowMultipleComponent] - public class TelepathyTransport : Transport + public class TelepathyTransport : Transport, PortTransport { // scheme used by this transport // "tcp4" means tcp with 4 bytes header, network byte order public const string Scheme = "tcp4"; public ushort port = 7777; + public ushort Port { get => port; set => port=value; } [Header("Common")] [Tooltip("Nagle Algorithm can be disabled by enabling NoDelay")] @@ -74,11 +75,15 @@ void Awake() Debug.Log("TelepathyTransport initialized!"); } - public override bool Available() - { - // C#'s built in TCP sockets run everywhere except on WebGL - return Application.platform != RuntimePlatform.WebGLPlayer; - } + // C#'s built in TCP sockets run everywhere except on WebGL + // Do not change this back to using Application.platform + // because that doesn't work in the Editor! + public override bool Available() => +#if UNITY_WEBGL + false; +#else + true; +#endif // client private void CreateClient() @@ -94,7 +99,10 @@ private void CreateClient() // (= lazy call) client.OnConnected = () => OnClientConnected.Invoke(); client.OnData = (segment) => OnClientDataReceived.Invoke(segment, Channels.Reliable); - client.OnDisconnected = () => OnClientDisconnected.Invoke(); + // fix: https://github.com/vis2k/Mirror/issues/3287 + // Telepathy may call OnDisconnected twice. + // Mirror may have cleared the callback already, so use "?." here. + client.OnDisconnected = () => OnClientDisconnected?.Invoke(); // client configuration client.NoDelay = NoDelay; @@ -130,6 +138,8 @@ public override void ClientDisconnect() { client?.Disconnect(); client = null; + // client triggers the disconnected event in client.Tick() which won't be run anymore + OnClientDisconnected?.Invoke(); } // messages should always be processed in early update @@ -168,7 +178,7 @@ public override void ServerStart() // 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.OnConnected = (connectionId, remoteClientAddress) => OnServerConnectedWithAddress.Invoke(connectionId, remoteClientAddress); server.OnData = (connectionId, segment) => OnServerDataReceived.Invoke(connectionId, segment, Channels.Reliable); server.OnDisconnected = (connectionId) => OnServerDisconnected.Invoke(connectionId); @@ -190,25 +200,7 @@ public override void ServerSend(int connectionId, ArraySegment segment, in 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 string ServerGetClientAddress(int connectionId) => server?.GetClientAddress(connectionId); public override void ServerStop() { server?.Stop(); @@ -244,25 +236,16 @@ 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)"; - } + // Keep it short and simple so it looks nice in the HUD. + // + // 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 just use the regular port instead. + public override string ToString() => $"Telepathy [{port}]"; } } diff --git a/Assets/Mirror/Transports/Telepathy/TelepathyTransport.cs.meta b/Assets/Mirror/Transports/Telepathy/TelepathyTransport.cs.meta new file mode 100644 index 0000000..3ed6a04 --- /dev/null +++ b/Assets/Mirror/Transports/Telepathy/TelepathyTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c7424c1070fad4ba2a7a96b02fbeb4bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 1000 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Telepathy/TelepathyTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Threaded.meta b/Assets/Mirror/Transports/Threaded.meta new file mode 100644 index 0000000..cef7c07 --- /dev/null +++ b/Assets/Mirror/Transports/Threaded.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4449b05e1376c4120afdce7b134887b4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs b/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs new file mode 100644 index 0000000..c1fea49 --- /dev/null +++ b/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs @@ -0,0 +1,756 @@ +// threaded transport to handle all the magic. +// implementations are automatically elevated to the worker thread +// by simply overwriting all the thread functions +// +// note that ThreadLog.cs is required for Debug.Log from threads to work in builds. +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Net; +using System.Threading; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace Mirror +{ + // buffered events for main thread + enum ClientMainEventType + { + OnClientConnected, + OnClientSent, + OnClientReceived, + OnClientError, + OnClientDisconnected, + } + + enum ServerMainEventType + { + OnServerConnected, + OnServerSent, + OnServerReceived, + OnServerError, + OnServerDisconnected, + } + + // buffered events for worker thread + enum ThreadEventType + { + DoServerStart, + DoServerSend, + DoServerDisconnect, + DoServerStop, + + DoClientConnect, + DoClientSend, + DoClientDisconnect, + + Sleep, + Wake, + + DoShutdown + } + + struct ClientMainEvent + { + public ClientMainEventType type; + public object param; + + // some events have value type parameters: connectionId, error. + // store them explicitly to avoid boxing allocations to 'object param'. + public int? channelId; // connect/disconnect don't have a channel + public TransportError? error; + + public ClientMainEvent( + ClientMainEventType type, + object param, + int? channelId = null, + TransportError? error = null) + { + this.type = type; + this.channelId = channelId; + this.error = error; + this.param = param; + } + } + + struct ServerMainEvent + { + public ServerMainEventType type; + public object param; + + // some events have value type parameters: connectionId, error. + // store them explicitly to avoid boxing allocations to 'object param'. + public int? connectionId; // only server needs connectionId + public int? channelId; // connect/disconnect don't have a channel + public TransportError? error; + + public ServerMainEvent( + ServerMainEventType type, + object param, + int? connectionId, + int? channelId = null, + TransportError? error = null) + { + this.type = type; + this.channelId = channelId; + this.connectionId = connectionId; + this.error = error; + this.param = param; + } + } + + struct ThreadEvent + { + public ThreadEventType type; + public object param; + + // some events have value type parameters: connectionId. + // store them explicitly to avoid boxing allocations to 'object param'. + public int? connectionId; + public int? channelId; + + public ThreadEvent( + ThreadEventType type, + object param, + int? connectionId = null, + int? channelId = null) + { + this.type = type; + this.connectionId = connectionId; + this.channelId = channelId; + this.param = param; + } + } + + public abstract class ThreadedTransport : Transport + { + WorkerThread thread; + + // main thread's event queue. + // worker thread puts events in, main thread processes them. + // client & server separate because EarlyUpdate is separate too. + // TODO nonalloc + readonly ConcurrentQueue clientMainQueue = new ConcurrentQueue(); + readonly ConcurrentQueue serverMainQueue = new ConcurrentQueue(); + + // worker thread's event queue + // main thread puts events in, worker thread processes them. + // TODO nonalloc + readonly ConcurrentQueue threadQueue = new ConcurrentQueue(); + + // active flags, since we can't access server/client from main thread + volatile bool serverActive; + volatile bool clientConnected; + + // max number of thread messages to process per tick in main thread. + // very large limit to prevent deadlocks. + const int MaxProcessingPerTick = 10_000_000; + + [Tooltip("Detect device sleep mode and automatically disconnect + hibernate the thread after 'sleepTimeout' seconds.\nFor example: on mobile / VR, we don't want to drain the battery after putting down the device.")] + public bool sleepDetection = true; + public float sleepTimeoutInSeconds = 30; + + // communication between main & worker thread ////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void EnqueueClientMain( + ClientMainEventType type, + object param, + int? channelId, + TransportError? error) => + clientMainQueue.Enqueue(new ClientMainEvent(type, param, channelId, error)); + + // add an event for main thread + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void EnqueueServerMain( + ServerMainEventType type, + object param, + int? connectionId, + int? channelId, + TransportError? error) => + serverMainQueue.Enqueue(new ServerMainEvent(type, param, connectionId, channelId, error)); + + void EnqueueThread( + ThreadEventType type, + object param, + int? channelId, + int? connectionId) => + threadQueue.Enqueue(new ThreadEvent(type, param, connectionId, channelId)); + + // Unity callbacks ///////////////////////////////////////////////////// + protected virtual void Awake() + { + // start the thread. + // if main application terminates, this thread needs to terminate too. + EnsureThread(); + } + + // starts the thread if not created or not active yet. + void EnsureThread() + { + if (thread != null && thread.IsAlive) return; + + thread = new WorkerThread(ToString()); + thread.Tick = ThreadTick; + thread.Cleanup = ThreadedShutdown; + thread.Start(); + Debug.Log($"ThreadedTransport: started worker thread!"); + } + + protected virtual void OnDestroy() + { + // stop thread fully + Shutdown(); + + // TODO recycle writers. + } + + // worker thread /////////////////////////////////////////////////////// + // sleep timeout to automatically end if the device was put to sleep. + Stopwatch sleepTimer = null; // NOT THREAD SAFE: ONLY USE THIS IN WORKER THREAD! + void ProcessThreadQueue() + { + // TODO deadlock protection. worker thread may be to slow to process all. + while (threadQueue.TryDequeue(out ThreadEvent elem)) + { + switch (elem.type) + { + // SERVER EVENTS /////////////////////////////////////////// + case ThreadEventType.DoServerStart: // start listening + { + // call the threaded function + ThreadedServerStart(); + break; + } + case ThreadEventType.DoServerSend: + { + // call the threaded function + ConcurrentNetworkWriterPooled writer = (ConcurrentNetworkWriterPooled)elem.param; + ThreadedServerSend(elem.connectionId.Value, writer, elem.channelId.Value); + + // recycle writer to thread safe pool for reuse + ConcurrentNetworkWriterPool.Return(writer); + break; + } + case ThreadEventType.DoServerDisconnect: + { + // call the threaded function + ThreadedServerDisconnect(elem.connectionId.Value); + break; + } + case ThreadEventType.DoServerStop: // stop listening + { + // call the threaded function + ThreadedServerStop(); + break; + } + + // CLIENT EVENTS /////////////////////////////////////////// + case ThreadEventType.DoClientConnect: + { + // call the threaded function + if (elem.param is string address) + ThreadedClientConnect(address); + else if (elem.param is Uri uri) + ThreadedClientConnect(uri); + break; + } + case ThreadEventType.DoClientSend: + { + // call the threaded function + ConcurrentNetworkWriterPooled writer = (ConcurrentNetworkWriterPooled)elem.param; + ThreadedClientSend(writer, elem.channelId.Value); + + // recycle writer to thread safe pool for reuse + ConcurrentNetworkWriterPool.Return(writer); + break; + } + case ThreadEventType.DoClientDisconnect: + { + // call the threaded function + ThreadedClientDisconnect(); + break; + } + + // SLEEP //////////////////////////////////////////////// + case ThreadEventType.Sleep: + { + // start the sleep timer if not started yet + if (sleepTimer == null) + { + Debug.Log($"ThreadedTransport: sleep detected, sleeping in {sleepTimeoutInSeconds:F0}s!"); + sleepTimer = Stopwatch.StartNew(); + } + break; + } + case ThreadEventType.Wake: + { + // stop the sleep timer (if any) + if (sleepTimer != null) + { + Debug.Log($"ThreadedTransport: Woke up, interrupting sleep timer!"); + sleepTimer = null; + } + break; + } + + // SHUTDOWN //////////////////////////////////////////////// + case ThreadEventType.DoShutdown: + { + // call the threaded function + ThreadedShutdown(); + break; + } + } + } + } + + // Tick() returns a bool so it can easily stop the thread + // without needing to throw InterruptExceptions or similar. + bool ThreadTick() + { + // was the device put to sleep? + if (sleepTimer != null && + sleepTimer.Elapsed.TotalSeconds >= sleepTimeoutInSeconds) + { + Debug.Log("ThreadedTransport: entering sleep mode and stopping/disconnecting."); + ThreadedServerStop(); + ThreadedClientDisconnect(); + sleepTimer = null; + + // if the device was put to sleep, end the thread gracefully. + // all threads must end, otherwise putting down the device would + // slowly drain the battery after a day or more. + return false; + } + + // early update the implementation first + ThreadedClientEarlyUpdate(); + ThreadedServerEarlyUpdate(); + + // process queued user requests + ProcessThreadQueue(); + + // late update the implementation at the end + ThreadedClientLateUpdate(); + ThreadedServerLateUpdate(); + + // save some cpu power. + Thread.Sleep(1); + return true; + } + + // threaded callbacks to call from transport thread. + // they will be queued up for main thread automatically. + protected void OnThreadedClientConnected() + { + EnqueueClientMain(ClientMainEventType.OnClientConnected, null, null, null); + } + + protected void OnThreadedClientSend(ArraySegment message, int channelId) + { + // ArraySegment is only valid until returning. + // copy to a writer until main thread processes it. + // make sure to recycle the writer in main thread. + ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get(); + writer.WriteBytes(message.Array, message.Offset, message.Count); + EnqueueClientMain(ClientMainEventType.OnClientSent, writer, channelId, null); + } + + protected void OnThreadedClientReceive(ArraySegment message, int channelId) + { + // ArraySegment is only valid until returning. + // copy to a writer until main thread processes it. + // make sure to recycle the writer in main thread. + ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get(); + writer.WriteBytes(message.Array, message.Offset, message.Count); + EnqueueClientMain(ClientMainEventType.OnClientReceived, writer, channelId, null); + } + + protected void OnThreadedClientError(TransportError error, string reason) + { + EnqueueClientMain(ClientMainEventType.OnClientError, reason, null, error); + } + + protected void OnThreadedClientDisconnected() + { + EnqueueClientMain(ClientMainEventType.OnClientDisconnected, null, null, null); + } + + protected void OnThreadedServerConnected(int connectionId, IPEndPoint endPoint) + { + // create string copy of address immediately before sending to another thread + string address = endPoint.PrettyAddress(); + EnqueueServerMain(ServerMainEventType.OnServerConnected, address, connectionId, null, null); + } + + protected void OnThreadedServerSend(int connectionId, ArraySegment message, int channelId) + { + // ArraySegment is only valid until returning. + // copy to a writer until main thread processes it. + // make sure to recycle the writer in main thread. + ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get(); + writer.WriteBytes(message.Array, message.Offset, message.Count); + EnqueueServerMain(ServerMainEventType.OnServerSent, writer, connectionId, channelId, null); + } + + protected void OnThreadedServerReceive(int connectionId, ArraySegment message, int channelId) + { + // ArraySegment is only valid until returning. + // copy to a writer until main thread processes it. + // make sure to recycle the writer in main thread. + ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get(); + writer.WriteBytes(message.Array, message.Offset, message.Count); + EnqueueServerMain(ServerMainEventType.OnServerReceived, writer, connectionId, channelId, null); + } + + protected void OnThreadedServerError(int connectionId, TransportError error, string reason) + { + EnqueueServerMain(ServerMainEventType.OnServerError, reason, connectionId, null, error); + } + + protected void OnThreadedServerDisconnected(int connectionId) + { + EnqueueServerMain(ServerMainEventType.OnServerDisconnected, null, connectionId, null, null); + } + + protected abstract void ThreadedClientConnect(string address); + protected abstract void ThreadedClientConnect(Uri address); + protected abstract void ThreadedClientSend(ArraySegment message, int channelId); + protected abstract void ThreadedClientDisconnect(); + + protected abstract void ThreadedServerStart(); + protected abstract void ThreadedServerStop(); + protected abstract void ThreadedServerSend(int connectionId, ArraySegment message, int channelId); + protected abstract void ThreadedServerDisconnect(int connectionId); + + // threaded update functions. + // make sure not to call main thread OnReceived etc. events. + // queue everything. + protected abstract void ThreadedClientEarlyUpdate(); + protected abstract void ThreadedClientLateUpdate(); + protected abstract void ThreadedServerEarlyUpdate(); + protected abstract void ThreadedServerLateUpdate(); + + protected abstract void ThreadedShutdown(); + + // client ////////////////////////////////////////////////////////////// + // implementations need to use ThreadedEarlyUpdate + public override void ClientEarlyUpdate() + { + // regular transports process OnReceive etc. from early update. + // need to process the worker thread's queued events here too. + // + // process only up to N messages per tick here. + // if main thread becomes too slow, we don't want to deadlock. + int processed = 0; + while (clientMainQueue.TryDequeue(out ClientMainEvent elem)) + { + switch (elem.type) + { + // CLIENT EVENTS /////////////////////////////////////////// + case ClientMainEventType.OnClientConnected: + { + // call original transport event + OnClientConnected?.Invoke(); + break; + } + case ClientMainEventType.OnClientSent: + { + // call original transport event + ConcurrentNetworkWriterPooled writer = (ConcurrentNetworkWriterPooled)elem.param; + OnClientDataSent?.Invoke(writer, elem.channelId.Value); + + // recycle writer to thread safe pool for reuse + ConcurrentNetworkWriterPool.Return(writer); + break; + } + case ClientMainEventType.OnClientReceived: + { + // immediately stop processing Data messages after ClientDisconnect() was called. + // ClientDisconnect() sets clientConnected=false, so we can simply check that here. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3787 + if (clientConnected) + { + // call original transport event + ConcurrentNetworkWriterPooled writer = (ConcurrentNetworkWriterPooled)elem.param; + OnClientDataReceived?.Invoke(writer, elem.channelId.Value); + + // recycle writer to thread safe pool for reuse + ConcurrentNetworkWriterPool.Return(writer); + } + break; + } + case ClientMainEventType.OnClientError: + { + // call original transport event + OnClientError?.Invoke(elem.error.Value, (string)elem.param); + break; + } + case ClientMainEventType.OnClientDisconnected: + { + // call original transport event + OnClientDisconnected?.Invoke(); + break; + } + } + + // process only up to N messages per tick here. + // if main thread becomes too slow, we don't want to deadlock. + if (++processed >= MaxProcessingPerTick) + { + Debug.LogWarning($"ThreadedTransport processed the limit of {MaxProcessingPerTick} messages this tick. Continuing next tick to prevent deadlock."); + break; + } + } + } + + // manual state flag because implementations can't access their + // threaded .server/.client state from main thread. + public override bool ClientConnected() => clientConnected; + + public override void ClientConnect(string address) + { + // don't connect the thread twice + if (ClientConnected()) + { + Debug.LogWarning($"Threaded transport: client already connected!"); + return; + } + + // start worker thread if not started yet + EnsureThread(); + + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoClientConnect, address, null, null); + + // manual state flag because implementations can't access their + // threaded .server/.client state from main thread. + clientConnected = true; + } + + public override void ClientConnect(Uri uri) + { + // don't connect the thread twice + if (ClientConnected()) + { + Debug.LogWarning($"Threaded transport: client already connected!"); + return; + } + + // start worker thread if not started yet + EnsureThread(); + + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoClientConnect, uri, null, null); + + // manual state flag because implementations can't access their + // threaded .server/.client state from main thread. + clientConnected = true; + } + + public override void ClientSend(ArraySegment segment, int channelId = Channels.Reliable) + { + if (!ClientConnected()) return; + + // segment is only valid until returning. + // copy it to a writer. + // make sure to recycle it from worker thread. + ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get(); + writer.WriteBytes(segment.Array, segment.Offset, segment.Count); + + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoClientSend, writer, channelId, null); + } + + public override void ClientDisconnect() + { + EnqueueThread(ThreadEventType.DoClientDisconnect, null, null, null); + + // manual state flag because implementations can't access their + // threaded .server/.client state from main thread. + clientConnected = false; + } + + // server ////////////////////////////////////////////////////////////// + // implementations need to use ThreadedEarlyUpdate + public override void ServerEarlyUpdate() + { + // regular transports process OnReceive etc. from early update. + // need to process the worker thread's queued events here too. + // + // process only up to N messages per tick here. + // if main thread becomes too slow, we don't want to deadlock. + int processed = 0; + while (serverMainQueue.TryDequeue(out ServerMainEvent elem)) + { + switch (elem.type) + { + // SERVER EVENTS /////////////////////////////////////////// + case ServerMainEventType.OnServerConnected: + { + // call original transport event with connectionId, address + string address = (string)elem.param; + OnServerConnectedWithAddress?.Invoke(elem.connectionId.Value, address); + break; + } + case ServerMainEventType.OnServerSent: + { + // call original transport event + ConcurrentNetworkWriterPooled writer = (ConcurrentNetworkWriterPooled)elem.param; + OnServerDataSent?.Invoke(elem.connectionId.Value, writer, elem.channelId.Value); + + // recycle writer to thread safe pool for reuse + ConcurrentNetworkWriterPool.Return(writer); + break; + } + case ServerMainEventType.OnServerReceived: + { + // call original transport event + ConcurrentNetworkWriterPooled writer = (ConcurrentNetworkWriterPooled)elem.param; + OnServerDataReceived?.Invoke(elem.connectionId.Value, writer, elem.channelId.Value); + + // recycle writer to thread safe pool for reuse + ConcurrentNetworkWriterPool.Return(writer); + break; + } + case ServerMainEventType.OnServerError: + { + // call original transport event + OnServerError?.Invoke(elem.connectionId.Value, elem.error.Value, (string)elem.param); + break; + } + case ServerMainEventType.OnServerDisconnected: + { + // call original transport event + OnServerDisconnected?.Invoke(elem.connectionId.Value); + break; + } + } + + // process only up to N messages per tick here. + // if main thread becomes too slow, we don't want to deadlock. + if (++processed >= MaxProcessingPerTick) + { + Debug.LogWarning($"ThreadedTransport processed the limit of {MaxProcessingPerTick} messages this tick. Continuing next tick to prevent deadlock."); + break; + } + } + } + + // implementations need to use ThreadedLateUpdate + public override void ServerLateUpdate() {} + + // manual state flag because implementations can't access their + // threaded .server/.client state from main thread. + public override bool ServerActive() => serverActive; + + public override void ServerStart() + { + // don't start the thread twice + if (ServerActive()) + { + Debug.LogWarning($"Threaded transport: server already started!"); + return; + } + + // start worker thread if not started yet + EnsureThread(); + + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoServerStart, null, null, null); + + // manual state flag because implementations can't access their + // threaded .server/.client state from main thread. + serverActive = true; + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId = Channels.Reliable) + { + if (!ServerActive()) return; + + // segment is only valid until returning. + // copy it to a writer. + // make sure to recycle it from worker thread. + ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get(); + writer.WriteBytes(segment.Array, segment.Offset, segment.Count); + + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoServerSend, writer, channelId, connectionId); + } + + public override void ServerDisconnect(int connectionId) + { + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoServerDisconnect, null, null, connectionId); + } + + // TODO pass address in OnConnected. + // querying this at runtime won't work for threaded transports. + public override string ServerGetClientAddress(int connectionId) + { + throw new NotImplementedException("ThreadedTransport passes each connection's address in OnServerConnectedThreaded. Don't use ServerGetClientAddress."); + } + + public override void ServerStop() + { + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoServerStop, null, null, null); + + // manual state flag because implementations can't access their + // threaded .server/.client state from main thread. + serverActive = false; + } + + // sleep /////////////////////////////////////////////////////////////// + // when a device goes to sleep, we must end the worker thread after a while. + // otherwise putting down the device would slowly drain the battery after a day or more. + void OnApplicationPause(bool pauseStatus) + { + Debug.Log($"{GetType()}: OnApplicationPause={pauseStatus}"); + + // is sleep detection feature enabled? + if (!sleepDetection) return; + + // pause thread if application pauses + if (pauseStatus) + { + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.Sleep, null, null, null); + } + // resume thread if application resumes + else + { + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.Wake, null, null, null); + } + } + + // shutdown //////////////////////////////////////////////////////////// + public override void Shutdown() + { + // enqueue to process in worker thread + EnqueueThread(ThreadEventType.DoShutdown, null, null, null); + + // need to wait a little for worker thread to process the enqueued + // Shutdown event and do proper cleanup. + // + // otherwise if a server with a connected client is stopped, + // and then started, a warning would be shown when starting again + // about an old connection not being found because it wasn't cleared + // in KCP + // TODO cleaner + Thread.Sleep(100); + + // stop thread fully, with timeout + // ?.: 'thread' might be null after script reload -> stop play + thread?.StopBlocking(1); + + // clear queues so we don't process old messages + // when listening again later + clientMainQueue.Clear(); + serverMainQueue.Clear(); + threadQueue.Clear(); + } + } +} diff --git a/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs.meta b/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs.meta new file mode 100644 index 0000000..6ddcc27 --- /dev/null +++ b/Assets/Mirror/Transports/Threaded/ThreadedTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: af1f2befa05c14b2ea463791ae56081e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Threaded/ThreadedTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/version.txt b/Assets/Mirror/version.txt new file mode 100644 index 0000000..4ee7f6c --- /dev/null +++ b/Assets/Mirror/version.txt @@ -0,0 +1 @@ +96.0.1 diff --git a/Assets/Mirror/version.txt.meta b/Assets/Mirror/version.txt.meta new file mode 100644 index 0000000..606664f --- /dev/null +++ b/Assets/Mirror/version.txt.meta @@ -0,0 +1,9 @@ +guid: 7382DF86FC5989F4A9532717EF49E7C8 +fileFormatVersion: 2 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/version.txt + uploadId: 736421 diff --git a/Assets/Prefabs/Player.prefab b/Assets/Prefabs/Player.prefab index 61e9209..51c27a4 100644 --- a/Assets/Prefabs/Player.prefab +++ b/Assets/Prefabs/Player.prefab @@ -29,12 +29,13 @@ Transform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1666928812108792425} + serializedVersion: 2 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_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!33 &5477517734192154168 MeshFilter: @@ -55,10 +56,17 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -80,9 +88,11 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} --- !u!65 &2750934347978954253 BoxCollider: m_ObjectHideFlags: 0 @@ -91,9 +101,17 @@ BoxCollider: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1666928812108792425} m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 m_IsTrigger: 0 + m_ProvidesContacts: 0 m_Enabled: 1 - serializedVersion: 2 + serializedVersion: 3 m_Size: {x: 1, y: 1, z: 1} m_Center: {x: 0, y: 0, z: 0} --- !u!114 &6803138963659689223 @@ -109,8 +127,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: sceneId: 0 + _assetId: 2946368604 serverOnly: 0 - m_AssetId: + visibility: 0 hasSpawned: 0 --- !u!114 &428742853628225930 MonoBehaviour: @@ -124,8 +143,13 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d43cf183cda48544ea9aa5bbfa7a4625, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 1 syncMode: 0 syncInterval: 0.1 + sessionId: + username: + ip: + platform: --- !u!114 &-4824969326312294342 MonoBehaviour: m_ObjectHideFlags: 0 @@ -135,12 +159,28 @@ MonoBehaviour: m_GameObject: {fileID: 1666928812108792425} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2f74aedd71d9a4f55b3ce499326d45fb, type: 3} + m_Script: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, type: 3} m_Name: m_EditorClassIdentifier: + syncDirection: 1 syncMode: 0 - syncInterval: 0.1 - clientAuthority: 1 - localPositionSensitivity: 0.01 - localRotationSensitivity: 0.01 - localScaleSensitivity: 0.01 + syncInterval: 0 + target: {fileID: 3019132547437604787} + syncPosition: 1 + syncRotation: 1 + syncScale: 0 + onlySyncOnChange: 1 + compressRotation: 1 + interpolatePosition: 1 + interpolateRotation: 1 + interpolateScale: 1 + coordinateSpace: 0 + timelineOffset: 1 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + onlySyncOnChangeCorrectionMultiplier: 2 + useFixedUpdate: 0 + rotationSensitivity: 0.01 + positionPrecision: 0.01 + scalePrecision: 0.01 diff --git a/Assets/Resources.meta b/Assets/Resources.meta new file mode 100644 index 0000000..4ad810d --- /dev/null +++ b/Assets/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb4eea86d95d8f140a6c9ed743e8cf67 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Main Mirror+Relay.unity b/Assets/Scenes/Main Mirror+Relay.unity new file mode 100644 index 0000000..1759e6f --- /dev/null +++ b/Assets/Scenes/Main Mirror+Relay.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 Mirror+Relay.unity.meta b/Assets/Scenes/Main Mirror+Relay.unity.meta new file mode 100644 index 0000000..9e9ad6d --- /dev/null +++ b/Assets/Scenes/Main Mirror+Relay.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e2f709018c33ad042b098d7c1dc0c54b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Main.unity b/Assets/Scenes/Main.unity index 1759e6f..bd234ae 100644 --- a/Assets/Scenes/Main.unity +++ b/Assets/Scenes/Main.unity @@ -37,8 +37,8 @@ RenderSettings: 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_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.37311924, g: 0.38073963, b: 0.3587269, a: 1} m_UseRadianceAmbientProbe: 0 --- !u!157 &3 LightmapSettings: @@ -98,7 +98,7 @@ LightmapSettings: m_TrainingDataDestination: TrainingData m_LightProbeSampleCountMultiplier: 4 m_LightingDataAsset: {fileID: 0} - m_LightingSettings: {fileID: 1874141296} + m_LightingSettings: {fileID: 0} --- !u!196 &4 NavMeshSettings: serializedVersion: 2 @@ -123,7 +123,7 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} ---- !u!1 &207291378 +--- !u!1 &711140055 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -131,186 +131,66 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 207291382} - - component: {fileID: 207291381} - - component: {fileID: 207291379} - - component: {fileID: 207291384} + - component: {fileID: 711140058} + - component: {fileID: 711140057} + - component: {fileID: 711140056} m_Layer: 0 - m_Name: NetworkManager + m_Name: EventSystem m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &207291379 +--- !u!114 &711140056 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 207291378} + m_GameObject: {fileID: 711140055} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 6d4f9593ba7f845458730213887b5758, type: 3} + m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3} m_Name: m_EditorClassIdentifier: - Port: 7777 - LoggerLevel: 3 - TimeoutMS: 1000 - useRelay: 0 ---- !u!114 &207291381 + m_SendPointerHoverToParent: 1 + m_HorizontalAxis: Horizontal + m_VerticalAxis: Vertical + m_SubmitButton: Submit + m_CancelButton: Cancel + m_InputActionsPerSecond: 10 + m_RepeatDelay: 0.5 + m_ForceModuleActive: 0 +--- !u!114 &711140057 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 207291378} + m_GameObject: {fileID: 711140055} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f9810247aefdc844781df8cf4c039e9a, type: 3} + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, 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 + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &711140058 Transform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 207291378} + m_GameObject: {fileID: 711140055} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -1.9334593, y: 13.584466, z: -13.890325} + m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 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 @@ -346,7 +226,7 @@ Camera: m_GameObject: {fileID: 963194225} m_Enabled: 1 serializedVersion: 2 - m_ClearFlags: 1 + m_ClearFlags: 2 m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} m_projectionMatrixMode: 1 m_GateFitMode: 2 @@ -388,70 +268,70 @@ Transform: 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_LocalPosition: {x: 0, y: 1, z: -20} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!850595691 &1874141296 -LightingSettings: +--- !u!1 &1186661038 +GameObject: 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 + serializedVersion: 6 + m_Component: + - component: {fileID: 1186661040} + - component: {fileID: 1186661039} + m_Layer: 0 + m_Name: GUIGame_OnGUI // MIRROR CHANGE + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1186661039 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1186661038} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 89b59180da8577947a00064507b6f488, type: 3} + m_Name: + m_EditorClassIdentifier: + _textLatency: {fileID: 0} + _updateLatencyTextInterval: 1 + _test_1: + SpawnCount: 500 + Prefab: {fileID: 2997158864174378348, guid: 296e7798207e69a40a871087348da0c3, + type: 3} + _test_2: + SpawnCount: 500 + Prefab: {fileID: 6859501204968983109, guid: a7278693f35741148b381178a112e24d, + type: 3} + _test_3: + SpawnCount: 500 + Prefab: {fileID: 6660102892434074099, guid: 9925ea7d66c38ac48a88a572a9236cbe, + type: 3} + _networkManagerPrefab: {fileID: 6902534888765376180, guid: 62d3b9c46c08c934ea2ac02811b3028d, + type: 3} +--- !u!4 &1186661040 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1186661038} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -8.328793, y: -9.683151, z: 3.160122} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Assets/Scenes/Main.unity.meta b/Assets/Scenes/Main.unity.meta index 952bd1e..238a617 100644 --- a/Assets/Scenes/Main.unity.meta +++ b/Assets/Scenes/Main.unity.meta @@ -5,3 +5,10 @@ DefaultImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Scenes/Main.unity + uploadId: 736421 diff --git a/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt b/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt index 7db3756..15b0611 100644 --- a/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt +++ b/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt @@ -12,14 +12,7 @@ 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(); - } + public static new #SCRIPTNAME# singleton => (#SCRIPTNAME#)NetworkManager.singleton; /// /// Runs on both Server and Client @@ -30,13 +23,19 @@ public class #SCRIPTNAME# : NetworkManager base.Awake(); } + #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 Start() { - singleton = this; base.Start(); } @@ -163,13 +162,22 @@ public class #SCRIPTNAME# : NetworkManager base.OnServerDisconnect(conn); } + /// + /// Called on server when transport raises an error. + /// NetworkConnection may be null. + /// + /// Connection of the client...may be null + /// TransportError enum + /// String message of the error. + public override void OnServerError(NetworkConnectionToClient conn, TransportError transportError, string message) { } + /// /// 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) { } + public override void OnServerTransportException(NetworkConnectionToClient conn, Exception exception) { } #endregion @@ -188,10 +196,7 @@ public class #SCRIPTNAME# : NetworkManager /// 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(); - } + public override void OnClientDisconnect() { } /// /// Called on clients when a servers tells the client it is no longer ready. @@ -199,11 +204,18 @@ public class #SCRIPTNAME# : NetworkManager /// public override void OnClientNotReady() { } + /// + /// Called on client when transport raises an error. + /// + /// TransportError enum. + /// String message of the error. + public override void OnClientError(TransportError transportError, string message) { } + /// /// Called on client when transport raises an exception. /// /// Exception thrown from the Transport. - public override void OnClientError(Exception exception) { } + public override void OnClientTransportException(Exception exception) { } #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 index 6221c57..b9f8ac8 100644 --- a/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt.meta +++ b/Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/ScriptTemplates/50-Mirror__Network Manager-NewNetworkManager.cs.txt + uploadId: 736421 diff --git a/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt index a53aee4..8e33803 100644 --- a/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt +++ b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt @@ -7,10 +7,29 @@ using Mirror; 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 Unity Callbacks + + /// + /// Add your validation code here after the base.OnValidate(); call. + /// + protected override void OnValidate() + { + base.OnValidate(); + } + + // NOTE: Do not put objects in DontDestroyOnLoad (DDOL) in Awake. You can do that in Start instead. + void Awake() + { + } + + void Start() + { + } + + #endregion + #region Start & Stop Callbacks /// diff --git a/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt.meta b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt.meta index c5a0018..8977c99 100644 --- a/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt.meta +++ b/Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/ScriptTemplates/52-Mirror__Network Behaviour-NewNetworkBehaviour.cs.txt + uploadId: 736421 diff --git a/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt b/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt index a067926..b93d5a9 100644 --- a/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt +++ b/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt @@ -18,6 +18,10 @@ using Mirror; /// public class #SCRIPTNAME# : NetworkRoomManager { + // Overrides the base singleton so we don't + // have to cast to this type everywhere. + public static new #SCRIPTNAME# singleton => (#SCRIPTNAME#)NetworkRoomManager.singleton; + #region Server Callbacks /// @@ -105,6 +109,14 @@ public class #SCRIPTNAME# : NetworkRoomManager return base.OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer); } + /// + /// This is called on server from NetworkRoomPlayer.CmdChangeReadyState when client indicates change in Ready status. + /// + public override void ReadyStatusChanged() + { + base.ReadyStatusChanged(); + } + /// /// 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. @@ -159,12 +171,6 @@ public class #SCRIPTNAME# : NetworkRoomManager /// 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 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 index fe5bc32..1c163e6 100644 --- a/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt.meta +++ b/Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/ScriptTemplates/54-Mirror__Network Room Manager-NewNetworkRoomManager.cs.txt + uploadId: 736421 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 index 36a48dd..48c28d6 100644 --- a/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt.meta +++ b/Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/ScriptTemplates/55-Mirror__Network Room Player-NewNetworkRoomPlayer.cs.txt + uploadId: 736421 diff --git a/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt index 8f16194..fded0cf 100644 --- a/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt +++ b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt @@ -7,20 +7,36 @@ using Mirror.Discovery; API Reference: https://mirror-networking.com/docs/api/Mirror.Discovery.NetworkDiscovery.html */ -public class DiscoveryRequest : NetworkMessage +public struct DiscoveryRequest : NetworkMessage { - // Add properties for whatever information you want sent by clients - // in their broadcast messages that servers will consume. + // Add public fields (not properties) for whatever information you want + // sent by clients in their broadcast messages that servers will use. } -public class DiscoveryResponse : NetworkMessage +public struct 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. + // Add public fields (not properties) for whatever information you want the server + // to return to clients for them to display or use for establishing a connection. } public class #SCRIPTNAME# : NetworkDiscoveryBase { + #region Unity Callbacks + +#if UNITY_EDITOR + public override void OnValidate() + { + base.OnValidate(); + } +#endif + + public override void Start() + { + base.Start(); + } + + #endregion + #region Server /// diff --git a/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt.meta b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt.meta index a034ec8..e3c5a4d 100644 --- a/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt.meta +++ b/Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/ScriptTemplates/56-Mirror__Network Discovery-NewNetworkDiscovery.cs.txt + uploadId: 736421 diff --git a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt index 0e38c1a..3f06e76 100644 --- a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt +++ b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt @@ -10,20 +10,10 @@ using Mirror; 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 Awake() { } + protected override void OnValidate() { base.OnValidate(); @@ -45,44 +35,56 @@ public class #SCRIPTNAME# : NetworkTransformBase base.OnDisable(); } + #endregion + + #region NT Base Callbacks + /// - /// 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. + /// NTSnapshot struct is created here /// - public override void Reset() + protected override TransformSnapshot Construct() { - base.Reset(); + return base.Construct(); } - #endregion + protected override Vector3 GetPosition() + { + return base.GetPosition(); + } - #region NT Base Callbacks + protected override Quaternion GetRotation() + { + return base.GetRotation(); + } - /// - /// 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) + protected override Vector3 GetScale() + { + return base.GetScale(); + } + + protected override void SetPosition(Vector3 position) { - base.OnServerToClientSync(position, rotation, scale); + base.SetPosition(position); + } + + protected override void SetRotation(Quaternion rotation) + { + base.SetRotation(rotation); + } + + protected override void SetScale(Vector3 scale) + { + base.SetScale(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. + /// 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 OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + protected override void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal) { - base.OnClientToServerSync(position, rotation, scale); + base.Apply(interpolated, endGoal); } /// @@ -106,48 +108,32 @@ public class #SCRIPTNAME# : NetworkTransformBase } /// - /// 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. + /// 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. /// - protected override bool CompareSnapshots(NTSnapshot currentSnapshot) + public override void ResetState() { - return base.CompareSnapshots(currentSnapshot); + base.ResetState(); } -#endif - #endregion #region GUI -#if UNITY_EDITOR || DEVELOPMENT_BUILD // OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD protected override void OnGUI() { base.OnGUI(); } - protected override void DrawGizmos(SortedList buffer) + protected override void DrawGizmos(SortedList buffer) { base.DrawGizmos(buffer); } diff --git a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt.meta b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt.meta index be7e6d7..1cb428c 100644 --- a/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt.meta +++ b/Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/ScriptTemplates/57-Mirror__Network Transform-NewNetworkTransform.cs.txt + uploadId: 736421 diff --git a/Assets/Scripts/MenuUI.cs b/Assets/Scripts/MenuUI.cs index 9264284..cf8165a 100644 --- a/Assets/Scripts/MenuUI.cs +++ b/Assets/Scripts/MenuUI.cs @@ -183,7 +183,7 @@ void StatusLabels() // server / client status message if (NetworkServer.active) { - GUILayout.Label("Server: active. Transport: " + Transport.activeTransport); + GUILayout.Label("Server: active. Transport: " + Transport.active); if (m_Manager.IsRelayEnabled()) { GUILayout.Label("Relay enabled. Join code: " + m_Manager.relayJoinCode); diff --git a/Assets/Scripts/MyNetworkManager.cs b/Assets/Scripts/MyNetworkManager.cs index 2adb001..a8d540d 100644 --- a/Assets/Scripts/MyNetworkManager.cs +++ b/Assets/Scripts/MyNetworkManager.cs @@ -142,14 +142,16 @@ public override void OnStopClient() m_SessionId = ""; } - public override void OnClientConnect(NetworkConnection conn) + public override void OnClientConnect() { + base.OnClientConnect(); + Debug.Log($"MyNetworkManager: {m_Username} Connected to Server!"); } - public override void OnClientDisconnect(NetworkConnection conn) + public override void OnClientDisconnect() { - base.OnClientDisconnect(conn); + base.OnClientDisconnect(); Debug.Log("MyNetworkManager: Disconnected from Server!"); } diff --git a/Assets/Scripts/Player.cs b/Assets/Scripts/Player.cs index dfc23f6..889d4d1 100644 --- a/Assets/Scripts/Player.cs +++ b/Assets/Scripts/Player.cs @@ -58,4 +58,5 @@ public override void OnStartServer() { Debug.Log("Player has been spawned on the server!"); } + } diff --git a/Assets/UTPTransport/Relay/RelayUtils.cs b/Assets/UTPTransport/Relay/RelayUtils.cs index 2e23d63..2bdbf53 100644 --- a/Assets/UTPTransport/Relay/RelayUtils.cs +++ b/Assets/UTPTransport/Relay/RelayUtils.cs @@ -34,7 +34,7 @@ public static RelayServerData HostRelayData(Allocation allocation, RelayServerEn } // Prepare the server endpoint using the Relay server IP and port - var serverEndpoint = NetworkEndPoint.Parse(endpoint.Host, (ushort)endpoint.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); @@ -45,7 +45,6 @@ public static RelayServerData HostRelayData(Allocation allocation, RelayServerEn // 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; } @@ -75,7 +74,7 @@ public static RelayServerData PlayerRelayData(JoinAllocation allocation, RelaySe } // Prepare the server endpoint using the Relay server IP and port - var serverEndpoint = NetworkEndPoint.Parse(endpoint.Host, (ushort)endpoint.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); @@ -87,7 +86,6 @@ public static RelayServerData PlayerRelayData(JoinAllocation allocation, RelaySe // 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; } diff --git a/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs b/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs index c6db453..756b4fd 100644 --- a/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs +++ b/Assets/UTPTransport/Relay/WrappedRelayServiceSDK.cs @@ -10,22 +10,22 @@ public class WrappedRelayServiceSDK : IRelayServiceSDK { public Task CreateAllocationAsync(int maxConnections, string region = null) { - return Relay.Instance.CreateAllocationAsync(maxConnections, region); + return RelayService.Instance.CreateAllocationAsync(maxConnections, region); } public Task GetJoinCodeAsync(Guid allocationId) { - return Relay.Instance.GetJoinCodeAsync(allocationId); + return RelayService.Instance.GetJoinCodeAsync(allocationId); } public Task JoinAllocationAsync(string joinCode) { - return Relay.Instance.JoinAllocationAsync(joinCode); + return RelayService.Instance.JoinAllocationAsync(joinCode); } public Task> ListRegionsAsync() { - return Relay.Instance.ListRegionsAsync(); + return RelayService.Instance.ListRegionsAsync(); } } } \ No newline at end of file diff --git a/Assets/UTPTransport/Tests/UTPTests.asmdef b/Assets/UTPTransport/Tests/UTPTests.asmdef index 5f3fc6e..a26ab83 100644 --- a/Assets/UTPTransport/Tests/UTPTests.asmdef +++ b/Assets/UTPTransport/Tests/UTPTests.asmdef @@ -1,14 +1,27 @@ { "name": "UTPTests", + "rootNamespace": "", "references": [ "Mirror", "UtpTransport", - "Unity.Services.Relay", "Unity.Services.Core", "Unity.Services.Authentication", - "Unity.Networking.Transport" + "Unity.Networking.Transport", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "Unity.Services.Multiplayer" ], - "optionalUnityReferences": [ - "TestAssemblies" - ] -} + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/UTPTransport/Utp/UtpClient.cs b/Assets/UTPTransport/Utp/UtpClient.cs index cdbcfd0..0f0a00b 100644 --- a/Assets/UTPTransport/Utp/UtpClient.cs +++ b/Assets/UTPTransport/Utp/UtpClient.cs @@ -2,11 +2,12 @@ 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; +using Unity.Networking.Transport; namespace Utp { @@ -257,8 +258,8 @@ public void Connect(string address, ushort port) } // TODO: Support for IPv6. - NetworkEndPoint endpoint; - if (!NetworkEndPoint.TryParse(address, port, out endpoint)) + NetworkEndpoint endpoint; + if (!NetworkEndpoint.TryParse(address, port, out endpoint)) { UtpLog.Error($"Abandoning connection attempt, failed to convert {address}:{port} into a valid NetworkEndpoint."); return; diff --git a/Assets/UTPTransport/Utp/UtpServer.cs b/Assets/UTPTransport/Utp/UtpServer.cs index d3b0a39..118a42f 100644 --- a/Assets/UTPTransport/Utp/UtpServer.cs +++ b/Assets/UTPTransport/Utp/UtpServer.cs @@ -3,11 +3,12 @@ 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; +using Unity.Networking.Transport; namespace Utp { @@ -294,7 +295,7 @@ public void Start(ushort port, bool useRelay = false, Allocation allocation = nu settings.WithNetworkConfigParameters(disconnectTimeoutMS: timeoutInMilliseconds); //Create IPV4 endpoint - NetworkEndPoint endpoint = NetworkEndPoint.AnyIpv4; + NetworkEndpoint endpoint = NetworkEndpoint.AnyIpv4; endpoint.Port = port; if (useRelay) @@ -487,7 +488,7 @@ 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); + NetworkEndpoint endpoint = driver.GetRemoteEndpoint(connection); return endpoint.Address; } else diff --git a/Assets/UTPTransport/UtpTransport.asmdef b/Assets/UTPTransport/UtpTransport.asmdef index 524a2bd..e340cdc 100644 --- a/Assets/UTPTransport/UtpTransport.asmdef +++ b/Assets/UTPTransport/UtpTransport.asmdef @@ -5,9 +5,9 @@ "Mirror", "Unity.Collections", "Unity.Jobs", - "Unity.Services.Relay", "Unity.Networking.Transport", - "Unity.Burst" + "Unity.Burst", + "Unity.Services.Multiplayer" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/UTPTransport/UtpTransport.cs b/Assets/UTPTransport/UtpTransport.cs index a562aa4..9374497 100644 --- a/Assets/UTPTransport/UtpTransport.cs +++ b/Assets/UTPTransport/UtpTransport.cs @@ -98,30 +98,32 @@ private void Awake() 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"); - } + // Commented out because always null + + //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"); + //} } /// diff --git a/Packages/manifest.json b/Packages/manifest.json index ebd66c2..b5af2df 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,16 +1,19 @@ { "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.ai.navigation": "2.0.10", + "com.unity.collab-proxy": "2.10.2", + "com.unity.collections": "2.6.4", + "com.unity.ide.rider": "3.0.39", + "com.unity.ide.visualstudio": "2.0.27", "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.multiplayer.center": "1.0.0", + "com.unity.nuget.newtonsoft-json": "3.2.2", + "com.unity.services.multiplayer": "1.1.8", + "com.unity.test-framework": "1.6.0", + "com.unity.timeline": "1.8.10", + "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.11", + "com.unity.ugui": "2.0.0", + "com.unity.modules.accessibility": "1.0.0", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 06ed057..7372b60 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,43 +1,52 @@ { "dependencies": { + "com.unity.ai.navigation": { + "version": "2.0.10", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.ai": "1.0.0" + }, + "url": "https://packages.unity.com" + }, "com.unity.burst": { - "version": "1.6.6", - "depth": 2, + "version": "1.8.25", + "depth": 1, "source": "registry", "dependencies": { - "com.unity.mathematics": "1.2.1" + "com.unity.mathematics": "1.2.1", + "com.unity.modules.jsonserialize": "1.0.0" }, "url": "https://packages.unity.com" }, "com.unity.collab-proxy": { - "version": "1.17.2", + "version": "2.10.2", "depth": 0, "source": "registry", - "dependencies": { - "com.unity.services.core": "1.0.1" - }, + "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.collections": { - "version": "1.4.0", - "depth": 1, + "version": "2.6.4", + "depth": 0, "source": "registry", "dependencies": { - "com.unity.burst": "1.6.6", - "com.unity.nuget.mono-cecil": "1.11.4", - "com.unity.test-framework": "1.1.31" + "com.unity.burst": "1.8.25", + "com.unity.mathematics": "1.3.2", + "com.unity.test-framework": "1.4.6", + "com.unity.nuget.mono-cecil": "1.11.6", + "com.unity.test-framework.performance": "3.0.3" }, "url": "https://packages.unity.com" }, "com.unity.ext.nunit": { - "version": "1.0.6", + "version": "2.0.5", "depth": 1, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" + "source": "builtin", + "dependencies": {} }, "com.unity.ide.rider": { - "version": "3.0.15", + "version": "3.0.39", "depth": 0, "source": "registry", "dependencies": { @@ -46,21 +55,14 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.16", + "version": "2.0.27", "depth": 0, "source": "registry", "dependencies": { - "com.unity.test-framework": "1.1.9" + "com.unity.test-framework": "1.1.33" }, "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, @@ -71,150 +73,186 @@ "url": "https://packages.unity.com" }, "com.unity.mathematics": { - "version": "1.2.6", - "depth": 2, + "version": "1.3.2", + "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.multiplayer.center": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.uielements": "1.0.0" + } + }, "com.unity.nuget.mono-cecil": { - "version": "1.11.4", - "depth": 2, + "version": "1.11.6", + "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.nuget.newtonsoft-json": { - "version": "3.0.2", - "depth": 1, + "version": "3.2.2", + "depth": 0, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.services.authentication": { - "version": "2.0.0", + "version": "3.4.1", "depth": 1, "source": "registry", "dependencies": { - "com.unity.nuget.newtonsoft-json": "3.0.2", - "com.unity.services.core": "1.3.1", + "com.unity.ugui": "1.0.0", + "com.unity.services.core": "1.14.0", + "com.unity.nuget.newtonsoft-json": "3.2.1", "com.unity.modules.unitywebrequest": "1.0.0" }, "url": "https://packages.unity.com" }, "com.unity.services.core": { - "version": "1.4.0", + "version": "1.14.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" + "com.unity.modules.androidjni": "1.0.0", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.modules.unitywebrequest": "1.0.0" }, "url": "https://packages.unity.com" }, - "com.unity.services.qos": { - "version": "1.0.2", + "com.unity.services.deployment": { + "version": "1.4.1", "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" + "com.unity.services.core": "1.12.0", + "com.unity.services.deployment.api": "1.1.2" }, "url": "https://packages.unity.com" }, - "com.unity.services.relay": { - "version": "1.0.4", + "com.unity.services.deployment.api": { + "version": "1.1.2", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.services.multiplayer": { + "version": "1.1.8", "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.transport": "2.5.0", + "com.unity.collections": "2.2.1", + "com.unity.services.qos": "1.3.0", + "com.unity.services.core": "1.13.0", + "com.unity.services.wire": "1.4.0", + "com.unity.services.deployment": "1.3.0", + "com.unity.nuget.newtonsoft-json": "3.2.1", "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.services.authentication": "3.3.3" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.qos": { + "version": "1.3.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.collections": "1.2.4", + "com.unity.services.core": "1.12.4", "com.unity.nuget.newtonsoft-json": "3.0.2", - "com.unity.transport": "1.1.0" + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.services.authentication": "2.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.services.wire": { + "version": "1.4.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.services.core": "1.12.5", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.services.authentication": "2.7.4" }, "url": "https://packages.unity.com" }, "com.unity.sysroot": { - "version": "2.0.3", + "version": "2.0.10", "depth": 1, "source": "registry", "dependencies": {}, "url": "https://packages.unity.com" }, "com.unity.sysroot.linux-x86_64": { - "version": "2.0.2", + "version": "2.0.9", "depth": 1, "source": "registry", "dependencies": { - "com.unity.sysroot": "2.0.3" + "com.unity.sysroot": "2.0.10" }, "url": "https://packages.unity.com" }, "com.unity.test-framework": { - "version": "1.1.31", + "version": "1.6.0", "depth": 0, - "source": "registry", + "source": "builtin", "dependencies": { - "com.unity.ext.nunit": "1.0.6", + "com.unity.ext.nunit": "2.0.3", "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, + "com.unity.test-framework.performance": { + "version": "3.2.0", + "depth": 1, "source": "registry", "dependencies": { - "com.unity.ugui": "1.0.0" + "com.unity.test-framework": "1.1.33", + "com.unity.modules.jsonserialize": "1.0.0" }, "url": "https://packages.unity.com" }, "com.unity.timeline": { - "version": "1.4.8", + "version": "1.8.10", "depth": 0, "source": "registry", "dependencies": { + "com.unity.modules.audio": "1.0.0", "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", + "version": "2.0.11", "depth": 0, "source": "registry", "dependencies": { - "com.unity.sysroot": "2.0.3", - "com.unity.sysroot.linux-x86_64": "2.0.2" + "com.unity.sysroot": "2.0.10", + "com.unity.sysroot.linux-x86_64": "2.0.9" }, "url": "https://packages.unity.com" }, "com.unity.transport": { - "version": "1.1.0", + "version": "2.6.0", "depth": 1, "source": "registry", "dependencies": { - "com.unity.collections": "1.2.4", - "com.unity.burst": "1.6.6", - "com.unity.mathematics": "1.2.6" + "com.unity.burst": "1.8.24", + "com.unity.collections": "2.2.1", + "com.unity.mathematics": "1.3.2" }, "url": "https://packages.unity.com" }, "com.unity.ugui": { - "version": "1.0.0", + "version": "2.0.0", "depth": 0, "source": "builtin", "dependencies": { @@ -222,6 +260,12 @@ "com.unity.modules.imgui": "1.0.0" } }, + "com.unity.modules.accessibility": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, "com.unity.modules.ai": { "version": "1.0.0", "depth": 0, @@ -269,6 +313,12 @@ "com.unity.modules.animation": "1.0.0" } }, + "com.unity.modules.hierarchycore": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": {} + }, "com.unity.modules.imageconversion": { "version": "1.0.0", "depth": 0, @@ -358,17 +408,8 @@ "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.hierarchycore": "1.0.0", + "com.unity.modules.physics": "1.0.0" } }, "com.unity.modules.umbra": { diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset index 93e7b16..6a8f218 100644 --- a/ProjectSettings/EditorBuildSettings.asset +++ b/ProjectSettings/EditorBuildSettings.asset @@ -6,6 +6,7 @@ EditorBuildSettings: serializedVersion: 2 m_Scenes: - enabled: 1 - path: Assets/Scenes/Main.unity - guid: 9fc0d4010bbf28b4594072e72b8655ab + path: Assets/Scenes/Main Mirror+Relay.unity + guid: e2f709018c33ad042b098d7c1dc0c54b m_configObjects: {} + m_UseUCBPForAssetBundles: 0 diff --git a/ProjectSettings/MemorySettings.asset b/ProjectSettings/MemorySettings.asset new file mode 100644 index 0000000..5b5face --- /dev/null +++ b/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/ProjectSettings/MultiplayerManager.asset b/ProjectSettings/MultiplayerManager.asset new file mode 100644 index 0000000..2a93664 --- /dev/null +++ b/ProjectSettings/MultiplayerManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!655991488 &1 +MultiplayerManager: + m_ObjectHideFlags: 0 + m_EnableMultiplayerRoles: 0 + m_StrippingTypes: {} diff --git a/ProjectSettings/Packages/com.unity.services.core/Settings.json b/ProjectSettings/Packages/com.unity.services.core/Settings.json new file mode 100644 index 0000000..4458075 --- /dev/null +++ b/ProjectSettings/Packages/com.unity.services.core/Settings.json @@ -0,0 +1,4 @@ +{ + "EnvironmentName": "", + "EnvironmentId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 8ab233c..0c5db63 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -3,7 +3,7 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 22 + serializedVersion: 28 productGUID: 04da073b036b76b4bb13c79105e8aa24 AndroidProfiler: 0 AndroidFilterTouchesWhenObscured: 0 @@ -48,14 +48,17 @@ PlayerSettings: defaultScreenHeightWeb: 600 m_StereoRenderingPath: 0 m_ActiveColorSpace: 1 + unsupportedMSAAFallback: 0 + m_SpriteBatchMaxVertexCount: 65535 + m_SpriteBatchVertexThreshold: 300 m_MTRendering: 1 mipStripping: 0 numberOfMipsStripped: 0 + numberOfMipsStrippedPerMipmapLimitGroup: {} m_StackTraceTypes: 010000000100000001000000010000000100000001000000 iosShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1 iosUseCustomAppBackgroundBehavior: 0 - iosAllowHTTPDownload: 1 allowedAutorotateToPortrait: 1 allowedAutorotateToPortraitUpsideDown: 1 allowedAutorotateToLandscapeRight: 1 @@ -67,24 +70,29 @@ PlayerSettings: androidStartInFullscreen: 1 androidRenderOutsideSafeArea: 1 androidUseSwappy: 1 + androidDisplayOptions: 1 androidBlitType: 0 - androidResizableWindow: 0 + androidResizeableActivity: 0 androidDefaultWindowWidth: 1920 androidDefaultWindowHeight: 1080 androidMinimumWindowWidth: 400 androidMinimumWindowHeight: 300 androidFullscreenMode: 1 + androidAutoRotationBehavior: 1 + androidPredictiveBackSupport: 1 + androidApplicationEntry: 1 defaultIsNativeResolution: 1 macRetinaSupport: 1 runInBackground: 1 - captureSingleScreen: 0 muteOtherAudioSources: 0 Prepare IOS For Recording: 0 Force IOS Speakers When Recording: 0 + audioSpatialExperience: 0 deferSystemGesturesMode: 0 hideHomeButton: 0 submitAnalytics: 1 usePlayerLog: 1 + dedicatedServerOptimizations: 1 bakeCollisionMeshes: 0 forceSingleInstance: 0 useFlipModelSwapchain: 1 @@ -92,6 +100,7 @@ PlayerSettings: useMacAppStoreValidation: 0 macAppStoreCategory: public.app-category.games gpuSkinning: 1 + meshDeformation: 2 xboxPIXTextureCapture: 0 xboxEnableAvatar: 0 xboxEnableKinect: 0 @@ -119,22 +128,22 @@ PlayerSettings: switchNVNShaderPoolsGranularity: 33554432 switchNVNDefaultPoolsGranularity: 16777216 switchNVNOtherPoolsGranularity: 16777216 + switchGpuScratchPoolGranularity: 2097152 + switchAllowGpuScratchShrinking: 0 switchNVNMaxPublicTextureIDCount: 0 switchNVNMaxPublicSamplerIDCount: 0 - stadiaPresentMode: 0 - stadiaTargetFramerate: 0 + switchMaxWorkerMultiple: 8 + switchNVNGraphicsFirmwareMemory: 32 + switchGraphicsJobsSyncAfterKick: 1 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 + loadStoreDebugModeEnabled: 0 + visionOSBundleVersion: 1.0 + tvOSBundleVersion: 1.0 + bundleVersion: 0.2 preloadedAssets: [] metroInputSource: 0 wsaTransparentSwapchain: 0 @@ -145,22 +154,27 @@ PlayerSettings: enable360StereoCapture: 0 isWsaHolographicRemotingEnabled: 0 enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 + allowHDRDisplaySupport: 0 useHDRDisplay: 0 - D3DHDRBitDepth: 0 + hdrBitDepth: 0 m_ColorGamuts: 00000000 targetPixelDensity: 30 resolutionScalingMode: 0 resetResolutionOnWindowResize: 0 androidSupportedAspectRatio: 1 androidMaxAspectRatio: 2.1 - applicationIdentifier: {} + androidMinAspectRatio: 1 + applicationIdentifier: + Android: com.UnityTechnologies.UnityRelayMirrorSample buildNumber: Standalone: 0 + VisionOS: 0 iPhone: 0 tvOS: 0 overrideDefaultApplicationIdentifier: 0 AndroidBundleVersionCode: 1 - AndroidMinSdkVersion: 19 + AndroidMinSdkVersion: 23 AndroidTargetSdkVersion: 0 AndroidPreferredInstallLocation: 1 aotOptions: @@ -170,15 +184,20 @@ PlayerSettings: ForceInternetPermission: 0 ForceSDCardPermission: 0 CreateWallpaper: 0 - APKExpansionFiles: 0 + androidSplitApplicationBinary: 0 keepLoadedShadersAlive: 0 StripUnusedMeshComponents: 1 + strictShaderVariantMatching: 0 VertexChannelCompressionMask: 4054 iPhoneSdkVersion: 988 - iOSTargetOSVersionString: 11.0 + iOSSimulatorArchitecture: 0 + iOSTargetOSVersionString: 13.0 tvOSSdkVersion: 0 + tvOSSimulatorArchitecture: 0 tvOSRequireExtendedGameController: 0 - tvOSTargetOSVersionString: 11.0 + tvOSTargetOSVersionString: 13.0 + VisionOSSdkVersion: 0 + VisionOSTargetOSVersionString: 1.0 uIPrerenderedIcon: 0 uIRequiresPersistentWiFi: 0 uIRequiresFullScreen: 1 @@ -203,7 +222,6 @@ PlayerSettings: rgba: 0 iOSLaunchScreenFillPct: 100 iOSLaunchScreenSize: 100 - iOSLaunchScreenCustomXibPath: iOSLaunchScreeniPadType: 0 iOSLaunchScreeniPadImage: {fileID: 0} iOSLaunchScreeniPadBackgroundColor: @@ -211,22 +229,25 @@ PlayerSettings: rgba: 0 iOSLaunchScreeniPadFillPct: 100 iOSLaunchScreeniPadSize: 100 - iOSLaunchScreeniPadCustomXibPath: iOSLaunchScreenCustomStoryboardPath: iOSLaunchScreeniPadCustomStoryboardPath: iOSDeviceRequirements: [] iOSURLSchemes: [] + macOSURLSchemes: [] iOSBackgroundModes: 0 iOSMetalForceHardShadows: 0 metalEditorSupport: 1 metalAPIValidation: 1 + metalCompileShaderBinary: 0 iOSRenderExtraFrameOnPause: 0 iosCopyPluginsCodeInsteadOfSymlink: 0 appleDeveloperTeamID: iOSManualSigningProvisioningProfileID: tvOSManualSigningProvisioningProfileID: + VisionOSManualSigningProvisioningProfileID: iOSManualSigningProvisioningProfileType: 0 tvOSManualSigningProvisioningProfileType: 0 + VisionOSManualSigningProvisioningProfileType: 0 appleEnableAutomaticSigning: 0 iOSRequireARKit: 0 iOSAutomaticallyDetectAndAddCapabilities: 1 @@ -241,16 +262,21 @@ PlayerSettings: useCustomLauncherGradleManifest: 0 useCustomBaseGradleTemplate: 0 useCustomGradlePropertiesTemplate: 0 + useCustomGradleSettingsTemplate: 0 useCustomProguardFile: 0 - AndroidTargetArchitectures: 1 - AndroidTargetDevices: 0 + AndroidTargetArchitectures: 2 AndroidSplashScreenScale: 0 androidSplashScreen: {fileID: 0} AndroidKeystoreName: AndroidKeyaliasName: + AndroidEnableArmv9SecurityFeatures: 0 + AndroidEnableArm64MTE: 0 AndroidBuildApkPerCpuArchitecture: 0 AndroidTVCompatibility: 0 AndroidIsGame: 1 + androidAppCategory: 3 + useAndroidAppCategory: 1 + androidAppCategoryOther: AndroidEnableTango: 0 androidEnableBanner: 1 androidUseLowAccuracyLocation: 0 @@ -260,14 +286,106 @@ PlayerSettings: height: 180 banner: {fileID: 0} androidGamepadSupportLevel: 0 - chromeosInputEmulation: 1 - AndroidMinifyWithR8: 0 AndroidMinifyRelease: 0 AndroidMinifyDebug: 0 AndroidValidateAppBundleSize: 1 AndroidAppBundleSizeToValidate: 150 + AndroidReportGooglePlayAppDependencies: 1 + androidSymbolsSizeThreshold: 800 m_BuildTargetIcons: [] - m_BuildTargetPlatformIcons: [] + m_BuildTargetPlatformIcons: + - m_BuildTarget: Android + m_Icons: + - m_Textures: [] + m_Width: 432 + m_Height: 432 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 324 + m_Height: 324 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 216 + m_Height: 216 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 162 + m_Height: 162 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 108 + m_Height: 108 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 81 + m_Height: 81 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 0 + m_SubKind: m_BuildTargetBatching: - m_BuildTarget: Standalone m_StaticBatching: 1 @@ -284,6 +402,7 @@ PlayerSettings: - m_BuildTarget: WebGL m_StaticBatching: 0 m_DynamicBatching: 0 + m_BuildTargetShaderSettings: [] m_BuildTargetGraphicsJobs: - m_BuildTarget: MacStandaloneSupport m_GraphicsJobs: 0 @@ -319,7 +438,7 @@ PlayerSettings: m_BuildTargetGraphicsAPIs: - m_BuildTarget: AndroidPlayer m_APIs: 150000000b000000 - m_Automatic: 0 + m_Automatic: 1 - m_BuildTarget: iOSSupport m_APIs: 10000000 m_Automatic: 1 @@ -329,12 +448,17 @@ PlayerSettings: - m_BuildTarget: WebGLSupport m_APIs: 0b000000 m_Automatic: 1 + - m_BuildTarget: WindowsStandaloneSupport + m_APIs: 0200000012000000 + m_Automatic: 0 m_BuildTargetVRSettings: - m_BuildTarget: Standalone m_Enabled: 0 m_Devices: - Oculus - OpenVR + m_DefaultShaderChunkSizeInMB: 16 + m_DefaultShaderChunkCount: 0 openGLRequireES31: 0 openGLRequireES31AEP: 0 openGLRequireES32: 0 @@ -344,11 +468,18 @@ PlayerSettings: iPhone: 1 tvOS: 1 m_BuildTargetGroupLightmapEncodingQuality: [] + m_BuildTargetGroupHDRCubemapEncodingQuality: [] m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetGroupLoadStoreDebugModeSettings: [] m_BuildTargetNormalMapEncoding: [] + m_BuildTargetDefaultTextureCompressionFormat: + - serializedVersion: 3 + m_BuildTarget: Android + m_Formats: 01000000 playModeTestRunnerEnabled: 0 runPlayModeTestAsEditModeTest: 0 actionOnDotNetUnhandledException: 1 + editorGfxJobOverride: 1 enableInternalProfiler: 0 logObjCUncaughtExceptions: 1 enableCrashReportAPI: 0 @@ -356,6 +487,7 @@ PlayerSettings: locationUsageDescription: microphoneUsageDescription: bluetoothUsageDescription: + macOSTargetOSVersion: 11.0 switchNMETAOverride: switchNetLibKey: switchSocketMemoryPoolSize: 6144 @@ -363,9 +495,11 @@ PlayerSettings: switchSocketConcurrencyLimit: 14 switchScreenResolutionBehavior: 2 switchUseCPUProfiler: 0 - switchUseGOLDLinker: 0 + switchEnableFileSystemTrace: 0 + switchLTOSetting: 0 switchApplicationID: 0x01004b9000490000 switchNSODependencies: + switchCompilerFlags: switchTitleNames_0: switchTitleNames_1: switchTitleNames_2: @@ -439,7 +573,6 @@ PlayerSettings: switchReleaseVersion: 0 switchDisplayVersion: 1.0.0 switchStartupUserAccount: 0 - switchTouchScreenUsage: 0 switchSupportedLanguagesMask: 0 switchLogoType: 0 switchApplicationErrorCodeCategory: @@ -481,6 +614,7 @@ PlayerSettings: switchNativeFsCacheSize: 32 switchIsHoldTypeHorizontal: 0 switchSupportedNpadCount: 8 + switchEnableTouchScreen: 1 switchSocketConfigEnabled: 0 switchTcpInitialSendBufferSize: 32 switchTcpInitialReceiveBufferSize: 64 @@ -491,12 +625,14 @@ PlayerSettings: switchSocketBufferEfficiency: 4 switchSocketInitializeEnabled: 1 switchNetworkInterfaceManagerInitializeEnabled: 1 - switchPlayerConnectionEnabled: 1 + switchDisableHTCSPlayerConnection: 0 switchUseNewStyleFilepaths: 0 + switchUseLegacyFmodPriorities: 0 switchUseMicroSleepForYield: 1 switchEnableRamDiskSupport: 0 switchMicroSleepForYieldTime: 25 switchRamDiskSpaceSize: 12 + switchUpgradedPlayerSettingsToNMETA: 0 ps4NPAgeRating: 12 ps4NPTitleSecret: ps4NPTrophyPackPath: @@ -580,6 +716,7 @@ PlayerSettings: webGLMemorySize: 16 webGLExceptionSupport: 1 webGLNameFilesAsHashes: 0 + webGLShowDiagnostics: 0 webGLDataCaching: 1 webGLDebugSymbols: 0 webGLEmscriptenArgs: @@ -592,25 +729,55 @@ PlayerSettings: webGLLinkerTarget: 1 webGLThreadsSupport: 0 webGLDecompressionFallback: 0 + webGLInitialMemorySize: 32 + webGLMaximumMemorySize: 2048 + webGLMemoryGrowthMode: 2 + webGLMemoryLinearGrowthStep: 16 + webGLMemoryGeometricGrowthStep: 0.2 + webGLMemoryGeometricGrowthCap: 96 + webGLPowerPreference: 2 + webGLWebAssemblyTable: 0 + webGLWebAssemblyBigInt: 0 + webGLCloseOnQuit: 0 + webWasm2023: 0 + webEnableSubmoduleStrippingCompatibility: 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 + Android: MIRROR;MIRROR_89_OR_NEWER;MIRROR_90_OR_NEWER;MIRROR_93_OR_NEWER;MIRROR_96_OR_NEWER;EDGEGAP_PLUGIN_SERVERS + Standalone: 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;MIRROR_89_OR_NEWER;MIRROR_90_OR_NEWER;MIRROR_93_OR_NEWER;MIRROR_96_OR_NEWER;EDGEGAP_PLUGIN_SERVERS additionalCompilerArguments: {} platformArchitecture: {} - scriptingBackend: {} + scriptingBackend: + Android: 1 il2cppCompilerConfiguration: {} - managedStrippingLevel: {} + il2cppCodeGeneration: {} + il2cppStacktraceInformation: {} + managedStrippingLevel: + Android: 1 + EmbeddedLinux: 1 + GameCoreScarlett: 1 + GameCoreXboxOne: 1 + Kepler: 1 + Nintendo Switch: 1 + PS4: 1 + PS5: 1 + QNX: 1 + ReservedCFE: 1 + VisionOS: 1 + WebGL: 1 + Windows Store Apps: 1 + XboxOne: 1 + iPhone: 1 + tvOS: 1 incrementalIl2cppBuild: {} suppressCommonWarnings: 1 allowUnsafeCode: 0 useDeterministicCompilation: 1 - useReferenceAssemblies: 1 - enableRoslynAnalyzers: 1 additionalIl2CppArgs: scriptingRuntimeVersion: 1 gcIncremental: 1 - assemblyVersionValidation: 1 gcWBarrierValidation: 0 apiCompatibilityLevelPerPlatform: {} + editorAssembliesCompatibilityLevel: 1 m_RenderingPath: 1 m_MobileRenderingPath: 1 metroPackageName: Template_3D @@ -634,6 +801,7 @@ PlayerSettings: 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 + syncCapabilities: 0 platformCapabilities: {} metroTargetDeviceFamilies: {} metroFTAName: @@ -681,13 +849,28 @@ PlayerSettings: luminVersion: m_VersionCode: 1 m_VersionName: + hmiPlayerDataPath: + hmiForceSRGBBlit: 0 + embeddedLinuxEnableGamepadInput: 0 + hmiCpuConfiguration: + hmiLogStartupTiming: 0 + qnxGraphicConfPath: apiCompatibilityLevel: 6 + captureStartupLogs: {} activeInputHandler: 0 - cloudProjectId: 9134ca3b-e51a-4d98-ab4e-65149173f3e8 + windowsGamepadBackendHint: 0 + cloudProjectId: framebufferDepthMemorylessMode: 0 qualitySettingsNames: [] - projectName: UnityMirrorSample - organizationId: wj_erik + projectName: + organizationId: cloudEnabled: 0 legacyClampBlendShapeWeights: 0 + hmiLoadingImage: {fileID: 0} + platformRequiresReadableAssets: 0 virtualTexturingSupportEnabled: 0 + insecureHttpOption: 0 + androidVulkanDenyFilterList: [] + androidVulkanAllowFilterList: [] + androidVulkanDeviceFilterListAsset: {fileID: 0} + d3d12DeviceFilterListAsset: {fileID: 0} diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index dd7e0b0..1ddb36d 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2020.3.40f1 -m_EditorVersionWithRevision: 2020.3.40f1 (ba48d4efcef1) +m_EditorVersion: 6000.2.10f1 +m_EditorVersionWithRevision: 6000.2.10f1 (d3d30d158480) diff --git a/README.md b/README.md index 708979e..ed638a9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ -# [DEPRECATED] Unity Relay Mirror Sample -> [!WARNING] -> This sample is deprecated. -> This sample was tested on Unity 2020.3 and is not maintained for later versions. +# 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. @@ -12,12 +9,12 @@ The Unity Relay Mirror Sample demonstrates how to use the [Unity Transport Packa 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 Editor version 6000.2.10f1](https://unity.com/releases/editor/whats-new/6000.2.10f1) * 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 Multiplayer Services](https://docs.unity3d.com/Packages/com.unity.services.multiplayer@1.1/manual/index.html) * [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/)