diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs b/Assets/Mirror/Authenticators/BasicAuthenticator.cs
index 0fdc7e2..3c077c7 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;
@@ -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/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/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/CompilerSymbols/PreprocessorDefine.cs b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs
index 89867c1..cb2b01f 100644
--- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs
+++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs
@@ -15,38 +15,20 @@ public static void AddDefineSymbols()
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_66_0_OR_NEWER",
+ "MIRROR_2022_9_OR_NEWER",
+ "MIRROR_2022_10_OR_NEWER",
+ "MIRROR_70_0_OR_NEWER",
+ "MIRROR_71_0_OR_NEWER",
+ "MIRROR_73_OR_NEWER"
+ // Remove oldest when adding next month's symbol.
+ // Keep a rolling 12 months of symbols.
};
- // 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)
diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta
index 30806d0..6306c16 100644
--- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta
+++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta
@@ -5,7 +5,7 @@ MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
- icon: {instanceID: 0}
+ icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs
index ddb38ea..cc9f116 100644
--- a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs
+++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs
@@ -30,7 +30,7 @@ public override void Start()
// so make sure we set it here in Start() (after awakes)
// Or just let the user assign it in the inspector
if (transport == null)
- transport = Transport.activeTransport;
+ transport = Transport.active;
base.Start();
}
@@ -68,11 +68,9 @@ protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoi
throw;
}
}
-
#endregion
#region Client
-
///
/// Create a message that will be broadcasted on the network to discover servers
///
@@ -108,7 +106,6 @@ protected override void ProcessResponse(ServerResponse response, IPEndPoint endp
OnServerFound.Invoke(response);
}
-
#endregion
}
}
diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs
index ac57b75..de74fbd 100644
--- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs
+++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs
@@ -39,6 +39,10 @@ public abstract class NetworkDiscoveryBase : MonoBehaviour
[Tooltip("Time in seconds between multi-cast messages")]
[Range(1, 60)]
float ActiveDiscoveryInterval = 3;
+
+ // broadcast address needs to be configurable on iOS:
+ // https://github.com/vis2k/Mirror/pull/3255
+ public string BroadcastAddress = "";
protected UdpClient serverUdpClient;
protected UdpClient clientUdpClient;
@@ -368,6 +372,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())
{
diff --git a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs
index 06a87a9..d9f3042 100644
--- a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs
+++ b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs
@@ -8,16 +8,17 @@ 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;
-
+ double nextSyncTime;
[SyncVar()]
Vector3 targetVelocity;
@@ -28,29 +29,22 @@ public class NetworkLerpRigidbody : NetworkBehaviour
///
/// Ignore value if is host or client with Authority
///
- ///
bool IgnoreSync => isServer || ClientWithAuthority;
- bool ClientWithAuthority => clientAuthority && hasAuthority;
+ bool ClientWithAuthority => clientAuthority && isOwned;
void OnValidate()
{
if (target == null)
- {
target = GetComponent();
- }
}
void Update()
{
if (isServer)
- {
SyncToClients();
- }
else if (ClientWithAuthority)
- {
SendToServer();
- }
}
void SyncToClients()
@@ -61,7 +55,7 @@ void SyncToClients()
void SendToServer()
{
- float now = Time.time;
+ double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet
if (now > nextSyncTime)
{
nextSyncTime = now + syncInterval;
diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs
index 4989d29..660adc5 100644
--- a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs
+++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs
@@ -13,7 +13,6 @@ public class NetworkRigidbody : NetworkBehaviour
public bool clientAuthority = false;
[Header("Velocity")]
-
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
@@ -23,9 +22,7 @@ public class NetworkRigidbody : NetworkBehaviour
[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;
@@ -43,13 +40,11 @@ public class NetworkRigidbody : NetworkBehaviour
void OnValidate()
{
if (target == null)
- {
target = GetComponent();
- }
}
-
#region Sync vars
+
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector3 velocity;
@@ -74,7 +69,7 @@ void OnValidate()
///
bool IgnoreSync => isServer || ClientWithAuthority;
- bool ClientWithAuthority => clientAuthority && hasAuthority;
+ bool ClientWithAuthority => clientAuthority && isOwned;
void OnVelocityChanged(Vector3 _, Vector3 newValue)
{
@@ -84,7 +79,6 @@ void OnVelocityChanged(Vector3 _, Vector3 newValue)
target.velocity = newValue;
}
-
void OnAngularVelocityChanged(Vector3 _, Vector3 newValue)
{
if (IgnoreSync)
@@ -124,32 +118,24 @@ void OnAngularDragChanged(float _, float newValue)
target.angularDrag = newValue;
}
- #endregion
+ #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;
- }
}
///
@@ -191,7 +177,7 @@ void SyncToClients()
[Client]
void SendToServer()
{
- if (!hasAuthority)
+ if (!isOwned)
{
Debug.LogWarning("SendToServer called without authority");
return;
@@ -204,7 +190,7 @@ void SendToServer()
[Client]
void SendVelocity()
{
- float now = Time.time;
+ double now = NetworkTime.localTime; // Unity 2019 doesn't have Time.timeAsDouble yet
if (now < previousValue.nextSyncTime)
return;
@@ -231,9 +217,7 @@ void SendVelocity()
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
- {
previousValue.nextSyncTime = now + syncInterval;
- }
}
[Client]
@@ -289,10 +273,9 @@ void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity)
if (syncVelocity)
{
this.velocity = velocity;
-
target.velocity = velocity;
-
}
+
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
}
@@ -349,7 +332,7 @@ public class ClientSyncState
///
/// Next sync time that velocity will be synced, based on syncInterval.
///
- public float nextSyncTime;
+ public double nextSyncTime;
public Vector3 velocity;
public Vector3 angularVelocity;
public bool isKinematic;
diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs
index c14b260..5a2c340 100644
--- a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs
+++ b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs
@@ -3,16 +3,16 @@
namespace Mirror.Experimental
{
[AddComponentMenu("Network/ Experimental/Network Rigidbody 2D")]
+ [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
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;
+ public bool clientAuthority = false;
[Header("Velocity")]
-
[Tooltip("Syncs Velocity every SyncInterval")]
[SerializeField] bool syncVelocity = true;
@@ -22,9 +22,7 @@ public class NetworkRigidbody2D : NetworkBehaviour
[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;
@@ -42,13 +40,11 @@ public class NetworkRigidbody2D : NetworkBehaviour
void OnValidate()
{
if (target == null)
- {
target = GetComponent();
- }
}
-
#region Sync vars
+
[SyncVar(hook = nameof(OnVelocityChanged))]
Vector2 velocity;
@@ -70,10 +66,9 @@ void OnValidate()
///
/// Ignore value if is host or client with Authority
///
- ///
bool IgnoreSync => isServer || ClientWithAuthority;
- bool ClientWithAuthority => clientAuthority && hasAuthority;
+ bool ClientWithAuthority => clientAuthority && isOwned;
void OnVelocityChanged(Vector2 _, Vector2 newValue)
{
@@ -83,7 +78,6 @@ void OnVelocityChanged(Vector2 _, Vector2 newValue)
target.velocity = newValue;
}
-
void OnAngularVelocityChanged(float _, float newValue)
{
if (IgnoreSync)
@@ -123,32 +117,24 @@ void OnAngularDragChanged(float _, float newValue)
target.angularDrag = newValue;
}
- #endregion
+ #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;
- }
}
///
@@ -190,7 +176,7 @@ void SyncToClients()
[Client]
void SendToServer()
{
- if (!hasAuthority)
+ if (!isOwned)
{
Debug.LogWarning("SendToServer called without authority");
return;
@@ -227,12 +213,9 @@ void SendVelocity()
previousValue.velocity = currentVelocity;
}
-
// only update syncTime if either has changed
if (angularVelocityChanged || velocityChanged)
- {
previousValue.nextSyncTime = now + syncInterval;
- }
}
[Client]
@@ -288,9 +271,7 @@ void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity)
if (syncVelocity)
{
this.velocity = velocity;
-
target.velocity = velocity;
-
}
this.angularVelocity = angularVelocity;
target.angularVelocity = angularVelocity;
diff --git a/Assets/Mirror/Components/Experimental/NetworkTransform.cs b/Assets/Mirror/Components/Experimental/NetworkTransform.cs
deleted file mode 100644
index ca52141..0000000
--- a/Assets/Mirror/Components/Experimental/NetworkTransform.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using UnityEngine;
-
-namespace Mirror.Experimental
-{
- [DisallowMultipleComponent]
- // Deprecated 2022-01-18
- [Obsolete("Use the default NetworkTransform instead, it has proper snapshot interpolation.")]
- [AddComponentMenu("")]
- public class NetworkTransform : NetworkTransformBase
- {
- protected override Transform targetTransform => transform;
- }
-}
diff --git a/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs b/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs
deleted file mode 100644
index 8bee5ec..0000000
--- a/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs
+++ /dev/null
@@ -1,531 +0,0 @@
-// vis2k:
-// base class for NetworkTransform and NetworkTransformChild.
-// New method is simple and stupid. No more 1500 lines of code.
-//
-// Server sends current data.
-// Client saves it and interpolates last and latest data points.
-// Update handles transform movement / rotation
-// FixedUpdate handles rigidbody movement / rotation
-//
-// Notes:
-// * Built-in Teleport detection in case of lags / teleport / obstacles
-// * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp
-// * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code.
-// * Initial delay might happen if server sends packet immediately after moving
-// just 1cm, hence we move 1cm and then wait 100ms for next packet
-// * Only way for smooth movement is to use a fixed movement speed during
-// interpolation. interpolation over time is never that good.
-//
-using System;
-using UnityEngine;
-
-namespace Mirror.Experimental
-{
- // Deprecated 2022-01-18
- [Obsolete("Use the default NetworkTransform instead, it has proper snapshot interpolation.")]
- public abstract class NetworkTransformBase : NetworkBehaviour
- {
- // target transform to sync. can be on a child.
- protected abstract Transform targetTransform { get; }
-
- [Header("Authority")]
-
- [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
- [SyncVar]
- public bool clientAuthority;
-
- [Tooltip("Set to true if updates from server should be ignored by owner")]
- [SyncVar]
- public bool excludeOwnerUpdate = true;
-
- [Header("Synchronization")]
-
- [Tooltip("Set to true if position should be synchronized")]
- [SyncVar]
- public bool syncPosition = true;
-
- [Tooltip("Set to true if rotation should be synchronized")]
- [SyncVar]
- public bool syncRotation = true;
-
- [Tooltip("Set to true if scale should be synchronized")]
- [SyncVar]
- public bool syncScale = true;
-
- [Header("Interpolation")]
-
- [Tooltip("Set to true if position should be interpolated")]
- [SyncVar]
- public bool interpolatePosition = true;
-
- [Tooltip("Set to true if rotation should be interpolated")]
- [SyncVar]
- public bool interpolateRotation = true;
-
- [Tooltip("Set to true if scale should be interpolated")]
- [SyncVar]
- public bool interpolateScale = true;
-
- // Sensitivity is added for VR where human players tend to have micro movements so this can quiet down
- // the network traffic. Additionally, rigidbody drift should send less traffic, e.g very slow sliding / rolling.
- [Header("Sensitivity")]
-
- [Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
- [SyncVar]
- public float localPositionSensitivity = .01f;
-
- [Tooltip("If rotation exceeds this angle, it will be transmitted on the network")]
- [SyncVar]
- public float localRotationSensitivity = .01f;
-
- [Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
- [SyncVar]
- public float localScaleSensitivity = .01f;
-
- [Header("Diagnostics")]
-
- // server
- public Vector3 lastPosition;
- public Quaternion lastRotation;
- public Vector3 lastScale;
-
- // client
- // use local position/rotation for VR support
- [Serializable]
- public struct DataPoint
- {
- public float timeStamp;
- public Vector3 localPosition;
- public Quaternion localRotation;
- public Vector3 localScale;
- public float movementSpeed;
-
- public bool isValid => timeStamp != 0;
- }
-
- // 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.
- bool IsOwnerWithClientAuthority => hasAuthority && clientAuthority;
-
- // interpolation start and goal
- public DataPoint start = new DataPoint();
- public DataPoint goal = new DataPoint();
-
- // We need to store this locally on the server so clients can't request Authority when ever they like
- bool clientAuthorityBeforeTeleport;
-
- void FixedUpdate()
- {
- // if server then always sync to others.
- // let the clients know that this has moved
- if (isServer && HasEitherMovedRotatedScaled())
- {
- ServerUpdate();
- }
-
- if (isClient)
- {
- // send to server if we have local authority (and aren't the server)
- // -> only if connectionToServer has been initialized yet too
- if (IsOwnerWithClientAuthority)
- {
- ClientAuthorityUpdate();
- }
- else if (goal.isValid)
- {
- ClientRemoteUpdate();
- }
- }
- }
-
- void ServerUpdate()
- {
- RpcMove(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
- }
-
- void ClientAuthorityUpdate()
- {
- if (!isServer && HasEitherMovedRotatedScaled())
- {
- // serialize
- // local position/rotation for VR support
- // send to server
- CmdClientToServerSync(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
- }
- }
-
- void ClientRemoteUpdate()
- {
- // teleport or interpolate
- if (NeedsTeleport())
- {
- // local position/rotation for VR support
- ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
-
- // reset data points so we don't keep interpolating
- start = new DataPoint();
- goal = new DataPoint();
- }
- else
- {
- // local position/rotation for VR support
- ApplyPositionRotationScale(InterpolatePosition(start, goal, targetTransform.localPosition),
- InterpolateRotation(start, goal, targetTransform.localRotation),
- InterpolateScale(start, goal, targetTransform.localScale));
- }
- }
-
- // moved or rotated or scaled since last time we checked it?
- bool HasEitherMovedRotatedScaled()
- {
- // Save last for next frame to compare only if change was detected, otherwise
- // slow moving objects might never sync because of C#'s float comparison tolerance.
- // See also: https://github.com/vis2k/Mirror/pull/428)
- bool changed = HasMoved || HasRotated || HasScaled;
- if (changed)
- {
- // local position/rotation for VR support
- if (syncPosition) lastPosition = targetTransform.localPosition;
- if (syncRotation) lastRotation = targetTransform.localRotation;
- if (syncScale) lastScale = targetTransform.localScale;
- }
- return changed;
- }
-
- // local position/rotation for VR support
- // SqrMagnitude is faster than Distance per Unity docs
- // https://docs.unity3d.com/ScriptReference/Vector3-sqrMagnitude.html
-
- bool HasMoved => syncPosition && Vector3.SqrMagnitude(lastPosition - targetTransform.localPosition) > localPositionSensitivity * localPositionSensitivity;
- bool HasRotated => syncRotation && Quaternion.Angle(lastRotation, targetTransform.localRotation) > localRotationSensitivity;
- bool HasScaled => syncScale && Vector3.SqrMagnitude(lastScale - targetTransform.localScale) > localScaleSensitivity * localScaleSensitivity;
-
- // teleport / lag / stuck detection
- // - checking distance is not enough since there could be just a tiny fence between us and the goal
- // - checking time always works, this way we just teleport if we still didn't reach the goal after too much time has elapsed
- bool NeedsTeleport()
- {
- // calculate time between the two data points
- float startTime = start.isValid ? start.timeStamp : Time.time - Time.fixedDeltaTime;
- float goalTime = goal.isValid ? goal.timeStamp : Time.time;
- float difference = goalTime - startTime;
- float timeSinceGoalReceived = Time.time - goalTime;
- return timeSinceGoalReceived > difference * 5;
- }
-
- // local authority client sends sync message to server for broadcasting
- [Command(channel = Channels.Unreliable)]
- void CmdClientToServerSync(Vector3 position, uint packedRotation, Vector3 scale)
- {
- // Ignore messages from client if not in client authority mode
- if (!clientAuthority)
- return;
-
- // deserialize payload
- SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
-
- // server-only mode does no interpolation to save computations, but let's set the position directly
- if (isServer && !isClient)
- ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
-
- RpcMove(position, packedRotation, scale);
- }
-
- [ClientRpc(channel = Channels.Unreliable)]
- void RpcMove(Vector3 position, uint packedRotation, Vector3 scale)
- {
- if (hasAuthority && excludeOwnerUpdate) return;
-
- if (!isServer)
- SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
- }
-
- // serialization is needed by OnSerialize and by manual sending from authority
- void SetGoal(Vector3 position, Quaternion rotation, Vector3 scale)
- {
- // put it into a data point immediately
- DataPoint temp = new DataPoint
- {
- // deserialize position
- localPosition = position,
- localRotation = rotation,
- localScale = scale,
- timeStamp = Time.time
- };
-
- // movement speed: based on how far it moved since last time has to be calculated before 'start' is overwritten
- temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetTransform, Time.fixedDeltaTime);
-
- // reassign start wisely
- // first ever data point? then make something up for previous one so that we can start interpolation without waiting for next.
- if (start.timeStamp == 0)
- {
- start = new DataPoint
- {
- timeStamp = Time.time - Time.fixedDeltaTime,
- // local position/rotation for VR support
- localPosition = targetTransform.localPosition,
- localRotation = targetTransform.localRotation,
- localScale = targetTransform.localScale,
- movementSpeed = temp.movementSpeed
- };
- }
- // second or nth data point? then update previous
- // but: we start at where ever we are right now, so that it's perfectly smooth and we don't jump anywhere
- //
- // example if we are at 'x':
- //
- // A--x->B
- //
- // and then receive a new point C:
- //
- // A--x--B
- // |
- // |
- // C
- //
- // then we don't want to just jump to B and start interpolation:
- //
- // x
- // |
- // |
- // C
- //
- // we stay at 'x' and interpolate from there to C:
- //
- // x..B
- // \ .
- // \.
- // C
- //
- else
- {
- float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition);
- float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition);
-
- start = goal;
-
- // local position/rotation for VR support
- // teleport / lag / obstacle detection: only continue at current position if we aren't too far away
- // XC < AB + BC (see comments above)
- if (Vector3.Distance(targetTransform.localPosition, start.localPosition) < oldDistance + newDistance)
- {
- start.localPosition = targetTransform.localPosition;
- start.localRotation = targetTransform.localRotation;
- start.localScale = targetTransform.localScale;
- }
- }
-
- // set new destination in any case. new data is best data.
- goal = temp;
- }
-
- // try to estimate movement speed for a data point based on how far it moved since the previous one
- // - if this is the first time ever then we use our best guess:
- // - delta based on transform.localPosition
- // - elapsed based on send interval hoping that it roughly matches
- static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval)
- {
- Vector3 delta = to.localPosition - (from.localPosition != transform.localPosition ? from.localPosition : transform.localPosition);
- float elapsed = from.isValid ? to.timeStamp - from.timeStamp : sendInterval;
-
- // avoid NaN
- return elapsed > 0 ? delta.magnitude / elapsed : 0;
- }
-
- // set position carefully depending on the target component
- void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale)
- {
- // local position/rotation for VR support
- if (syncPosition) targetTransform.localPosition = position;
- if (syncRotation) targetTransform.localRotation = rotation;
- if (syncScale) targetTransform.localScale = scale;
- }
-
- // where are we in the timeline between start and goal? [0,1]
- Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition)
- {
- if (!interpolatePosition)
- return currentPosition;
-
- if (start.movementSpeed != 0)
- {
- // Option 1: simply interpolate based on time, but stutter will happen, it's not that smooth.
- // This is especially noticeable if the camera automatically follows the player
- // - Tell SonarCloud this isn't really commented code but actual comments and to stfu about it
- // - float t = CurrentInterpolationFactor();
- // - return Vector3.Lerp(start.position, goal.position, t);
-
- // Option 2: always += speed
- // speed is 0 if we just started after idle, so always use max for best results
- float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed);
- return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime);
- }
-
- return currentPosition;
- }
-
- Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation)
- {
- if (!interpolateRotation)
- return defaultRotation;
-
- if (start.localRotation != goal.localRotation)
- {
- float t = CurrentInterpolationFactor(start, goal);
- return Quaternion.Slerp(start.localRotation, goal.localRotation, t);
- }
-
- return defaultRotation;
- }
-
- Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale)
- {
- if (!interpolateScale)
- return currentScale;
-
- if (start.localScale != goal.localScale)
- {
- float t = CurrentInterpolationFactor(start, goal);
- return Vector3.Lerp(start.localScale, goal.localScale, t);
- }
-
- return currentScale;
- }
-
- static float CurrentInterpolationFactor(DataPoint start, DataPoint goal)
- {
- if (start.isValid)
- {
- float difference = goal.timeStamp - start.timeStamp;
-
- // the moment we get 'goal', 'start' is supposed to start, so elapsed time is based on:
- float elapsed = Time.time - goal.timeStamp;
-
- // avoid NaN
- return difference > 0 ? elapsed / difference : 1;
- }
- return 1;
- }
-
- #region Server Teleport (force move player)
-
- ///
- /// This method will override this GameObject's current Transform.localPosition to the specified Vector3 and update all clients.
- /// NOTE: position must be in LOCAL space if the transform has a parent
- ///
- /// Where to teleport this GameObject
- [Server]
- public void ServerTeleport(Vector3 localPosition)
- {
- Quaternion localRotation = targetTransform.localRotation;
- ServerTeleport(localPosition, localRotation);
- }
-
- ///
- /// This method will override this GameObject's current Transform.localPosition and Transform.localRotation
- /// to the specified Vector3 and Quaternion and update all clients.
- /// NOTE: localPosition must be in LOCAL space if the transform has a parent
- /// NOTE: localRotation must be in LOCAL space if the transform has a parent
- ///
- /// Where to teleport this GameObject
- /// Which rotation to set this GameObject
- [Server]
- public void ServerTeleport(Vector3 localPosition, Quaternion localRotation)
- {
- // To prevent applying the position updates received from client (if they have ClientAuth) while being teleported.
- // clientAuthorityBeforeTeleport defaults to false when not teleporting, if it is true then it means that teleport
- // was previously called but not finished therefore we should keep it as true so that 2nd teleport call doesn't clear authority
- clientAuthorityBeforeTeleport = clientAuthority || clientAuthorityBeforeTeleport;
- clientAuthority = false;
-
- DoTeleport(localPosition, localRotation);
-
- // tell all clients about new values
- RpcTeleport(localPosition, Compression.CompressQuaternion(localRotation), clientAuthorityBeforeTeleport);
- }
-
- void DoTeleport(Vector3 newLocalPosition, Quaternion newLocalRotation)
- {
- targetTransform.localPosition = newLocalPosition;
- targetTransform.localRotation = newLocalRotation;
-
- // Since we are overriding the position we don't need a goal and start.
- // Reset them to null for fresh start
- goal = new DataPoint();
- start = new DataPoint();
- lastPosition = newLocalPosition;
- lastRotation = newLocalRotation;
- }
-
- [ClientRpc(channel = Channels.Unreliable)]
- void RpcTeleport(Vector3 newPosition, uint newPackedRotation, bool isClientAuthority)
- {
- DoTeleport(newPosition, Compression.DecompressQuaternion(newPackedRotation));
-
- // only send finished if is owner and is ClientAuthority on server
- if (hasAuthority && isClientAuthority)
- CmdTeleportFinished();
- }
-
- ///
- /// This RPC will be invoked on server after client finishes overriding the position.
- ///
- ///
- [Command(channel = Channels.Unreliable)]
- void CmdTeleportFinished()
- {
- if (clientAuthorityBeforeTeleport)
- {
- clientAuthority = true;
-
- // reset value so doesn't effect future calls, see note in ServerTeleport
- clientAuthorityBeforeTeleport = false;
- }
- else
- {
- Debug.LogWarning("Client called TeleportFinished when clientAuthority was false on server", this);
- }
- }
-
- #endregion
-
- #region Debug Gizmos
-
- // draw the data points for easier debugging
- void OnDrawGizmos()
- {
- // draw start and goal points and a line between them
- if (start.localPosition != goal.localPosition)
- {
- DrawDataPointGizmo(start, Color.yellow);
- DrawDataPointGizmo(goal, Color.green);
- DrawLineBetweenDataPoints(start, goal, Color.cyan);
- }
- }
-
- static void DrawDataPointGizmo(DataPoint data, Color color)
- {
- // use a little offset because transform.localPosition might be in the ground in many cases
- Vector3 offset = Vector3.up * 0.01f;
-
- // draw position
- Gizmos.color = color;
- Gizmos.DrawSphere(data.localPosition + offset, 0.5f);
-
- // draw forward and up like unity move tool
- Gizmos.color = Color.blue;
- Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward);
- Gizmos.color = Color.green;
- Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up);
- }
-
- static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color)
- {
- Gizmos.color = color;
- Gizmos.DrawLine(data1.localPosition, data2.localPosition);
- }
-
- #endregion
- }
-}
diff --git a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs b/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs
deleted file mode 100644
index 1ade1de..0000000
--- a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using UnityEngine;
-
-namespace Mirror.Experimental
-{
- ///
- /// 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 received values.
- ///
- // Deprecated 2022-01-18
- [Obsolete("Use the default NetworkTransform instead, it has proper snapshot interpolation.")]
- [AddComponentMenu("")]
- public class NetworkTransformChild : NetworkTransformBase
- {
- [Header("Target")]
- public Transform target;
-
- protected override Transform targetTransform => target;
- }
-}
diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs
index 116051b..0441558 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()
{
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)
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/Match/MatchInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs
index ff2eadc..71238d2 100644
--- a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs
+++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs
@@ -15,34 +15,42 @@ public class MatchInterestManagement : InterestManagement
readonly HashSet dirtyMatches = new HashSet();
+ [ServerCallback]
public override void OnSpawned(NetworkIdentity identity)
{
- if (!identity.TryGetComponent(out NetworkMatch networkMatch))
+ if (!identity.TryGetComponent(out NetworkMatch networkMatch))
return;
- Guid currentMatch = networkMatch.matchId;
- lastObjectMatch[identity] = currentMatch;
+ Guid networkMatchId = networkMatch.matchId;
+ lastObjectMatch[identity] = networkMatchId;
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
- if (currentMatch == Guid.Empty)
+ if (networkMatchId == Guid.Empty)
return;
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
- if (!matchObjects.TryGetValue(currentMatch, out HashSet objects))
+ if (!matchObjects.TryGetValue(networkMatchId, out HashSet objects))
{
objects = new HashSet();
- matchObjects.Add(currentMatch, objects);
+ matchObjects.Add(networkMatchId, objects);
}
objects.Add(identity);
+
+ // 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.
+ RebuildMatchObservers(networkMatchId);
}
+ [ServerCallback]
public override void OnDestroyed(NetworkIdentity identity)
{
- lastObjectMatch.TryGetValue(identity, out Guid currentMatch);
- lastObjectMatch.Remove(identity);
- if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet objects) && objects.Remove(identity))
- RebuildMatchObservers(currentMatch);
+ if (lastObjectMatch.TryGetValue(identity, out Guid currentMatch))
+ {
+ lastObjectMatch.Remove(identity);
+ if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet objects) && objects.Remove(identity))
+ RebuildMatchObservers(currentMatch);
+ }
}
// internal so we can update from tests
@@ -53,14 +61,15 @@ internal void Update()
// if match changed:
// add previous to dirty
// add new to dirty
- foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
+ foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
{
// Ignore objects that don't have a NetworkMatch component
- if (!netIdentity.TryGetComponent(out NetworkMatch networkMatch))
+ if (!identity.TryGetComponent(out NetworkMatch networkMatch))
continue;
Guid newMatch = networkMatch.matchId;
- lastObjectMatch.TryGetValue(netIdentity, out Guid currentMatch);
+ if (!lastObjectMatch.TryGetValue(identity, out Guid currentMatch))
+ continue;
// Guid.Empty is never a valid matchId
// Nothing to do if matchId hasn't changed
@@ -72,10 +81,10 @@ internal void Update()
// 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);
+ UpdateMatchObjects(identity, newMatch, currentMatch);
}
- // rebuild all dirty matchs
+ // rebuild all dirty matches
foreach (Guid dirtyMatch in dirtyMatches)
RebuildMatchObservers(dirtyMatch);
@@ -119,7 +128,7 @@ void RebuildMatchObservers(Guid matchId)
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 +136,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
@@ -139,7 +148,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
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;
diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs
index 8cbfa3b..cc4c1b4 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,12 +32,15 @@ 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);
+ if (lastObjectScene.TryGetValue(identity, out Scene currentScene))
+ {
+ lastObjectScene.Remove(identity);
+ if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity))
+ RebuildSceneObservers(currentScene);
+ }
}
// internal so we can update from tests
@@ -49,7 +53,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/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/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs
index eb4c2c5..2986034 100644
--- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs
+++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs
@@ -12,8 +12,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;
@@ -31,7 +40,8 @@ public enum CheckMethod
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) =>
diff --git a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs
index e6033ad..ee02a07 100644
--- a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs
+++ b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs
@@ -9,9 +9,9 @@ namespace Mirror
public class NetworkTeam : NetworkBehaviour
{
[Tooltip("Set this to the same value on all networked objects that belong to a given team")]
- public string teamId = string.Empty;
+ [SyncVar] public string teamId = string.Empty;
[Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")]
- public bool forceShown;
+ [SyncVar] public bool forceShown;
}
}
diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs
index 22a8eb0..b543586 100644
--- a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs
+++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs
@@ -6,42 +6,47 @@ 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 Dictionary lastObjectTeam = new Dictionary();
readonly HashSet dirtyTeams = new HashSet();
+ [ServerCallback]
public override void OnSpawned(NetworkIdentity identity)
{
- if (!identity.TryGetComponent(out NetworkTeam networkTeam))
+ if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam))
return;
- string currentTeam = networkTeam.teamId;
- lastObjectTeam[identity] = currentTeam;
+ string networkTeamId = identityNetworkTeam.teamId;
+ lastObjectTeam[identity] = networkTeamId;
- // string.Empty is never a valid teamId...do not add to teamObjects collection
- if (currentTeam == string.Empty)
+ // Null / Empty string is never a valid teamId...do not add to teamObjects collection
+ if (string.IsNullOrWhiteSpace(networkTeamId))
return;
- // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentTeam}");
- if (!teamObjects.TryGetValue(currentTeam, out HashSet objects))
+ //Debug.Log($"TeamInterestManagement.OnSpawned {identity.name} {networkTeamId}");
+
+ if (!teamObjects.TryGetValue(networkTeamId, out HashSet objects))
{
objects = new HashSet();
- teamObjects.Add(currentTeam, objects);
+ teamObjects.Add(networkTeamId, objects);
}
objects.Add(identity);
+
+ // 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.
+ RebuildTeamObservers(networkTeamId);
}
+ [ServerCallback]
public override void OnDestroyed(NetworkIdentity identity)
{
- lastObjectTeam.TryGetValue(identity, out string currentTeam);
- lastObjectTeam.Remove(identity);
- if (currentTeam != string.Empty && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity))
- RebuildTeamObservers(currentTeam);
+ if (lastObjectTeam.TryGetValue(identity, out string currentTeam))
+ {
+ lastObjectTeam.Remove(identity);
+ if (!string.IsNullOrWhiteSpace(currentTeam) && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity))
+ RebuildTeamObservers(currentTeam);
+ }
}
// internal so we can update from tests
@@ -55,24 +60,24 @@ internal void Update()
foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
{
// Ignore objects that don't have a NetworkTeam component
- if (!netIdentity.TryGetComponent(out NetworkTeam networkTeam))
+ if (!netIdentity.TryGetComponent(out NetworkTeam identityNetworkTeam))
continue;
- string newTeam = networkTeam.teamId;
+ string networkTeamId = identityNetworkTeam.teamId;
if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam))
continue;
- // string.Empty is never a valid teamId
+ // Null / Empty string is never a valid teamId
// Nothing to do if teamId hasn't changed
- if (string.IsNullOrWhiteSpace(newTeam) || newTeam == currentTeam)
+ if (string.IsNullOrWhiteSpace(networkTeamId) || networkTeamId == currentTeam)
continue;
// Mark new/old Teams as dirty so they get rebuilt
- UpdateDirtyTeams(newTeam, currentTeam);
+ UpdateDirtyTeams(networkTeamId, currentTeam);
// This object is in a new team so observers in the prior team
// and the new team need to rebuild their respective observers lists.
- UpdateTeamObjects(netIdentity, newTeam, currentTeam);
+ UpdateTeamObjects(netIdentity, networkTeamId, currentTeam);
}
// rebuild all dirty teams
@@ -84,8 +89,8 @@ internal void Update()
void UpdateDirtyTeams(string newTeam, string currentTeam)
{
- // string.Empty is never a valid teamId
- if (currentTeam != string.Empty)
+ // Null / Empty string is never a valid teamId
+ if (!string.IsNullOrWhiteSpace(currentTeam))
dirtyTeams.Add(currentTeam);
dirtyTeams.Add(newTeam);
@@ -119,7 +124,7 @@ void RebuildTeamObservers(string 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)
@@ -130,16 +135,15 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
return false;
// Always observed if no NetworkTeam component
- if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam))
- return true;
-
- if (newObserverNetworkTeam.forceShown)
+ if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam))
return true;
- // string.Empty is never a valid teamId
+ // Null / Empty string is never a valid teamId
if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId))
return false;
+ //Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}");
+
// Observed only if teamId's match
return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
}
@@ -147,7 +151,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
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,8 +164,8 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet
- /// 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++)
@@ -390,16 +384,9 @@ public override bool OnSerialize(NetworkWriter writer, bool initialState)
writer.WriteFloat(animator.GetLayerWeight(i));
}
WriteParameters(writer, initialState);
- return true;
}
- return changed;
}
- ///
- /// Custom Deserialization
- ///
- ///
- ///
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
base.OnDeserialize(reader, initialState);
@@ -441,7 +428,7 @@ public void SetTrigger(int hash)
return;
}
- if (!hasAuthority)
+ if (!isOwned)
{
Debug.LogWarning("Only the client with authority can set animations");
return;
@@ -476,9 +463,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)
{
@@ -490,7 +475,7 @@ public void ResetTrigger(int hash)
return;
}
- if (!hasAuthority)
+ if (!isOwned)
{
Debug.LogWarning("Only the client with authority can reset animations");
return;
@@ -558,7 +543,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 +561,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);
@@ -615,7 +600,7 @@ void RpcOnAnimationParametersClientMessage(byte[] parameters)
void RpcOnAnimationTriggerClientMessage(int hash)
{
// host/owner handles this before it is sent
- if (isServer || (clientAuthority && hasAuthority)) return;
+ if (isServer || (clientAuthority && isOwned)) return;
HandleAnimTriggerMsg(hash);
}
@@ -624,7 +609,7 @@ void RpcOnAnimationTriggerClientMessage(int hash)
void RpcOnAnimationResetTriggerClientMessage(int hash)
{
// host/owner handles this before it is sent
- if (isServer || (clientAuthority && hasAuthority)) return;
+ if (isServer || (clientAuthority && isOwned)) return;
HandleAnimResetTriggerMsg(hash);
}
diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs b/Assets/Mirror/Components/NetworkPingDisplay.cs
index 61e9241..156a48c 100644
--- a/Assets/Mirror/Components/NetworkPingDisplay.cs
+++ b/Assets/Mirror/Components/NetworkPingDisplay.cs
@@ -13,8 +13,8 @@ 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()
{
diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs b/Assets/Mirror/Components/NetworkRoomManager.cs
index d432fbb..bde8180 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;
@@ -22,11 +21,10 @@ public class NetworkRoomManager : NetworkManager
public struct PendingPlayer
{
public NetworkConnectionToClient conn;
- public GameObject roomPlayer;
+ public GameObject roomPlayer;
}
[Header("Room Settings")]
-
[FormerlySerializedAs("m_ShowRoomGUI")]
[SerializeField]
[Tooltip("This flag controls whether the default UI is shown for the room")]
@@ -61,7 +59,6 @@ public struct PendingPlayer
public List pendingPlayers = new List();
[Header("Diagnostics")]
-
///
/// True when all players have submitted a Ready message
///
@@ -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,8 +116,6 @@ public override void OnValidate()
Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component.");
}
}
-
- base.OnValidate();
}
public void ReadyStatusChanged()
@@ -169,7 +163,7 @@ void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
{
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;
@@ -202,7 +196,7 @@ void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
///
public void CheckReadyToBegin()
{
- if (!IsSceneActive(RoomScene))
+ if (!Utils.IsSceneActive(RoomScene))
return;
int numberOfReadyPlayers = NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.identity.gameObject.GetComponent().readyToBegin);
@@ -247,15 +241,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 +267,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,7 +283,7 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn)
player.GetComponent().readyToBegin = false;
}
- if (IsSceneActive(RoomScene))
+ if (Utils.IsSceneActive(RoomScene))
RecalculateRoomPlayerIndices();
OnRoomServerDisconnect(conn);
@@ -319,11 +308,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 +321,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]
@@ -472,10 +462,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 +472,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 +492,7 @@ public override void OnStopClient()
///
public override void OnClientSceneChanged()
{
- if (IsSceneActive(RoomScene))
+ if (Utils.IsSceneActive(RoomScene))
{
if (NetworkClient.isConnected)
CallOnClientEnterRoom();
@@ -516,10 +501,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
@@ -647,19 +629,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 +649,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 +661,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 +669,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/NetworkRoomPlayer.cs b/Assets/Mirror/Components/NetworkRoomPlayer.cs
index d2763d5..9f5e158 100644
--- a/Assets/Mirror/Components/NetworkRoomPlayer.cs
+++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs
@@ -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/NetworkStatistics.cs b/Assets/Mirror/Components/NetworkStatistics.cs
index a95d4a9..5d09fd0 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;
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/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/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransformBase.cs
new file mode 100644
index 0000000..1cfe668
--- /dev/null
+++ b/Assets/Mirror/Components/NetworkTransformBase.cs
@@ -0,0 +1,385 @@
+// 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 abstract class NetworkTransformBase : NetworkBehaviour
+ {
+ // target transform to sync. can be on a child.
+ [Header("Target")]
+ [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
+ public Transform target;
+
+ // TODO SyncDirection { ClientToServer, ServerToClient } is easier?
+ // Deprecated 2022-10-25
+ [Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")]
+ [Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually.
+ [Tooltip("Obsolete: NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")]
+ 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 => isClient && authority;
+ public readonly SortedList clientSnapshots = new SortedList();
+ public readonly SortedList serverSnapshots = new SortedList();
+
+ // selective sync //////////////////////////////////////////////////////
+ [Header("Selective Sync & Interpolation\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.
+
+ // debugging ///////////////////////////////////////////////////////////
+ [Header("Debug")]
+ 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 virtual void OnValidate()
+ {
+ // set target to self if none yet
+ if (target == null) target = transform;
+
+ // time snapshot interpolation happens globally.
+ // value (transform) happens in here.
+ // both always need to be on the same send interval.
+ // force the setting to '0' in OnValidate to make it obvious that we
+ // actually use NetworkServer.sendInterval.
+ syncInterval = 0;
+
+ // obsolete clientAuthority compatibility:
+ // if it was used, then set the new SyncDirection automatically.
+ // if it wasn't used, then don't touch syncDirection.
+ #pragma warning disable CS0618
+ if (clientAuthority)
+ {
+ syncDirection = SyncDirection.ClientToServer;
+ Debug.LogWarning($"{name}'s NetworkTransform component has obsolete .clientAuthority enabled. Please disable it and set SyncDirection to ClientToServer instead.");
+ }
+ #pragma warning restore CS0618
+ }
+
+ // snapshot functions //////////////////////////////////////////////////
+ // 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
+#if !UNITY_2020_3_OR_NEWER
+ NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
+#else
+ Time.timeAsDouble,
+#endif
+ // the other end fills out local time itself
+ 0,
+ target.localPosition,
+ target.localRotation,
+ target.localScale
+ );
+ }
+
+ 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 : target.localPosition;
+ if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : target.localRotation;
+ if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : target.localScale;
+
+ // insert transform snapshot
+ SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
+ timeStamp, // arrival remote timestamp. NOT remote time.
+#if !UNITY_2020_3_OR_NEWER
+ NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
+#else
+ Time.timeAsDouble,
+#endif
+ 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)
+ {
+ // 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) target.localPosition = interpolated.position;
+ if (syncRotation) target.localRotation = interpolated.rotation;
+ if (syncScale) target.localScale = interpolated.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);
+ }
+
+ [ClientRpc]
+ void RpcReset()
+ {
+ Reset();
+ }
+
+ // 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.
+ 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
+ Reset();
+
+ // 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?
+ }
+
+ public virtual void Reset()
+ {
+ // disabled objects aren't updated anymore.
+ // so let's clear the buffers.
+ serverSnapshots.Clear();
+ clientSnapshots.Clear();
+ }
+
+ protected virtual void OnEnable()
+ {
+ Reset();
+
+ if (NetworkServer.active)
+ NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged;
+ }
+
+ protected virtual void OnDisable()
+ {
+ Reset();
+
+ 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)
+ {
+ Reset();
+ RpcReset();
+ }
+ }
+
+ // 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.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/Experimental/NetworkTransformBase.cs.meta b/Assets/Mirror/Components/NetworkTransformBase.cs.meta
similarity index 83%
rename from Assets/Mirror/Components/Experimental/NetworkTransformBase.cs.meta
rename to Assets/Mirror/Components/NetworkTransformBase.cs.meta
index d737bed..37a1147 100644
--- a/Assets/Mirror/Components/Experimental/NetworkTransformBase.cs.meta
+++ b/Assets/Mirror/Components/NetworkTransformBase.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: ea7c690c4fbf8c4439726f4c62eda6d3
+guid: 7c44135fde488424eaf28566206ce473
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Assets/Mirror/Components/NetworkTransformReliable.meta b/Assets/Mirror/Components/NetworkTransformReliable.meta
new file mode 100644
index 0000000..1b6770d
--- /dev/null
+++ b/Assets/Mirror/Components/NetworkTransformReliable.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 36de72d9255741659bcbd1971ed29822
+timeCreated: 1668358590
\ No newline at end of file
diff --git a/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs
new file mode 100644
index 0000000..718335b
--- /dev/null
+++ b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs
@@ -0,0 +1,404 @@
+// 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
+ {
+ [Header("Sync Only If Changed")]
+ [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
+ public bool onlySyncOnChange = true;
+ [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;
+
+ [Header("Rotation")]
+ [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
+ public float rotationSensitivity = 0.01f;
+ [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 = false;
+
+ // 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;
+
+ int lastClientCount = 0;
+
+ // 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 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);
+ }
+ }
+
+ // set dirty to trigger OnSerialize. either always, or only if changed.
+ // technically snapshot interpolation requires constant sending.
+ // however, with reliable it should be fine without constant sends.
+ //
+ // detect changes _after_ all changes were applied above.
+ if (!onlySyncOnChange || Changed(Construct()))
+ SetDirty();
+ }
+
+ void UpdateClient()
+ {
+ // client authority, and local player (= allowed to move myself)?
+ if (IsClientWithAuthority)
+ {
+ // https://github.com/vis2k/Mirror/pull/2992/
+ if (!NetworkClient.ready) return;
+
+ // set dirty to trigger OnSerialize. either always, or only if changed.
+ // technically snapshot interpolation requires constant sending.
+ // however, with reliable it should be fine without constant sends.
+ if (!onlySyncOnChange || Changed(Construct()))
+ SetDirty();
+ }
+ // for all other clients (and for local player if !authority),
+ // we need to apply snapshots from the buffer
+ else
+ {
+
+ // 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);
+
+ }
+
+ // 'only sync if moved'
+ // explain..
+ // from 1 snap to next snap..
+ // it'll be old...
+ if (lastClientCount > 1 && clientSnapshots.Count == 1)
+ {
+ // this is it. snapshots are down to '1'.
+ // does this cause stuck?
+ }
+
+ lastClientCount = clientSnapshots.Count;
+ }
+ }
+
+ // 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 (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);
+ }
+
+ // int written = writer.Position - before;
+ // Debug.Log($"{name} compressed to {written} bytes");
+ }
+
+ // 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, onlySyncOnChangeCorrectionMultiplier))
+ {
+ RewriteHistory(
+ serverSnapshots,
+ connectionToClient.remoteTimeStamp,
+ NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
+ NetworkServer.sendInterval, // Unity 2019 doesn't have timeAsDouble yet
+ target.localPosition,
+ target.localRotation,
+ target.localScale);
+ // Debug.Log($"{name}: corrected history on server to fix initial stutter after not sending for a while.");
+ }
+
+ AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp, 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, onlySyncOnChangeCorrectionMultiplier))
+ {
+ RewriteHistory(
+ clientSnapshots,
+ NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
+ NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
+ NetworkClient.sendInterval,
+ target.localPosition,
+ target.localRotation,
+ target.localScale);
+ // Debug.Log($"{name}: corrected history on client to fix initial stutter after not sending for a while.");
+ }
+
+ AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp, 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, new TransformSnapshot(
+ remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
+ localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
+ position,
+ rotation,
+ scale
+ ));
+ }
+
+ public override void Reset()
+ {
+ base.Reset();
+
+ // reset delta
+ lastSerializedPosition = Vector3Long.zero;
+ lastDeserializedPosition = Vector3Long.zero;
+
+ lastSerializedScale = Vector3Long.zero;
+ lastDeserializedScale = Vector3Long.zero;
+ }
+ }
+}
diff --git a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs.meta b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs.meta
similarity index 86%
rename from Assets/Mirror/Components/Experimental/NetworkTransformChild.cs.meta
rename to Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs.meta
index 30f0d89..ece9c7d 100644
--- a/Assets/Mirror/Components/Experimental/NetworkTransformChild.cs.meta
+++ b/Assets/Mirror/Components/NetworkTransformReliable/NetworkTransformReliable.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: f65214da13a861f4a8ae309d3daea1c6
+guid: 8ff3ba0becae47b8b9381191598957c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Assets/Mirror/Components/NetworkTransform2k.meta b/Assets/Mirror/Components/NetworkTransformUnreliable.meta
similarity index 100%
rename from Assets/Mirror/Components/NetworkTransform2k.meta
rename to Assets/Mirror/Components/NetworkTransformUnreliable.meta
diff --git a/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs
new file mode 100644
index 0000000..35a9154
--- /dev/null
+++ b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs
@@ -0,0 +1,359 @@
+// NetworkTransform V2 by mischa (2021-07)
+// comment out the below line to quickly revert the onlySyncOnChange feature
+#define onlySyncOnChange_BANDWIDTH_SAVING
+using UnityEngine;
+
+namespace Mirror
+{
+ [AddComponentMenu("Network/Network Transform (Unreliable)")]
+ public class NetworkTransform : NetworkTransformBase
+ {
+ // only sync when changed hack /////////////////////////////////////////
+#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 TransformSnapshot lastSnapshot;
+ protected bool cachedSnapshotComparison;
+ protected bool hasSentUnchangedPosition;
+#endif
+
+ double lastClientSendTime;
+ double lastServerSendTime;
+
+ // 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 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.
+ // TODO send same time that NetworkServer sends time snapshot?
+ if (NetworkTime.localTime >= lastServerSendTime + NetworkServer.sendInterval && // 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();
+#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.
+ // -> 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);
+ }
+ }
+ }
+
+ 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 + NetworkClient.sendInterval) // same interval as time interpolation!
+ {
+ // send snapshot without timestamp.
+ // receiver gets it from batch timestamp to save bandwidth.
+ TransformSnapshot snapshot = Construct();
+#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
+ {
+ // 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);
+ }
+ }
+ }
+
+ 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(target.localPosition);
+ if (syncRotation) writer.WriteQuaternion(target.localRotation);
+ if (syncScale) writer.WriteVector3(target.localScale);
+ }
+ }
+
+ 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) target.localPosition = reader.ReadVector3();
+ if (syncRotation) target.localRotation = reader.ReadQuaternion();
+ if (syncScale) target.localScale = reader.ReadVector3();
+ }
+ }
+
+#if onlySyncOnChange_BANDWIDTH_SAVING
+ // Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
+ protected virtual bool CompareSnapshots(TransformSnapshot 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 (syncDirection == SyncDirection.ClientToServer)
+ {
+ 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 (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_BANDWIDTH_SAVING
+ if (onlySyncOnChange)
+ {
+ double timeIntervalCheck = bufferResetMultiplier * NetworkClient.sendInterval;
+
+ if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
+ {
+ Reset();
+ }
+ }
+#endif
+ AddSnapshot(serverSnapshots, timestamp, position, rotation, scale);
+ }
+
+ // 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;
+
+ // 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 * NetworkServer.sendInterval;
+
+ if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
+ {
+ Reset();
+ }
+ }
+#endif
+ AddSnapshot(clientSnapshots, timestamp, position, rotation, scale);
+ }
+ }
+}
diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs.meta
similarity index 100%
rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta
rename to Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransform.cs.meta
diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs
similarity index 54%
rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs
rename to Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs
index 8032506..a844d9d 100644
--- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs
+++ b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs
@@ -1,14 +1,12 @@
// 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 System;
using UnityEngine;
namespace Mirror
{
- [AddComponentMenu("Network/Network Transform Child")]
- public class NetworkTransformChild : NetworkTransformBase
- {
- [Header("Target")]
- public Transform target;
- protected override Transform targetComponent => target;
- }
+ // Deprecated 2022-10-25
+ [AddComponentMenu("")]
+ [Obsolete("NetworkTransformChild is not needed anymore. The .target is now exposed in NetworkTransform itself. Note you can open the Inspector in debug view and replace the source script instead of reassigning everything.")]
+ public class NetworkTransformChild : NetworkTransform {}
}
diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta b/Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs.meta
similarity index 100%
rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta
rename to Assets/Mirror/Components/NetworkTransformUnreliable/NetworkTransformChild.cs.meta
diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs b/Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs
similarity index 80%
rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs
rename to Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs
index efd91c0..912b10d 100644
--- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs
+++ b/Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs
@@ -6,7 +6,7 @@
namespace Mirror
{
// NetworkTransform Snapshot
- public struct NTSnapshot : Snapshot
+ public struct TransformSnapshot : Snapshot
{
// time or sequence are needed to throw away older snapshots.
//
@@ -23,30 +23,31 @@ public struct NTSnapshot : Snapshot
//
// [REMOTE TIME, NOT LOCAL TIME]
// => DOUBLE for long term accuracy & batching gives us double anyway
- public double remoteTimestamp { get; set; }
+ 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 localTimestamp { get; set; }
+ public double localTime { get; set; }
- public Vector3 position;
+ public Vector3 position;
public Quaternion rotation;
- public Vector3 scale;
+ public Vector3 scale;
- public NTSnapshot(double remoteTimestamp, double localTimestamp, Vector3 position, Quaternion rotation, Vector3 scale)
+ public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
{
- this.remoteTimestamp = remoteTimestamp;
- this.localTimestamp = localTimestamp;
+ this.remoteTime = remoteTime;
+ this.localTime = localTime;
this.position = position;
this.rotation = rotation;
this.scale = scale;
}
- public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t)
+ 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 NTSnapshot(
+ return new TransformSnapshot(
// interpolated snapshot is applied directly. don't need timestamps.
0, 0,
// lerp position/rotation/scale unclamped in case we ever need
diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta b/Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs.meta
similarity index 100%
rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta
rename to Assets/Mirror/Components/NetworkTransformUnreliable/TransformSnapshot.cs.meta
diff --git a/Assets/Mirror/Components/RemoteStatistics.cs b/Assets/Mirror/Components/RemoteStatistics.cs
new file mode 100644
index 0000000..6e872b1
--- /dev/null
+++ b/Assets/Mirror/Components/RemoteStatistics.cs
@@ -0,0 +1,439 @@
+// 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.F11;
+ 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}");
+ }
+ }
+
+ void 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/Experimental/NetworkTransform.cs.meta b/Assets/Mirror/Components/RemoteStatistics.cs.meta
similarity index 86%
rename from Assets/Mirror/Components/Experimental/NetworkTransform.cs.meta
rename to Assets/Mirror/Components/RemoteStatistics.cs.meta
index 2bc16dd..4c4d043 100644
--- a/Assets/Mirror/Components/Experimental/NetworkTransform.cs.meta
+++ b/Assets/Mirror/Components/RemoteStatistics.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 741bbe11f5357b44593b15c0d11b16bd
+guid: ba360e4ff6b44fc6898f56322b90c6c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
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/Runtime/AssemblyInfo.cs b/Assets/Mirror/Core/AssemblyInfo.cs
similarity index 100%
rename from Assets/Mirror/Runtime/AssemblyInfo.cs
rename to Assets/Mirror/Core/AssemblyInfo.cs
diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs.meta b/Assets/Mirror/Core/AssemblyInfo.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/AssemblyInfo.cs.meta
rename to Assets/Mirror/Core/AssemblyInfo.cs.meta
diff --git a/Assets/Mirror/Runtime/Attributes.cs b/Assets/Mirror/Core/Attributes.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Attributes.cs
rename to Assets/Mirror/Core/Attributes.cs
diff --git a/Assets/Mirror/Runtime/Attributes.cs.meta b/Assets/Mirror/Core/Attributes.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Attributes.cs.meta
rename to Assets/Mirror/Core/Attributes.cs.meta
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/Runtime/Batching/Batcher.cs b/Assets/Mirror/Core/Batching/Batcher.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Batching/Batcher.cs
rename to Assets/Mirror/Core/Batching/Batcher.cs
diff --git a/Assets/Mirror/Runtime/Batching/Batcher.cs.meta b/Assets/Mirror/Core/Batching/Batcher.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Batching/Batcher.cs.meta
rename to Assets/Mirror/Core/Batching/Batcher.cs.meta
diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs b/Assets/Mirror/Core/Batching/Unbatcher.cs
similarity index 99%
rename from Assets/Mirror/Runtime/Batching/Unbatcher.cs
rename to Assets/Mirror/Core/Batching/Unbatcher.cs
index 495ada9..997b54a 100644
--- a/Assets/Mirror/Runtime/Batching/Unbatcher.cs
+++ b/Assets/Mirror/Core/Batching/Unbatcher.cs
@@ -101,7 +101,7 @@ public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp
}
// was our reader pointed to anything yet?
- if (reader.Length == 0)
+ if (reader.Capacity == 0)
{
remoteTimeStamp = 0;
return false;
diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta b/Assets/Mirror/Core/Batching/Unbatcher.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta
rename to Assets/Mirror/Core/Batching/Unbatcher.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty.meta b/Assets/Mirror/Core/Empty.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty.meta
rename to Assets/Mirror/Core/Empty.meta
diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs b/Assets/Mirror/Core/Empty/ClientScene.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/ClientScene.cs
rename to Assets/Mirror/Core/Empty/ClientScene.cs
diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta b/Assets/Mirror/Core/Empty/ClientScene.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/ClientScene.cs.meta
rename to Assets/Mirror/Core/Empty/ClientScene.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud.meta b/Assets/Mirror/Core/Empty/Cloud.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud.meta
rename to Assets/Mirror/Core/Empty/Cloud.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs b/Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs
rename to Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ApiConnector.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs b/Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs
rename to Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ApiUpdater.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs b/Assets/Mirror/Core/Empty/Cloud/Ball.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Ball.cs
rename to Assets/Mirror/Core/Empty/Cloud/Ball.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Ball.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/Ball.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs b/Assets/Mirror/Core/Empty/Cloud/BallManager.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs
rename to Assets/Mirror/Core/Empty/Cloud/BallManager.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta b/Assets/Mirror/Core/Empty/Cloud/BallManager.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/BallManager.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs b/Assets/Mirror/Core/Empty/Cloud/BaseApi.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs
rename to Assets/Mirror/Core/Empty/Cloud/BaseApi.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/BaseApi.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/BaseApi.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs b/Assets/Mirror/Core/Empty/Cloud/Events.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Events.cs
rename to Assets/Mirror/Core/Empty/Cloud/Events.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Events.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/Events.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs b/Assets/Mirror/Core/Empty/Cloud/Extensions.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs
rename to Assets/Mirror/Core/Empty/Cloud/Extensions.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Extensions.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/Extensions.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs b/Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs
rename to Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ICoroutineRunner.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs b/Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs
rename to Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta b/Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/IRequestCreator.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs b/Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs
rename to Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta b/Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/IUnityEqualCheck.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs b/Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs
rename to Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta b/Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/InstantiateNetworkManager.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs b/Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs
rename to Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta b/Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/JsonStructs.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs b/Assets/Mirror/Core/Empty/Cloud/ListServer.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs
rename to Assets/Mirror/Core/Empty/Cloud/ListServer.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServer.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ListServer.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs
rename to Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ListServerBaseApi.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs
rename to Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ListServerClientApi.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs
rename to Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ListServerJson.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs b/Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs
rename to Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ListServerServerApi.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs b/Assets/Mirror/Core/Empty/Cloud/Logger.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Logger.cs
rename to Assets/Mirror/Core/Empty/Cloud/Logger.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Logger.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/Logger.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs
rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServer.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs
rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta b/Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/NetworkManagerListServerPong.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs b/Assets/Mirror/Core/Empty/Cloud/Player.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Player.cs
rename to Assets/Mirror/Core/Empty/Cloud/Player.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta b/Assets/Mirror/Core/Empty/Cloud/Player.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/Player.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs b/Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs
rename to Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta b/Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/QuickListServerDebug.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs b/Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs
rename to Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta b/Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/QuitButtonHUD.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs b/Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs
rename to Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta b/Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/RequestCreator.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs b/Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs
rename to Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ServerListManager.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs b/Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs
rename to Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ServerListUI.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs b/Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs
rename to Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs
diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta b/Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta
rename to Assets/Mirror/Core/Empty/Cloud/ServerListUIItem.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs b/Assets/Mirror/Core/Empty/DotNetCompatibility.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs
rename to Assets/Mirror/Core/Empty/DotNetCompatibility.cs
diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta b/Assets/Mirror/Core/Empty/DotNetCompatibility.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta
rename to Assets/Mirror/Core/Empty/DotNetCompatibility.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs b/Assets/Mirror/Core/Empty/FallbackTransport.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/FallbackTransport.cs
rename to Assets/Mirror/Core/Empty/FallbackTransport.cs
diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta b/Assets/Mirror/Core/Empty/FallbackTransport.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta
rename to Assets/Mirror/Core/Empty/FallbackTransport.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs b/Assets/Mirror/Core/Empty/LogFactory.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/LogFactory.cs
rename to Assets/Mirror/Core/Empty/LogFactory.cs
diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta b/Assets/Mirror/Core/Empty/LogFactory.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/LogFactory.cs.meta
rename to Assets/Mirror/Core/Empty/LogFactory.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs b/Assets/Mirror/Core/Empty/LogFilter.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/LogFilter.cs
rename to Assets/Mirror/Core/Empty/LogFilter.cs
diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta b/Assets/Mirror/Core/Empty/LogFilter.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/LogFilter.cs.meta
rename to Assets/Mirror/Core/Empty/LogFilter.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Logging.meta b/Assets/Mirror/Core/Empty/Logging.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging.meta
rename to Assets/Mirror/Core/Empty/Logging.meta
diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs b/Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs
rename to Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs
diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta b/Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta
rename to Assets/Mirror/Core/Empty/Logging/ConsoleColorLogHandler.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs b/Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs
rename to Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs
diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta b/Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta
rename to Assets/Mirror/Core/Empty/Logging/EditorLogSettingsLoader.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs b/Assets/Mirror/Core/Empty/Logging/LogFactory.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs
rename to Assets/Mirror/Core/Empty/Logging/LogFactory.cs
diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta b/Assets/Mirror/Core/Empty/Logging/LogFactory.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta
rename to Assets/Mirror/Core/Empty/Logging/LogFactory.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs b/Assets/Mirror/Core/Empty/Logging/LogSettings.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs
rename to Assets/Mirror/Core/Empty/Logging/LogSettings.cs
diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta b/Assets/Mirror/Core/Empty/Logging/LogSettings.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta
rename to Assets/Mirror/Core/Empty/Logging/LogSettings.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs b/Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs
rename to Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs
diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta b/Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta
rename to Assets/Mirror/Core/Empty/Logging/NetworkHeadlessLogger.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs b/Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs
rename to Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs
diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta b/Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta
rename to Assets/Mirror/Core/Empty/Logging/NetworkLogSettings.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs b/Assets/Mirror/Core/Empty/NetworkMatchChecker.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs
rename to Assets/Mirror/Core/Empty/NetworkMatchChecker.cs
diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkMatchChecker.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta
rename to Assets/Mirror/Core/Empty/NetworkMatchChecker.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs b/Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs
rename to Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs
diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta
rename to Assets/Mirror/Core/Empty/NetworkOwnerChecker.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs b/Assets/Mirror/Core/Empty/NetworkProximityChecker.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs
rename to Assets/Mirror/Core/Empty/NetworkProximityChecker.cs
diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkProximityChecker.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta
rename to Assets/Mirror/Core/Empty/NetworkProximityChecker.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs b/Assets/Mirror/Core/Empty/NetworkSceneChecker.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs
rename to Assets/Mirror/Core/Empty/NetworkSceneChecker.cs
diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta b/Assets/Mirror/Core/Empty/NetworkSceneChecker.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta
rename to Assets/Mirror/Core/Empty/NetworkSceneChecker.cs.meta
diff --git a/Assets/Mirror/Core/Empty/NetworkTransformBase.cs b/Assets/Mirror/Core/Empty/NetworkTransformBase.cs
new file mode 100644
index 0000000..79e858f
--- /dev/null
+++ b/Assets/Mirror/Core/Empty/NetworkTransformBase.cs
@@ -0,0 +1 @@
+// removed 2022-10-24
diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta b/Assets/Mirror/Core/Empty/NetworkTransformBase.cs.meta
similarity index 100%
rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta
rename to Assets/Mirror/Core/Empty/NetworkTransformBase.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs b/Assets/Mirror/Core/Empty/NetworkVisibility.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkVisibility.cs
rename to Assets/Mirror/Core/Empty/NetworkVisibility.cs
diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta b/Assets/Mirror/Core/Empty/NetworkVisibility.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta
rename to Assets/Mirror/Core/Empty/NetworkVisibility.cs.meta
diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs b/Assets/Mirror/Core/Empty/StringHash.cs
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/StringHash.cs
rename to Assets/Mirror/Core/Empty/StringHash.cs
diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs.meta b/Assets/Mirror/Core/Empty/StringHash.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Empty/StringHash.cs.meta
rename to Assets/Mirror/Core/Empty/StringHash.cs.meta
diff --git a/Assets/Mirror/Core/Empty/SyncVar.cs b/Assets/Mirror/Core/Empty/SyncVar.cs
new file mode 100644
index 0000000..aaa3b9d
--- /dev/null
+++ b/Assets/Mirror/Core/Empty/SyncVar.cs
@@ -0,0 +1 @@
+// removed 2022-11-03
diff --git a/Assets/Mirror/Runtime/SyncVar.cs.meta b/Assets/Mirror/Core/Empty/SyncVar.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/SyncVar.cs.meta
rename to Assets/Mirror/Core/Empty/SyncVar.cs.meta
diff --git a/Assets/Mirror/Core/Empty/SyncVarGameObject.cs b/Assets/Mirror/Core/Empty/SyncVarGameObject.cs
new file mode 100644
index 0000000..aaa3b9d
--- /dev/null
+++ b/Assets/Mirror/Core/Empty/SyncVarGameObject.cs
@@ -0,0 +1 @@
+// removed 2022-11-03
diff --git a/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta b/Assets/Mirror/Core/Empty/SyncVarGameObject.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/SyncVarGameObject.cs.meta
rename to Assets/Mirror/Core/Empty/SyncVarGameObject.cs.meta
diff --git a/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs b/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs
new file mode 100644
index 0000000..aaa3b9d
--- /dev/null
+++ b/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs
@@ -0,0 +1 @@
+// removed 2022-11-03
diff --git a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta b/Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta
rename to Assets/Mirror/Core/Empty/SyncVarNetworkBehaviour.cs.meta
diff --git a/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs b/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs
new file mode 100644
index 0000000..aaa3b9d
--- /dev/null
+++ b/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs
@@ -0,0 +1 @@
+// removed 2022-11-03
diff --git a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta b/Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta
rename to Assets/Mirror/Core/Empty/SyncVarNetworkIdentity.cs.meta
diff --git a/Assets/Mirror/Core/HostMode.cs b/Assets/Mirror/Core/HostMode.cs
new file mode 100644
index 0000000..e5bb2c0
--- /dev/null
+++ b/Assets/Mirror/Core/HostMode.cs
@@ -0,0 +1,48 @@
+// 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();
+ }
+
+ // DEPRECATED 2023-01-28
+ [Obsolete("ActivateHostScene did nothing, since identities all had .isClient set in NetworkServer.SpawnObjects.")]
+ public static void ActivateHostScene() {}
+ }
+}
diff --git a/Assets/Mirror/Core/HostMode.cs.meta b/Assets/Mirror/Core/HostMode.cs.meta
new file mode 100644
index 0000000..bf6faed
--- /dev/null
+++ b/Assets/Mirror/Core/HostMode.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d27175a08d5341fc97645b49ee533d5a
+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/Core/InterestManagement.cs
similarity index 97%
rename from Assets/Mirror/Runtime/InterestManagement.cs
rename to Assets/Mirror/Core/InterestManagement.cs
index ab149c3..8556831 100644
--- a/Assets/Mirror/Runtime/InterestManagement.cs
+++ b/Assets/Mirror/Core/InterestManagement.cs
@@ -12,7 +12,8 @@ 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()
+ // make sure to call base.Awake when overwriting!
+ protected virtual void Awake()
{
if (NetworkServer.aoi == null)
{
diff --git a/Assets/Mirror/Runtime/InterestManagement.cs.meta b/Assets/Mirror/Core/InterestManagement.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/InterestManagement.cs.meta
rename to Assets/Mirror/Core/InterestManagement.cs.meta
diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs b/Assets/Mirror/Core/LocalConnectionToClient.cs
similarity index 100%
rename from Assets/Mirror/Runtime/LocalConnectionToClient.cs
rename to Assets/Mirror/Core/LocalConnectionToClient.cs
diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta b/Assets/Mirror/Core/LocalConnectionToClient.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta
rename to Assets/Mirror/Core/LocalConnectionToClient.cs.meta
diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs b/Assets/Mirror/Core/LocalConnectionToServer.cs
similarity index 100%
rename from Assets/Mirror/Runtime/LocalConnectionToServer.cs
rename to Assets/Mirror/Core/LocalConnectionToServer.cs
diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta b/Assets/Mirror/Core/LocalConnectionToServer.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta
rename to Assets/Mirror/Core/LocalConnectionToServer.cs.meta
diff --git a/Assets/Mirror/Runtime/Messages.cs b/Assets/Mirror/Core/Messages.cs
similarity index 78%
rename from Assets/Mirror/Runtime/Messages.cs
rename to Assets/Mirror/Core/Messages.cs
index d3816f8..6ba0bf0 100644
--- a/Assets/Mirror/Runtime/Messages.cs
+++ b/Assets/Mirror/Core/Messages.cs
@@ -3,6 +3,14 @@
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 {}
@@ -28,7 +36,7 @@ public struct CommandMessage : NetworkMessage
{
public uint netId;
public byte componentIndex;
- public int functionHash;
+ public ushort functionHash;
// the parameters for the Cmd function
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment payload;
@@ -38,12 +46,21 @@ public struct RpcMessage : NetworkMessage
{
public uint netId;
public byte componentIndex;
- public int functionHash;
+ public ushort functionHash;
// the parameters for the Cmd function
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment payload;
}
+ // holds multiple buffered rpcs for the given connection.
+ // more efficient than sending one message per rpc.
+ public struct RpcBufferMessage : NetworkMessage
+ {
+ // payload contains multiple serialized RpcMessages.
+ // but without the message header.
+ public ArraySegment payload;
+ }
+
public struct SpawnMessage : NetworkMessage
{
// netId of new or existing object
@@ -53,7 +70,7 @@ public struct SpawnMessage : NetworkMessage
public bool isOwner;
public ulong sceneId;
// If sceneId != 0 then it is used instead of assetId
- public Guid assetId;
+ public uint assetId;
// Local position
public Vector3 position;
// Local rotation
diff --git a/Assets/Mirror/Runtime/Messages.cs.meta b/Assets/Mirror/Core/Messages.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Messages.cs.meta
rename to Assets/Mirror/Core/Messages.cs.meta
diff --git a/Assets/Mirror/Core/Mirror.asmdef b/Assets/Mirror/Core/Mirror.asmdef
new file mode 100644
index 0000000..59e32aa
--- /dev/null
+++ b/Assets/Mirror/Core/Mirror.asmdef
@@ -0,0 +1,18 @@
+{
+ "name": "Mirror",
+ "rootNamespace": "",
+ "references": [
+ "GUID:325984b52e4128546bc7558552f8b1d2",
+ "GUID:725ee7191c021de4dbf9269590ded755",
+ "GUID:6806a62c384838046a3c66c44f06d75f"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": true,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Runtime/Mirror.asmdef.meta b/Assets/Mirror/Core/Mirror.asmdef.meta
similarity index 100%
rename from Assets/Mirror/Runtime/Mirror.asmdef.meta
rename to Assets/Mirror/Core/Mirror.asmdef.meta
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/Runtime/NetworkAuthenticator.cs.meta b/Assets/Mirror/Core/NetworkAuthenticator.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta
rename to Assets/Mirror/Core/NetworkAuthenticator.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs
similarity index 68%
rename from Assets/Mirror/Runtime/NetworkBehaviour.cs
rename to Assets/Mirror/Core/NetworkBehaviour.cs
index 94cd930..630d3af 100644
--- a/Assets/Mirror/Runtime/NetworkBehaviour.cs
+++ b/Assets/Mirror/Core/NetworkBehaviour.cs
@@ -6,14 +6,28 @@
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.
[AddComponentMenu("")]
[RequireComponent(typeof(NetworkIdentity))]
[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")]
@@ -44,8 +58,31 @@ public abstract class NetworkBehaviour : MonoBehaviour
/// 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;
+ /// 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;
+
+ // Deprecated 2022-10-13
+ [Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")]
+ public bool hasAuthority => 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 owned and if clientAuthority sync direction
+ // on the server: if serverAuthority sync direction
+ //
+ // 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 =>
+ isClient
+ ? syncDirection == SyncDirection.ClientToServer && isOwned
+ : syncDirection == SyncDirection.ServerToClient;
/// The unique network Id of this object (unique at runtime).
public uint netId => netIdentity.netId;
@@ -71,7 +108,7 @@ public abstract class NetworkBehaviour : MonoBehaviour
public NetworkIdentity netIdentity { get; internal set; }
/// Returns the index of the component on this object
- public int ComponentIndex { get; internal set; }
+ public byte ComponentIndex { get; internal set; }
// to avoid fully serializing entities every time, we have two options:
// * run a delta compression algorithm
@@ -102,10 +139,6 @@ public abstract class NetworkBehaviour : MonoBehaviour
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)
{
@@ -117,31 +150,40 @@ protected void SetSyncVarHookGuard(ulong dirtyBit, bool value)
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);
+ [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;
}
- // Deprecated 2021-09-19
- [Obsolete("SetDirtyBit was renamed to SetSyncVarDirtyBit because that's what it does")]
- public void SetDirtyBit(ulong dirtyBit) => SetSyncVarDirtyBit(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
- public bool IsDirty()
- {
- if (NetworkTime.localTime - lastSyncTime >= syncInterval)
- {
- // OR both bitmasks. != 0 if either was dirty.
- return (syncVarDirtyBits | syncObjectDirtyBits) != 0UL;
- }
- return false;
- }
+ // 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 SetDirtyBits()
// automatically invoked when an update is sent for this object, but can
@@ -177,14 +219,73 @@ protected void InitSyncObject(SyncObject 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;
+ 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
@@ -195,7 +296,7 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer
// to avoid Wrapper functions. a lot of people requested this.
if (!NetworkClient.active)
{
- Debug.LogError($"Command Function {functionFullName} called without an active client.");
+ Debug.LogError($"Command Function {functionFullName} called on {name} without an active client.", gameObject);
return;
}
@@ -207,14 +308,14 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer
// 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.");
+ Debug.LogWarning($"Command Function {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 || hasAuthority))
+ if (!(!requiresAuthority || isLocalPlayer || isOwned))
{
- Debug.LogWarning($"Trying to send command for object without authority. {functionFullName}");
+ Debug.LogWarning($"Command Function {functionFullName} called on {name} without authority.", gameObject);
return;
}
@@ -225,7 +326,7 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer
// => see also: https://github.com/vis2k/Mirror/issues/2629
if (NetworkClient.connection == null)
{
- Debug.LogError("Send command attempted with no client running.");
+ Debug.LogError($"Command Function {functionFullName} called on {name} with no client running.", gameObject);
return;
}
@@ -233,9 +334,9 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer
CommandMessage message = new CommandMessage
{
netId = netId,
- componentIndex = (byte)ComponentIndex,
+ componentIndex = ComponentIndex,
// type+func so Inventory.RpcUse != Equipment.RpcUse
- functionHash = functionFullName.GetStableHashCode(),
+ functionHash = (ushort)functionFullName.GetStableHashCode(),
// segment to avoid reader allocations
payload = writer.ToArraySegment()
};
@@ -245,6 +346,8 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer
// 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);
}
@@ -254,14 +357,14 @@ protected void SendRPCInternal(string functionFullName, NetworkWriter writer, in
// this was in Weaver before
if (!NetworkServer.active)
{
- Debug.LogError($"RPC Function {functionFullName} called on Client.");
+ Debug.LogError($"RPC Function {functionFullName} called on Client.", 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}");
+ Debug.LogWarning($"ClientRpc {functionFullName} called on un-spawned object: {name}", gameObject);
return;
}
@@ -269,14 +372,36 @@ protected void SendRPCInternal(string functionFullName, NetworkWriter writer, in
RpcMessage message = new RpcMessage
{
netId = netId,
- componentIndex = (byte)ComponentIndex,
+ componentIndex = ComponentIndex,
// type+func so Inventory.RpcUse != Equipment.RpcUse
- functionHash = functionFullName.GetStableHashCode(),
+ functionHash = (ushort)functionFullName.GetStableHashCode(),
// segment to avoid reader allocations
payload = writer.ToArraySegment()
};
- NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId);
+ // 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)
+ {
+ // serialize the message only once
+ using (NetworkWriterPooled serialized = NetworkWriterPool.Get())
+ {
+ serialized.Write(message);
+
+ // add to every observer's connection's rpc buffer
+ foreach (NetworkConnectionToClient conn in netIdentity.observers.Values)
+ {
+ bool isOwner = conn == netIdentity.connectionToClient;
+ if ((!isOwner || includeOwner) && conn.isReady)
+ {
+ conn.BufferRpc(message, channelId);
+ }
+ }
+ }
+ }
}
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
@@ -284,13 +409,13 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull
{
if (!NetworkServer.active)
{
- Debug.LogError($"TargetRPC {functionFullName} called when server not 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");
+ Debug.LogWarning($"TargetRpc {functionFullName} called on {name} but that object has not been spawned or has been unspawned.", gameObject);
return;
}
@@ -303,13 +428,14 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull
// 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");
+ 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;
}
- if (!(conn is NetworkConnectionToClient))
+ // TODO change conn type to NetworkConnectionToClient to begin with.
+ if (!(conn is NetworkConnectionToClient connToClient))
{
- Debug.LogError($"TargetRPC {functionFullName} requires a NetworkConnectionToClient but was given {conn.GetType().Name}");
+ Debug.LogError($"TargetRPC {functionFullName} called on {name} requires a NetworkConnectionToClient but was given {conn.GetType().Name}", gameObject);
return;
}
@@ -317,14 +443,17 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull
RpcMessage message = new RpcMessage
{
netId = netId,
- componentIndex = (byte)ComponentIndex,
+ componentIndex = ComponentIndex,
// type+func so Inventory.RpcUse != Equipment.RpcUse
- functionHash = functionFullName.GetStableHashCode(),
+ functionHash = (ushort)functionFullName.GetStableHashCode(),
// segment to avoid reader allocations
payload = writer.ToArraySegment()
};
- conn.Send(message, channelId);
+ // serialize it to the connection's rpc buffer.
+ // send them all at once, instead of sending one message per rpc.
+ // conn.Send(message, channelId);
+ connToClient.BufferRpc(message, channelId);
}
// move the [SyncVar] generated property's .set into C# to avoid much IL
@@ -344,7 +473,7 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull
// {
// int oldValue = health;
// SetSyncVar(value, ref health, 1uL);
- // if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL))
+ // if (NetworkServer.activeHost && !GetSyncVarHookGuard(1uL))
// {
// SetSyncVarHookGuard(1uL, value: true);
// OnChanged(oldValue, value);
@@ -368,7 +497,7 @@ public void GeneratedSyncVarSetter(T value, ref T field, ulong dirtyBit, Acti
// 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))
+ if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
@@ -395,7 +524,7 @@ public void GeneratedSyncVarSetter_GameObject(GameObject value, ref GameObject f
// 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))
+ if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
@@ -422,7 +551,7 @@ public void GeneratedSyncVarSetter_NetworkIdentity(NetworkIdentity value, ref Ne
// 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))
+ if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
@@ -450,7 +579,7 @@ public void GeneratedSyncVarSetter_NetworkBehaviour(T value, ref T field, ulo
// 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))
+ if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit))
{
SetSyncVarHookGuard(dirtyBit, true);
OnChanged(oldValue, value);
@@ -469,8 +598,7 @@ public static bool SyncVarGameObjectEqual(GameObject newGameObject, uint netIdFi
uint newNetId = 0;
if (newGameObject != null)
{
- NetworkIdentity identity = newGameObject.GetComponent();
- if (identity != null)
+ if (newGameObject.TryGetComponent(out NetworkIdentity identity))
{
newNetId = identity.netId;
if (newNetId == 0)
@@ -493,8 +621,7 @@ protected void SetSyncVarGameObject(GameObject newGameObject, ref GameObject gam
uint newNetId = 0;
if (newGameObject != null)
{
- NetworkIdentity identity = newGameObject.GetComponent();
- if (identity != null)
+ if (newGameObject.TryGetComponent(out NetworkIdentity identity))
{
newNetId = identity.netId;
if (newNetId == 0)
@@ -838,7 +965,7 @@ protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdent
protected static bool SyncVarNetworkBehaviourEqual(T newBehaviour, NetworkBehaviourSyncVar syncField) where T : NetworkBehaviour
{
uint newNetId = 0;
- int newComponentIndex = 0;
+ byte newComponentIndex = 0;
if (newBehaviour != null)
{
newNetId = newBehaviour.netId;
@@ -861,7 +988,7 @@ protected void SetSyncVarNetworkBehaviour(T newBehaviour, ref T behaviourFiel
return;
uint newNetId = 0;
- int componentIndex = 0;
+ byte componentIndex = 0;
if (newBehaviour != null)
{
newNetId = newBehaviour.netId;
@@ -903,35 +1030,6 @@ protected T GetSyncVarNetworkBehaviour(NetworkBehaviourSyncVar syncNetBehavio
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?
@@ -955,35 +1053,46 @@ protected void SetSyncVar(T value, ref T fieldValue, ulong dirtyBit)
//
// 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)
+ public virtual void 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;
+ 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);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ void SerializeSyncObjects(NetworkWriter writer, bool initialState)
+ {
+ // if initialState: write all SyncVars.
+ // otherwise write dirtyBits+dirty SyncVars
+ if (initialState)
+ SerializeObjectsAll(writer);
+ else
+ SerializeObjectsDelta(writer);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ void DeserializeSyncObjects(NetworkReader reader, bool initialState)
{
if (initialState)
{
- DeSerializeObjectsAll(reader);
+ DeserializeObjectsAll(reader);
}
else
{
- DeSerializeObjectsDelta(reader);
+ DeserializeObjectsDelta(reader);
}
-
- DeserializeSyncVars(reader, initialState);
}
// USED BY WEAVER
- protected virtual bool SerializeSyncVars(NetworkWriter writer, bool initialState)
+ protected virtual void SerializeSyncVars(NetworkWriter writer, bool initialState)
{
- return false;
-
// SyncVar are written here in subclass
// if initialState
@@ -1005,23 +1114,20 @@ protected virtual void DeserializeSyncVars(NetworkReader reader, bool initialSta
// read dirty SyncVars
}
- public bool SerializeObjectsAll(NetworkWriter writer)
+ public void 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)
+ public void 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++)
{
@@ -1030,13 +1136,11 @@ public bool SerializeObjectsDelta(NetworkWriter writer)
if ((syncObjectDirtyBits & (1UL << i)) != 0)
{
syncObject.OnSerializeDelta(writer);
- dirty = true;
}
}
- return dirty;
}
- internal void DeSerializeObjectsAll(NetworkReader reader)
+ internal void DeserializeObjectsAll(NetworkReader reader)
{
for (int i = 0; i < syncObjects.Count; i++)
{
@@ -1045,7 +1149,7 @@ internal void DeSerializeObjectsAll(NetworkReader reader)
}
}
- internal void DeSerializeObjectsDelta(NetworkReader reader)
+ internal void DeserializeObjectsDelta(NetworkReader reader)
{
ulong dirty = reader.ReadULong();
for (int i = 0; i < syncObjects.Count; i++)
@@ -1059,6 +1163,130 @@ internal void DeSerializeObjectsDelta(NetworkReader 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)
diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta b/Assets/Mirror/Core/NetworkBehaviour.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkBehaviour.cs.meta
rename to Assets/Mirror/Core/NetworkBehaviour.cs.meta
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..47e3893
--- /dev/null
+++ b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b04fe7518657486089dfaf811db0b3ea
+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/Core/NetworkClient.cs
similarity index 74%
rename from Assets/Mirror/Runtime/NetworkClient.cs
rename to Assets/Mirror/Core/NetworkClient.cs
index e5dabe3..c94eea1 100644
--- a/Assets/Mirror/Runtime/NetworkClient.cs
+++ b/Assets/Mirror/Core/NetworkClient.cs
@@ -18,8 +18,21 @@ public enum ConnectState
}
/// NetworkClient with connection to server.
- public static class NetworkClient
+ 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;
+
// message handlers by messageId
internal static readonly Dictionary handlers =
new Dictionary();
@@ -50,19 +63,24 @@ public static class NetworkClient
// empty if the client has not connected yet.
public static string serverIp => connection.address;
- /// active is true while a client is connecting/connected
+ /// 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;
- /// True if client is running in host mode.
- public static bool isHostClient => connection is LocalConnectionToServer;
+ // Deprecated 2022-12-12
+ [Obsolete("NetworkClient.isHostClient was renamed to .activeHost to be more obvious")]
+ public static bool isHostClient => activeHost;
// OnConnected / OnDisconnected used to be NetworkMessages that were
// invoked. this introduced a bug where external clients could send
@@ -71,19 +89,19 @@ public static class NetworkClient
// => public so that custom NetworkManagers can hook into it
public static Action OnConnectedEvent;
public static Action OnDisconnectedEvent;
- public static Action OnErrorEvent;
+ public static Action OnErrorEvent;
/// Registered spawnable prefabs by assetId.
- public static readonly Dictionary prefabs =
- new Dictionary();
+ public static readonly Dictionary prefabs =
+ new Dictionary();
- // custom spawn / unspawn handlers.
+ // 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();
+ internal static readonly Dictionary spawnHandlers =
+ new Dictionary();
+ internal static readonly Dictionary unspawnHandlers =
+ new Dictionary();
// spawning
// internal for tests
@@ -105,86 +123,62 @@ public static class NetworkClient
// 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.activeTransport.OnClientConnected += OnTransportConnected;
- Transport.activeTransport.OnClientDataReceived += OnTransportData;
- Transport.activeTransport.OnClientDisconnected += OnTransportDisconnected;
- Transport.activeTransport.OnClientError += OnError;
+ Transport.active.OnClientConnected += OnTransportConnected;
+ Transport.active.OnClientDataReceived += OnTransportData;
+ Transport.active.OnClientDisconnected += OnTransportDisconnected;
+ Transport.active.OnClientError += OnTransportError;
}
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;
+ Transport.active.OnClientConnected -= OnTransportConnected;
+ Transport.active.OnClientDataReceived -= OnTransportData;
+ Transport.active.OnClientDisconnected -= OnTransportDisconnected;
+ Transport.active.OnClientError -= OnTransportError;
}
- internal static void RegisterSystemHandlers(bool hostMode)
+ // connect /////////////////////////////////////////////////////////////
+ // initialize is called before every connect
+ static void Initialize(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);
- }
+ // 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");
- // These handlers are the same for host and remote clients
- RegisterHandler(OnChangeOwner);
- RegisterHandler(OnRPCMessage);
+ // reset time interpolation on every new connect.
+ // ensures last sessions' state is cleared before starting again.
+ InitTimeInterpolation();
+
+ RegisterMessageHandlers(hostMode);
+ Transport.active.enabled = true;
}
- // 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");
+ Initialize(false);
- RegisterSystemHandlers(false);
- Transport.activeTransport.enabled = true;
AddTransportHandlers();
-
connectState = ConnectState.Connecting;
- Transport.activeTransport.ClientConnect(address);
-
+ Transport.active.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");
+ Initialize(false);
- RegisterSystemHandlers(false);
- Transport.activeTransport.enabled = true;
AddTransportHandlers();
-
connectState = ConnectState.Connecting;
- Transport.activeTransport.ClientConnect(uri);
-
+ Transport.active.ClientConnect(uri);
connection = new NetworkConnectionToServer();
}
@@ -192,42 +186,14 @@ public static void Connect(Uri uri)
// called from NetworkManager.FinishStartHost()
public static void ConnectHost()
{
- //Debug.Log("Client Connect Host to Server");
-
- RegisterSystemHandlers(true);
-
+ Initialize(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);
+ HostMode.SetupConnections();
}
- /// 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();
- }
+ // Deprecated 2022-12-12
+ [Obsolete("NetworkClient.ConnectLocalServer was moved to HostMode.InvokeOnConnected")]
+ public static void ConnectLocalServer() => HostMode.InvokeOnConnected();
// disconnect //////////////////////////////////////////////////////////
/// Disconnect from server.
@@ -280,7 +246,7 @@ static void OnTransportConnected()
// helper function
static bool UnpackAndInvoke(NetworkReader reader, int channelId)
{
- if (MessagePacking.Unpack(reader, out ushort msgType))
+ if (NetworkMessages.UnpackId(reader, out ushort msgType))
{
// try to invoke the handler for that message
if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler))
@@ -347,7 +313,7 @@ internal static void OnTransportData(ArraySegment data, int channelId)
unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp))
{
// enough to read at least header size?
- if (reader.Remaining >= MessagePacking.HeaderSize)
+ if (reader.Remaining >= NetworkMessages.IdSize)
{
// make remoteTimeStamp available to the user
connection.remoteTimeStamp = remoteTimestamp;
@@ -417,10 +383,18 @@ internal static void OnTransportDisconnected()
// Raise the event before changing ConnectState
// because 'active' depends on this during shutdown
- if (connection != null) OnDisconnectedEvent?.Invoke();
+ //
+ // 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
@@ -432,10 +406,13 @@ internal static void OnTransportDisconnected()
RemoveTransportHandlers();
}
- static void OnError(Exception exception)
+ // transport errors are forwarded to high level
+ static void OnTransportError(TransportError error, string reason)
{
- Debug.LogException(exception);
- OnErrorEvent?.Invoke(exception);
+ // 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);
}
// send ////////////////////////////////////////////////////////////////
@@ -455,20 +432,56 @@ public static void Send(T message, int channelId = Channels.Reliable)
}
// 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)
+ {
+ 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(OnTimeSnapshotMessage);
+ RegisterHandler(OnChangeOwner);
+ RegisterHandler(OnRPCBufferMessage);
+ }
+
/// 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();
+ ushort msgType = NetworkMessages.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);
+ handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication);
}
/// Replace a handler for a particular message type. Should require authentication by default.
@@ -477,8 +490,8 @@ public static void RegisterHandler(Action handler, bool requireAuthenticat
public static void ReplaceHandler(Action handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
- ushort msgType = MessagePacking.GetId();
- handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication);
+ ushort msgType = NetworkMessages.GetId();
+ handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication);
}
/// Replace a handler for a particular message type. Should require authentication by default.
@@ -495,24 +508,25 @@ public static bool UnregisterHandler()
where T : struct, NetworkMessage
{
// use int to minimize collisions
- ushort msgType = MessagePacking.GetId();
+ ushort msgType = NetworkMessages.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)
+ public static bool GetPrefab(uint assetId, out GameObject prefab)
{
prefab = null;
- return assetId != Guid.Empty &&
- prefabs.TryGetValue(assetId, out 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 == Guid.Empty)
+ 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;
@@ -550,7 +564,7 @@ static void RegisterPrefabIdentity(NetworkIdentity prefab)
// 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)
+ public static void RegisterPrefab(GameObject prefab, uint newAssetId)
{
if (prefab == null)
{
@@ -558,20 +572,19 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId)
return;
}
- if (newAssetId == Guid.Empty)
+ if (newAssetId == 0)
{
Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty");
return;
}
- NetworkIdentity identity = prefab.GetComponent();
- if (identity == null)
+ if (!prefab.TryGetComponent(out NetworkIdentity identity))
{
Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
- if (identity.assetId != Guid.Empty && identity.assetId != newAssetId)
+ 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;
@@ -591,8 +604,7 @@ public static void RegisterPrefab(GameObject prefab)
return;
}
- NetworkIdentity identity = prefab.GetComponent();
- if (identity == null)
+ if (!prefab.TryGetComponent(out NetworkIdentity identity))
{
Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component");
return;
@@ -606,7 +618,7 @@ public static void RegisterPrefab(GameObject prefab)
// 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)
+ 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)
@@ -628,8 +640,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler,
return;
}
- NetworkIdentity identity = prefab.GetComponent();
- if (identity == null)
+ if (!prefab.TryGetComponent(out NetworkIdentity identity))
{
Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
return;
@@ -641,9 +652,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler,
return;
}
- Guid assetId = identity.assetId;
-
- if (assetId == Guid.Empty)
+ 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;
@@ -652,7 +661,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler,
// 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}");
+ Debug.LogError($"Can not Register null SpawnHandler for {identity.assetId}");
return;
}
@@ -664,9 +673,9 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler,
// 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)
+ public static void RegisterPrefab(GameObject prefab, uint newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
- if (newAssetId == Guid.Empty)
+ if (newAssetId == 0)
{
Debug.LogError($"Could not register handler for '{prefab.name}' with new assetId because the new assetId was empty");
return;
@@ -678,14 +687,13 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandl
return;
}
- NetworkIdentity identity = prefab.GetComponent();
- if (identity == null)
+ 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 != Guid.Empty && identity.assetId != newAssetId)
+ 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;
@@ -698,7 +706,7 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandl
}
identity.assetId = newAssetId;
- Guid assetId = identity.assetId;
+ uint assetId = identity.assetId;
if (spawnHandler == null)
{
@@ -745,8 +753,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnH
return;
}
- NetworkIdentity identity = prefab.GetComponent();
- if (identity == null)
+ if (!prefab.TryGetComponent(out NetworkIdentity identity))
{
Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
return;
@@ -758,9 +765,9 @@ public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnH
return;
}
- Guid assetId = identity.assetId;
+ uint assetId = identity.assetId;
- if (assetId == Guid.Empty)
+ 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;
@@ -810,14 +817,13 @@ public static void UnregisterPrefab(GameObject prefab)
return;
}
- NetworkIdentity identity = prefab.GetComponent();
- if (identity == null)
+ if (!prefab.TryGetComponent(out NetworkIdentity identity))
{
Debug.LogError($"Could not unregister '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
- Guid assetId = identity.assetId;
+ uint assetId = identity.assetId;
prefabs.Remove(assetId);
spawnHandlers.Remove(assetId);
@@ -831,7 +837,7 @@ public static void UnregisterPrefab(GameObject prefab)
// 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)
+ 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)
@@ -849,7 +855,7 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler
// 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)
+ public static void RegisterSpawnHandler(uint assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
if (spawnHandler == null)
{
@@ -863,9 +869,9 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawn
return;
}
- if (assetId == Guid.Empty)
+ if (assetId == 0)
{
- Debug.LogError("Can not Register SpawnHandler for empty Guid");
+ Debug.LogError("Can not Register SpawnHandler for empty assetId");
return;
}
@@ -887,7 +893,7 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawn
}
/// Removes a registered spawn handler function that was registered with NetworkClient.RegisterHandler().
- public static void UnregisterSpawnHandler(Guid assetId)
+ public static void UnregisterSpawnHandler(uint assetId)
{
spawnHandlers.Remove(assetId);
unspawnHandlers.Remove(assetId);
@@ -901,7 +907,7 @@ public static void ClearSpawners()
unspawnHandlers.Clear();
}
- internal static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj)
+ internal static bool InvokeUnSpawnHandler(uint assetId, GameObject obj)
{
if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null)
{
@@ -964,7 +970,7 @@ internal static void InternalAddPlayer(NetworkIdentity identity)
{
connection.identity = identity;
}
- else Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer");
+ 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.
@@ -999,7 +1005,7 @@ public static bool AddPlayer()
// spawning ////////////////////////////////////////////////////////////
internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message)
{
- if (message.assetId != Guid.Empty)
+ if (message.assetId != 0)
identity.assetId = message.assetId;
if (!identity.gameObject.activeSelf)
@@ -1011,23 +1017,37 @@ internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage me
identity.transform.localPosition = message.position;
identity.transform.localRotation = message.rotation;
identity.transform.localScale = message.scale;
- identity.hasAuthority = message.isOwner;
+
+ // 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.isOwned = message.isOwner;
identity.netId = message.netId;
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.OnDeserializeAllSafely(payloadReader, true);
+ identity.DeserializeClient(payloadReader, true);
}
}
spawned[message.netId] = identity;
+ if (identity.isOwned) connection?.owned.Add(identity);
// the initial spawn with OnObjectSpawnStarted/Finished calls all
// object's OnStartClient/OnStartLocalPlayer after they were all
@@ -1037,9 +1057,7 @@ internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage me
// here immediately since there won't be another OnObjectSpawnFinished.
if (isSpawnFinished)
{
- identity.NotifyAuthority();
- identity.OnStartClient();
- CheckForLocalPlayer(identity);
+ InvokeIdentityCallbacks(identity);
}
}
@@ -1055,7 +1073,7 @@ internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity
return true;
}
- if (message.assetId == Guid.Empty && message.sceneId == 0)
+ if (message.assetId == 0 && message.sceneId == 0)
{
Debug.LogError($"OnSpawn message with netId '{message.netId}' has no AssetId or sceneId");
return false;
@@ -1074,8 +1092,8 @@ internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity
static NetworkIdentity GetExistingObject(uint netid)
{
- spawned.TryGetValue(netid, out NetworkIdentity localObject);
- return localObject;
+ spawned.TryGetValue(netid, out NetworkIdentity identity);
+ return identity;
}
static NetworkIdentity SpawnPrefab(SpawnMessage message)
@@ -1095,12 +1113,13 @@ static NetworkIdentity SpawnPrefab(SpawnMessage message)
Debug.LogError($"Spawn Handler returned null, Handler assetId '{message.assetId}'");
return null;
}
- NetworkIdentity identity = obj.GetComponent();
- if (identity == 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;
}
@@ -1141,16 +1160,6 @@ static NetworkIdentity GetAndRemoveSceneObject(ulong sceneId)
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()
{
@@ -1162,9 +1171,23 @@ public static void PrepareToSpawnSceneObjects()
foreach (NetworkIdentity identity in allIdentities)
{
// add all unspawned NetworkIdentities to spawnable objects
- if (ConsiderForSpawning(identity))
+ // need to ensure it's not active yet because
+ // PrepareToSpawnSceneObjects may be called multiple times in case
+ // the ObjectSpawnStarted message is received multiple times.
+ if (Utils.IsSceneObject(identity) &&
+ !identity.gameObject.activeSelf)
{
- spawnableObjects.Add(identity.sceneId, identity);
+ 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);
+ }
}
}
}
@@ -1178,60 +1201,42 @@ internal static void OnObjectSpawnStarted(ObjectSpawnStartedMessage _)
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)
+ // NetworkIdentities should always be removed from .spawned when
+ // they are destroyed. for safety, let's double check here.
+ if (identity != null)
{
- removeFromSpawned.Add(kvp.Key);
+ BootstrapIdentity(identity);
}
+ else Debug.LogWarning("Found null entry in NetworkClient.spawned. This is unexpected. Was the NetworkIdentity not destroyed properly?");
}
-
- // can't modify NetworkIdentity.spawned inside foreach so need 2nd loop to remove
- foreach (uint id in removeFromSpawned)
- {
- spawned.Remove(id);
- }
- removeFromSpawned.Clear();
+ isSpawnFinished = true;
}
// host mode callbacks /////////////////////////////////////////////////
static void OnHostClientObjectDestroy(ObjectDestroyMessage message)
{
//Debug.Log($"NetworkClient.OnLocalObjectObjDestroy netId:{message.netId}");
+
+ // remove from owned (if any)
+ if (spawned.TryGetValue(message.netId, out NetworkIdentity identity))
+ connection.owned.Remove(identity);
+
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 (spawned.TryGetValue(message.netId, out NetworkIdentity identity) &&
+ identity != null)
{
if (aoi != null)
- aoi.SetHostVisibility(localObject, false);
+ aoi.SetHostVisibility(identity, false);
}
}
@@ -1239,22 +1244,21 @@ 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)
+ if (NetworkServer.spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
- spawned[message.netId] = localObject;
+ spawned[message.netId] = identity;
+ if (message.isOwner) connection.owned.Add(identity);
// now do the actual 'spawning' on host mode
if (message.isLocalPlayer)
- InternalAddPlayer(localObject);
-
- localObject.hasAuthority = message.isOwner;
- localObject.NotifyAuthority();
- localObject.OnStartClient();
+ InternalAddPlayer(identity);
+ // set visibility before invoking OnStartClient etc. callbacks
if (aoi != null)
- aoi.SetHostVisibility(localObject, true);
+ aoi.SetHostVisibility(identity, true);
- CheckForLocalPlayer(localObject);
+ identity.isOwned = message.isOwner;
+ BootstrapIdentity(identity);
}
}
@@ -1262,21 +1266,37 @@ internal static void OnHostClientSpawn(SpawnMessage message)
static void OnEntityStateMessage(EntityStateMessage message)
{
// Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
- if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null)
+ if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
- using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload))
- localObject.OnDeserializeAllSafely(networkReader, false);
+ using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
+ identity.DeserializeClient(reader, 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}");
+ // Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.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);
+ 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 OnRPCBufferMessage(RpcBufferMessage message)
+ {
+ // Debug.Log($"NetworkClient.OnRPCBufferMessage of {message.payload.Count} bytes");
+ // parse all rpc messages from the buffer
+ using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
+ {
+ while (reader.Remaining > 0)
+ {
+ // read message without header
+ RpcMessage rpcMessage = reader.Read();
+ OnRPCMessage(rpcMessage);
+ }
}
}
@@ -1315,7 +1335,15 @@ internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage me
}
// set ownership flag (aka authority)
- identity.hasAuthority = message.isOwner;
+ 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
@@ -1325,66 +1353,113 @@ internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage me
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.
}
-
- // call OnStartLocalPlayer if it's the local player now.
- CheckForLocalPlayer(identity);
}
- internal static void CheckForLocalPlayer(NetworkIdentity 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)
{
- if (identity == localPlayer)
- {
- // Set isLocalPlayer to true on this NetworkIdentity and trigger
- // OnStartLocalPlayer in all scripts on the same GO
+ // 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 OnStartAuthority
+ identity.NotifyAuthority();
+
+ // invoke OnStartClient
+ identity.OnStartClient();
+
+ // invoke OnStartLocalPlayer
+ if (identity.isLocalPlayer)
identity.OnStartLocalPlayer();
- // Debug.Log($"NetworkClient.OnOwnerMessage player:{identity.name}");
- }
}
- // destroy /////////////////////////////////////////////////////////////
- static void DestroyObject(uint netId)
+ // configure flags & invoke callbacks
+ static void BootstrapIdentity(NetworkIdentity identity)
{
- // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}");
- if (spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null)
- {
- if (localObject.isLocalPlayer)
- localObject.OnStopLocalPlayer();
+ InitializeIdentityFlags(identity);
+ InvokeIdentityCallbacks(identity);
+ }
- localObject.OnStopClient();
+ // broadcast ///////////////////////////////////////////////////////////
+ static void BroadcastTimeSnapshot()
+ {
+ Send(new TimeSnapshotMessage(), Channels.Unreliable);
+ }
- // 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
+ // 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.
+ BroadcastTimeSnapshot();
+
+ // 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)
{
- localObject.gameObject.SetActive(false);
- spawnableObjects[localObject.sceneId] = localObject;
- // reset for scene objects
- localObject.Reset();
- }
+ 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);
- // remove from dictionary no matter how it is unspawned
- spawned.Remove(netId);
+ // reset dirty bits so it's not resent next time.
+ identity.ClearDirtyComponentsDirtyBits();
+ }
+ }
+ }
+ // 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.");
}
- //else Debug.LogWarning($"Did not find target for destroy message for {netId}");
}
// update //////////////////////////////////////////////////////////////
@@ -1393,14 +1468,49 @@ static void DestroyObject(uint netId)
internal static void NetworkEarlyUpdate()
{
// process all incoming messages first before updating the world
- if (Transport.activeTransport != null)
- Transport.activeTransport.ClientEarlyUpdate();
+ 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
+ // note that Broadcast() runs every update.
+ // on clients with 120 Hz, this will run 120 times per second.
+ // however, Broadcast only checks .owned, which usually aren't many.
+ //
+ // we could use a .sendInterval, but it would also put a minimum
+ // limit to every component's sendInterval automatically.
+ 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
+ if (!Application.isPlaying ||
+ AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime))
+ {
+ Broadcast();
+ }
+ }
+
+ // update connections to flush out messages _after_ broadcast
// local connection?
if (connection is LocalConnectionToServer localConnection)
{
@@ -1421,11 +1531,11 @@ internal static void NetworkLateUpdate()
}
// process all outgoing messages after updating the world
- if (Transport.activeTransport != null)
- Transport.activeTransport.ClientLateUpdate();
+ if (Transport.active != null)
+ Transport.active.ClientLateUpdate();
}
- // shutdown ////////////////////////////////////////////////////////////
+ // destroy /////////////////////////////////////////////////////////////
/// Destroys all networked objects on the client.
// Note: NetworkServer.CleanupNetworkIdentities does the same on server.
public static void DestroyAllClientObjects()
@@ -1482,6 +1592,7 @@ public static void DestroyAllClientObjects()
}
}
spawned.Clear();
+ connection?.owned.Clear();
}
catch (InvalidOperationException e)
{
@@ -1490,6 +1601,45 @@ public static void DestroyAllClientObjects()
}
}
+ 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.Reset();
+ }
+ // 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.Reset();
+ }
+
+ // 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)]
@@ -1497,15 +1647,17 @@ 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();
- // calls spawned.Clear() if no exception occurs
- DestroyAllClientObjects();
-
spawned.Clear();
+ connection?.owned.Clear();
handlers.Clear();
spawnableObjects.Clear();
@@ -1521,8 +1673,8 @@ public static void Shutdown()
// 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();
+ if (Transport.active != null)
+ Transport.active.ClientDisconnect();
// reset statics
connectState = ConnectState.None;
@@ -1531,6 +1683,7 @@ public static void Shutdown()
ready = false;
isSpawnFinished = false;
isLoadingScene = false;
+ lastSendTime = 0;
unbatcher = new Unbatcher();
@@ -1540,5 +1693,30 @@ public static void Shutdown()
OnDisconnectedEvent = null;
OnErrorEvent = 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, 500, 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($"timescale: {localTimescale:F2}");
+ GUILayout.Box($"BTM: {bufferTimeMultiplier:F2}");
+ GUILayout.EndHorizontal();
+
+ GUILayout.EndArea();
+ }
}
}
diff --git a/Assets/Mirror/Runtime/NetworkClient.cs.meta b/Assets/Mirror/Core/NetworkClient.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkClient.cs.meta
rename to Assets/Mirror/Core/NetworkClient.cs.meta
diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs
new file mode 100644
index 0000000..060dae5
--- /dev/null
+++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs
@@ -0,0 +1,181 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Mirror
+{
+ public static partial class NetworkClient
+ {
+ // TODO expose the settings to the user later.
+ // via NetMan or NetworkClientConfig or NetworkClient as component etc.
+
+ // 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("Snapshot Interpolation: 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 static double bufferTimeMultiplier = 2;
+ 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 /////////////////////////////////////////////////////////////
+ // catchup thresholds in 'frames'.
+ // half a frame might be too aggressive.
+ [Header("Snapshot Interpolation: 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 static 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 static float catchupPositiveThreshold = 1;
+
+ [Tooltip("Local timeline acceleration in % while catching up.")]
+ [Range(0, 1)]
+ public static double catchupSpeed = 0.01f; // 1%
+
+ [Tooltip("Local timeline slowdown in % while slowing down.")]
+ [Range(0, 1)]
+ public static double slowdownSpeed = 0.01f; // 1%
+
+ [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
+ public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
+
+ // 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 //////////////////////////////////////
+ // 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("Snapshot Interpolation: Dynamic Adjustment")]
+ [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
+ public static bool dynamicAdjustment = true;
+
+ [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
+ public static 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 static int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
+ 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)
+ // Don't reset bufferTimeMultiplier here - whatever their network condition
+ // was when they disconnected, it won't have changed on immediate reconnect.
+ 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 * driftEmaDuration);
+ deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * 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.
+
+#if !UNITY_2020_3_OR_NEWER
+ // Unity 2019 doesn't have Time.timeAsDouble yet
+ OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime));
+#else
+ OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble));
+#endif
+ }
+
+ // 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 (dynamicAdjustment)
+ {
+ // set bufferTime on the fly.
+ // shows in inspector for easier debugging :)
+ bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
+ NetworkServer.sendInterval,
+ deliveryTimeEma.StandardDeviation,
+ dynamicAdjustmentTolerance
+ );
+ }
+
+ // insert into the buffer & initialize / adjust / catchup
+ SnapshotInterpolation.InsertAndAdjust(
+ snapshots,
+ snap,
+ ref localTimeline,
+ ref localTimescale,
+ NetworkServer.sendInterval,
+ bufferTime,
+ catchupSpeed,
+ slowdownSpeed,
+ ref driftEma,
+ catchupNegativeThreshold,
+ 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.
+ 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..3c52ae1
--- /dev/null
+++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ad039071a9cc487b9f7831d28bbe8e83
+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/Core/NetworkConnection.cs
similarity index 92%
rename from Assets/Mirror/Runtime/NetworkConnection.cs
rename to Assets/Mirror/Core/NetworkConnection.cs
index 14729c6..6b47abb 100644
--- a/Assets/Mirror/Runtime/NetworkConnection.cs
+++ b/Assets/Mirror/Core/NetworkConnection.cs
@@ -10,11 +10,6 @@ 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.
@@ -42,13 +37,12 @@ public abstract class NetworkConnection
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.
- // DEPRECATED 2022-02-05
- [Obsolete("Cast to NetworkConnectionToClient to access .clientOwnedObjects")]
- public HashSet clientOwnedObjects => ((NetworkConnectionToClient)this).clientOwnedObjects;
+ public readonly HashSet owned = new HashSet();
// batching from server to client & client to server.
// fewer transport calls give us significantly better performance/scale.
@@ -91,7 +85,7 @@ protected Batcher GetBatchForChannelId(int channelId)
if (!batches.TryGetValue(channelId, out batch))
{
// get max batch size for this channel
- int threshold = Transport.activeTransport.GetBatchThreshold(channelId);
+ int threshold = Transport.active.GetBatchThreshold(channelId);
// create batcher
batch = new Batcher(threshold);
@@ -107,7 +101,7 @@ protected Batcher GetBatchForChannelId(int channelId)
// => 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);
+ int max = Transport.active.GetMaxPacketSize(channelId);
if (segment.Count > max)
{
Debug.LogError($"NetworkConnection.ValidatePacketSize: cannot send packet larger than {max} bytes, was {segment.Count} bytes");
@@ -134,7 +128,7 @@ public void Send(T message, int channelId = Channels.Reliable)
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// pack message and send allocation free
- MessagePacking.Pack(message, writer);
+ NetworkMessages.Pack(message, writer);
NetworkDiagnostics.OnSend(message, channelId, writer.Position, 1);
Send(writer.ToArraySegment(), channelId);
}
@@ -175,15 +169,15 @@ internal virtual void Send(ArraySegment segment, int channelId = Channels.
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.
- Batcher batcher = kvp.Value;
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// make a batch with our local time (double precision)
- while (batcher.GetBatch(writer))
+ while (kvp.Value.GetBatch(writer))
{
// validate packet before handing the batch to the
// transport. this guarantees that we always stay
diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs.meta b/Assets/Mirror/Core/NetworkConnection.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkConnection.cs.meta
rename to Assets/Mirror/Core/NetworkConnection.cs.meta
diff --git a/Assets/Mirror/Core/NetworkConnectionToClient.cs b/Assets/Mirror/Core/NetworkConnectionToClient.cs
new file mode 100644
index 0000000..e7c0bd0
--- /dev/null
+++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs
@@ -0,0 +1,256 @@
+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 override string address =>
+ Transport.active.ServerGetClientAddress(connectionId);
+
+ /// NetworkIdentities that this connection can see
+ // TODO move to server's NetworkConnectionToClient?
+ public readonly HashSet observing = new HashSet();
+
+ // Deprecated 2022-10-13
+ [Obsolete(".clientOwnedObjects was renamed to .owned :)")]
+ public HashSet clientOwnedObjects => owned;
+
+ // 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;
+
+ public NetworkConnectionToClient(int networkConnectionId)
+ : base(networkConnectionId)
+ {
+ // 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.driftEmaDuration);
+ deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration);
+
+ // buffer limit should be at least multiplier to have enough in there
+ snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit);
+ }
+
+ public void OnTimeSnapshot(TimeSnapshot snapshot)
+ {
+ // protect against ever growing buffer size attacks
+ if (snapshots.Count >= snapshotBufferSizeLimit) return;
+
+ // (optional) dynamic adjustment
+ if (NetworkClient.dynamicAdjustment)
+ {
+ // set bufferTime on the fly.
+ // shows in inspector for easier debugging :)
+ bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
+ NetworkServer.sendInterval,
+ deliveryTimeEma.StandardDeviation,
+ NetworkClient.dynamicAdjustmentTolerance
+ );
+ // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
+ }
+
+ // insert into the server buffer & initialize / adjust / catchup
+ SnapshotInterpolation.InsertAndAdjust(
+ snapshots,
+ snapshot,
+ ref remoteTimeline,
+ ref remoteTimescale,
+ NetworkServer.sendInterval,
+ bufferTime,
+ NetworkClient.catchupSpeed,
+ NetworkClient.slowdownSpeed,
+ ref driftEma,
+ NetworkClient.catchupNegativeThreshold,
+ NetworkClient.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);
+
+ void FlushRpcs(NetworkWriter buffer, int channelId)
+ {
+ if (buffer.Position > 0)
+ {
+ Send(new RpcBufferMessage{ payload = buffer }, channelId);
+ buffer.Position = 0;
+ }
+ }
+
+ // helper for both channels
+ void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize)
+ {
+ // calculate buffer limit. we can only fit so much into a message.
+ // max - message header - WriteArraySegment size header - batch header
+ int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize;
+
+ // remember previous valid position
+ int before = buffer.Position;
+
+ // serialize the message without header
+ buffer.Write(message);
+
+ // before we potentially flush out old messages,
+ // let's ensure this single message can even fit the limit.
+ // otherwise no point in flushing.
+ int messageSize = buffer.Position - before;
+ if (messageSize > bufferLimit)
+ {
+ Debug.LogWarning($"NetworkConnectionToClient: discarded RpcMesage for netId={message.netId} componentIndex={message.componentIndex} functionHash={message.functionHash} because it's larger than the rpc buffer limit of {bufferLimit} bytes for the channel: {channelId}");
+ return;
+ }
+
+ // too much to fit into max message size?
+ // then flush first, then write it again.
+ // (message + message header + 4 bytes WriteArraySegment header)
+ if (buffer.Position > bufferLimit)
+ {
+ buffer.Position = before;
+ FlushRpcs(buffer, channelId); // this resets position
+ buffer.Write(message);
+ }
+ }
+
+ internal void BufferRpc(RpcMessage message, int channelId)
+ {
+ int maxMessageSize = Transport.active.GetMaxPacketSize(channelId);
+ if (channelId == Channels.Reliable)
+ {
+ BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize);
+ }
+ else if (channelId == Channels.Unreliable)
+ {
+ BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize);
+ }
+ }
+
+ internal override void Update()
+ {
+ // send rpc buffers
+ FlushRpcs(reliableRpcs, Channels.Reliable);
+ FlushRpcs(unreliableRpcs, Channels.Unreliable);
+
+ // call base update to flush out batched messages
+ 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)
+ {
+ NetworkServer.Destroy(netIdentity.gameObject);
+ }
+ }
+
+ // clear the hashset because we destroyed them all
+ owned.Clear();
+ }
+ }
+}
diff --git a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta b/Assets/Mirror/Core/NetworkConnectionToClient.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta
rename to Assets/Mirror/Core/NetworkConnectionToClient.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs b/Assets/Mirror/Core/NetworkConnectionToServer.cs
similarity index 86%
rename from Assets/Mirror/Runtime/NetworkConnectionToServer.cs
rename to Assets/Mirror/Core/NetworkConnectionToServer.cs
index a1ebc5f..58e60e9 100644
--- a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs
+++ b/Assets/Mirror/Core/NetworkConnectionToServer.cs
@@ -10,7 +10,7 @@ public class NetworkConnectionToServer : NetworkConnection
// 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 +20,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/Runtime/NetworkConnectionToServer.cs.meta b/Assets/Mirror/Core/NetworkConnectionToServer.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta
rename to Assets/Mirror/Core/NetworkConnectionToServer.cs.meta
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/Runtime/NetworkDiagnostics.cs.meta b/Assets/Mirror/Core/NetworkDiagnostics.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta
rename to Assets/Mirror/Core/NetworkDiagnostics.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs
similarity index 72%
rename from Assets/Mirror/Runtime/NetworkIdentity.cs
rename to Assets/Mirror/Core/NetworkIdentity.cs
index 6c3c122..bd3f06f 100644
--- a/Assets/Mirror/Runtime/NetworkIdentity.cs
+++ b/Assets/Mirror/Core/NetworkIdentity.cs
@@ -1,17 +1,18 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using Mirror.RemoteCalls;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
- using UnityEditor;
+using UnityEditor;
- #if UNITY_2021_2_OR_NEWER
- using UnityEditor.SceneManagement;
- #elif UNITY_2018_3_OR_NEWER
+#if UNITY_2021_2_OR_NEWER
+using UnityEditor.SceneManagement;
+#elif UNITY_2018_3_OR_NEWER
using UnityEditor.Experimental.SceneManagement;
- #endif
+#endif
#endif
namespace Mirror
@@ -88,13 +89,17 @@ public sealed class NetworkIdentity : MonoBehaviour
/// 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; }
+ /// 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; }
+
+ // Deprecated 2022-10-13
+ [Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")]
+ public bool hasAuthority => isOwned;
/// 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;
+ public readonly Dictionary observers =
+ new Dictionary();
/// The unique network Id of this object (unique at runtime).
public uint netId { get; internal set; }
@@ -104,6 +109,55 @@ public sealed class NetworkIdentity : MonoBehaviour
[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] 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")]
@@ -130,35 +184,14 @@ internal set
}
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; }
+ // 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
@@ -183,68 +216,43 @@ public static Dictionary spawned
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
+ // 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)
{
- get
+ // check if unity object has been destroyed
+ if (this == null)
{
-#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);
+ Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]");
+ return;
}
- 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;
- }
+ // find the right component to invoke the function on
+ if (componentIndex >= NetworkBehaviours.Length)
+ {
+ Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]");
+ return;
+ }
- // old is empty
- m_AssetId = newAssetIdString;
- // Debug.Log($"Settings AssetId on NetworkIdentity '{name}', new assetId '{newAssetIdString}'");
+ 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}].");
}
}
- [SerializeField, HideInInspector] string m_AssetId;
- // Keep track of all sceneIds to detect scene duplicates
- static readonly Dictionary sceneIds =
- new Dictionary();
+ // 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.
@@ -260,33 +268,9 @@ 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++;
@@ -311,15 +295,29 @@ 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);
+ ValidateComponents();
// initialize each one
for (int i = 0; i < NetworkBehaviours.Length; ++i)
{
NetworkBehaviour component = NetworkBehaviours[i];
component.netIdentity = this;
- component.ComponentIndex = i;
+ 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);
}
}
@@ -358,7 +356,10 @@ 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);
+ {
+ Guid guid = new Guid(AssetDatabase.AssetPathToGUID(path));
+ assetId = (uint)guid.GetHashCode(); // deterministic
+ }
}
void AssignAssetID(GameObject prefab) => AssignAssetID(AssetDatabase.GetAssetPath(prefab));
@@ -557,7 +558,7 @@ void SetupIDs()
// anymore because assetId was cleared
if (!EditorApplication.isPlaying)
{
- m_AssetId = "";
+ _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.");
@@ -612,55 +613,25 @@ void OnDestroy()
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)
+ if (isClient)
{
- 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;
+ // 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
+ NetworkClient.spawned.Remove(netId);
}
+ }
+ internal void OnStartServer()
+ {
foreach (NetworkBehaviour comp in NetworkBehaviours)
{
// an exception in OnStartServer should be caught, so that one
@@ -702,20 +673,9 @@ internal void OnStopServer()
bool clientStarted;
internal void OnStartClient()
{
- if (clientStarted)
- return;
- clientStarted = true;
-
- isClient = true;
+ if (clientStarted) return;
- // 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;
- }
+ clientStarted = true;
// Debug.Log($"OnStartClient {gameObject} netId:{netId}");
foreach (NetworkBehaviour comp in NetworkBehaviours)
@@ -739,6 +699,10 @@ internal void OnStartClient()
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
@@ -757,26 +721,33 @@ internal void OnStopClient()
}
}
- // 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()
{
+ // 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;
- isLocalPlayer = true;
-
foreach (NetworkBehaviour comp in NetworkBehaviours)
{
// an exception in OnStartLocalPlayer should be caught, so that
@@ -815,150 +786,264 @@ internal void OnStopLocalPlayer()
}
}
- bool hadAuthority;
- internal void NotifyAuthority()
+ // 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)
{
- if (!hadAuthority && hasAuthority)
- OnStartAuthority();
- if (hadAuthority && !hasAuthority)
- OnStopAuthority();
- hadAuthority = hasAuthority;
+ ulong ownerMask = 0;
+ ulong observerMask = 0;
+
+ NetworkBehaviour[] components = NetworkBehaviours;
+ for (int i = 0; i < components.Length; ++i)
+ {
+ NetworkBehaviour component = components[i];
+
+ bool dirty = component.IsDirty();
+ ulong nthBit = (1u << i);
+
+ // 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
+ //
+ // 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 (component.syncMode == SyncMode.Observers && (initialState || dirty))
+ observerMask |= nthBit;
+ }
+
+ return (ownerMask, observerMask);
}
- internal void OnStartAuthority()
+ // build dirty mask for client.
+ // server always knows initialState, so we don't need it here.
+ ulong ClientDirtyMask()
{
- foreach (NetworkBehaviour comp in NetworkBehaviours)
+ ulong mask = 0;
+
+ NetworkBehaviour[] components = NetworkBehaviours;
+ for (int i = 0; i < components.Length; ++i)
{
- // 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)
+ // 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];
+ if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
- Debug.LogException(e, comp);
+ // set the n-th bit if dirty
+ // shifting from small to large numbers is varint-efficient.
+ if (component.IsDirty()) mask |= (1u << i);
}
}
+
+ return mask;
}
- internal void OnStopAuthority()
+ // 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)
{
- foreach (NetworkBehaviour comp in NetworkBehaviours)
+ ulong nthBit = (ulong)(1 << index);
+ return (mask & nthBit) != 0;
+ }
+
+ // serialize components into writer on the server.
+ // 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 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)
{
- // 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)
+ for (int i = 0; i < components.Length; ++i)
{
- Debug.LogException(e, comp);
+ 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);
+ }
+ }
}
}
}
- // 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)
+ // serialize components into writer on the client.
+ internal void SerializeClient(NetworkWriter writer)
{
- // 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;
+ // ensure NetworkBehaviours are valid before usage
+ ValidateComponents();
+ NetworkBehaviour[] components = NetworkBehaviours;
- // fill in length now
- writer.Position = headerPosition;
- writer.WriteInt(endPosition - contentPosition);
- writer.Position = endPosition;
+ // 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);
- //Debug.Log($"OnSerializeSafely written for object {comp.name} component:{comp.GetType()} sceneId:{sceneId:X} header:{headerPosition} content:{contentPosition} end:{endPosition} contentSize:{endPosition - contentPosition}");
+ // 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];
- return result;
+ // 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);
+ }
+ }
+ }
}
- // 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)
+ // deserialize components from the client on the server.
+ // there's no 'initialState'. server always knows the initial state.
+ internal bool DeserializeServer(NetworkReader reader)
{
- // check if components are in byte.MaxRange just to be 100% sure
- // that we avoid overflows
+ // ensure NetworkBehaviours are valid before usage
+ ValidateComponents();
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
+ // 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)
{
- // 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())
+ // was this one dirty?
+ if (IsDirty(mask, i))
{
- //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)
+ NetworkBehaviour comp = components[i];
+
+ // safety check to ensure clients can only modify their own
+ // ClientToServer components, nothing else.
+ if (comp.syncDirection == SyncDirection.ClientToServer)
{
- ArraySegment segment = ownerWriter.ToArraySegment();
- int length = ownerWriter.Position - startPosition;
- observersWriter.WriteBytes(segment.Array, startPosition, length);
+ // 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;
}
- // 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)
+ // 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.
@@ -966,16 +1051,20 @@ internal NetworkIdentitySerialization GetSerializationAtTick(int tick)
// (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)
+ if (lastSerialization.tick != tick
+#if UNITY_EDITOR
+ || !Application.isPlaying
+#endif
+ )
{
// reset
lastSerialization.ownerWriter.Position = 0;
lastSerialization.observersWriter.Position = 0;
// serialize
- OnSerializeAllSafely(false,
- lastSerialization.ownerWriter,
- lastSerialization.observersWriter);
+ SerializeServer(false,
+ lastSerialization.ownerWriter,
+ lastSerialization.observersWriter);
// clear dirty bits for the components that we serialized.
// previously we did this in NetworkServer.BroadcastToConnection
@@ -984,7 +1073,7 @@ internal NetworkIdentitySerialization GetSerializationAtTick(int tick)
// 'lastSerialization.tick != tick' scope.
// so only do it once.
//
- // NOTE: not in OnSerializeAllSafely as that should only do one
+ // NOTE: not in Serializell as that should only do one
// thing: serialize data.
//
//
@@ -1007,101 +1096,25 @@ internal NetworkIdentitySerialization GetSerializationAtTick(int tick)
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)
+ // 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()
{
- 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)
+ foreach (NetworkBehaviour comp in NetworkBehaviours)
{
- // read & check index [0..255]
- byte index = reader.ReadByte();
- if (index < components.Length)
+ if (comp.IsDirty())
{
- // deserialize this component
- OnDeserializeSafely(components[index], reader, initialState);
+ comp.ClearAllDirtyBits();
}
}
}
- // 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
@@ -1139,23 +1152,19 @@ internal void AddObserver(NetworkConnectionToClient conn)
conn.AddToObserving(this);
}
- // this is used when a connection is destroyed, since the "observers" property is read-only
- internal void RemoveObserver(NetworkConnection conn)
+ // clear all component's dirty bits no matter what
+ internal void ClearAllComponentsDirtyBits()
{
- observers?.Remove(conn.connectionId);
+ foreach (NetworkBehaviour comp in NetworkBehaviours)
+ {
+ comp.ClearAllDirtyBits();
+ }
}
- // Called when NetworkIdentity is destroyed
- internal void ClearObservers()
+ // this is used when a connection is destroyed, since the "observers" property is read-only
+ internal void RemoveObserver(NetworkConnection conn)
{
- if (observers != null)
- {
- foreach (NetworkConnectionToClient conn in observers.Values)
- {
- conn.RemoveFromObserving(this, true);
- }
- observers.Clear();
- }
+ observers.Remove(conn.connectionId);
}
/// Assign control of an object to a client via the client's NetworkConnection.
@@ -1197,6 +1206,20 @@ public bool AssignClientAuthority(NetworkConnectionToClient conn)
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.
@@ -1232,6 +1255,10 @@ public void RemoveClientAuthority()
// 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
@@ -1239,9 +1266,6 @@ public void RemoveClientAuthority()
// after OnDestroy is called.
internal void Reset()
{
- // make sure to call this before networkBehavioursCache is cleared below
- ResetSyncObjects();
-
hasSpawned = false;
clientStarted = false;
isClient = false;
@@ -1249,7 +1273,7 @@ internal void Reset()
//isLocalPlayer = false; <- cleared AFTER ClearLocalPlayer below!
// remove authority flag. This object may be unspawned, not destroyed, on client.
- hasAuthority = false;
+ isOwned = false;
NotifyAuthority();
netId = 0;
@@ -1273,45 +1297,64 @@ internal void Reset()
isLocalPlayer = false;
}
- // clear all component's dirty bits no matter what
- internal void ClearAllComponentsDirtyBits()
+ bool hadAuthority;
+ internal void NotifyAuthority()
+ {
+ if (!hadAuthority && isOwned)
+ OnStartAuthority();
+ if (hadAuthority && !isOwned)
+ OnStopAuthority();
+ hadAuthority = isOwned;
+ }
+
+ internal void OnStartAuthority()
{
foreach (NetworkBehaviour comp in NetworkBehaviours)
{
- comp.ClearAllDirtyBits();
+ // 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);
+ }
}
}
- // 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()
+ internal void OnStopAuthority()
{
foreach (NetworkBehaviour comp in NetworkBehaviours)
{
- if (comp.IsDirty())
+ // 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.ClearAllDirtyBits();
+ comp.OnStopAuthority();
+ }
+ catch (Exception e)
+ {
+ Debug.LogException(e, comp);
}
}
}
- void ResetSyncObjects()
+ // Called when NetworkIdentity is destroyed
+ internal void ClearObservers()
{
- // 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)
+ foreach (NetworkConnectionToClient conn in observers.Values)
{
- comp.ResetSyncObjects();
+ conn.RemoveFromObserving(this, true);
}
+ observers.Clear();
}
}
}
diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs.meta b/Assets/Mirror/Core/NetworkIdentity.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkIdentity.cs.meta
rename to Assets/Mirror/Core/NetworkIdentity.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs b/Assets/Mirror/Core/NetworkLoop.cs
similarity index 91%
rename from Assets/Mirror/Runtime/NetworkLoop.cs
rename to Assets/Mirror/Core/NetworkLoop.cs
index 50d9e95..d341b4b 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;
@@ -128,7 +119,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 +138,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 +157,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
diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs.meta b/Assets/Mirror/Core/NetworkLoop.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkLoop.cs.meta
rename to Assets/Mirror/Core/NetworkLoop.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs
similarity index 85%
rename from Assets/Mirror/Runtime/NetworkManager.cs
rename to Assets/Mirror/Core/NetworkManager.cs
index 37be9ae..f24d8ce 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;
@@ -29,13 +28,37 @@ public class NetworkManager : MonoBehaviour
public bool runInBackground = true;
/// Should the server auto-start when 'Server Build' is checked in build settings
+ [Header("Headless Builds")]
[Tooltip("Should the server auto-start when 'Server Build' is checked in build settings")]
[FormerlySerializedAs("startOnHeadless")]
public bool autoStartServerBuild = true;
+ [Tooltip("Automatically connect the client in headless builds. Useful for CCU tests with bot clients.\n\nAddress may be passed as command line argument.\n\nMake sure that only 'autostartServer' or 'autoconnectClient' is enabled, not both!")]
+ public bool autoConnectClientBuild;
+
/// 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. 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.")]
+ [FormerlySerializedAs("serverTickRate")]
+ public int sendRate = 30;
+
+ // Deprecated 2022-10-31
+ [Obsolete("NetworkManager.serverTickRate was renamed to sendRate because that's what it configures for both server & client now.")]
+ public int serverTickRate => sendRate;
+
+ // 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.
+ // Deprecated 2022-10-06
+ [Obsolete("NetworkManager.serverTickInterval was moved to NetworkServer.tickInterval for consistency.")]
+ public float serverTickInterval => NetworkServer.tickInterval;
+
+ // 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")]
@@ -53,8 +76,7 @@ public class NetworkManager : MonoBehaviour
// 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")]
@@ -96,6 +118,9 @@ public class NetworkManager : MonoBehaviour
public static List startPositions = new List();
public static int startPositionIndex;
+ [Header("Debug")]
+ public bool timeInterpolationGui = false;
+
/// The one and only NetworkManager
public static NetworkManager singleton { get; internal set; }
@@ -128,7 +153,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;
@@ -161,24 +186,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 +194,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.
@@ -210,26 +218,45 @@ public virtual void Start()
{
StartServer();
}
+ // only start server or client, never both
+ else if(autoConnectClientBuild)
+ {
+ StartClient();
+ }
#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;
+
+ // Deprecated 2022-12-12
+ [Obsolete("NetworkManager.IsSceneActive moved to Utils.IsSceneActive")]
+ public static bool IsSceneActive(string scene) => Utils.IsSceneActive(scene);
+
+ // 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;
}
// full server setup code, without spawning objects yet
@@ -252,18 +279,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 +315,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,17 +337,8 @@ 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();
if (runInBackground)
@@ -329,6 +350,22 @@ public void StartClient()
authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated);
}
+ // NetworkClient.sendRate = clientSendRate;
+ }
+
+ /// Starts the client, connects it to the server with networkAddress.
+ public void StartClient()
+ {
+ if (NetworkClient.active)
+ {
+ Debug.LogWarning("Client already started.");
+ return;
+ }
+
+ mode = NetworkManagerMode.ClientOnly;
+
+ SetupClient();
+
// In case this is a headless client...
ConfigureHeadlessFrameRate();
@@ -357,16 +394,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 +441,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 +497,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 +520,14 @@ 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);
- }
+ SetupClient();
networkAddress = "localhost";
- NetworkServer.ActivateHostScene();
RegisterClientMessages();
- // ConnectLocalServer needs to be called AFTER RegisterClientMessages
+ // call OnConencted needs to be called AFTER RegisterClientMessages
// (https://github.com/vis2k/Mirror/pull/1249/)
- NetworkClient.ConnectLocalServer();
+ HostMode.InvokeOnConnected();
OnStartClient();
}
@@ -570,45 +596,22 @@ public void StopClient()
if (mode == NetworkManagerMode.Offline)
return;
- if (authenticator != null)
- {
- authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated);
- authenticator.OnStopClient();
- }
-
- // Get Network Manager out of DDOL before going to offline scene
- // to avoid collision and let a fresh Network Manager be created.
- // IMPORTANT: .gameObject can be null if StopClient is called from
- // OnApplicationQuit or from tests!
- if (gameObject != null
- && gameObject.scene.name == "DontDestroyOnLoad"
- && !string.IsNullOrWhiteSpace(offlineScene)
- && SceneManager.GetActiveScene().path != offlineScene)
- SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene());
-
- OnStopClient();
-
- //Debug.Log("NetworkManager StopClient");
-
- // set offline mode BEFORE changing scene so that FinishStartScene
- // doesn't think we need initialize anything.
- // set offline mode BEFORE NetworkClient.Disconnect so StopClient
- // only runs once.
- mode = NetworkManagerMode.Offline;
-
- // shutdown client
+ // 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 = "";
+ // UNET invoked OnDisconnected cleanup immediately.
+ // let's keep it for now, in case any projects depend on it.
+ // TODO simply remove this in the future.
+ OnClientDisconnectInternal();
}
// called when quitting the application by closing the window / pressing
@@ -642,7 +645,7 @@ public virtual void OnApplicationQuit()
public virtual void ConfigureHeadlessFrameRate()
{
#if UNITY_SERVER
- Application.targetFrameRate = serverTickRate;
+ Application.targetFrameRate = sendRate;
// Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz.");
#endif
}
@@ -680,7 +683,7 @@ bool InitializeSingleton()
// set active transport AFTER setting singleton.
// so only if we didn't destroy ourselves.
- Transport.activeTransport = transport;
+ Transport.active = transport;
return true;
}
@@ -781,7 +784,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 +961,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 +994,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 +1020,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 +1057,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 +1105,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 +1131,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 +1170,86 @@ 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)
+ {
+ ClientChangeScene(offlineScene, SceneOperation.Normal);
+ }
+
+ networkSceneName = "";
}
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.
@@ -1269,8 +1306,16 @@ public virtual void OnServerAddPlayer(NetworkConnectionToClient conn)
NetworkServer.AddPlayerForConnection(conn, player);
}
- /// Called on server when transport raises an exception. NetworkConnection may be null.
+ // Deprecated 2022-05-12
+ [Obsolete("OnServerError(conn, Exception) was changed to OnServerError(conn, TransportError, string)")]
public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) {}
+ /// Called on server when transport raises an exception. NetworkConnection may be null.
+ public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason)
+ {
+#pragma warning disable CS0618
+ OnServerError(conn, new Exception(reason));
+#pragma warning restore CS0618
+ }
/// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed
public virtual void OnServerChangeScene(string newSceneName) {}
@@ -1296,33 +1341,23 @@ 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();
- }
-
- // Deprecated 2021-12-11
- [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")]
- public virtual void OnClientDisconnect(NetworkConnection conn) => OnClientDisconnect();
+ public virtual void OnClientDisconnect() {}
- /// Called on client when transport raises an exception.
+ // Deprecated 2022-05-12
+ [Obsolete("OnClientError(Exception) was changed to OnClientError(TransportError, string)")]
public virtual void OnClientError(Exception exception) {}
+ /// Called on client when transport raises an exception.
+ public virtual void OnClientError(TransportError error, string reason)
+ {
+#pragma warning disable CS0618
+ OnClientError(new Exception(reason));
+#pragma warning restore CS0618
+ }
/// Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes.
public virtual void OnClientNotReady() {}
- // Deprecated 2021-12-11
- [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")]
- public virtual void OnClientNotReady(NetworkConnection conn) {}
-
/// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed
// customHandling: indicates if scene loading will be handled through overrides
public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) {}
@@ -1334,20 +1369,16 @@ 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
@@ -1370,5 +1401,12 @@ public virtual void OnStopClient() {}
/// This is called when a host is stopped.
public virtual void OnStopHost() {}
+
+ // keep OnGUI even in builds. useful to debug snap interp.
+ void OnGUI()
+ {
+ if (!timeInterpolationGui) return;
+ NetworkClient.OnGUI();
+ }
}
}
diff --git a/Assets/Mirror/Runtime/NetworkManager.cs.meta b/Assets/Mirror/Core/NetworkManager.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkManager.cs.meta
rename to Assets/Mirror/Core/NetworkManager.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs b/Assets/Mirror/Core/NetworkManagerHUD.cs
similarity index 93%
rename from Assets/Mirror/Runtime/NetworkManagerHUD.cs
rename to Assets/Mirror/Core/NetworkManagerHUD.cs
index cba968d..0a267fb 100644
--- a/Assets/Mirror/Runtime/NetworkManagerHUD.cs
+++ b/Assets/Mirror/Core/NetworkManagerHUD.cs
@@ -23,7 +23,7 @@ void Awake()
void OnGUI()
{
- GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 215, 9999));
+ GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 250, 9999));
if (!NetworkClient.isConnected && !NetworkServer.active)
{
StartButtons();
@@ -104,17 +104,17 @@ void StatusLabels()
// Client: ...
if (NetworkServer.active && NetworkClient.active)
{
- GUILayout.Label($"Host: running via {Transport.activeTransport}");
+ GUILayout.Label($"Host: running via {Transport.active}");
}
// server only
else if (NetworkServer.active)
{
- GUILayout.Label($"Server: running via {Transport.activeTransport}");
+ GUILayout.Label($"Server: running via {Transport.active}");
}
// client only
else if (NetworkClient.isConnected)
{
- GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.activeTransport}");
+ GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.active}");
}
}
@@ -123,10 +123,16 @@ void StopButtons()
// stop host if host mode
if (NetworkServer.active && NetworkClient.isConnected)
{
+ GUILayout.BeginHorizontal();
if (GUILayout.Button("Stop Host"))
{
manager.StopHost();
}
+ if (GUILayout.Button("Stop Client"))
+ {
+ manager.StopClient();
+ }
+ GUILayout.EndHorizontal();
}
// stop client if client-only
else if (NetworkClient.isConnected)
diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta b/Assets/Mirror/Core/NetworkManagerHUD.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta
rename to Assets/Mirror/Core/NetworkManagerHUD.cs.meta
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/Runtime/NetworkMessage.cs.meta b/Assets/Mirror/Core/NetworkMessage.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkMessage.cs.meta
rename to Assets/Mirror/Core/NetworkMessage.cs.meta
diff --git a/Assets/Mirror/Core/NetworkMessages.cs b/Assets/Mirror/Core/NetworkMessages.cs
new file mode 100644
index 0000000..c53072e
--- /dev/null
+++ b/Assets/Mirror/Core/NetworkMessages.cs
@@ -0,0 +1,146 @@
+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 NetworkMessages
+ {
+ // size of message id header in bytes
+ public const int IdSize = 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.active.GetMaxPacketSize()
+ - IdSize
+ - Batcher.HeaderSize;
+ }
+
+ // 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.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ushort GetId() where T : struct, NetworkMessage =>
+ (ushort)(typeof(T).FullName.GetStableHashCode());
+
+ // 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(GetId());
+ writer.Write(message);
+ }
+
+ // read only the message id.
+ // common function in case we ever change the header size.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ 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.
+ [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/Core/NetworkMessages.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/MessagePacking.cs.meta
rename to Assets/Mirror/Core/NetworkMessages.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkReader.cs b/Assets/Mirror/Core/NetworkReader.cs
similarity index 64%
rename from Assets/Mirror/Runtime/NetworkReader.cs
rename to Assets/Mirror/Core/NetworkReader.cs
index 86eeef4..bec63ce 100644
--- a/Assets/Mirror/Runtime/NetworkReader.cs
+++ b/Assets/Mirror/Core/NetworkReader.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
+using System.Text;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
@@ -10,57 +11,70 @@ namespace Mirror
// 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
- ArraySegment buffer;
+ 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;
- /// 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 int Remaining => buffer.Count - Position;
- public NetworkReader(byte[] bytes)
- {
- buffer = new ArraySegment(bytes);
- }
+ /// 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);
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(byte[] bytes)
+ public void SetBuffer(ArraySegment segment)
{
- buffer = new ArraySegment(bytes);
+ buffer = segment;
Position = 0;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void SetBuffer(ArraySegment segment)
+#if !UNITY_2021_3_OR_NEWER
+ // Unity 2019 doesn't have the implicit byte[] to segment conversion yet
+ public void SetBuffer(byte[] bytes)
{
- buffer = segment;
+ buffer = new ArraySegment(bytes, 0, bytes.Length);
Position = 0;
}
+#endif
// ReadBlittable from DOTSNET
// this is extremely fast, but only works for blittable types.
@@ -71,6 +85,26 @@ public void SetBuffer(ArraySegment segment)
// 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.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal unsafe T ReadBlittable()
where T : unmanaged
@@ -91,10 +125,10 @@ internal unsafe T ReadBlittable()
// 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)
+ // ensure remaining
+ if (Remaining < size)
{
- throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> out of range: {ToString()}");
+ throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> not enough data in buffer to read {size} bytes: {ToString()}");
}
// read blittable
@@ -132,23 +166,24 @@ internal unsafe T ReadBlittable()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal T? ReadBlittableNullable()
where T : unmanaged =>
- ReadByte() != 0 ? ReadBlittable() : default(T?);
+ 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)
{
+ // 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}");
}
- // check if within buffer limits
- if (Position + count > buffer.Count)
+ // 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()}");
}
@@ -159,11 +194,13 @@ public byte[] ReadBytes(byte[] bytes, int count)
}
/// 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)
+ // 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()}");
}
@@ -174,10 +211,6 @@ public ArraySegment ReadBytesSegment(int 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()
@@ -185,11 +218,15 @@ 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");
+ 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.
diff --git a/Assets/Mirror/Runtime/NetworkReader.cs.meta b/Assets/Mirror/Core/NetworkReader.cs.meta
similarity index 100%
rename from Assets/Mirror/Runtime/NetworkReader.cs.meta
rename to Assets/Mirror/Core/NetworkReader.cs.meta
diff --git a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs b/Assets/Mirror/Core/NetworkReaderExtensions.cs
similarity index 71%
rename from Assets/Mirror/Runtime/NetworkReaderExtensions.cs
rename to Assets/Mirror/Core/NetworkReaderExtensions.cs
index 6137866..8c340f0 100644
--- a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs
+++ b/Assets/Mirror/Core/NetworkReaderExtensions.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Runtime.CompilerServices;
-using System.Text;
using UnityEngine;
namespace Mirror
@@ -11,85 +9,52 @@ namespace Mirror
// 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
@@ -110,11 +75,12 @@ public static string ReadString(this NetworkReader reader)
ArraySegment data = reader.ReadBytesSegment(realSize);
// convert directly from buffer to string via encoding
- return encoding.GetString(data.Array, data.Offset, data.Count);
+ // throws in case of invalid utf8.
+ // see test: ReadString_InvalidUTF8()
+ return reader.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
@@ -124,7 +90,6 @@ public static byte[] ReadBytesAndSize(this NetworkReader reader)
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];
@@ -133,7 +98,6 @@ public static byte[] ReadBytes(this NetworkReader reader, int count)
}
/// if count is invalid
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ArraySegment ReadBytesAndSizeSegment(this NetworkReader reader)
{
// count = 0 means the array was null
@@ -143,72 +107,61 @@ public static ArraySegment ReadBytesAndSizeSegment(this NetworkReader read
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 ReadMatrix4x4(this NetworkReader reader) => reader.ReadBlittable();
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 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?);
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader)
{
uint netId = reader.ReadUInt();
@@ -222,7 +175,6 @@ public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader)
return Utils.GetSpawnedInServerOrClient(netId);
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader)
{
// read netId first.
@@ -247,18 +199,16 @@ public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader)
NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId);
return identity != null
- ? identity.NetworkBehaviours[componentIndex]
- : 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)
+ public static NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader)
{
uint netId = reader.ReadUInt();
byte componentIndex = default;
@@ -269,10 +219,9 @@ public static NetworkBehaviour.NetworkBehaviourSyncVar ReadNetworkBehaviourSyncV
componentIndex = reader.ReadByte();
}
- return new NetworkBehaviour.NetworkBehaviourSyncVar(netId, componentIndex);
+ return new 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
@@ -280,7 +229,6 @@ public static Transform ReadTransform(this NetworkReader reader)
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
@@ -288,7 +236,10 @@ public static GameObject ReadGameObject(this NetworkReader reader)
return networkIdentity != null ? networkIdentity.gameObject : null;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ // 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)
{
int length = reader.ReadInt();
@@ -302,7 +253,26 @@ public static List ReadList(this NetworkReader reader)
return result;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ // 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.
+ // TODO writer not found. need to adjust weaver first. see tests.
+ /*
+ public static HashSet ReadHashSet(this NetworkReader reader)
+ {
+ int length = reader.ReadInt();
+ if (length < 0)
+ return null;
+ HashSet result = new HashSet();
+ for (int i = 0; i < length; i++)
+ {
+ result.Add(reader.Read