diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e3c76..e043ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). -## [1.0.0-pre.7] - 2022-04-01 +## [1.0.0-pre.8] - 2022-04-27 -### Added +### Changed + +- `unmanaged` structs are no longer universally accepted as RPC parameters because some structs (i.e., structs with pointers in them, such as `NativeList`) can't be supported by the default memcpy struct serializer. Structs that are intended to be serialized across the network must add `INetworkSerializeByMemcpy` to the interface list (i.e., `struct Foo : INetworkSerializeByMemcpy`). This interface is empty and just serves to mark the struct as compatible with memcpy serialization. For external structs you can't edit, you can pass them to RPCs by wrapping them in `ForceNetworkSerializeByMemcpy`. (#1901) + +### Removed +- Removed `SIPTransport` (#1870) + +- Removed `ClientNetworkTransform` from the package samples and moved to Boss Room's Utilities package which can be found [here](https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop/blob/main/Packages/com.unity.multiplayer.samples.coop/Utilities/Net/ClientAuthority/ClientNetworkTransform.cs). + +### Fixed + +- Fixed `NetworkTransform` generating false positive rotation delta checks when rolling over between 0 and 360 degrees. (#1890) +- Fixed client throwing an exception if it has messages in the outbound queue when processing the `NetworkEvent.Disconnect` event and is using UTP. (#1884) +- Fixed issue during client synchronization if 'ValidateSceneBeforeLoading' returned false it would halt the client synchronization process resulting in a client that was approved but not synchronized or fully connected with the server. (#1883) +- Fixed an issue where UNetTransport.StartServer would return success even if the underlying transport failed to start (#854) +- Passing generic types to RPCs no longer causes a native crash (#1901) +- Fixed an issue where calling `Shutdown` on a `NetworkManager` that was already shut down would cause an immediate shutdown the next time it was started (basically the fix makes `Shutdown` idempotent). (#1877) +## [1.0.0-pre.7] - 2022-04-06 + +### Added - Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828) - Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823) - Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762) - `UnityTransport` settings can now be set programmatically. (#1845) - `FastBufferWriter` and Reader IsInitialized property. (#1859) +- Prefabs can now be added to the network at **runtime** (i.e., from an addressable asset). If `ForceSamePrefabs` is false, this can happen after a connection has been formed. (#1882) +- When `ForceSamePrefabs` is false, a configurable delay (default 1 second, configurable via `NetworkConfig.SpawnTimeout`) has been introduced to gracefully handle race conditions where a spawn call has been received for an object whose prefab is still being loaded. (#1882) ### Changed +- Changed `NetcodeIntegrationTestHelpers` to use `UnityTransport` (#1870) - Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849) ### Removed @@ -27,6 +49,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Removed `com.unity.collections` dependency from the package (#1849) ### Fixed + - Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850) - Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847) - Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841) @@ -49,8 +72,6 @@ Additional documentation and release notes are available at [Multiplayer Documen - NetworkAnimator now properly synchrhonizes all animation layers as well as runtime-adjusted weighting between them (#1765) - Added first set of tests for NetworkAnimator - parameter syncing, trigger set / reset, override network animator (#1735) -### Changed - ### Fixed - Fixed an issue where sometimes the first client to connect to the server could see messages from the server as coming from itself. (#1683) - Fixed an issue where clients seemed to be able to send messages to ClientId 1, but these messages would actually still go to the server (id 0) instead of that client. (#1683) diff --git a/Components/Interpolator/BufferedLinearInterpolator.cs b/Components/Interpolator/BufferedLinearInterpolator.cs index efa5de4..634baff 100644 --- a/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/Components/Interpolator/BufferedLinearInterpolator.cs @@ -4,13 +4,11 @@ namespace Unity.Netcode { - /// /// Solves for incoming values that are jittered /// Partially solves for message loss. Unclamped lerping helps hide this, but not completely /// - /// - internal abstract class BufferedLinearInterpolator where T : struct + public abstract class BufferedLinearInterpolator where T : struct { private struct BufferedItem { @@ -24,6 +22,10 @@ public BufferedItem(T item, double timeSent) } } + /// + /// There’s two factors affecting interpolation: buffering (set in NetworkManager’s NetworkTimeSystem) and interpolation time, which is the amount of time it’ll take to reach the target. This is to affect the second one. + /// + public float MaximumInterpolationTime = 0.1f; private const double k_SmallValue = 9.999999439624929E-11; // copied from Vector3's equal operator @@ -69,6 +71,9 @@ public BufferedItem(T item, double timeSent) private bool InvalidState => m_Buffer.Count == 0 && m_LifetimeConsumedCount == 0; + /// + /// Resets Interpolator to initial state + /// public void Clear() { m_Buffer.Clear(); @@ -76,6 +81,9 @@ public void Clear() m_StartTimeConsumed = 0.0d; } + /// + /// Teleports current interpolation value to targetValue. + /// public void ResetTo(T targetValue, double serverTime) { m_LifetimeConsumedCount = 1; @@ -89,7 +97,6 @@ public void ResetTo(T targetValue, double serverTime) Update(0, serverTime, serverTime); } - // todo if I have value 1, 2, 3 and I'm treating 1 to 3, I shouldn't interpolate between 1 and 3, I should interpolate from 1 to 2, then from 2 to 3 to get the best path private void TryConsumeFromBuffer(double renderTime, double serverTime) { @@ -205,14 +212,16 @@ public T Update(float deltaTime, double renderTime, double serverTime) } var target = InterpolateUnclamped(m_InterpStartValue, m_InterpEndValue, t); - float maxInterpTime = 0.1f; - m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / maxInterpTime); // second interpolate to smooth out extrapolation jumps + m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / MaximumInterpolationTime); // second interpolate to smooth out extrapolation jumps } m_NbItemsReceivedThisFrame = 0; return m_CurrentInterpValue; } + /// + /// Add measurements to be used during interpolation. These will be buffered before being made available to be displayed as "latest value". + /// public void AddMeasurement(T newMeasurement, double sentTime) { m_NbItemsReceivedThisFrame++; @@ -239,17 +248,25 @@ public void AddMeasurement(T newMeasurement, double sentTime) } } + /// + /// Gets latest value from the interpolator. This is updated every update as time goes by. + /// public T GetInterpolatedValue() { return m_CurrentInterpValue; } + /// + /// Method to override and adapted to the generic type. This assumes interpolation for that value will be clamped. + /// protected abstract T Interpolate(T start, T end, float time); + /// + /// Method to override and adapted to the generic type. This assumes interpolation for that value will not be clamped. + /// protected abstract T InterpolateUnclamped(T start, T end, float time); } - - internal class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator + public class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator { protected override float InterpolateUnclamped(float start, float end, float time) { @@ -262,7 +279,7 @@ protected override float Interpolate(float start, float end, float time) } } - internal class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator + public class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator { protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time) { diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs index 9635cf9..e11ac77 100644 --- a/Components/NetworkAnimator.cs +++ b/Components/NetworkAnimator.cs @@ -15,11 +15,11 @@ public class NetworkAnimator : NetworkBehaviour internal struct AnimationMessage : INetworkSerializable { // state hash per layer. if non-zero, then Play() this animation, skipping transitions - public int StateHash; - public float NormalizedTime; - public int Layer; - public float Weight; - public byte[] Parameters; + internal int StateHash; + internal float NormalizedTime; + internal int Layer; + internal float Weight; + internal byte[] Parameters; public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { @@ -33,8 +33,8 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade internal struct AnimationTriggerMessage : INetworkSerializable { - public int Hash; - public bool Reset; + internal int Hash; + internal bool Reset; public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { @@ -57,7 +57,7 @@ public Animator Animator private bool m_SendMessagesAllowed = false; // Animators only support up to 32 params - public static int K_MaxAnimationParams = 32; + private const int k_MaxAnimationParams = 32; private int[] m_TransitionHash; private int[] m_AnimationHash; @@ -65,21 +65,21 @@ public Animator Animator private unsafe struct AnimatorParamCache { - public int Hash; - public int Type; - public fixed byte Value[4]; // this is a max size of 4 bytes + internal int Hash; + internal int Type; + internal fixed byte Value[4]; // this is a max size of 4 bytes } // 128 bytes per Animator - private FastBufferWriter m_ParameterWriter = new FastBufferWriter(K_MaxAnimationParams * sizeof(float), Allocator.Persistent); + private FastBufferWriter m_ParameterWriter = new FastBufferWriter(k_MaxAnimationParams * sizeof(float), Allocator.Persistent); private NativeArray m_CachedAnimatorParameters; // We cache these values because UnsafeUtility.EnumToInt uses direct IL that allows a non-boxing conversion private struct AnimationParamEnumWrapper { - public static readonly int AnimatorControllerParameterInt; - public static readonly int AnimatorControllerParameterFloat; - public static readonly int AnimatorControllerParameterBool; + internal static readonly int AnimatorControllerParameterInt; + internal static readonly int AnimatorControllerParameterFloat; + internal static readonly int AnimatorControllerParameterBool; static AnimationParamEnumWrapper() { diff --git a/Components/NetworkRigidbody.cs b/Components/NetworkRigidbody.cs index 5093fb0..6515f7e 100644 --- a/Components/NetworkRigidbody.cs +++ b/Components/NetworkRigidbody.cs @@ -5,77 +5,97 @@ namespace Unity.Netcode.Components { /// /// NetworkRigidbody allows for the use of on network objects. By controlling the kinematic - /// mode of the rigidbody and disabling it on all peers but the authoritative one. + /// mode of the and disabling it on all peers but the authoritative one. /// [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(NetworkTransform))] public class NetworkRigidbody : NetworkBehaviour { + /// + /// Determines if we are server (true) or owner (false) authoritative + /// + private bool m_IsServerAuthoritative; + private Rigidbody m_Rigidbody; private NetworkTransform m_NetworkTransform; - - private bool m_OriginalKinematic; private RigidbodyInterpolation m_OriginalInterpolation; - // Used to cache the authority state of this rigidbody during the last frame + // Used to cache the authority state of this Rigidbody during the last frame private bool m_IsAuthority; - /// - /// Gets a bool value indicating whether this on this peer currently holds authority. - /// - private bool HasAuthority => m_NetworkTransform.CanCommitToTransform; - private void Awake() { - m_Rigidbody = GetComponent(); m_NetworkTransform = GetComponent(); + m_IsServerAuthoritative = m_NetworkTransform.IsServerAuthoritative(); + + m_Rigidbody = GetComponent(); + m_OriginalInterpolation = m_Rigidbody.interpolation; + + // Set interpolation to none if NetworkTransform is handling interpolation, otherwise it sets it to the original value + m_Rigidbody.interpolation = m_NetworkTransform.Interpolate ? RigidbodyInterpolation.None : m_OriginalInterpolation; + + // Turn off physics for the rigid body until spawned, otherwise + // clients can run fixed update before the first full + // NetworkTransform update + m_Rigidbody.isKinematic = true; + } + + /// + /// For owner authoritative (i.e. ClientNetworkTransform) + /// we adjust our authority when we gain ownership + /// + public override void OnGainedOwnership() + { + UpdateOwnershipAuthority(); } - private void FixedUpdate() + /// + /// For owner authoritative(i.e. ClientNetworkTransform) + /// we adjust our authority when we have lost ownership + /// + public override void OnLostOwnership() { - if (NetworkManager.IsListening) - { - if (HasAuthority != m_IsAuthority) - { - m_IsAuthority = HasAuthority; - UpdateRigidbodyKinematicMode(); - } - } + UpdateOwnershipAuthority(); } - // Puts the rigidbody in a kinematic non-interpolated mode on everyone but the server. - private void UpdateRigidbodyKinematicMode() + /// + /// Sets the authority differently depending upon + /// whether it is server or owner authoritative + /// + private void UpdateOwnershipAuthority() { - if (m_IsAuthority == false) + if (m_IsServerAuthoritative) { - m_OriginalKinematic = m_Rigidbody.isKinematic; - m_Rigidbody.isKinematic = true; - - m_OriginalInterpolation = m_Rigidbody.interpolation; - // Set interpolation to none, the NetworkTransform component interpolates the position of the object. - m_Rigidbody.interpolation = RigidbodyInterpolation.None; + m_IsAuthority = NetworkManager.IsServer; } else { - // Resets the rigidbody back to it's non replication only state. Happens on shutdown and when authority is lost - m_Rigidbody.isKinematic = m_OriginalKinematic; - m_Rigidbody.interpolation = m_OriginalInterpolation; + m_IsAuthority = IsOwner; } + + // If you have authority then you are not kinematic + m_Rigidbody.isKinematic = !m_IsAuthority; + + // Set interpolation of the Rigidbody based on authority + // With authority: let local transform handle interpolation + // Without authority: let the NetworkTransform handle interpolation + m_Rigidbody.interpolation = m_IsAuthority ? m_OriginalInterpolation : RigidbodyInterpolation.None; } /// public override void OnNetworkSpawn() { - m_IsAuthority = HasAuthority; - m_OriginalKinematic = m_Rigidbody.isKinematic; - m_OriginalInterpolation = m_Rigidbody.interpolation; - UpdateRigidbodyKinematicMode(); + UpdateOwnershipAuthority(); } /// public override void OnNetworkDespawn() { - UpdateRigidbodyKinematicMode(); + m_Rigidbody.interpolation = m_OriginalInterpolation; + // Turn off physics for the rigid body until spawned, otherwise + // non-owners can run fixed updates before the first full + // NetworkTransform update and physics will be applied (i.e. gravity, etc) + m_Rigidbody.isKinematic = true; } } } diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs index dac7690..6425fb1 100644 --- a/Components/NetworkTransform.cs +++ b/Components/NetworkTransform.cs @@ -15,9 +15,10 @@ namespace Unity.Netcode.Components [DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts public class NetworkTransform : NetworkBehaviour { - public const float PositionThresholdDefault = .001f; - public const float RotAngleThresholdDefault = .01f; - public const float ScaleThresholdDefault = .01f; + public const float PositionThresholdDefault = 0.001f; + public const float RotAngleThresholdDefault = 0.01f; + public const float ScaleThresholdDefault = 0.01f; + public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale); public OnClientRequestChangeDelegate OnClientRequestChange; @@ -38,7 +39,7 @@ internal struct NetworkTransformState : INetworkSerializable // 11-15: private ushort m_Bitset; - public bool InLocalSpace + internal bool InLocalSpace { get => (m_Bitset & (1 << k_InLocalSpaceBit)) != 0; set @@ -49,7 +50,7 @@ public bool InLocalSpace } // Position - public bool HasPositionX + internal bool HasPositionX { get => (m_Bitset & (1 << k_PositionXBit)) != 0; set @@ -59,7 +60,7 @@ public bool HasPositionX } } - public bool HasPositionY + internal bool HasPositionY { get => (m_Bitset & (1 << k_PositionYBit)) != 0; set @@ -69,7 +70,7 @@ public bool HasPositionY } } - public bool HasPositionZ + internal bool HasPositionZ { get => (m_Bitset & (1 << k_PositionZBit)) != 0; set @@ -80,7 +81,7 @@ public bool HasPositionZ } // RotAngles - public bool HasRotAngleX + internal bool HasRotAngleX { get => (m_Bitset & (1 << k_RotAngleXBit)) != 0; set @@ -90,7 +91,7 @@ public bool HasRotAngleX } } - public bool HasRotAngleY + internal bool HasRotAngleY { get => (m_Bitset & (1 << k_RotAngleYBit)) != 0; set @@ -100,7 +101,7 @@ public bool HasRotAngleY } } - public bool HasRotAngleZ + internal bool HasRotAngleZ { get => (m_Bitset & (1 << k_RotAngleZBit)) != 0; set @@ -111,7 +112,7 @@ public bool HasRotAngleZ } // Scale - public bool HasScaleX + internal bool HasScaleX { get => (m_Bitset & (1 << k_ScaleXBit)) != 0; set @@ -121,7 +122,7 @@ public bool HasScaleX } } - public bool HasScaleY + internal bool HasScaleY { get => (m_Bitset & (1 << k_ScaleYBit)) != 0; set @@ -131,7 +132,7 @@ public bool HasScaleY } } - public bool HasScaleZ + internal bool HasScaleZ { get => (m_Bitset & (1 << k_ScaleZBit)) != 0; set @@ -141,7 +142,7 @@ public bool HasScaleZ } } - public bool IsTeleportingNextFrame + internal bool IsTeleportingNextFrame { get => (m_Bitset & (1 << k_TeleportingBit)) != 0; set @@ -151,12 +152,12 @@ public bool IsTeleportingNextFrame } } - public float PositionX, PositionY, PositionZ; - public float RotAngleX, RotAngleY, RotAngleZ; - public float ScaleX, ScaleY, ScaleZ; - public double SentTime; + internal float PositionX, PositionY, PositionZ; + internal float RotAngleX, RotAngleY, RotAngleZ; + internal float ScaleX, ScaleY, ScaleZ; + internal double SentTime; - public Vector3 Position + internal Vector3 Position { get { return new Vector3(PositionX, PositionY, PositionZ); } set @@ -167,7 +168,7 @@ public Vector3 Position } } - public Vector3 Rotation + internal Vector3 Rotation { get { return new Vector3(RotAngleX, RotAngleY, RotAngleZ); } set @@ -178,7 +179,7 @@ public Vector3 Rotation } } - public Vector3 Scale + internal Vector3 Scale { get { return new Vector3(ScaleX, ScaleY, ScaleZ); } set @@ -249,7 +250,10 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade public bool SyncScaleX = true, SyncScaleY = true, SyncScaleZ = true; public float PositionThreshold = PositionThresholdDefault; + + [Range(0.001f, 360.0f)] public float RotAngleThreshold = RotAngleThresholdDefault; + public float ScaleThreshold = ScaleThresholdDefault; /// @@ -280,8 +284,6 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade private NetworkTransformState m_LocalAuthoritativeNetworkState; - private NetworkTransformState m_PrevNetworkState; - private const int k_DebugDrawLineTime = 10; private bool m_HasSentLastValue = false; // used to send one last value, so clients can make the difference between lost replication data (clients extrapolate) and no more data to send. @@ -390,6 +392,16 @@ private void ResetInterpolatedStateToCurrentAuthoritativeState() m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime); } + /// + /// Will apply the transform to the LocalAuthoritativeNetworkState and get detailed isDirty information returned. + /// + /// transform to apply + /// bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty + internal (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyLocalNetworkState(Transform transform) + { + return ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, m_CachedNetworkManager.LocalTime.Time, transform); + } + // updates `NetworkState` properties if they need to and returns a `bool` indicating whether or not there was any changes made // returned boolean would be useful to change encapsulating `NetworkVariable`'s dirty state, e.g. ReplNetworkState.SetDirty(isDirty); internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) @@ -450,7 +462,7 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat } if (SyncRotAngleX && - Mathf.Abs(networkState.RotAngleX - rotAngles.x) > RotAngleThreshold) + Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) > RotAngleThreshold) { networkState.RotAngleX = rotAngles.x; networkState.HasRotAngleX = true; @@ -458,7 +470,7 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat } if (SyncRotAngleY && - Mathf.Abs(networkState.RotAngleY - rotAngles.y) > RotAngleThreshold) + Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) > RotAngleThreshold) { networkState.RotAngleY = rotAngles.y; networkState.HasRotAngleY = true; @@ -466,7 +478,7 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat } if (SyncRotAngleZ && - Mathf.Abs(networkState.RotAngleZ - rotAngles.z) > RotAngleThreshold) + Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) > RotAngleThreshold) { networkState.RotAngleZ = rotAngles.z; networkState.HasRotAngleZ = true; @@ -509,8 +521,6 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState networkState, Transform transformToUpdate) { - m_PrevNetworkState = networkState; - var interpolatedPosition = InLocalSpace ? transformToUpdate.localPosition : transformToUpdate.position; // todo: we should store network state w/ quats vs. euler angles @@ -587,8 +597,6 @@ private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState netw { transformToUpdate.position = interpolatedPosition; } - - m_PrevNetworkState.Position = interpolatedPosition; } // RotAngles Apply @@ -602,15 +610,12 @@ private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState netw { transformToUpdate.rotation = Quaternion.Euler(interpolatedRotAngles); } - - m_PrevNetworkState.Rotation = interpolatedRotAngles; } // Scale Apply if (SyncScaleX || SyncScaleY || SyncScaleZ) { transformToUpdate.localScale = interpolatedScale; - m_PrevNetworkState.Scale = interpolatedScale; } } @@ -790,8 +795,6 @@ private void Initialize() } } - #region state set - /// /// Directly sets a state on the authoritative transform. /// This will override any changes made previously to the transform @@ -851,7 +854,6 @@ private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool m_Transform.localScale = scale; m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; } - #endregion // todo this is currently in update, to be able to catch any transform changes. A FixedUpdate mode could be added to be less intense, but it'd be // conditional to users only making transform update changes in FixedUpdate. @@ -879,8 +881,6 @@ protected virtual void Update() { TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time); } - - m_PrevNetworkState = m_LocalAuthoritativeNetworkState; } // apply interpolated value @@ -904,36 +904,10 @@ protected virtual void Update() if (!CanCommitToTransform) { -#if NGO_TRANSFORM_DEBUG - if (m_CachedNetworkManager.LogLevel == LogLevel.Developer) - { - // TODO: This should be a component gizmo - not some debug draw based on log level - var interpolatedPosition = new Vector3(m_PositionXInterpolator.GetInterpolatedValue(), m_PositionYInterpolator.GetInterpolatedValue(), m_PositionZInterpolator.GetInterpolatedValue()); - Debug.DrawLine(interpolatedPosition, interpolatedPosition + Vector3.up, Color.magenta, k_DebugDrawLineTime, false); - - // try to update previously consumed NetworkState - // if we have any changes, that means made some updates locally - // we apply the latest ReplNetworkState again to revert our changes - var oldStateDirtyInfo = ApplyTransformToNetworkStateWithInfo(ref m_PrevNetworkState, 0, m_Transform); - - // there are several bugs in this code, as we the message is dumped out under odd circumstances - // For Matt, it would trigger when an object's rotation was perturbed by colliding with another - // object vs. explicitly rotating it - if (oldStateDirtyInfo.isPositionDirty || oldStateDirtyInfo.isScaleDirty || (oldStateDirtyInfo.isRotationDirty && SyncRotAngleX && SyncRotAngleY && SyncRotAngleZ)) - { - // ignoring rotation dirty since quaternions will mess with euler angles, making this impossible to determine if the change to a single axis comes - // from an unauthorized transform change or euler to quaternion conversion artifacts. - var dirtyField = oldStateDirtyInfo.isPositionDirty ? "position" : oldStateDirtyInfo.isRotationDirty ? "rotation" : "scale"; - Debug.LogWarning($"A local change to {dirtyField} without authority detected, reverting back to latest interpolated network state!", this); - } - } -#endif - // Apply updated interpolated value ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); } } - m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; } @@ -960,5 +934,22 @@ public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newSca TryCommitValuesToServer(newPosition, newRotationEuler, newScale, m_CachedNetworkManager.LocalTime.Time); m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; } + + /// + /// Override this and return false to follow the owner authoritative + /// Otherwise, it defaults to server authoritative + /// + protected virtual bool OnIsServerAuthoritatitive() + { + return true; + } + + /// + /// Used by to determines if this is server or owner authoritative. + /// + internal bool IsServerAuthoritative() + { + return OnIsServerAuthoritatitive(); + } } } diff --git a/Documentation~/Index.md b/Documentation~/Index.md deleted file mode 100644 index 616406c..0000000 --- a/Documentation~/Index.md +++ /dev/null @@ -1,31 +0,0 @@ -# About Netcode for GameObjects - -Unity Netcode for GameObjects is a high-level networking library built to abstract networking. This allows developers to focus on the game rather than low level protocols and networking frameworks. - -## Guides - -See guides below to install Unity Netcode for GameObjects, set up your project, and get started with your first networked game: - -* [Documentation](https://docs-multiplayer.unity3d.com/docs/getting-started/about-mlapi) -* [Installation](https://docs-multiplayer.unity3d.com/docs/migration/install) -* [First Steps](https://docs-multiplayer.unity3d.com/docs/tutorials/helloworld/helloworldintro) -* [API Reference](https://docs-multiplayer.unity3d.com/docs/mlapi-api/introduction) - -# Technical details - -## Requirements - -This version of Netcode for GameObjects is compatible with the following Unity versions and platforms: - -* 2020.3 and later -* Windows, Mac, Linux platforms - -## Document revision history - -|Date|Reason| -|---|---| -|March 10, 2021|Document created. Matches package version 0.1.0| -|June 1, 2021|Update and add links for additional content. Matches patch version 0.1.0 and hotfixes.| -|June 3, 2021|Update document to acknowledge Unity min version change. Matches package version 0.2.0| -|August 5, 2021|Update product/package name| -|September 9,2021|Updated the links and name of the file.| \ No newline at end of file diff --git a/Documentation~/index.md b/Documentation~/index.md new file mode 100644 index 0000000..e03233b --- /dev/null +++ b/Documentation~/index.md @@ -0,0 +1,35 @@ +# About Netcode for GameObjects + +Netcode for GameObjects is a Unity package that provides networking capabilities to GameObject & MonoBehaviour workflows. + +## Guides + +See guides below to install Unity Netcode for GameObjects, set up your project, and get started with your first networked game: + +- [Documentation](https://docs-multiplayer.unity3d.com/netcode/current/about) +- [Installation](https://docs-multiplayer.unity3d.com/netcode/current/migration/install) +- [First Steps](https://docs-multiplayer.unity3d.com/netcode/current/tutorials/helloworld/helloworldintro) +- [API Reference](https://docs-multiplayer.unity3d.com/netcode/current/api/introduction) + +# Technical details + +## Requirements + +Netcode for GameObjects targets the following Unity versions: +- Unity 2020.3, 2021.1, 2021.2 and 2021.3 + +On the following runtime platforms: +- Windows, MacOS, and Linux +- iOS and Android +- Most closed platforms, such as consoles. Contact us for more information about specific closed platforms. + +## Document revision history + +|Date|Reason| +|---|---| +|March 10, 2021|Document created. Matches package version 0.1.0| +|June 1, 2021|Update and add links for additional content. Matches patch version 0.1.0 and hotfixes.| +|June 3, 2021|Update document to acknowledge Unity min version change. Matches package version 0.2.0| +|August 5, 2021|Update product/package name| +|September 9,2021|Updated the links and name of the file.| +|April 20, 2022|Updated links| \ No newline at end of file diff --git a/Editor/CodeGen/CodeGenHelpers.cs b/Editor/CodeGen/CodeGenHelpers.cs index af66327..de27055 100644 --- a/Editor/CodeGen/CodeGenHelpers.cs +++ b/Editor/CodeGen/CodeGenHelpers.cs @@ -27,6 +27,7 @@ internal static class CodeGenHelpers public static readonly string ServerRpcSendParams_FullName = typeof(ServerRpcSendParams).FullName; public static readonly string ServerRpcReceiveParams_FullName = typeof(ServerRpcReceiveParams).FullName; public static readonly string INetworkSerializable_FullName = typeof(INetworkSerializable).FullName; + public static readonly string INetworkSerializeByMemcpy_FullName = typeof(INetworkSerializeByMemcpy).FullName; public static readonly string UnityColor_FullName = typeof(Color).FullName; public static readonly string UnityColor32_FullName = typeof(Color32).FullName; public static readonly string UnityVector2_FullName = typeof(Vector2).FullName; @@ -77,6 +78,35 @@ public static bool IsSubclassOf(this TypeDefinition typeDefinition, string class return false; } + public static string FullNameWithGenericParameters(this TypeReference typeReference, GenericParameter[] contextGenericParameters, TypeReference[] contextGenericParameterTypes) + { + var name = typeReference.FullName; + if (typeReference.HasGenericParameters) + { + name += "<"; + for (var i = 0; i < typeReference.Resolve().GenericParameters.Count; ++i) + { + if (i != 0) + { + name += ", "; + } + + for (var j = 0; j < contextGenericParameters.Length; ++j) + { + if (typeReference.GenericParameters[i].FullName == contextGenericParameters[i].FullName) + { + name += contextGenericParameterTypes[i].FullName; + break; + } + } + } + + name += ">"; + } + + return name; + } + public static bool HasInterface(this TypeReference typeReference, string interfaceTypeFullName) { if (typeReference.IsArray) diff --git a/Editor/CodeGen/INetworkSerializableILPP.cs b/Editor/CodeGen/INetworkSerializableILPP.cs index 49af82b..27c6bfb 100644 --- a/Editor/CodeGen/INetworkSerializableILPP.cs +++ b/Editor/CodeGen/INetworkSerializableILPP.cs @@ -24,6 +24,30 @@ public override bool WillProcess(ICompiledAssembly compiledAssembly) => private readonly List m_Diagnostics = new List(); + private TypeReference ResolveGenericType(TypeReference type, List typeStack) + { + var genericName = type.Name; + var lastType = (GenericInstanceType)typeStack[typeStack.Count - 1]; + var resolvedType = lastType.Resolve(); + typeStack.RemoveAt(typeStack.Count - 1); + for (var i = 0; i < resolvedType.GenericParameters.Count; ++i) + { + var parameter = resolvedType.GenericParameters[i]; + if (parameter.Name == genericName) + { + var underlyingType = lastType.GenericArguments[i]; + if (underlyingType.Resolve() == null) + { + return ResolveGenericType(underlyingType, typeStack); + } + + return underlyingType; + } + } + + return null; + } + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) { if (!WillProcess(compiledAssembly)) @@ -31,7 +55,6 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) return null; } - m_Diagnostics.Clear(); // read @@ -50,16 +73,128 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) { if (ImportReferences(mainModule)) { - var types = mainModule.GetTypes() - .Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializable_FullName) && !t.Resolve().IsAbstract && t.Resolve().IsValueType) + // Initialize all the delegates for various NetworkVariable types to ensure they can be serailized + + // Find all types we know we're going to want to serialize. + // The list of these types includes: + // - Non-generic INetworkSerializable types + // - Non-Generic INetworkSerializeByMemcpy types + // - Enums that are not declared within generic types + // We can't process generic types because, to initialize a generic, we need a value + // for `T` to initialize it with. + var networkSerializableTypes = mainModule.GetTypes() + .Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializable_FullName) && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType) + .ToList(); + var structTypes = mainModule.GetTypes() + .Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName) && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType) + .ToList(); + var enumTypes = mainModule.GetTypes() + .Where(t => t.Resolve().IsEnum && !t.Resolve().IsAbstract && !t.Resolve().HasGenericParameters && t.Resolve().IsValueType) .ToList(); - // process `INetworkMessage` types - if (types.Count == 0) + + // Now, to support generics, we have to do an extra pass. + // We look for any type that's a NetworkBehaviour type + // Then we look through all the fields in that type, finding any field whose type is + // descended from `NetworkVariableSerialization`. Then we check `NetworkVariableSerialization`'s + // `T` value, and if it's a generic, then we know it was missed in the above sweep and needs + // to be initialized. Now we have a full generic instance rather than a generic definition, + // so we can validly generate an initializer for that particular instance of the generic type. + var networkSerializableTypesSet = new HashSet(networkSerializableTypes); + var structTypesSet = new HashSet(structTypes); + var enumTypesSet = new HashSet(enumTypes); + var typeStack = new List(); + foreach (var type in mainModule.GetTypes()) + { + // Check if it's a NetworkBehaviour + if (type.IsSubclassOf(CodeGenHelpers.NetworkBehaviour_FullName)) + { + // Iterate fields looking for NetworkVariableSerialization fields + foreach (var field in type.Fields) + { + // Get the field type and its base type + var fieldType = field.FieldType; + var baseType = fieldType.Resolve().BaseType; + if (baseType == null) + { + continue; + } + // This type stack is used for resolving NetworkVariableSerialization's T value + // When looking at base types, we get the type definition rather than the + // type reference... which means that we get the generic definition with an + // undefined T rather than the instance with the type filled in. + // We then have to walk backward back down the type stack to resolve what T + // is. + typeStack.Clear(); + typeStack.Add(fieldType); + // Iterate through the base types until we get to Object. + // Object is the base for everything so we'll stop when we hit that. + while (baseType.Name != mainModule.TypeSystem.Object.Name) + { + // If we've found a NetworkVariableSerialization type... + if (baseType.IsGenericInstance && baseType.Resolve() == m_NetworkVariableSerializationType) + { + // Then we need to figure out what T is + var genericType = (GenericInstanceType)baseType; + var underlyingType = genericType.GenericArguments[0]; + if (underlyingType.Resolve() == null) + { + underlyingType = ResolveGenericType(underlyingType, typeStack); + } + + // If T is generic... + if (underlyingType.IsGenericInstance) + { + // Then we pick the correct set to add it to and set it up + // for initialization. + if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializable_FullName)) + { + networkSerializableTypesSet.Add(underlyingType); + } + + if (underlyingType.HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName)) + { + structTypesSet.Add(underlyingType); + } + + if (underlyingType.Resolve().IsEnum) + { + enumTypesSet.Add(underlyingType); + } + } + + break; + } + + typeStack.Add(baseType); + baseType = baseType.Resolve().BaseType; + } + } + } + // We'll also avoid some confusion by ensuring users only choose one of the two + // serialization schemes - by method OR by memcpy, not both. We'll also do a cursory + // check that INetworkSerializeByMemcpy types are unmanaged. + else if (type.HasInterface(CodeGenHelpers.INetworkSerializeByMemcpy_FullName)) + { + if (type.HasInterface(CodeGenHelpers.INetworkSerializable_FullName)) + { + m_Diagnostics.AddError($"{nameof(INetworkSerializeByMemcpy)} types may not implement {nameof(INetworkSerializable)} - choose one or the other."); + } + if (!type.IsValueType) + { + m_Diagnostics.AddError($"{nameof(INetworkSerializeByMemcpy)} types must be unmanaged types."); + } + } + } + + if (networkSerializableTypes.Count + structTypes.Count + enumTypes.Count == 0) { return null; } - CreateModuleInitializer(assemblyDefinition, types); + // Finally we add to the module initializer some code to initialize the delegates in + // NetworkVariableSerialization for all necessary values of T, by calling initialization + // methods in NetworkVariableHelpers. + CreateModuleInitializer(assemblyDefinition, networkSerializableTypesSet.ToList(), structTypesSet.ToList(), enumTypesSet.ToList()); } else { @@ -94,9 +229,15 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics); } - private MethodReference m_InitializeDelegates_MethodRef; + private MethodReference m_InitializeDelegatesNetworkSerializable_MethodRef; + private MethodReference m_InitializeDelegatesStruct_MethodRef; + private MethodReference m_InitializeDelegatesEnum_MethodRef; - private const string k_InitializeMethodName = nameof(NetworkVariableHelper.InitializeDelegates); + private TypeDefinition m_NetworkVariableSerializationType; + + private const string k_InitializeNetworkSerializableMethodName = nameof(NetworkVariableHelper.InitializeDelegatesNetworkSerializable); + private const string k_InitializeStructMethodName = nameof(NetworkVariableHelper.InitializeDelegatesStruct); + private const string k_InitializeEnumMethodName = nameof(NetworkVariableHelper.InitializeDelegatesEnum); private bool ImportReferences(ModuleDefinition moduleDefinition) { @@ -106,11 +247,18 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) { switch (methodInfo.Name) { - case k_InitializeMethodName: - m_InitializeDelegates_MethodRef = moduleDefinition.ImportReference(methodInfo); + case k_InitializeNetworkSerializableMethodName: + m_InitializeDelegatesNetworkSerializable_MethodRef = moduleDefinition.ImportReference(methodInfo); + break; + case k_InitializeStructMethodName: + m_InitializeDelegatesStruct_MethodRef = moduleDefinition.ImportReference(methodInfo); + break; + case k_InitializeEnumMethodName: + m_InitializeDelegatesEnum_MethodRef = moduleDefinition.ImportReference(methodInfo); break; } } + m_NetworkVariableSerializationType = moduleDefinition.ImportReference(typeof(NetworkVariableSerialization<>)).Resolve(); return true; } @@ -139,7 +287,7 @@ private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinit // C# (that attribute doesn't exist in Unity, but the static module constructor still works) // https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-5.0 // https://web.archive.org/web/20100212140402/http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx - private void CreateModuleInitializer(AssemblyDefinition assembly, List networkSerializableTypes) + private void CreateModuleInitializer(AssemblyDefinition assembly, List networkSerializableTypes, List structTypes, List enumTypes) { foreach (var typeDefinition in assembly.MainModule.Types) { @@ -151,9 +299,23 @@ private void CreateModuleInitializer(AssemblyDefinition assembly, List(); + foreach (var type in structTypes) + { + var method = new GenericInstanceMethod(m_InitializeDelegatesStruct_MethodRef); + method.GenericArguments.Add(type); + instructions.Add(processor.Create(OpCodes.Call, method)); + } + foreach (var type in networkSerializableTypes) { - var method = new GenericInstanceMethod(m_InitializeDelegates_MethodRef); + var method = new GenericInstanceMethod(m_InitializeDelegatesNetworkSerializable_MethodRef); + method.GenericArguments.Add(type); + instructions.Add(processor.Create(OpCodes.Call, method)); + } + + foreach (var type in enumTypes) + { + var method = new GenericInstanceMethod(m_InitializeDelegatesEnum_MethodRef); method.GenericArguments.Add(type); instructions.Add(processor.Create(OpCodes.Call, method)); } diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs b/Editor/CodeGen/NetworkBehaviourILPP.cs index 0162d5b..5da75bd 100644 --- a/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -509,6 +509,12 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio isValid = false; } + if (methodDefinition.HasGenericParameters) + { + m_Diagnostics.AddError(methodDefinition, "RPC method must not be generic!"); + isValid = false; + } + if (methodDefinition.ReturnType != methodDefinition.Module.TypeSystem.Void) { m_Diagnostics.AddError(methodDefinition, "RPC method must return `void`!"); @@ -533,6 +539,10 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio { rpcAttribute = customAttribute; } + else + { + return null; + } } } @@ -575,11 +585,17 @@ private MethodReference GetFastBufferWriterWriteMethod(string name, TypeReferenc var checkType = paramType.Resolve(); if (paramType.IsArray) { - checkType = paramType.GetElementType().Resolve(); + checkType = ((ArrayType)paramType).ElementType.Resolve(); } if ((parameters[0].ParameterType.Resolve() == checkType || - (parameters[0].ParameterType.Resolve() == checkType.MakeByReferenceType().Resolve() && parameters[0].IsIn))) + (parameters[0].ParameterType.Resolve() == checkType.MakeByReferenceType().Resolve() && parameters[0].IsIn))) + { + return method; + } + + if (parameters[0].ParameterType == paramType || + (parameters[0].ParameterType == paramType.MakeByReferenceType() && parameters[0].IsIn)) { return method; } @@ -593,8 +609,9 @@ private MethodReference GetFastBufferWriterWriteMethod(string name, TypeReferenc { var resolvedConstraint = constraint.Resolve(); - if ((resolvedConstraint.IsInterface && !checkType.HasInterface(resolvedConstraint.FullName)) || - (resolvedConstraint.IsClass && !checkType.Resolve().IsSubclassOf(resolvedConstraint.FullName)) || + var resolvedConstraintName = resolvedConstraint.FullNameWithGenericParameters(new[] { method.GenericParameters[0] }, new[] { checkType }); + if ((resolvedConstraint.IsInterface && !checkType.HasInterface(resolvedConstraintName)) || + (resolvedConstraint.IsClass && !checkType.Resolve().IsSubclassOf(resolvedConstraintName)) || (resolvedConstraint.Name == "ValueType" && !checkType.IsValueType)) { meetsConstraints = false; @@ -605,7 +622,14 @@ private MethodReference GetFastBufferWriterWriteMethod(string name, TypeReferenc if (meetsConstraints) { var instanceMethod = new GenericInstanceMethod(method); - instanceMethod.GenericArguments.Add(checkType); + if (paramType.IsArray) + { + instanceMethod.GenericArguments.Add(((ArrayType)paramType).ElementType); + } + else + { + instanceMethod.GenericArguments.Add(paramType); + } return instanceMethod; } } @@ -653,13 +677,7 @@ private bool GetWriteMethodForParameter(TypeReference paramType, out MethodRefer } } - // Try NetworkSerializable first because INetworkSerializable may also be valid for WriteValueSafe - // and that would cause boxing if so. - var typeMethod = GetFastBufferWriterWriteMethod("WriteNetworkSerializable", paramType); - if (typeMethod == null) - { - typeMethod = GetFastBufferWriterWriteMethod(k_WriteValueMethodName, paramType); - } + var typeMethod = GetFastBufferWriterWriteMethod(k_WriteValueMethodName, paramType); if (typeMethod != null) { methodRef = m_MainModule.ImportReference(typeMethod); @@ -699,29 +717,53 @@ private MethodReference GetFastBufferReaderReadMethod(string name, TypeReference var checkType = paramType.Resolve(); if (paramType.IsArray) { - checkType = paramType.GetElementType().Resolve(); + checkType = ((ArrayType)paramType).ElementType.Resolve(); } if (methodParam.Resolve() == checkType.Resolve() || methodParam.Resolve() == checkType.MakeByReferenceType().Resolve()) { return method; } + + if (methodParam.Resolve() == paramType || methodParam.Resolve() == paramType.MakeByReferenceType().Resolve()) + { + return method; + } + if (method.HasGenericParameters && method.GenericParameters.Count == 1) { if (method.GenericParameters[0].HasConstraints) { + var meetsConstraints = true; foreach (var constraint in method.GenericParameters[0].Constraints) { var resolvedConstraint = constraint.Resolve(); - if ((resolvedConstraint.IsInterface && checkType.HasInterface(resolvedConstraint.FullName)) || - (resolvedConstraint.IsClass && checkType.Resolve().IsSubclassOf(resolvedConstraint.FullName))) + var resolvedConstraintName = resolvedConstraint.FullNameWithGenericParameters(new[] { method.GenericParameters[0] }, new[] { checkType }); + + if ((resolvedConstraint.IsInterface && !checkType.HasInterface(resolvedConstraintName)) || + (resolvedConstraint.IsClass && !checkType.Resolve().IsSubclassOf(resolvedConstraintName)) || + (resolvedConstraint.Name == "ValueType" && !checkType.IsValueType)) { - var instanceMethod = new GenericInstanceMethod(method); - instanceMethod.GenericArguments.Add(checkType); - return instanceMethod; + meetsConstraints = false; + break; } } + + if (meetsConstraints) + { + var instanceMethod = new GenericInstanceMethod(method); + if (paramType.IsArray) + { + instanceMethod.GenericArguments.Add(((ArrayType)paramType).ElementType); + } + else + { + instanceMethod.GenericArguments.Add(paramType); + } + + return instanceMethod; + } } } } @@ -751,13 +793,7 @@ private bool GetReadMethodForParameter(TypeReference paramType, out MethodRefere } } - // Try NetworkSerializable first because INetworkSerializable may also be valid for ReadValueSafe - // and that would cause boxing if so. - var typeMethod = GetFastBufferReaderReadMethod("ReadNetworkSerializable", paramType); - if (typeMethod == null) - { - typeMethod = GetFastBufferReaderReadMethod(k_ReadValueMethodName, paramType); - } + var typeMethod = GetFastBufferReaderReadMethod(k_ReadValueMethodName, paramType); if (typeMethod != null) { methodRef = m_MainModule.ImportReference(typeMethod); @@ -1003,6 +1039,17 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA // bufferWriter.WriteValueSafe(isSet); instructions.Add(processor.Create(OpCodes.Ldloca, bufWriterLocIdx)); instructions.Add(processor.Create(OpCodes.Ldloca, isSetLocalIndex)); + + for (var i = 1; i < boolMethodRef.Parameters.Count; ++i) + { + var param = boolMethodRef.Parameters[i]; + methodDefinition.Body.Variables.Add(new VariableDefinition(param.ParameterType)); + int overloadParamLocalIdx = methodDefinition.Body.Variables.Count - 1; + instructions.Add(processor.Create(OpCodes.Ldloca, overloadParamLocalIdx)); + instructions.Add(processor.Create(OpCodes.Initobj, param.ParameterType)); + instructions.Add(processor.Create(OpCodes.Ldloc, overloadParamLocalIdx)); + } + instructions.Add(processor.Create(OpCodes.Call, boolMethodRef)); // if(isSet) { @@ -1055,11 +1102,38 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA { instructions.Add(processor.Create(OpCodes.Ldc_I4_0)); } + else + { + if (isExtensionMethod && methodRef.Parameters.Count > 2) + { + for (var i = 2; i < methodRef.Parameters.Count; ++i) + { + var param = methodRef.Parameters[i]; + methodDefinition.Body.Variables.Add(new VariableDefinition(param.ParameterType)); + int overloadParamLocalIdx = methodDefinition.Body.Variables.Count - 1; + instructions.Add(processor.Create(OpCodes.Ldloca, overloadParamLocalIdx)); + instructions.Add(processor.Create(OpCodes.Initobj, param.ParameterType)); + instructions.Add(processor.Create(OpCodes.Ldloc, overloadParamLocalIdx)); + } + } + else if (!isExtensionMethod && methodRef.Parameters.Count > 1) + { + for (var i = 1; i < methodRef.Parameters.Count; ++i) + { + var param = methodRef.Parameters[i]; + methodDefinition.Body.Variables.Add(new VariableDefinition(param.ParameterType)); + int overloadParamLocalIdx = methodDefinition.Body.Variables.Count - 1; + instructions.Add(processor.Create(OpCodes.Ldloca, overloadParamLocalIdx)); + instructions.Add(processor.Create(OpCodes.Initobj, param.ParameterType)); + instructions.Add(processor.Create(OpCodes.Ldloc, overloadParamLocalIdx)); + } + } + } instructions.Add(processor.Create(OpCodes.Call, methodRef)); } else { - m_Diagnostics.AddError(methodDefinition, $"Don't know how to serialize {paramType.Name} - implement {nameof(INetworkSerializable)} or add an extension method for {nameof(FastBufferWriter)}.{k_WriteValueMethodName} to define serialization."); + m_Diagnostics.AddError(methodDefinition, $"Don't know how to serialize {paramType.Name} - implement {nameof(INetworkSerializable)}, tag memcpyable struct with {nameof(INetworkSerializeByMemcpy)}, or add an extension method for {nameof(FastBufferWriter)}.{k_WriteValueMethodName} to define serialization."); continue; } @@ -1298,6 +1372,17 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition int isSetLocalIndex = rpcHandler.Body.Variables.Count - 1; processor.Emit(OpCodes.Ldarga, 1); processor.Emit(OpCodes.Ldloca, isSetLocalIndex); + + for (var i = 1; i < boolMethodRef.Parameters.Count; ++i) + { + var param = boolMethodRef.Parameters[i]; + rpcHandler.Body.Variables.Add(new VariableDefinition(param.ParameterType)); + int overloadParamLocalIdx = rpcHandler.Body.Variables.Count - 1; + processor.Emit(OpCodes.Ldloca, overloadParamLocalIdx); + processor.Emit(OpCodes.Initobj, param.ParameterType); + processor.Emit(OpCodes.Ldloc, overloadParamLocalIdx); + } + processor.Emit(OpCodes.Call, boolMethodRef); // paramType param = null; @@ -1331,11 +1416,38 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition { processor.Emit(OpCodes.Ldc_I4_0); } + else + { + if (isExtensionMethod && methodRef.Parameters.Count > 2) + { + for (var i = 2; i < methodRef.Parameters.Count; ++i) + { + var param = methodRef.Parameters[i]; + rpcHandler.Body.Variables.Add(new VariableDefinition(param.ParameterType)); + int overloadParamLocalIdx = rpcHandler.Body.Variables.Count - 1; + processor.Emit(OpCodes.Ldloca, overloadParamLocalIdx); + processor.Emit(OpCodes.Initobj, param.ParameterType); + processor.Emit(OpCodes.Ldloc, overloadParamLocalIdx); + } + } + else if (!isExtensionMethod && methodRef.Parameters.Count > 1) + { + for (var i = 1; i < methodRef.Parameters.Count; ++i) + { + var param = methodRef.Parameters[i]; + rpcHandler.Body.Variables.Add(new VariableDefinition(param.ParameterType)); + int overloadParamLocalIdx = rpcHandler.Body.Variables.Count - 1; + processor.Emit(OpCodes.Ldloca, overloadParamLocalIdx); + processor.Emit(OpCodes.Initobj, param.ParameterType); + processor.Emit(OpCodes.Ldloc, overloadParamLocalIdx); + } + } + } processor.Emit(OpCodes.Call, methodRef); } else { - m_Diagnostics.AddError(methodDefinition, $"Don't know how to deserialize {paramType.Name} - implement {nameof(INetworkSerializable)} or add an extension method for {nameof(FastBufferReader)}.{k_ReadValueMethodName} to define serialization."); + m_Diagnostics.AddError(methodDefinition, $"Don't know how to serialize {paramType.Name} - implement {nameof(INetworkSerializable)}, tag memcpyable struct with {nameof(INetworkSerializeByMemcpy)}, or add an extension method for {nameof(FastBufferWriter)}.{k_WriteValueMethodName} to define serialization."); continue; } diff --git a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs index ae4b24e..d439cc5 100644 --- a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs +++ b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs @@ -107,7 +107,15 @@ private void ProcessNetworkVariableHelper(TypeDefinition typeDefinition) { foreach (var methodDefinition in typeDefinition.Methods) { - if (methodDefinition.Name == nameof(NetworkVariableHelper.InitializeDelegates)) + if (methodDefinition.Name == nameof(NetworkVariableHelper.InitializeDelegatesEnum)) + { + methodDefinition.IsPublic = true; + } + if (methodDefinition.Name == nameof(NetworkVariableHelper.InitializeDelegatesStruct)) + { + methodDefinition.IsPublic = true; + } + if (methodDefinition.Name == nameof(NetworkVariableHelper.InitializeDelegatesNetworkSerializable)) { methodDefinition.IsPublic = true; } diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index 6496afe..348d7fe 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/Editor/NetworkManagerEditor.cs @@ -363,7 +363,7 @@ private static void DrawInstallMultiplayerToolsTip() const string getToolsText = "Access additional tools for multiplayer development by installing the Multiplayer Tools package in the Package Manager."; const string openDocsButtonText = "Open Docs"; const string dismissButtonText = "Dismiss"; - const string targetUrl = "https://docs-multiplayer.unity3d.com/docs/tools/install-tools"; + const string targetUrl = "https://docs-multiplayer.unity3d.com/netcode/current/tools/install-tools"; const string infoIconName = "console.infoicon"; if (PlayerPrefs.GetInt(InstallMultiplayerToolsTipDismissedPlayerPrefKey, 0) != 0) diff --git a/README.md b/README.md index 6ce7816..a264b60 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # Netcode for GameObjects [![Forums](https://img.shields.io/badge/unity--forums-multiplayer-blue)](https://forum.unity.com/forums/multiplayer.26/) [![Discord](https://img.shields.io/discord/449263083769036810.svg?label=discord&logo=discord&color=informational)](https://discord.gg/FM8SE9E) -[![Website](https://img.shields.io/badge/docs-website-informational.svg)](https://docs-multiplayer.unity3d.com/) [![Api](https://img.shields.io/badge/docs-api-informational.svg)](https://docs-multiplayer.unity3d.com/docs/mlapi-api/introduction) +[![Manual](https://img.shields.io/badge/docs-manual-informational.svg)](https://docs-multiplayer.unity3d.com/netcode/current/about) [![API](https://img.shields.io/badge/docs-api-informational.svg)](https://docs-multiplayer.unity3d.com/netcode/current/api/introduction) -Netcode for GameObjects provides networking capabilities to GameObject & MonoBehaviour Unity workflows. The framework is interoperable with many low-level transports, including the official [Unity Transport Package](https://docs-multiplayer.unity3d.com/transport/1.0.0/introduction). +Netcode for GameObjects is a Unity package that provides networking capabilities to GameObject & MonoBehaviour workflows. The framework is interoperable with many low-level transports, including the official [Unity Transport Package](https://docs-multiplayer.unity3d.com/transport/current/about). ### Getting Started + Visit the [Multiplayer Docs Site](https://docs-multiplayer.unity3d.com/) for package & API documentation, as well as information about several samples which leverage the Netcode for GameObjects package. +You can also jump right into our [Hello World](https://docs-multiplayer.unity3d.com/netcode/current/tutorials/helloworld/helloworldintro) guide for a taste of how to use the framework for basic networked tasks. + ### Community and Feedback + For general questions, networking advice or discussions about Netcode for GameObjects, please join our [Discord Community](https://discord.gg/FM8SE9E) or create a post in the [Unity Multiplayer Forum](https://forum.unity.com/forums/multiplayer.26/). diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs index 2ff2f06..0dddbab 100644 --- a/Runtime/AssemblyInfo.cs +++ b/Runtime/AssemblyInfo.cs @@ -5,6 +5,7 @@ [assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")] [assembly: InternalsVisibleTo("Unity.Netcode.Editor")] [assembly: InternalsVisibleTo("TestProject.EditorTests")] +[assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")] #endif [assembly: InternalsVisibleTo("TestProject.ToolsIntegration.RuntimeTests")] [assembly: InternalsVisibleTo("TestProject.RuntimeTests")] diff --git a/Runtime/Configuration/NetworkConfig.cs b/Runtime/Configuration/NetworkConfig.cs index 5d9b92e..9606641 100644 --- a/Runtime/Configuration/NetworkConfig.cs +++ b/Runtime/Configuration/NetworkConfig.cs @@ -128,10 +128,10 @@ public class NetworkConfig public int LoadSceneTimeOut = 120; /// - /// The amount of time a message should be buffered for without being consumed. If it is not consumed within this time, it will be dropped. + /// The amount of time a message should be buffered if the asset or object needed to process it doesn't exist yet. If the asset is not added/object is not spawned within this time, it will be dropped. /// - [Tooltip("The amount of time a message should be buffered for without being consumed. If it is not consumed within this time, it will be dropped")] - public float MessageBufferTimeout = 20f; + [Tooltip("The amount of time a message should be buffered if the asset or object needed to process it doesn't exist yet. If the asset is not added/object is not spawned within this time, it will be dropped")] + public float SpawnTimeout = 1f; /// /// Whether or not to enable network logs. diff --git a/Runtime/Core/ComponentFactory.cs b/Runtime/Core/ComponentFactory.cs new file mode 100644 index 0000000..6ed6380 --- /dev/null +++ b/Runtime/Core/ComponentFactory.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace Unity.Netcode +{ + /// + /// This class is used to support testable code by allowing any supported component used by NetworkManager to be replaced + /// with a mock component or a test version that overloads certain methods to change or record their behavior. + /// Components currently supported by ComponentFactory: + /// - IDeferredMessageManager + /// + internal static class ComponentFactory + { + internal delegate object CreateObjectDelegate(NetworkManager networkManager); + + private static Dictionary s_Delegates = new Dictionary(); + + /// + /// Instantiates an instance of a given interface + /// + /// The network manager + /// The interface to instantiate it with + /// + public static T Create(NetworkManager networkManager) + { + return (T)s_Delegates[typeof(T)](networkManager); + } + + /// + /// Overrides the default creation logic for a given interface type + /// + /// The factory delegate to create the instance + /// The interface type to override + public static void Register(CreateObjectDelegate creator) + { + s_Delegates[typeof(T)] = creator; + } + + /// + /// Reverts the creation logic for a given interface type to the default logic + /// + /// The interface type to revert + public static void Deregister() + { + s_Delegates.Remove(typeof(T)); + SetDefaults(); + } + + /// + /// Initializes the default creation logic for all supported component types + /// + public static void SetDefaults() + { + SetDefault(networkManager => new DeferredMessageManager(networkManager)); + } + + private static void SetDefault(CreateObjectDelegate creator) + { + if (!s_Delegates.ContainsKey(typeof(T))) + { + s_Delegates[typeof(T)] = creator; + } + } + } +} diff --git a/Runtime/Core/ComponentFactory.cs.meta b/Runtime/Core/ComponentFactory.cs.meta new file mode 100644 index 0000000..8798ba2 --- /dev/null +++ b/Runtime/Core/ComponentFactory.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fda4c0eb89644fcea5416bbf98ea0ba0 +timeCreated: 1649966562 \ No newline at end of file diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index f2f585e..cdb0852 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -122,7 +122,7 @@ public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelive return !m_NetworkManager.m_StopProcessingMessages; } - public bool OnVerifyCanReceive(ulong senderId, Type messageType) + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) { if (m_NetworkManager.PendingClients.TryGetValue(senderId, out PendingClient client) && (client.ConnectionState == PendingClient.State.PendingApproval || (client.ConnectionState == PendingClient.State.PendingConnection && messageType != typeof(ConnectionRequestMessage)))) @@ -223,6 +223,8 @@ public GameObject GetNetworkPrefabOverride(GameObject gameObject) /// public NetworkSpawnManager SpawnManager { get; private set; } + internal IDeferredMessageManager DeferredMessageManager { get; private set; } + public CustomMessagingManager CustomMessagingManager { get; private set; } public NetworkSceneManager SceneManager { get; private set; } @@ -484,6 +486,253 @@ private void OnValidate() } } #endif + /// + /// Adds a new prefab to the network prefab list. + /// This can be any GameObject with a NetworkObject component, from any source (addressables, asset + /// bundles, Resource.Load, dynamically created, etc) + /// + /// There are three limitations to this method: + /// - If you have NetworkConfig.ForceSamePrefabs enabled, you can only do this before starting + /// networking, and the server and all connected clients must all have the same exact set of prefabs + /// added via this method before connecting + /// - Adding a prefab on the server does not automatically add it on the client - it's up to you + /// to make sure the client and server are synchronized via whatever method makes sense for your game + /// (RPCs, configs, deterministic loading, etc) + /// - If the server sends a Spawn message to a client that has not yet added a prefab for, the spawn message + /// and any other relevant messages will be held for a configurable time (default 1 second, configured via + /// NetworkConfig.SpawnTimeout) before an error is logged. This is intented to enable the SDK to gracefully + /// handle unexpected conditions (slow disks, slow network, etc) that slow down asset loading. This timeout + /// should not be relied on and code shouldn't be written around it - your code should be written so that + /// the asset is expected to be loaded before it's needed. + /// + /// + /// + public void AddNetworkPrefab(GameObject prefab) + { + if (IsListening && NetworkConfig.ForceSamePrefabs) + { + throw new Exception($"All prefabs must be registered before starting {nameof(NetworkManager)} when {nameof(NetworkConfig.ForceSamePrefabs)} is enabled."); + } + + var networkObject = prefab.GetComponent(); + if (!networkObject) + { + throw new Exception($"All {nameof(NetworkPrefab)}s must contain a {nameof(NetworkObject)} component."); + } + + var networkPrefab = new NetworkPrefab { Prefab = prefab }; + NetworkConfig.NetworkPrefabs.Add(networkPrefab); + if (IsListening) + { + var sourcePrefabGlobalObjectIdHash = (uint)0; + var targetPrefabGlobalObjectIdHash = (uint)0; + if (!ShouldAddPrefab(networkPrefab, out sourcePrefabGlobalObjectIdHash, out targetPrefabGlobalObjectIdHash)) + { + NetworkConfig.NetworkPrefabs.Remove(networkPrefab); + return; + } + + if (!AddPrefabRegistration(networkPrefab, sourcePrefabGlobalObjectIdHash, targetPrefabGlobalObjectIdHash)) + { + NetworkConfig.NetworkPrefabs.Remove(networkPrefab); + return; + } + DeferredMessageManager.ProcessTriggers(IDeferredMessageManager.TriggerType.OnAddPrefab, networkObject.GlobalObjectIdHash); + } + } + + private bool ShouldAddPrefab(NetworkPrefab networkPrefab, out uint sourcePrefabGlobalObjectIdHash, out uint targetPrefabGlobalObjectIdHash, int index = -1) + { + sourcePrefabGlobalObjectIdHash = 0; + targetPrefabGlobalObjectIdHash = 0; + var networkObject = (NetworkObject)null; + if (networkPrefab == null || (networkPrefab.Prefab == null && networkPrefab.Override == NetworkPrefabOverride.None)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning( + $"{nameof(NetworkPrefab)} cannot be null ({nameof(NetworkPrefab)} at index: {index})"); + } + return false; + } + else if (networkPrefab.Override == NetworkPrefabOverride.None) + { + networkObject = networkPrefab.Prefab.GetComponent(); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} is missing " + + $"a {nameof(NetworkObject)} component (entry will be ignored)."); + } + return false; + } + + // Otherwise get the GlobalObjectIdHash value + sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; + } + else // Validate Overrides + { + // Validate source prefab override values first + switch (networkPrefab.Override) + { + case NetworkPrefabOverride.Hash: + { + if (networkPrefab.SourceHashToOverride == 0) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourceHashToOverride)} is zero " + + "(entry will be ignored)."); + } + return false; + } + sourcePrefabGlobalObjectIdHash = networkPrefab.SourceHashToOverride; + break; + } + case NetworkPrefabOverride.Prefab: + { + if (networkPrefab.SourcePrefabToOverride == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourcePrefabToOverride)} is null (entry will be ignored)."); + } + + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {networkPrefab.SourceHashToOverride} will be removed and ignored."); + return false; + } + else + { + networkObject = networkPrefab.SourcePrefabToOverride.GetComponent(); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} ({networkPrefab.SourcePrefabToOverride.name}) " + + $"is missing a {nameof(NetworkObject)} component (entry will be ignored)."); + } + + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry (\"{networkPrefab.SourcePrefabToOverride.name}\") will be removed and ignored."); + return false; + } + + sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; + } + break; + } + } + + // Validate target prefab override values next + if (networkPrefab.OverridingTargetPrefab == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.OverridingTargetPrefab)} is null!"); + } + switch (networkPrefab.Override) + { + case NetworkPrefabOverride.Hash: + { + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {networkPrefab.SourceHashToOverride} will be removed and ignored."); + break; + } + case NetworkPrefabOverride.Prefab: + { + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry ({networkPrefab.SourcePrefabToOverride.name}) will be removed and ignored."); + break; + } + } + return false; + } + else + { + targetPrefabGlobalObjectIdHash = networkPrefab.OverridingTargetPrefab.GetComponent().GlobalObjectIdHash; + } + } + return true; + } + + internal bool AddPrefabRegistration(NetworkPrefab networkPrefab, uint sourcePrefabGlobalObjectIdHash, uint targetPrefabGlobalObjectIdHash) + { + // Assign the appropriate GlobalObjectIdHash to the appropriate NetworkPrefab + if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(sourcePrefabGlobalObjectIdHash)) + { + if (networkPrefab.Override == NetworkPrefabOverride.None) + { + NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab); + } + else + { + if (!NetworkConfig.OverrideToNetworkPrefab.ContainsKey(targetPrefabGlobalObjectIdHash)) + { + switch (networkPrefab.Override) + { + case NetworkPrefabOverride.Prefab: + { + NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab); + NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); + } + break; + case NetworkPrefabOverride.Hash: + { + NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, networkPrefab); + NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); + } + break; + } + } + else + { + var networkObject = networkPrefab.Prefab.GetComponent(); + // This can happen if a user tries to make several GlobalObjectIdHash values point to the same target + Debug.LogError($"{nameof(NetworkPrefab)} (\"{networkObject.name}\") has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} target entry value of: {targetPrefabGlobalObjectIdHash}! Removing entry from list!"); + return false; + } + } + } + else + { + var networkObject = networkPrefab.Prefab.GetComponent(); + // This should never happen, but in the case it somehow does log an error and remove the duplicate entry + Debug.LogError($"{nameof(NetworkPrefab)} ({networkObject.name}) has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} source entry value of: {sourcePrefabGlobalObjectIdHash}! Removing entry from list!"); + return false; + } + return true; + } + + private void InitializePrefabs(int startIdx = 0) + { + // This is used to remove entries not needed or invalid + var removeEmptyPrefabs = new List(); + + // Build the NetworkPrefabOverrideLinks dictionary + for (int i = startIdx; i < NetworkConfig.NetworkPrefabs.Count; i++) + { + var sourcePrefabGlobalObjectIdHash = (uint)0; + var targetPrefabGlobalObjectIdHash = (uint)0; + if (!ShouldAddPrefab(NetworkConfig.NetworkPrefabs[i], out sourcePrefabGlobalObjectIdHash, out targetPrefabGlobalObjectIdHash, i)) + { + removeEmptyPrefabs.Add(i); + continue; + } + + if (!AddPrefabRegistration(NetworkConfig.NetworkPrefabs[i], sourcePrefabGlobalObjectIdHash, targetPrefabGlobalObjectIdHash)) + { + removeEmptyPrefabs.Add(i); + continue; + } + } + + // Clear out anything that is invalid or not used (for invalid entries we already logged warnings to the user earlier) + // Iterate backwards so indices don't shift as we remove + for (int i = removeEmptyPrefabs.Count - 1; i >= 0; i--) + { + NetworkConfig.NetworkPrefabs.RemoveAt(removeEmptyPrefabs[i]); + } + + removeEmptyPrefabs.Clear(); + } private void Initialize(bool server) { @@ -494,6 +743,8 @@ private void Initialize(bool server) return; } + ComponentFactory.SetDefaults(); + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(Initialize)); @@ -524,6 +775,8 @@ private void Initialize(bool server) // Create spawn manager instance SpawnManager = new NetworkSpawnManager(this); + DeferredMessageManager = ComponentFactory.Create(this); + CustomMessagingManager = new CustomMessagingManager(this); SceneManager = new NetworkSceneManager(this); @@ -573,171 +826,11 @@ private void Initialize(bool server) this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); - // This is used to remove entries not needed or invalid - var removeEmptyPrefabs = new List(); - // Always clear our prefab override links before building NetworkConfig.NetworkPrefabOverrideLinks.Clear(); NetworkConfig.OverrideToNetworkPrefab.Clear(); - // Build the NetworkPrefabOverrideLinks dictionary - for (int i = 0; i < NetworkConfig.NetworkPrefabs.Count; i++) - { - var sourcePrefabGlobalObjectIdHash = (uint)0; - var targetPrefabGlobalObjectIdHash = (uint)0; - var networkObject = (NetworkObject)null; - if (NetworkConfig.NetworkPrefabs[i] == null || (NetworkConfig.NetworkPrefabs[i].Prefab == null && NetworkConfig.NetworkPrefabs[i].Override == NetworkPrefabOverride.None)) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning( - $"{nameof(NetworkPrefab)} cannot be null ({nameof(NetworkPrefab)} at index: {i})"); - } - - removeEmptyPrefabs.Add(i); - continue; - } - else if (NetworkConfig.NetworkPrefabs[i].Override == NetworkPrefabOverride.None) - { - var networkPrefab = NetworkConfig.NetworkPrefabs[i]; - networkObject = networkPrefab.Prefab.GetComponent(); - if (networkObject == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} is missing " + - $"a {nameof(NetworkObject)} component (entry will be ignored)."); - } - removeEmptyPrefabs.Add(i); - continue; - } - - // Otherwise get the GlobalObjectIdHash value - sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; - } - else // Validate Overrides - { - // Validate source prefab override values first - switch (NetworkConfig.NetworkPrefabs[i].Override) - { - case NetworkPrefabOverride.Hash: - { - if (NetworkConfig.NetworkPrefabs[i].SourceHashToOverride == 0) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourceHashToOverride)} is zero " + - "(entry will be ignored)."); - } - removeEmptyPrefabs.Add(i); - continue; - } - sourcePrefabGlobalObjectIdHash = NetworkConfig.NetworkPrefabs[i].SourceHashToOverride; - break; - } - case NetworkPrefabOverride.Prefab: - { - if (NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourcePrefabToOverride)} is null (entry will be ignored)."); - } - Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {NetworkConfig.NetworkPrefabs[i].SourceHashToOverride} will be removed and ignored."); - removeEmptyPrefabs.Add(i); - continue; - } - else - { - networkObject = NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.GetComponent(); - if (networkObject == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} ({NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.name}) " + - $"is missing a {nameof(NetworkObject)} component (entry will be ignored)."); - } - Debug.LogWarning($"{nameof(NetworkPrefab)} override entry (\"{NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.name}\") will be removed and ignored."); - removeEmptyPrefabs.Add(i); - continue; - } - sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; - } - break; - } - } - - // Validate target prefab override values next - if (NetworkConfig.NetworkPrefabs[i].OverridingTargetPrefab == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) - { - NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.OverridingTargetPrefab)} is null!"); - } - removeEmptyPrefabs.Add(i); - switch (NetworkConfig.NetworkPrefabs[i].Override) - { - case NetworkPrefabOverride.Hash: - { - Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {NetworkConfig.NetworkPrefabs[i].SourceHashToOverride} will be removed and ignored."); - break; - } - case NetworkPrefabOverride.Prefab: - { - Debug.LogWarning($"{nameof(NetworkPrefab)} override entry ({NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.name}) will be removed and ignored."); - break; - } - } - continue; - } - else - { - targetPrefabGlobalObjectIdHash = NetworkConfig.NetworkPrefabs[i].OverridingTargetPrefab.GetComponent().GlobalObjectIdHash; - } - } - - // Assign the appropriate GlobalObjectIdHash to the appropriate NetworkPrefab - if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(sourcePrefabGlobalObjectIdHash)) - { - if (NetworkConfig.NetworkPrefabs[i].Override == NetworkPrefabOverride.None) - { - NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, NetworkConfig.NetworkPrefabs[i]); - } - else - { - if (!NetworkConfig.OverrideToNetworkPrefab.ContainsKey(targetPrefabGlobalObjectIdHash)) - { - switch (NetworkConfig.NetworkPrefabs[i].Override) - { - case NetworkPrefabOverride.Prefab: - { - NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, NetworkConfig.NetworkPrefabs[i]); - NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); - } - break; - case NetworkPrefabOverride.Hash: - { - NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, NetworkConfig.NetworkPrefabs[i]); - NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); - } - break; - } - } - else - { - // This can happen if a user tries to make several GlobalObjectIdHash values point to the same target - Debug.LogError($"{nameof(NetworkPrefab)} (\"{networkObject.name}\") has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} target entry value of: {targetPrefabGlobalObjectIdHash}! Removing entry from list!"); - removeEmptyPrefabs.Add(i); - } - } - } - else - { - // This should never happen, but in the case it somehow does log an error and remove the duplicate entry - Debug.LogError($"{nameof(NetworkPrefab)} ({networkObject.name}) has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} source entry value of: {sourcePrefabGlobalObjectIdHash}! Removing entry from list!"); - removeEmptyPrefabs.Add(i); - } - } + InitializePrefabs(); // If we have a player prefab, then we need to verify it is in the list of NetworkPrefabOverrideLinks for client side spawning. if (NetworkConfig.PlayerPrefab != null) @@ -764,15 +857,6 @@ private void Initialize(bool server) } } - // Clear out anything that is invalid or not used (for invalid entries we already logged warnings to the user earlier) - // Iterate backwards so indices don't shift as we remove - for (int i = removeEmptyPrefabs.Count - 1; i >= 0; i--) - { - NetworkConfig.NetworkPrefabs.RemoveAt(removeEmptyPrefabs[i]); - } - - removeEmptyPrefabs.Clear(); - NetworkConfig.NetworkTransport.OnTransportEvent += HandleRawTransportPoll; NetworkConfig.NetworkTransport.Initialize(this); @@ -930,7 +1014,9 @@ private bool CanStart(StartType type) return false; } - if (NetworkConfig.ConnectionApproval) + // Only if it is starting as a server or host do we need to check this + // Clients don't invoke the ConnectionApprovalCallback + if (NetworkConfig.ConnectionApproval && type != StartType.Client) { if (ConnectionApprovalCallback == null) { @@ -985,6 +1071,7 @@ private void OnEnable() private void Awake() { UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnloaded; + NetworkVariableHelper.InitializeAllBaseDelegates(); } /// @@ -1081,8 +1168,13 @@ public void Shutdown(bool discardMessageQueue = false) NetworkLog.LogInfo(nameof(Shutdown)); } - m_ShuttingDown = true; - m_StopProcessingMessages = discardMessageQueue; + // If we're not running, don't start shutting down, it would only cause an immediate + // shutdown the next time the manager is started. + if (IsServer || IsClient) + { + m_ShuttingDown = true; + m_StopProcessingMessages = discardMessageQueue; + } } internal void ShutdownInternal() @@ -1165,13 +1257,17 @@ internal void ShutdownInternal() if (SpawnManager != null) { - SpawnManager.CleanupAllTriggers(); SpawnManager.DespawnAndDestroyNetworkObjects(); SpawnManager.ServerResetShudownStateForSceneObjects(); SpawnManager = null; } + if (DeferredMessageManager != null) + { + DeferredMessageManager.CleanupAllTriggers(); + } + if (SceneManager != null) { // Let the NetworkSceneManager clean up its two SceneEvenData instances @@ -1287,7 +1383,7 @@ private void OnNetworkPostLateUpdate() NetworkObject.VerifyParentingStatus(); } - SpawnManager.CleanupStaleTriggers(); + DeferredMessageManager.CleanupStaleTriggers(); if (m_ShuttingDown) { @@ -1415,11 +1511,6 @@ private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, A break; case NetworkEvent.Data: { - if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) - { - NetworkLog.LogInfo($"Incoming Data From {clientId}: {payload.Count} bytes"); - } - clientId = TransportIdToClientId(clientId); HandleIncomingData(clientId, payload, receiveTime); @@ -1444,7 +1535,11 @@ private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, A } else { - Shutdown(); + // We must pass true here and not process any sends messages + // as we are no longer connected and thus there is no one to + // send any messages to and this will cause an exception within + // UnityTransport as the client ID is no longer valid. + Shutdown(true); } #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportDisconnect.End(); diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index d9b8ee1..241ee8d 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -826,7 +826,7 @@ internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index) internal struct SceneObject { - public struct HeaderData + public struct HeaderData : INetworkSerializeByMemcpy { public ulong NetworkObjectId; public ulong OwnerClientId; @@ -845,7 +845,7 @@ public struct HeaderData public ulong ParentObjectId; //If(Metadata.HasTransform) - public struct TransformData + public struct TransformData : INetworkSerializeByMemcpy { public Vector3 Position; public Quaternion Rotation; diff --git a/Runtime/Messaging/BatchHeader.cs b/Runtime/Messaging/BatchHeader.cs index a71fd6a..a7302f5 100644 --- a/Runtime/Messaging/BatchHeader.cs +++ b/Runtime/Messaging/BatchHeader.cs @@ -3,7 +3,7 @@ namespace Unity.Netcode /// /// Header placed at the start of each message batch /// - internal struct BatchHeader + internal struct BatchHeader : INetworkSerializeByMemcpy { /// /// Total number of messages in the batch. diff --git a/Runtime/Messaging/DeferredMessageManager.cs b/Runtime/Messaging/DeferredMessageManager.cs new file mode 100644 index 0000000..8ee919b --- /dev/null +++ b/Runtime/Messaging/DeferredMessageManager.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using Unity.Collections; +using Time = UnityEngine.Time; + +namespace Unity.Netcode +{ + internal class DeferredMessageManager : IDeferredMessageManager + { + protected struct TriggerData + { + public FastBufferReader Reader; + public MessageHeader Header; + public ulong SenderId; + public float Timestamp; + public int SerializedHeaderSize; + } + protected struct TriggerInfo + { + public float Expiry; + public NativeList TriggerData; + } + + protected readonly Dictionary> m_Triggers = new Dictionary>(); + + private readonly NetworkManager m_NetworkManager; + + internal DeferredMessageManager(NetworkManager networkManager) + { + m_NetworkManager = networkManager; + } + + /// + /// Defers processing of a message until the moment a specific networkObjectId is spawned. + /// This is to handle situations where an RPC or other object-specific message arrives before the spawn does, + /// either due to it being requested in OnNetworkSpawn before the spawn call has been executed + /// + /// There is a one second maximum lifetime of triggers to avoid memory leaks. After one second has passed + /// without the requested object ID being spawned, the triggers for it are automatically deleted. + /// + public virtual unsafe void DeferMessage(IDeferredMessageManager.TriggerType trigger, ulong key, FastBufferReader reader, ref NetworkContext context) + { + if (!m_Triggers.TryGetValue(trigger, out var triggers)) + { + triggers = new Dictionary(); + m_Triggers[trigger] = triggers; + } + + if (!triggers.TryGetValue(key, out var triggerInfo)) + { + triggerInfo = new TriggerInfo + { + Expiry = Time.realtimeSinceStartup + m_NetworkManager.NetworkConfig.SpawnTimeout, + TriggerData = new NativeList(Allocator.Persistent) + }; + triggers[key] = triggerInfo; + } + + triggerInfo.TriggerData.Add(new TriggerData + { + Reader = new FastBufferReader(reader.GetUnsafePtr(), Allocator.Persistent, reader.Length), + Header = context.Header, + Timestamp = context.Timestamp, + SenderId = context.SenderId, + SerializedHeaderSize = context.SerializedHeaderSize + }); + } + + /// + /// Cleans up any trigger that's existed for more than a second. + /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. + /// + public virtual unsafe void CleanupStaleTriggers() + { + foreach (var kvp in m_Triggers) + { + ulong* staleKeys = stackalloc ulong[kvp.Value.Count]; + int index = 0; + foreach (var kvp2 in kvp.Value) + { + if (kvp2.Value.Expiry < Time.realtimeSinceStartup) + { + staleKeys[index++] = kvp2.Key; + PurgeTrigger(kvp.Key, kvp2.Key, kvp2.Value); + } + } + + for (var i = 0; i < index; ++i) + { + kvp.Value.Remove(staleKeys[i]); + } + } + } + + protected virtual void PurgeTrigger(IDeferredMessageManager.TriggerType triggerType, ulong key, TriggerInfo triggerInfo) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Deferred messages were received for a trigger of type {triggerType} with key {key}, but that trigger was not received within within {m_NetworkManager.NetworkConfig.SpawnTimeout} second(s)."); + } + + foreach (var data in triggerInfo.TriggerData) + { + data.Reader.Dispose(); + } + + triggerInfo.TriggerData.Dispose(); + } + + public virtual void ProcessTriggers(IDeferredMessageManager.TriggerType trigger, ulong key) + { + if (m_Triggers.TryGetValue(trigger, out var triggers)) + { + // This must happen after InvokeBehaviourNetworkSpawn, otherwise ClientRPCs and other messages can be + // processed before the object is fully spawned. This must be the last thing done in the spawn process. + if (triggers.TryGetValue(key, out var triggerInfo)) + { + foreach (var deferredMessage in triggerInfo.TriggerData) + { + // Reader will be disposed within HandleMessage + m_NetworkManager.MessagingSystem.HandleMessage(deferredMessage.Header, deferredMessage.Reader, deferredMessage.SenderId, deferredMessage.Timestamp, deferredMessage.SerializedHeaderSize); + } + + triggerInfo.TriggerData.Dispose(); + triggers.Remove(key); + } + } + } + + /// + /// Cleans up any trigger that's existed for more than a second. + /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. + /// + public virtual void CleanupAllTriggers() + { + foreach (var kvp in m_Triggers) + { + foreach (var kvp2 in kvp.Value) + { + foreach (var data in kvp2.Value.TriggerData) + { + data.Reader.Dispose(); + } + kvp2.Value.TriggerData.Dispose(); + } + } + m_Triggers.Clear(); + } + } +} diff --git a/Runtime/Messaging/DeferredMessageManager.cs.meta b/Runtime/Messaging/DeferredMessageManager.cs.meta new file mode 100644 index 0000000..714d98d --- /dev/null +++ b/Runtime/Messaging/DeferredMessageManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ac7f57f7d16a46e2aba65558e873727f +timeCreated: 1649799187 \ No newline at end of file diff --git a/Runtime/Messaging/IDeferredMessageManager.cs b/Runtime/Messaging/IDeferredMessageManager.cs new file mode 100644 index 0000000..d1f35c3 --- /dev/null +++ b/Runtime/Messaging/IDeferredMessageManager.cs @@ -0,0 +1,35 @@ +namespace Unity.Netcode +{ + internal interface IDeferredMessageManager + { + internal enum TriggerType + { + OnSpawn, + OnAddPrefab, + } + + /// + /// Defers processing of a message until the moment a specific networkObjectId is spawned. + /// This is to handle situations where an RPC or other object-specific message arrives before the spawn does, + /// either due to it being requested in OnNetworkSpawn before the spawn call has been executed + /// + /// There is a one second maximum lifetime of triggers to avoid memory leaks. After one second has passed + /// without the requested object ID being spawned, the triggers for it are automatically deleted. + /// + void DeferMessage(TriggerType trigger, ulong key, FastBufferReader reader, ref NetworkContext context); + + /// + /// Cleans up any trigger that's existed for more than a second. + /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. + /// + void CleanupStaleTriggers(); + + void ProcessTriggers(TriggerType trigger, ulong key); + + /// + /// Cleans up any trigger that's existed for more than a second. + /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. + /// + void CleanupAllTriggers(); + } +} diff --git a/Runtime/Messaging/IDeferredMessageManager.cs.meta b/Runtime/Messaging/IDeferredMessageManager.cs.meta new file mode 100644 index 0000000..39f3c78 --- /dev/null +++ b/Runtime/Messaging/IDeferredMessageManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7fb73a029c314763a04ebb015a07664d +timeCreated: 1649966331 \ No newline at end of file diff --git a/Runtime/Messaging/INetworkHooks.cs b/Runtime/Messaging/INetworkHooks.cs index 0818046..7543a11 100644 --- a/Runtime/Messaging/INetworkHooks.cs +++ b/Runtime/Messaging/INetworkHooks.cs @@ -91,8 +91,10 @@ internal interface INetworkHooks /// /// The source clientId /// The type of the message + /// The FastBufferReader containing the message + /// The NetworkContext the message is being processed in /// - bool OnVerifyCanReceive(ulong senderId, Type messageType); + bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context); /// /// Called after a message is serialized, but before it's handled. diff --git a/Runtime/Messaging/MessageHeader.cs b/Runtime/Messaging/MessageHeader.cs index a7e3ef1..993616e 100644 --- a/Runtime/Messaging/MessageHeader.cs +++ b/Runtime/Messaging/MessageHeader.cs @@ -3,7 +3,7 @@ namespace Unity.Netcode /// /// This is the header data that's serialized to the network when sending an /// - internal struct MessageHeader + internal struct MessageHeader : INetworkSerializeByMemcpy { /// /// The byte representation of the message type. This is automatically assigned to each message diff --git a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs index f1ed9f4..19f84bc 100644 --- a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs +++ b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs @@ -1,6 +1,6 @@ namespace Unity.Netcode { - internal struct ChangeOwnershipMessage : INetworkMessage + internal struct ChangeOwnershipMessage : INetworkMessage, INetworkSerializeByMemcpy { public ulong NetworkObjectId; public ulong OwnerClientId; @@ -20,7 +20,7 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context) reader.ReadValueSafe(out this); if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId)) { - networkManager.SpawnManager.TriggerOnSpawn(NetworkObjectId, reader, ref context); + networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context); return false; } diff --git a/Runtime/Messaging/Messages/CreateObjectMessage.cs b/Runtime/Messaging/Messages/CreateObjectMessage.cs index 4b753a0..56e6e35 100644 --- a/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -19,6 +19,11 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context) } ObjectInfo.Deserialize(reader); + if (!networkManager.NetworkConfig.ForceSamePrefabs && !networkManager.SpawnManager.HasPrefab(ObjectInfo.Header.IsSceneObject, ObjectInfo.Header.Hash)) + { + networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnAddPrefab, ObjectInfo.Header.Hash, reader, ref context); + return false; + } m_ReceivedNetworkVariableData = reader; return true; diff --git a/Runtime/Messaging/Messages/DestroyObjectMessage.cs b/Runtime/Messaging/Messages/DestroyObjectMessage.cs index a36f4a0..ef7ff74 100644 --- a/Runtime/Messaging/Messages/DestroyObjectMessage.cs +++ b/Runtime/Messaging/Messages/DestroyObjectMessage.cs @@ -1,6 +1,6 @@ namespace Unity.Netcode { - internal struct DestroyObjectMessage : INetworkMessage + internal struct DestroyObjectMessage : INetworkMessage, INetworkSerializeByMemcpy { public ulong NetworkObjectId; @@ -16,7 +16,14 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context) { return false; } + reader.ReadValueSafe(out this); + + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject)) + { + networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context); + return false; + } return true; } diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs index a7d1e0d..2258821 100644 --- a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs +++ b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs @@ -54,6 +54,14 @@ public void Serialize(FastBufferWriter writer) networkVariable.CanClientRead(TargetClientId) && (NetworkBehaviour.NetworkManager.IsServer || networkVariable.CanClientWrite(NetworkBehaviour.NetworkManager.LocalClientId)); + // Prevent the server from writing to the client that owns a given NetworkVariable + // Allowing the write would send an old value to the client and cause jitter + if (networkVariable.WritePerm == NetworkVariableWritePermission.Owner && + networkVariable.OwnerClientId() == TargetClientId) + { + shouldWrite = false; + } + if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { if (!shouldWrite) @@ -225,7 +233,7 @@ public void Handle(ref NetworkContext context) } else { - networkManager.SpawnManager.TriggerOnSpawn(NetworkObjectId, m_ReceivedNetworkVariableData, ref context); + networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnSpawn, NetworkObjectId, m_ReceivedNetworkVariableData, ref context); } } } diff --git a/Runtime/Messaging/Messages/ParentSyncMessage.cs b/Runtime/Messaging/Messages/ParentSyncMessage.cs index af4ca11..95d20ea 100644 --- a/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -48,7 +48,7 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context) if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId)) { - networkManager.SpawnManager.TriggerOnSpawn(NetworkObjectId, reader, ref context); + networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context); return false; } diff --git a/Runtime/Messaging/Messages/RpcMessages.cs b/Runtime/Messaging/Messages/RpcMessages.cs index 1645f71..1ab0492 100644 --- a/Runtime/Messaging/Messages/RpcMessages.cs +++ b/Runtime/Messaging/Messages/RpcMessages.cs @@ -30,7 +30,7 @@ public static unsafe bool Deserialize(ref FastBufferReader reader, ref NetworkCo var networkManager = (NetworkManager)context.SystemOwner; if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(metadata.NetworkObjectId)) { - networkManager.SpawnManager.TriggerOnSpawn(metadata.NetworkObjectId, reader, ref context); + networkManager.DeferredMessageManager.DeferMessage(IDeferredMessageManager.TriggerType.OnSpawn, metadata.NetworkObjectId, reader, ref context); return false; } @@ -83,7 +83,7 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, } } - internal struct RpcMetadata + internal struct RpcMetadata : INetworkSerializeByMemcpy { public ulong NetworkObjectId; public ushort NetworkBehaviourId; diff --git a/Runtime/Messaging/Messages/TimeSyncMessage.cs b/Runtime/Messaging/Messages/TimeSyncMessage.cs index 5430acb..613bb90 100644 --- a/Runtime/Messaging/Messages/TimeSyncMessage.cs +++ b/Runtime/Messaging/Messages/TimeSyncMessage.cs @@ -1,6 +1,6 @@ namespace Unity.Netcode { - internal struct TimeSyncMessage : INetworkMessage + internal struct TimeSyncMessage : INetworkMessage, INetworkSerializeByMemcpy { public int Tick; diff --git a/Runtime/Messaging/MessagingSystem.cs b/Runtime/Messaging/MessagingSystem.cs index 422401c..4bec42e 100644 --- a/Runtime/Messaging/MessagingSystem.cs +++ b/Runtime/Messaging/MessagingSystem.cs @@ -136,6 +136,11 @@ public void Hook(INetworkHooks hooks) m_Hooks.Add(hooks); } + public void Unhook(INetworkHooks hooks) + { + m_Hooks.Remove(hooks); + } + private void RegisterMessageType(MessageWithHandler messageWithHandler) { m_MessageHandlers[m_HighMessageType] = messageWithHandler.Handler; @@ -208,11 +213,11 @@ internal void HandleIncomingData(ulong clientId, ArraySegment data, float } } - private bool CanReceive(ulong clientId, Type messageType) + private bool CanReceive(ulong clientId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) { for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) { - if (!m_Hooks[hookIdx].OnVerifyCanReceive(clientId, messageType)) + if (!m_Hooks[hookIdx].OnVerifyCanReceive(clientId, messageType, messageContent, ref context)) { return false; } @@ -240,7 +245,7 @@ public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulon }; var type = m_ReverseTypeMap[header.MessageType]; - if (!CanReceive(senderId, type)) + if (!CanReceive(senderId, type, reader, ref context)) { reader.Dispose(); return; diff --git a/Runtime/Metrics/MetricHooks.cs b/Runtime/Metrics/MetricHooks.cs index 9e77376..fd12bc5 100644 --- a/Runtime/Metrics/MetricHooks.cs +++ b/Runtime/Metrics/MetricHooks.cs @@ -52,7 +52,7 @@ public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelive return true; } - public bool OnVerifyCanReceive(ulong senderId, Type messageType) + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) { return true; } diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs index a6f221e..9a891ec 100644 --- a/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -8,7 +8,7 @@ namespace Unity.Netcode /// Event based NetworkVariable container for syncing Lists /// /// The type for the list - public class NetworkList : NetworkVariableBase where T : unmanaged, IEquatable + public class NetworkList : NetworkVariableSerialization where T : unmanaged, IEquatable { private NativeList m_List = new NativeList(64, Allocator.Persistent); private NativeList> m_DirtyEvents = new NativeList>(64, Allocator.Persistent); @@ -72,18 +72,18 @@ public override void WriteDelta(FastBufferWriter writer) { case NetworkListEvent.EventType.Add: { - NetworkVariable.Write(writer, m_DirtyEvents[i].Value); + Write(writer, m_DirtyEvents[i].Value); } break; case NetworkListEvent.EventType.Insert: { writer.WriteValueSafe(m_DirtyEvents[i].Index); - NetworkVariable.Write(writer, m_DirtyEvents[i].Value); + Write(writer, m_DirtyEvents[i].Value); } break; case NetworkListEvent.EventType.Remove: { - NetworkVariable.Write(writer, m_DirtyEvents[i].Value); + Write(writer, m_DirtyEvents[i].Value); } break; case NetworkListEvent.EventType.RemoveAt: @@ -94,7 +94,7 @@ public override void WriteDelta(FastBufferWriter writer) case NetworkListEvent.EventType.Value: { writer.WriteValueSafe(m_DirtyEvents[i].Index); - NetworkVariable.Write(writer, m_DirtyEvents[i].Value); + Write(writer, m_DirtyEvents[i].Value); } break; case NetworkListEvent.EventType.Clear: @@ -112,7 +112,7 @@ public override void WriteField(FastBufferWriter writer) writer.WriteValueSafe((ushort)m_List.Length); for (int i = 0; i < m_List.Length; i++) { - NetworkVariable.Write(writer, m_List[i]); + Write(writer, m_List[i]); } } @@ -123,7 +123,7 @@ public override void ReadField(FastBufferReader reader) reader.ReadValueSafe(out ushort count); for (int i = 0; i < count; i++) { - NetworkVariable.Read(reader, out T value); + Read(reader, out T value); m_List.Add(value); } } @@ -139,7 +139,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { case NetworkListEvent.EventType.Add: { - NetworkVariable.Read(reader, out T value); + Read(reader, out T value); m_List.Add(value); if (OnListChanged != null) @@ -166,7 +166,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) case NetworkListEvent.EventType.Insert: { reader.ReadValueSafe(out int index); - NetworkVariable.Read(reader, out T value); + Read(reader, out T value); m_List.InsertRangeWithBeginEnd(index, index + 1); m_List[index] = value; @@ -193,7 +193,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) break; case NetworkListEvent.EventType.Remove: { - NetworkVariable.Read(reader, out T value); + Read(reader, out T value); int index = m_List.IndexOf(value); if (index == -1) { @@ -253,7 +253,7 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) case NetworkListEvent.EventType.Value: { reader.ReadValueSafe(out int index); - NetworkVariable.Read(reader, out T value); + Read(reader, out T value); if (index >= m_List.Length) { throw new Exception("Shouldn't be here, index is higher than list length"); diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index e7c0ca6..693df0d 100644 --- a/Runtime/NetworkVariable/NetworkVariable.cs +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -9,55 +9,8 @@ namespace Unity.Netcode /// A variable that can be synchronized over the network. /// [Serializable] - public class NetworkVariable : NetworkVariableBase where T : unmanaged + public class NetworkVariable : NetworkVariableSerialization where T : unmanaged { - // Functions that know how to serialize INetworkSerializable - internal static void WriteNetworkSerializable(FastBufferWriter writer, in TForMethod value) - where TForMethod : INetworkSerializable, new() - { - writer.WriteNetworkSerializable(value); - } - internal static void ReadNetworkSerializable(FastBufferReader reader, out TForMethod value) - where TForMethod : INetworkSerializable, new() - { - reader.ReadNetworkSerializable(out value); - } - - // Functions that serialize other types - private static void WriteValue(FastBufferWriter writer, in TForMethod value) - where TForMethod : unmanaged - { - writer.WriteValueSafe(value); - } - - private static void ReadValue(FastBufferReader reader, out TForMethod value) - where TForMethod : unmanaged - { - reader.ReadValueSafe(out value); - } - - internal delegate void WriteDelegate(FastBufferWriter writer, in TForMethod value); - - internal delegate void ReadDelegate(FastBufferReader reader, out TForMethod value); - - // These static delegates provide the right implementation for writing and reading a particular network variable type. - // For most types, these default to WriteValue() and ReadValue(), which perform simple memcpy operations. - // - // INetworkSerializableILPP will generate startup code that will set it to WriteNetworkSerializable() - // and ReadNetworkSerializable() for INetworkSerializable types, which will call NetworkSerialize(). - // - // In the future we may be able to use this to provide packing implementations for floats and integers to optimize bandwidth usage. - // - // The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this, - // NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable, - // *both* of which would cause a boxing allocation. Alternatively, NetworkVariable could have been split into - // NetworkVariable and NetworkSerializableVariable or something like that, which would have caused a poor - // user experience and an API that's easier to get wrong than right. This is a bit ugly on the implementation - // side, but it gets the best achievable user experience and performance. - internal static WriteDelegate Write = WriteValue; - internal static ReadDelegate Read = ReadValue; - - /// /// Delegate type for value changed event /// @@ -143,6 +96,11 @@ public override void WriteDelta(FastBufferWriter writer) /// Whether or not the container should keep the dirty delta, or mark the delta as consumed public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { + // todo: + // keepDirtyDelta marks a variable received as dirty and causes the server to send the value to clients + // In a prefect world, whether a variable was A) modified locally or B) received and needs retransmit + // would be stored in different fields + T previousValue = m_InternalValue; Read(reader, out m_InternalValue); diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index e383c31..67c69d4 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -94,6 +94,14 @@ public bool CanClientWrite(ulong clientId) } } + /// + /// Returns the ClientId of the owning client + /// + internal ulong OwnerClientId() + { + return m_NetworkBehaviour.NetworkObject.OwnerClientId; + } + /// /// Writes the dirty changes, that is, the changes since the variable was last dirty, to the writer /// diff --git a/Runtime/NetworkVariable/NetworkVariableHelper.cs b/Runtime/NetworkVariable/NetworkVariableHelper.cs index 1800910..8c86a60 100644 --- a/Runtime/NetworkVariable/NetworkVariableHelper.cs +++ b/Runtime/NetworkVariable/NetworkVariableHelper.cs @@ -1,3 +1,6 @@ +using System; +using UnityEngine; + namespace Unity.Netcode { public class NetworkVariableHelper @@ -13,10 +16,62 @@ public class NetworkVariableHelper // side, but it gets the best achievable user experience and performance. // // RuntimeAccessModifiersILPP will make this `public` - internal static void InitializeDelegates() where T : unmanaged, INetworkSerializable + internal static void InitializeDelegatesNetworkSerializable() where T : unmanaged, INetworkSerializable { - NetworkVariable.Write = NetworkVariable.WriteNetworkSerializable; - NetworkVariable.Read = NetworkVariable.ReadNetworkSerializable; + NetworkVariableSerialization.SetWriteDelegate(NetworkVariableSerialization.WriteNetworkSerializable); + NetworkVariableSerialization.SetReadDelegate(NetworkVariableSerialization.ReadNetworkSerializable); + } + internal static void InitializeDelegatesStruct() where T : unmanaged, INetworkSerializeByMemcpy + { + NetworkVariableSerialization.SetWriteDelegate(NetworkVariableSerialization.WriteStruct); + NetworkVariableSerialization.SetReadDelegate(NetworkVariableSerialization.ReadStruct); + } + internal static void InitializeDelegatesEnum() where T : unmanaged, Enum + { + NetworkVariableSerialization.SetWriteDelegate(NetworkVariableSerialization.WriteEnum); + NetworkVariableSerialization.SetReadDelegate(NetworkVariableSerialization.ReadEnum); + } + internal static void InitializeDelegatesPrimitive() where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable + { + NetworkVariableSerialization.SetWriteDelegate(NetworkVariableSerialization.WritePrimitive); + NetworkVariableSerialization.SetReadDelegate(NetworkVariableSerialization.ReadPrimitive); + } + + internal static void InitializeAllBaseDelegates() + { + // Built-in C# types, serialized through a generic method + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + InitializeDelegatesPrimitive(); + + // Built-in Unity types, serialized with specific overloads because they're structs without ISerializeByMemcpy attached + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Vector2 value) => { writer.WriteValueSafe(value); }); + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Vector3 value) => { writer.WriteValueSafe(value); }); + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Vector4 value) => { writer.WriteValueSafe(value); }); + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Quaternion value) => { writer.WriteValueSafe(value); }); + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Color value) => { writer.WriteValueSafe(value); }); + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Color32 value) => { writer.WriteValueSafe(value); }); + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Ray value) => { writer.WriteValueSafe(value); }); + NetworkVariableSerialization.SetWriteDelegate((FastBufferWriter writer, in Ray2D value) => { writer.WriteValueSafe(value); }); + + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Vector2 value) => { reader.ReadValueSafe(out value); }); + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Vector3 value) => { reader.ReadValueSafe(out value); }); + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Vector4 value) => { reader.ReadValueSafe(out value); }); + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Quaternion value) => { reader.ReadValueSafe(out value); }); + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Color value) => { reader.ReadValueSafe(out value); }); + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Color32 value) => { reader.ReadValueSafe(out value); }); + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Ray value) => { reader.ReadValueSafe(out value); }); + NetworkVariableSerialization.SetReadDelegate((FastBufferReader reader, out Ray2D value) => { reader.ReadValueSafe(out value); }); } } } diff --git a/Runtime/NetworkVariable/NetworkVariableSerialization.cs b/Runtime/NetworkVariable/NetworkVariableSerialization.cs new file mode 100644 index 0000000..b55aac9 --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariableSerialization.cs @@ -0,0 +1,169 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Support methods for reading/writing NetworkVariables + /// Because there are multiple overloads of WriteValue/ReadValue based on different generic constraints, + /// but there's no way to achieve the same thing with a class, this includes various read/write delegates + /// based on which constraints are met by `T`. These constraints are set up by `NetworkVariableHelpers`, + /// which is invoked by code generated by ILPP during module load. + /// (As it turns out, IL has support for a module initializer that C# doesn't expose.) + /// This installs the correct delegate for each `T` to ensure that each type is serialized properly. + /// + /// Any type that inherits from `NetworkVariableSerialization` will implicitly result in any `T` + /// passed to it being picked up and initialized by ILPP. + /// + /// The methods here, despite being static, are `protected` specifically to ensure that anything that + /// wants access to them has to inherit from this base class, thus enabling ILPP to find and initialize it. + /// + [Serializable] + public abstract class NetworkVariableSerialization : NetworkVariableBase where T : unmanaged + { + // Functions that know how to serialize INetworkSerializable + internal static void WriteNetworkSerializable(FastBufferWriter writer, in TForMethod value) + where TForMethod : unmanaged, INetworkSerializable + { + writer.WriteNetworkSerializable(value); + } + + internal static void ReadNetworkSerializable(FastBufferReader reader, out TForMethod value) + where TForMethod : unmanaged, INetworkSerializable + { + reader.ReadNetworkSerializable(out value); + } + + // Functions that serialize structs + internal static void WriteStruct(FastBufferWriter writer, in TForMethod value) + where TForMethod : unmanaged, INetworkSerializeByMemcpy + { + writer.WriteValueSafe(value); + } + internal static void ReadStruct(FastBufferReader reader, out TForMethod value) + where TForMethod : unmanaged, INetworkSerializeByMemcpy + { + reader.ReadValueSafe(out value); + } + + // Functions that serialize enums + internal static void WriteEnum(FastBufferWriter writer, in TForMethod value) + where TForMethod : unmanaged, Enum + { + writer.WriteValueSafe(value); + } + internal static void ReadEnum(FastBufferReader reader, out TForMethod value) + where TForMethod : unmanaged, Enum + { + reader.ReadValueSafe(out value); + } + + // Functions that serialize other types + internal static void WritePrimitive(FastBufferWriter writer, in TForMethod value) + where TForMethod : unmanaged, IComparable, IConvertible, IComparable, IEquatable + { + writer.WriteValueSafe(value); + } + + internal static void ReadPrimitive(FastBufferReader reader, out TForMethod value) + where TForMethod : unmanaged, IComparable, IConvertible, IComparable, IEquatable + { + reader.ReadValueSafe(out value); + } + + // Should never be reachable at runtime. All calls to this should be replaced with the correct + // call above by ILPP. + private static void WriteValue(FastBufferWriter writer, in TForMethod value) + where TForMethod : unmanaged + { + if (value is INetworkSerializable) + { + typeof(NetworkVariableHelper).GetMethod(nameof(NetworkVariableHelper.InitializeDelegatesNetworkSerializable)).MakeGenericMethod(typeof(TForMethod)).Invoke(null, null); + } + else if (value is INetworkSerializeByMemcpy) + { + typeof(NetworkVariableHelper).GetMethod(nameof(NetworkVariableHelper.InitializeDelegatesStruct)).MakeGenericMethod(typeof(TForMethod)).Invoke(null, null); + } + else if (value is Enum) + { + typeof(NetworkVariableHelper).GetMethod(nameof(NetworkVariableHelper.InitializeDelegatesEnum)).MakeGenericMethod(typeof(TForMethod)).Invoke(null, null); + } + else + { + throw new Exception($"Type {typeof(T).FullName} is not serializable - it must implement either INetworkSerializable or ISerializeByMemcpy"); + + } + NetworkVariableSerialization.Write(writer, value); + } + + private static void ReadValue(FastBufferReader reader, out TForMethod value) + where TForMethod : unmanaged + { + if (typeof(INetworkSerializable).IsAssignableFrom(typeof(TForMethod))) + { + typeof(NetworkVariableHelper).GetMethod(nameof(NetworkVariableHelper.InitializeDelegatesNetworkSerializable)).MakeGenericMethod(typeof(TForMethod)).Invoke(null, null); + } + else if (typeof(INetworkSerializeByMemcpy).IsAssignableFrom(typeof(TForMethod))) + { + typeof(NetworkVariableHelper).GetMethod(nameof(NetworkVariableHelper.InitializeDelegatesStruct)).MakeGenericMethod(typeof(TForMethod)).Invoke(null, null); + } + else if (typeof(Enum).IsAssignableFrom(typeof(TForMethod))) + { + typeof(NetworkVariableHelper).GetMethod(nameof(NetworkVariableHelper.InitializeDelegatesEnum)).MakeGenericMethod(typeof(TForMethod)).Invoke(null, null); + } + else + { + throw new Exception($"Type {typeof(T).FullName} is not serializable - it must implement either INetworkSerializable or ISerializeByMemcpy"); + + } + NetworkVariableSerialization.Read(reader, out value); + } + + protected internal delegate void WriteDelegate(FastBufferWriter writer, in TForMethod value); + + protected internal delegate void ReadDelegate(FastBufferReader reader, out TForMethod value); + + // These static delegates provide the right implementation for writing and reading a particular network variable type. + // For most types, these default to WriteValue() and ReadValue(), which perform simple memcpy operations. + // + // INetworkSerializableILPP will generate startup code that will set it to WriteNetworkSerializable() + // and ReadNetworkSerializable() for INetworkSerializable types, which will call NetworkSerialize(). + // + // In the future we may be able to use this to provide packing implementations for floats and integers to optimize bandwidth usage. + // + // The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this, + // NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable, + // *both* of which would cause a boxing allocation. Alternatively, NetworkVariable could have been split into + // NetworkVariable and NetworkSerializableVariable or something like that, which would have caused a poor + // user experience and an API that's easier to get wrong than right. This is a bit ugly on the implementation + // side, but it gets the best achievable user experience and performance. + private static WriteDelegate s_Write = WriteValue; + private static ReadDelegate s_Read = ReadValue; + + protected static void Write(FastBufferWriter writer, in T value) + { + s_Write(writer, value); + } + + protected static void Read(FastBufferReader reader, out T value) + { + s_Read(reader, out value); + } + + internal static void SetWriteDelegate(WriteDelegate write) + { + s_Write = write; + } + + internal static void SetReadDelegate(ReadDelegate read) + { + s_Read = read; + } + + protected NetworkVariableSerialization( + NetworkVariableReadPermission readPerm = DefaultReadPerm, + NetworkVariableWritePermission writePerm = DefaultWritePerm) + : base(readPerm, writePerm) + { + } + } +} diff --git a/Runtime/NetworkVariable/NetworkVariableSerialization.cs.meta b/Runtime/NetworkVariable/NetworkVariableSerialization.cs.meta new file mode 100644 index 0000000..7ab0efc --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariableSerialization.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2c6ef5fdf2e94ec3b4ce8086d52700b3 +timeCreated: 1650985453 \ No newline at end of file diff --git a/Runtime/Profiling/ProfilingHooks.cs b/Runtime/Profiling/ProfilingHooks.cs index 94c0910..10e19e8 100644 --- a/Runtime/Profiling/ProfilingHooks.cs +++ b/Runtime/Profiling/ProfilingHooks.cs @@ -82,7 +82,7 @@ public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelive return true; } - public bool OnVerifyCanReceive(ulong senderId, Type messageType) + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) { return true; } diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs index 17c8e53..c90ba04 100644 --- a/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1446,12 +1446,9 @@ private void OnClientBeginSync(uint sceneEventId) var loadSceneMode = sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive; - // Always check to see if the scene needs to be validated - if (!ValidateSceneBeforeLoading(sceneHash, loadSceneMode)) - { - EndSceneEvent(sceneEventId); - return; - } + // Store the sceneHandle and hash + sceneEventData.ClientSceneHandle = sceneHandle; + sceneEventData.ClientSceneHash = sceneHash; // If this is the beginning of the synchronization event, then send client a notification that synchronization has begun if (sceneHash == sceneEventData.SceneHash) @@ -1468,9 +1465,16 @@ private void OnClientBeginSync(uint sceneEventId) ScenePlacedObjects.Clear(); } - // Store the sceneHandle and hash - sceneEventData.ClientSceneHandle = sceneHandle; - sceneEventData.ClientSceneHash = sceneHash; + // Always check to see if the scene needs to be validated + if (!ValidateSceneBeforeLoading(sceneHash, loadSceneMode)) + { + HandleClientSceneEvent(sceneEventId); + if (m_NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogInfo($"Client declined to load the scene {sceneName}, continuing with synchronization."); + } + return; + } var shouldPassThrough = false; var sceneLoad = (AsyncOperation)null; @@ -1746,7 +1750,9 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId) // NetworkObjects m_NetworkManager.InvokeOnClientConnectedCallback(clientId); - if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization) + // Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid + // a potential crash within the MessageSystem (i.e. sending to a client that no longer exists) + if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization && m_NetworkManager.ConnectedClients.ContainsKey(clientId)) { sceneEventData.SceneEventType = SceneEventType.ReSynchronize; SendSceneEventData(sceneEventId, new ulong[] { clientId }); diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs index 65f2f03..feb3256 100644 --- a/Runtime/SceneManagement/SceneEventData.cs +++ b/Runtime/SceneManagement/SceneEventData.cs @@ -91,7 +91,7 @@ internal class SceneEventData : IDisposable { internal SceneEventType SceneEventType; internal LoadSceneMode LoadSceneMode; - internal Guid SceneEventProgressId; + internal ForceNetworkSerializeByMemcpy SceneEventProgressId; internal uint SceneEventId; diff --git a/Runtime/Serialization/BufferSerializer.cs b/Runtime/Serialization/BufferSerializer.cs index 661975c..92da24b 100644 --- a/Runtime/Serialization/BufferSerializer.cs +++ b/Runtime/Serialization/BufferSerializer.cs @@ -1,13 +1,16 @@ +using System; +using UnityEngine; + namespace Unity.Netcode { /// /// Two-way serializer wrapping FastBufferReader or FastBufferWriter. - /// + /// /// Implemented as a ref struct for two reasons: /// 1. The BufferSerializer cannot outlive the FBR/FBW it wraps or using it will cause a crash /// 2. The BufferSerializer must always be passed by reference and can't be copied /// - /// Ref structs help enforce both of those rules: they can't out live the stack context in which they were + /// Ref structs help enforce both of those rules: they can't ref live the stack context in which they were /// created, and they're always passed by reference no matter what. /// /// BufferSerializer doesn't wrapp FastBufferReader or FastBufferWriter directly because it can't. @@ -58,168 +61,63 @@ public FastBufferWriter GetFastBufferWriter() return m_Implementation.GetFastBufferWriter(); } - /// - /// Serialize an INetworkSerializable - /// - /// Throws OverflowException if the end of the buffer has been reached. - /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. - /// - /// Value to serialize - public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() - { - m_Implementation.SerializeNetworkSerializable(ref value); - } - - /// - /// Serialize a string. - /// - /// Note: Will ALWAYS allocate a new string when reading. - /// - /// Throws OverflowException if the end of the buffer has been reached. - /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. - /// - /// Value to serialize - /// - /// If true, will truncate each char to one byte. - /// This is slower than two-byte chars, but uses less bandwidth. - /// - public void SerializeValue(ref string s, bool oneByteChars = false) - { - m_Implementation.SerializeValue(ref s, oneByteChars); - } + public void SerializeValue(ref string s, bool oneByteChars = false) => m_Implementation.SerializeValue(ref s, oneByteChars); + public void SerializeValue(ref byte value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Vector2 value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Vector2[] value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Vector3 value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Vector3[] value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Vector4 value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Vector4[] value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Quaternion value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Quaternion[] value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Color value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Color[] value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Color32 value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Color32[] value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Ray value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Ray[] value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Ray2D value) => m_Implementation.SerializeValue(ref value); + public void SerializeValue(ref Ray2D[] value) => m_Implementation.SerializeValue(ref value); - /// - /// Serialize an array value. - /// - /// Note: Will ALWAYS allocate a new array when reading. - /// If you have a statically-sized array that you know is large enough, it's recommended to - /// serialize the size yourself and iterate serializing array members. - /// - /// (This is because C# doesn't allow setting an array's length value, so deserializing - /// into an existing array of larger size would result in an array that doesn't have as many values - /// as its Length indicates it should.) - /// - /// Throws OverflowException if the end of the buffer has been reached. - /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. - /// - /// Value to serialize - public void SerializeValue(ref T[] array) where T : unmanaged - { - m_Implementation.SerializeValue(ref array); - } - - /// - /// Serialize a single byte - /// - /// Throws OverflowException if the end of the buffer has been reached. - /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. - /// - /// Value to serialize - public void SerializeValue(ref byte value) - { - m_Implementation.SerializeValue(ref value); - } - - /// - /// Serialize an unmanaged type. Supports basic value types as well as structs. - /// The provided type will be copied to/from the buffer as it exists in memory. - /// - /// Throws OverflowException if the end of the buffer has been reached. - /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. - /// - /// Value to serialize - public void SerializeValue(ref T value) where T : unmanaged - { - m_Implementation.SerializeValue(ref value); - } + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() => m_Implementation.SerializeNetworkSerializable(ref value); - /// - /// Allows faster serialization by batching bounds checking. - /// When you know you will be writing multiple fields back-to-back and you know the total size, - /// you can call PreCheck() once on the total size, and then follow it with calls to - /// SerializeValuePreChecked() for faster serialization. Write buffers will grow during PreCheck() - /// if needed. - /// - /// PreChecked serialization operations will throw OverflowException in editor and development builds if you - /// go past the point you've marked using PreCheck(). In release builds, OverflowException will not be thrown - /// for performance reasons, since the point of using PreCheck is to avoid bounds checking in the following - /// operations in release builds. - /// - /// To get the correct size to check for, use FastBufferWriter.GetWriteSize(value) or - /// FastBufferWriter.GetWriteSize<type>() - /// - /// Number of bytes you plan to read or write - /// True if the read/write can proceed, false otherwise. public bool PreCheck(int amount) { return m_Implementation.PreCheck(amount); } - /// - /// Serialize a string. - /// - /// Note: Will ALWAYS allocate a new string when reading. - /// - /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only - /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple - /// serialization operations in one function call instead of having to do bounds checking on every call. - /// - /// Value to serialize - /// - /// If true, will truncate each char to one byte. - /// This is slower than two-byte chars, but uses less bandwidth. - /// - public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) - { - m_Implementation.SerializeValuePreChecked(ref s, oneByteChars); - } - - /// - /// Serialize an array value. - /// - /// Note: Will ALWAYS allocate a new array when reading. - /// If you have a statically-sized array that you know is large enough, it's recommended to - /// serialize the size yourself and iterate serializing array members. - /// - /// (This is because C# doesn't allow setting an array's length value, so deserializing - /// into an existing array of larger size would result in an array that doesn't have as many values - /// as its Length indicates it should.) - /// - /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only - /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple - /// serialization operations in one function call instead of having to do bounds checking on every call. - /// - /// Value to serialize - public void SerializeValuePreChecked(ref T[] array) where T : unmanaged - { - m_Implementation.SerializeValuePreChecked(ref array); - } - - /// - /// Serialize a single byte - /// - /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only - /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple - /// serialization operations in one function call instead of having to do bounds checking on every call. - /// - /// Value to serialize - public void SerializeValuePreChecked(ref byte value) - { - m_Implementation.SerializeValuePreChecked(ref value); - } - - /// - /// Serialize an unmanaged type. Supports basic value types as well as structs. - /// The provided type will be copied to/from the buffer as it exists in memory. - /// - /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only - /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple - /// serialization operations in one function call instead of having to do bounds checking on every call. - /// - /// Value to serialize - public void SerializeValuePreChecked(ref T value) where T : unmanaged - { - m_Implementation.SerializeValuePreChecked(ref value); - } + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) => m_Implementation.SerializeValuePreChecked(ref s, oneByteChars); + public void SerializeValuePreChecked(ref byte value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Vector2 value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Vector2[] value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Vector3 value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Vector3[] value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Vector4 value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Vector4[] value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Quaternion value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Quaternion[] value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Color value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Color[] value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Color32 value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Color32[] value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Ray value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Ray[] value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Ray2D value) => m_Implementation.SerializeValuePreChecked(ref value); + public void SerializeValuePreChecked(ref Ray2D[] value) => m_Implementation.SerializeValuePreChecked(ref value); } } diff --git a/Runtime/Serialization/BufferSerializerReader.cs b/Runtime/Serialization/BufferSerializerReader.cs index 4796b6c..7936131 100644 --- a/Runtime/Serialization/BufferSerializerReader.cs +++ b/Runtime/Serialization/BufferSerializerReader.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; namespace Unity.Netcode { @@ -24,54 +25,63 @@ public FastBufferWriter GetFastBufferWriter() throw new InvalidOperationException("Cannot retrieve a FastBufferWriter from a serializer where IsWriter = false"); } - public void SerializeValue(ref string s, bool oneByteChars = false) - { - m_Reader.ReadValueSafe(out s, oneByteChars); - } - - public void SerializeValue(ref T[] array) where T : unmanaged - { - m_Reader.ReadValueSafe(out array); - } - - public void SerializeValue(ref byte value) - { - m_Reader.ReadByteSafe(out value); - } - - public void SerializeValue(ref T value) where T : unmanaged - { - m_Reader.ReadValueSafe(out value); - } + public void SerializeValue(ref string s, bool oneByteChars = false) => m_Reader.ReadValueSafe(out s, oneByteChars); + public void SerializeValue(ref byte value) => m_Reader.ReadByteSafe(out value); + public void SerializeValue(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Reader.ReadValue(out value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Reader.ReadValue(out value); + public void SerializeValue(ref Vector2 value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Vector2[] value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Vector3 value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Vector3[] value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Vector4 value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Vector4[] value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Quaternion value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Quaternion[] value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Color value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Color[] value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Color32 value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Color32[] value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Ray value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Ray[] value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Ray2D value) => m_Reader.ReadValueSafe(out value); + public void SerializeValue(ref Ray2D[] value) => m_Reader.ReadValueSafe(out value); - public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() - { - m_Reader.ReadNetworkSerializable(out value); - } + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() => m_Reader.ReadNetworkSerializable(out value); public bool PreCheck(int amount) { return m_Reader.TryBeginRead(amount); } - public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) - { - m_Reader.ReadValue(out s, oneByteChars); - } - - public void SerializeValuePreChecked(ref T[] array) where T : unmanaged - { - m_Reader.ReadValue(out array); - } - - public void SerializeValuePreChecked(ref byte value) - { - m_Reader.ReadValue(out value); - } - - public void SerializeValuePreChecked(ref T value) where T : unmanaged - { - m_Reader.ReadValue(out value); - } + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) => m_Reader.ReadValue(out s, oneByteChars); + public void SerializeValuePreChecked(ref byte value) => m_Reader.ReadByte(out value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Vector2 value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Vector2[] value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Vector3 value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Vector3[] value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Vector4 value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Vector4[] value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Quaternion value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Quaternion[] value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Color value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Color[] value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Color32 value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Color32[] value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Ray value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Ray[] value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Ray2D value) => m_Reader.ReadValue(out value); + public void SerializeValuePreChecked(ref Ray2D[] value) => m_Reader.ReadValue(out value); } } diff --git a/Runtime/Serialization/BufferSerializerWriter.cs b/Runtime/Serialization/BufferSerializerWriter.cs index f13d989..71f56d6 100644 --- a/Runtime/Serialization/BufferSerializerWriter.cs +++ b/Runtime/Serialization/BufferSerializerWriter.cs @@ -1,4 +1,5 @@ using System; +using UnityEngine; namespace Unity.Netcode { @@ -24,25 +25,32 @@ public FastBufferWriter GetFastBufferWriter() return m_Writer; } - public void SerializeValue(ref string s, bool oneByteChars = false) - { - m_Writer.WriteValueSafe(s, oneByteChars); - } - - public void SerializeValue(ref T[] array) where T : unmanaged - { - m_Writer.WriteValueSafe(array); - } - - public void SerializeValue(ref byte value) - { - m_Writer.WriteByteSafe(value); - } - - public void SerializeValue(ref T value) where T : unmanaged - { - m_Writer.WriteValueSafe(value); - } + public void SerializeValue(ref string s, bool oneByteChars = false) => m_Writer.WriteValueSafe(s, oneByteChars); + public void SerializeValue(ref byte value) => m_Writer.WriteByteSafe(value); + public void SerializeValue(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Writer.WriteValue(value); + public void SerializeValue(ref T[] value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => m_Writer.WriteValue(value); + public void SerializeValue(ref Vector2 value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Vector2[] value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Vector3 value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Vector3[] value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Vector4 value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Vector4[] value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Quaternion value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Quaternion[] value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Color value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Color[] value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Color32 value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Color32[] value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Ray value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Ray[] value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Ray2D value) => m_Writer.WriteValueSafe(value); + public void SerializeValue(ref Ray2D[] value) => m_Writer.WriteValueSafe(value); public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() { @@ -54,24 +62,30 @@ public bool PreCheck(int amount) return m_Writer.TryBeginWrite(amount); } - public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) - { - m_Writer.WriteValue(s, oneByteChars); - } - - public void SerializeValuePreChecked(ref T[] array) where T : unmanaged - { - m_Writer.WriteValue(array); - } + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) => m_Writer.WriteValue(s, oneByteChars); + public void SerializeValuePreChecked(ref byte value) => m_Writer.WriteByte(value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => m_Writer.WriteValue(value); - public void SerializeValuePreChecked(ref byte value) - { - m_Writer.WriteByte(value); - } - - public void SerializeValuePreChecked(ref T value) where T : unmanaged - { - m_Writer.WriteValue(value); - } + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Vector2 value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Vector2[] value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Vector3 value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Vector3[] value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Vector4 value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Vector4[] value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Quaternion value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Quaternion[] value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Color value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Color[] value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Color32 value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Color32[] value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Ray value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Ray[] value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Ray2D value) => m_Writer.WriteValue(value); + public void SerializeValuePreChecked(ref Ray2D[] value) => m_Writer.WriteValue(value); } } diff --git a/Runtime/Serialization/FastBufferReader.cs b/Runtime/Serialization/FastBufferReader.cs index e56d867..4aa82ae 100644 --- a/Runtime/Serialization/FastBufferReader.cs +++ b/Runtime/Serialization/FastBufferReader.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; namespace Unity.Netcode { @@ -195,6 +196,30 @@ public unsafe FastBufferReader(FastBufferWriter writer, Allocator allocator, int Handle = CreateHandle(writer.GetUnsafePtr(), length == -1 ? writer.Length : length, offset, allocator); } + /// + /// Create a FastBufferReader from another existing FastBufferReader. This is typically used when you + /// want to change the allocator that a reader is allocated to - for example, upgrading a Temp reader to + /// a Persistent one to be processed later. + /// + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The exception to this is when the allocator passed in is Allocator.None. In this scenario, + /// ownership of the data remains with the caller and the reader will point at it directly. + /// When created with Allocator.None, FastBufferReader will allocate some internal data using + /// Allocator.Temp, so it should be treated as if it's a ref struct and not allowed to outlive + /// the context in which it was created (it should neither be returned from that function nor + /// stored anywhere in heap memory). + /// + /// The reader to copy from + /// The allocator to use + /// The number of bytes to copy (all if this is -1) + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(FastBufferReader reader, Allocator allocator, int length = -1, int offset = 0) + { + Handle = CreateHandle(reader.GetUnsafePtr(), length == -1 ? reader.Length : length, offset, allocator); + } + /// /// Frees the allocated buffer /// @@ -492,61 +517,6 @@ public unsafe void ReadValueSafe(out string s, bool oneByteChars = false) } } - /// - /// Writes an unmanaged array - /// NOTE: ALLOCATES - /// - /// Stores the read array - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void ReadValue(out T[] array) where T : unmanaged - { - ReadValue(out int sizeInTs); - int sizeInBytes = sizeInTs * sizeof(T); - array = new T[sizeInTs]; - fixed (T* native = array) - { - byte* bytes = (byte*)(native); - ReadBytes(bytes, sizeInBytes); - } - } - - /// - /// Reads an unmanaged array - /// NOTE: ALLOCATES - /// - /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking - /// for multiple reads at once by calling TryBeginRead. - /// - /// Stores the read array - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void ReadValueSafe(out T[] array) where T : unmanaged - { -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (Handle->InBitwiseContext) - { - throw new InvalidOperationException( - "Cannot use BufferReader in bytewise mode while in a bitwise context."); - } -#endif - - if (!TryBeginReadInternal(sizeof(int))) - { - throw new OverflowException("Reading past the end of the buffer"); - } - ReadValue(out int sizeInTs); - int sizeInBytes = sizeInTs * sizeof(T); - if (!TryBeginReadInternal(sizeInBytes)) - { - throw new OverflowException("Reading past the end of the buffer"); - } - array = new T[sizeInTs]; - fixed (T* native = array) - { - byte* bytes = (byte*)(native); - ReadBytes(bytes, sizeInBytes); - } - } - /// /// Read a partial value. The value is zero-initialized and then the specified number of bytes is read into it. /// @@ -711,69 +681,155 @@ public unsafe void ReadBytesSafe(ref byte[] value, int size, int offset = 0) } } - /// - /// Read a value of any unmanaged type to the buffer. - /// It will be copied from the buffer exactly as it existed in memory on the writing end. - /// - /// The read value - /// Any unmanaged type [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void ReadValue(out T value) where T : unmanaged + private unsafe void ReadUnmanaged(out T value) where T : unmanaged { - int len = sizeof(T); - -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (Handle->InBitwiseContext) - { - throw new InvalidOperationException( - "Cannot use BufferReader in bytewise mode while in a bitwise context."); - } - if (Handle->Position + len > Handle->AllowedReadMark) + fixed (T* ptr = &value) { - throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + byte* bytes = (byte*)ptr; + ReadBytes(bytes, sizeof(T)); } -#endif - + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ReadUnmanagedSafe(out T value) where T : unmanaged + { fixed (T* ptr = &value) { - UnsafeUtility.MemCpy((byte*)ptr, Handle->BufferPointer + Handle->Position, len); + byte* bytes = (byte*)ptr; + ReadBytesSafe(bytes, sizeof(T)); } - Handle->Position += len; } - - /// - /// Read a value of any unmanaged type to the buffer. - /// It will be copied from the buffer exactly as it existed in memory on the writing end. - /// - /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking - /// for multiple reads at once by calling TryBeginRead. - /// - /// The read value - /// Any unmanaged type [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void ReadValueSafe(out T value) where T : unmanaged + private unsafe void ReadUnmanaged(out T[] value) where T : unmanaged { - int len = sizeof(T); - -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (Handle->InBitwiseContext) + ReadUnmanaged(out int sizeInTs); + int sizeInBytes = sizeInTs * sizeof(T); + value = new T[sizeInTs]; + fixed (T* ptr = value) { - throw new InvalidOperationException( - "Cannot use BufferReader in bytewise mode while in a bitwise context."); + byte* bytes = (byte*)ptr; + ReadBytes(bytes, sizeInBytes); } -#endif - - if (!TryBeginReadInternal(len)) + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ReadUnmanagedSafe(out T[] value) where T : unmanaged + { + ReadUnmanagedSafe(out int sizeInTs); + int sizeInBytes = sizeInTs * sizeof(T); + value = new T[sizeInTs]; + fixed (T* ptr = value) { - throw new OverflowException("Reading past the end of the buffer"); + byte* bytes = (byte*)ptr; + ReadBytesSafe(bytes, sizeInBytes); } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => ReadUnmanaged(out value); - fixed (T* ptr = &value) - { - UnsafeUtility.MemCpy((byte*)ptr, Handle->BufferPointer + Handle->Position, len); - } - Handle->Position += len; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => ReadUnmanagedSafe(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => ReadUnmanaged(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + public void ReadValueSafe(out T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + + public void ReadValueSafe(out T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum => ReadUnmanagedSafe(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => ReadUnmanaged(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => ReadUnmanagedSafe(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => ReadNetworkSerializable(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out T[] value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => ReadNetworkSerializable(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => ReadNetworkSerializable(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out T[] value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new() => ReadNetworkSerializable(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Vector2 value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Vector2[] value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Vector3 value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Vector3[] value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Vector4 value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Vector4[] value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Quaternion value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Quaternion[] value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Color value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Color[] value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Color32 value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Color32[] value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Ray value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Ray[] value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Ray2D value) => ReadUnmanaged(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValue(out Ray2D[] value) => ReadUnmanaged(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Vector2 value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Vector2[] value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Vector3 value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Vector3[] value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Vector4 value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Vector4[] value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Quaternion value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Quaternion[] value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Color value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Color[] value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Color32 value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Color32[] value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Ray value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Ray[] value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Ray2D value) => ReadUnmanagedSafe(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueSafe(out Ray2D[] value) => ReadUnmanagedSafe(out value); } } diff --git a/Runtime/Serialization/FastBufferWriter.cs b/Runtime/Serialization/FastBufferWriter.cs index 5f8dd25..25e4577 100644 --- a/Runtime/Serialization/FastBufferWriter.cs +++ b/Runtime/Serialization/FastBufferWriter.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; namespace Unity.Netcode { @@ -528,60 +529,6 @@ public static unsafe int GetWriteSize(T[] array, int count = -1, int offset = return sizeof(int) + sizeInBytes; } - /// - /// Writes an unmanaged array - /// - /// The array to write - /// The amount of elements to write - /// Where in the array to start - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void WriteValue(T[] array, int count = -1, int offset = 0) where T : unmanaged - { - int sizeInTs = count != -1 ? count : array.Length - offset; - int sizeInBytes = sizeInTs * sizeof(T); - WriteValue(sizeInTs); - fixed (T* native = array) - { - byte* bytes = (byte*)(native + offset); - WriteBytes(bytes, sizeInBytes); - } - } - - /// - /// Writes an unmanaged array - /// - /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking - /// for multiple writes at once by calling TryBeginWrite. - /// - /// The array to write - /// The amount of elements to write - /// Where in the array to start - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void WriteValueSafe(T[] array, int count = -1, int offset = 0) where T : unmanaged - { -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (Handle->InBitwiseContext) - { - throw new InvalidOperationException( - "Cannot use BufferWriter in bytewise mode while in a bitwise context."); - } -#endif - - int sizeInTs = count != -1 ? count : array.Length - offset; - int sizeInBytes = sizeInTs * sizeof(T); - - if (!TryBeginWriteInternal(sizeInBytes + sizeof(int))) - { - throw new OverflowException("Writing past the end of the buffer"); - } - WriteValue(sizeInTs); - fixed (T* native = array) - { - byte* bytes = (byte*)(native + offset); - WriteBytes(bytes, sizeInBytes); - } - } - /// /// Write a partial value. The specified number of bytes is written from the value and the rest is ignored. /// @@ -790,68 +737,170 @@ public static unsafe int GetWriteSize() where T : unmanaged return sizeof(T); } - /// - /// Write a value of any unmanaged type (including unmanaged structs) to the buffer. - /// It will be copied into the buffer exactly as it exists in memory. - /// - /// The value to copy - /// Any unmanaged type [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void WriteValue(in T value) where T : unmanaged + private unsafe void WriteUnmanaged(in T value) where T : unmanaged { - int len = sizeof(T); - -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (Handle->InBitwiseContext) - { - throw new InvalidOperationException( - "Cannot use BufferWriter in bytewise mode while in a bitwise context."); - } - if (Handle->Position + len > Handle->AllowedWriteMark) + fixed (T* ptr = &value) { - throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + byte* bytes = (byte*)ptr; + WriteBytes(bytes, sizeof(T)); } -#endif - + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void WriteUnmanagedSafe(in T value) where T : unmanaged + { fixed (T* ptr = &value) { - UnsafeUtility.MemCpy(Handle->BufferPointer + Handle->Position, (byte*)ptr, len); + byte* bytes = (byte*)ptr; + WriteBytesSafe(bytes, sizeof(T)); } - Handle->Position += len; } - /// - /// Write a value of any unmanaged type (including unmanaged structs) to the buffer. - /// It will be copied into the buffer exactly as it exists in memory. - /// - /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking - /// for multiple writes at once by calling TryBeginWrite. - /// - /// The value to copy - /// Any unmanaged type [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void WriteValueSafe(in T value) where T : unmanaged + private unsafe void WriteUnmanaged(T[] value) where T : unmanaged { - int len = sizeof(T); - -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (Handle->InBitwiseContext) + WriteUnmanaged(value.Length); + fixed (T* ptr = value) { - throw new InvalidOperationException( - "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + byte* bytes = (byte*)ptr; + WriteBytes(bytes, sizeof(T) * value.Length); } -#endif - - if (!TryBeginWriteInternal(len)) + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void WriteUnmanagedSafe(T[] value) where T : unmanaged + { + WriteUnmanagedSafe(value.Length); + fixed (T* ptr = value) { - throw new OverflowException("Writing past the end of the buffer"); + byte* bytes = (byte*)ptr; + WriteBytesSafe(bytes, sizeof(T) * value.Length); } + } + + // These structs enable overloading of WriteValue with different generic constraints. + // The compiler's actually able to distinguish between overloads based on generic constraints. + // But at the bytecode level, the constraints aren't included in the method signature. + // By adding a second parameter with a defaulted value, the signatures of each generic are different, + // thus allowing overloads of methods based on the first parameter meeting constraints. + public struct ForPrimitives + { - fixed (T* ptr = &value) - { - UnsafeUtility.MemCpy(Handle->BufferPointer + Handle->Position, (byte*)ptr, len); - } - Handle->Position += len; } + + public struct ForEnums + { + + } + + public struct ForStructs + { + + } + + public struct ForNetworkSerializable + { + + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in T value, ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(T[] value, ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in T value, ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(T[] value, ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in T value, ForEnums unused = default) where T : unmanaged, Enum => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(T[] value, ForEnums unused = default) where T : unmanaged, Enum => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in T value, ForEnums unused = default) where T : unmanaged, Enum => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(T[] value, ForEnums unused = default) where T : unmanaged, Enum => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in T value, ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(T[] value, ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in T value, ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(T[] value, ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in T value, ForNetworkSerializable unused = default) where T : INetworkSerializable => WriteNetworkSerializable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(T[] value, ForNetworkSerializable unused = default) where T : INetworkSerializable => WriteNetworkSerializable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in T value, ForNetworkSerializable unused = default) where T : INetworkSerializable => WriteNetworkSerializable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(T[] value, ForNetworkSerializable unused = default) where T : INetworkSerializable => WriteNetworkSerializable(value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Vector2 value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Vector2[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Vector3 value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Vector3[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Vector4 value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Vector4[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Quaternion value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Quaternion[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Color value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Color[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Color32 value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Color32[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Ray value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Ray[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(in Ray2D value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValue(Ray2D[] value) => WriteUnmanaged(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Vector2 value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Vector2[] value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Vector3 value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Vector3[] value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Vector4 value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Vector4[] value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Quaternion value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Quaternion[] value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Color value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Color[] value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Color32 value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Color32[] value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Ray value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Ray[] value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(in Ray2D value) => WriteUnmanagedSafe(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueSafe(Ray2D[] value) => WriteUnmanagedSafe(value); } } diff --git a/Runtime/Serialization/ForceNetworkSerializeByMemcpy.cs b/Runtime/Serialization/ForceNetworkSerializeByMemcpy.cs new file mode 100644 index 0000000..ca181b5 --- /dev/null +++ b/Runtime/Serialization/ForceNetworkSerializeByMemcpy.cs @@ -0,0 +1,37 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// This is a wrapper that adds `INetworkSerializeByMemcpy` support to existing structs that the developer + /// doesn't have the ability to modify (for example, external structs like `Guid`). + /// + /// + public struct ForceNetworkSerializeByMemcpy : INetworkSerializeByMemcpy, IEquatable> where T : unmanaged, IEquatable + { + public T Value; + + public ForceNetworkSerializeByMemcpy(T value) + { + Value = value; + } + + public static implicit operator T(ForceNetworkSerializeByMemcpy container) => container.Value; + public static implicit operator ForceNetworkSerializeByMemcpy(T underlyingValue) => new ForceNetworkSerializeByMemcpy { Value = underlyingValue }; + + public bool Equals(ForceNetworkSerializeByMemcpy other) + { + return Value.Equals(other.Value); + } + + public override bool Equals(object obj) + { + return obj is ForceNetworkSerializeByMemcpy other && Equals(other); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + } +} diff --git a/Runtime/Serialization/ForceNetworkSerializeByMemcpy.cs.meta b/Runtime/Serialization/ForceNetworkSerializeByMemcpy.cs.meta new file mode 100644 index 0000000..6c1a5dc --- /dev/null +++ b/Runtime/Serialization/ForceNetworkSerializeByMemcpy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d56016695cd44430a345671f7d56b18e +timeCreated: 1647635768 \ No newline at end of file diff --git a/Runtime/Serialization/INetworkSerializeByMemcpy.cs b/Runtime/Serialization/INetworkSerializeByMemcpy.cs new file mode 100644 index 0000000..c1996c4 --- /dev/null +++ b/Runtime/Serialization/INetworkSerializeByMemcpy.cs @@ -0,0 +1,15 @@ + +namespace Unity.Netcode +{ + /// + /// This interface is a "tag" that can be applied to a struct to mark that struct as being serializable + /// by memcpy. It's up to the developer of the struct to analyze the struct's contents and ensure it + /// is actually serializable by memcpy. This requires all of the members of the struct to be + /// `unmanaged` Plain-Old-Data values - if your struct contains a pointer (or a type that contains a pointer, + /// like `NativeList`), it should be serialized via `INetworkSerializable` or via + /// `FastBufferReader`/`FastBufferWriter` extension methods. + /// + public interface INetworkSerializeByMemcpy + { + } +} diff --git a/Runtime/Serialization/INetworkSerializeByMemcpy.cs.meta b/Runtime/Serialization/INetworkSerializeByMemcpy.cs.meta new file mode 100644 index 0000000..694e4b0 --- /dev/null +++ b/Runtime/Serialization/INetworkSerializeByMemcpy.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 11b763f46b18465cbffb1972d737a83e +timeCreated: 1647635592 \ No newline at end of file diff --git a/Runtime/Serialization/IReaderWriter.cs b/Runtime/Serialization/IReaderWriter.cs index a50e536..5f6e0ea 100644 --- a/Runtime/Serialization/IReaderWriter.cs +++ b/Runtime/Serialization/IReaderWriter.cs @@ -1,3 +1,6 @@ +using System; +using UnityEngine; + namespace Unity.Netcode { public interface IReaderWriter @@ -9,17 +12,60 @@ public interface IReaderWriter FastBufferWriter GetFastBufferWriter(); void SerializeValue(ref string s, bool oneByteChars = false); - void SerializeValue(ref T[] array) where T : unmanaged; void SerializeValue(ref byte value); - void SerializeValue(ref T value) where T : unmanaged; + void SerializeValue(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable; + void SerializeValue(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable; + void SerializeValue(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum; + void SerializeValue(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum; + void SerializeValue(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy; + void SerializeValue(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy; + void SerializeValue(ref T value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new(); + void SerializeValue(ref T[] value, FastBufferWriter.ForNetworkSerializable unused = default) where T : INetworkSerializable, new(); + void SerializeValue(ref Vector2 value); + void SerializeValue(ref Vector2[] value); + void SerializeValue(ref Vector3 value); + void SerializeValue(ref Vector3[] value); + void SerializeValue(ref Vector4 value); + void SerializeValue(ref Vector4[] value); + void SerializeValue(ref Quaternion value); + void SerializeValue(ref Quaternion[] value); + void SerializeValue(ref Color value); + void SerializeValue(ref Color[] value); + void SerializeValue(ref Color32 value); + void SerializeValue(ref Color32[] value); + void SerializeValue(ref Ray value); + void SerializeValue(ref Ray[] value); + void SerializeValue(ref Ray2D value); + void SerializeValue(ref Ray2D[] value); // Has to have a different name to avoid conflicting with "where T: unmananged" void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new(); bool PreCheck(int amount); void SerializeValuePreChecked(ref string s, bool oneByteChars = false); - void SerializeValuePreChecked(ref T[] array) where T : unmanaged; void SerializeValuePreChecked(ref byte value); - void SerializeValuePreChecked(ref T value) where T : unmanaged; + void SerializeValuePreChecked(ref T value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable; + void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForPrimitives unused = default) where T : unmanaged, IComparable, IConvertible, IComparable, IEquatable; + void SerializeValuePreChecked(ref T value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum; + void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForEnums unused = default) where T : unmanaged, Enum; + void SerializeValuePreChecked(ref T value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy; + void SerializeValuePreChecked(ref T[] value, FastBufferWriter.ForStructs unused = default) where T : unmanaged, INetworkSerializeByMemcpy; + + void SerializeValuePreChecked(ref Vector2 value); + void SerializeValuePreChecked(ref Vector2[] value); + void SerializeValuePreChecked(ref Vector3 value); + void SerializeValuePreChecked(ref Vector3[] value); + void SerializeValuePreChecked(ref Vector4 value); + void SerializeValuePreChecked(ref Vector4[] value); + void SerializeValuePreChecked(ref Quaternion value); + void SerializeValuePreChecked(ref Quaternion[] value); + void SerializeValuePreChecked(ref Color value); + void SerializeValuePreChecked(ref Color[] value); + void SerializeValuePreChecked(ref Color32 value); + void SerializeValuePreChecked(ref Color32[] value); + void SerializeValuePreChecked(ref Ray value); + void SerializeValuePreChecked(ref Ray[] value); + void SerializeValuePreChecked(ref Ray2D value); + void SerializeValuePreChecked(ref Ray2D[] value); } } diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index fb6ae5b..8f5fe16 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Unity.Collections; using UnityEngine; namespace Unity.Netcode @@ -136,23 +135,6 @@ public List GetClientOwnedObjects(ulong clientId) return OwnershipToObjectsTable[clientId].Values.ToList(); } - - private struct TriggerData - { - public FastBufferReader Reader; - public MessageHeader Header; - public ulong SenderId; - public float Timestamp; - public int SerializedHeaderSize; - } - private struct TriggerInfo - { - public float Expiry; - public NativeList TriggerData; - } - - private readonly Dictionary m_Triggers = new Dictionary(); - /// /// Gets the NetworkManager associated with this SpawnManager. /// @@ -209,87 +191,6 @@ public NetworkObject GetPlayerNetworkObject(ulong clientId) return null; } - /// - /// Defers processing of a message until the moment a specific networkObjectId is spawned. - /// This is to handle situations where an RPC or other object-specific message arrives before the spawn does, - /// either due to it being requested in OnNetworkSpawn before the spawn call has been executed - /// - /// There is a one second maximum lifetime of triggers to avoid memory leaks. After one second has passed - /// without the requested object ID being spawned, the triggers for it are automatically deleted. - /// - internal unsafe void TriggerOnSpawn(ulong networkObjectId, FastBufferReader reader, ref NetworkContext context) - { - if (!m_Triggers.ContainsKey(networkObjectId)) - { - m_Triggers[networkObjectId] = new TriggerInfo - { - Expiry = Time.realtimeSinceStartup + 1, - TriggerData = new NativeList(Allocator.Persistent) - }; - } - - m_Triggers[networkObjectId].TriggerData.Add(new TriggerData - { - Reader = new FastBufferReader(reader.GetUnsafePtr(), Allocator.Persistent, reader.Length), - Header = context.Header, - Timestamp = context.Timestamp, - SenderId = context.SenderId, - SerializedHeaderSize = context.SerializedHeaderSize - }); - } - - /// - /// Cleans up any trigger that's existed for more than a second. - /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. - /// - internal unsafe void CleanupStaleTriggers() - { - ulong* staleKeys = stackalloc ulong[m_Triggers.Count()]; - int index = 0; - foreach (var kvp in m_Triggers) - { - if (kvp.Value.Expiry < Time.realtimeSinceStartup) - { - - staleKeys[index++] = kvp.Key; - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning($"Deferred messages were received for {nameof(NetworkObject)} #{kvp.Key}, but it did not spawn within 1 second."); - } - - foreach (var data in kvp.Value.TriggerData) - { - data.Reader.Dispose(); - } - - kvp.Value.TriggerData.Dispose(); - } - } - - for (var i = 0; i < index; ++i) - { - m_Triggers.Remove(staleKeys[i]); - } - } - /// - /// Cleans up any trigger that's existed for more than a second. - /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. - /// - internal void CleanupAllTriggers() - { - foreach (var kvp in m_Triggers) - { - foreach (var data in kvp.Value.TriggerData) - { - data.Reader.Dispose(); - } - - kvp.Value.TriggerData.Dispose(); - } - - m_Triggers.Clear(); - } - internal void RemoveOwnership(NetworkObject networkObject) { if (!NetworkManager.IsServer) @@ -382,6 +283,33 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId) } } + internal bool HasPrefab(bool isSceneObject, uint globalObjectIdHash) + { + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !isSceneObject) + { + if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) + { + return true; + } + if (NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks.TryGetValue(globalObjectIdHash, out var networkPrefab)) + { + switch (networkPrefab.Override) + { + default: + case NetworkPrefabOverride.None: + return networkPrefab.Prefab != null; + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + return networkPrefab.OverridingTargetPrefab != null; + } + } + + return false; + } + var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash); + return networkObject != null; + } + /// /// Should only run on the client /// @@ -612,20 +540,7 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkObject.InvokeBehaviourNetworkSpawn(); - // This must happen after InvokeBehaviourNetworkSpawn, otherwise ClientRPCs and other messages can be - // processed before the object is fully spawned. This must be the last thing done in the spawn process. - if (m_Triggers.ContainsKey(networkId)) - { - var triggerInfo = m_Triggers[networkId]; - foreach (var trigger in triggerInfo.TriggerData) - { - // Reader will be disposed within HandleMessage - NetworkManager.MessagingSystem.HandleMessage(trigger.Header, trigger.Reader, trigger.SenderId, trigger.Timestamp, trigger.SerializedHeaderSize); - } - - triggerInfo.TriggerData.Dispose(); - m_Triggers.Remove(networkId); - } + NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredMessageManager.TriggerType.OnSpawn, networkId); // propagate the IsSceneObject setting to child NetworkObjects var children = networkObject.GetComponentsInChildren(); diff --git a/Runtime/Timing/NetworkTime.cs b/Runtime/Timing/NetworkTime.cs index 0f0f1d9..0a6cca9 100644 --- a/Runtime/Timing/NetworkTime.cs +++ b/Runtime/Timing/NetworkTime.cs @@ -81,7 +81,10 @@ public NetworkTime(uint tickRate, int tick, double tickOffset = 0d) : this(tickRate) { Assert.IsTrue(tickOffset < 1d / tickRate); - this += tick * m_TickInterval + tickOffset; + + m_CachedTickOffset = tickOffset; + m_CachedTick = tick; + m_TimeSec = tick * m_TickInterval + tickOffset; } /// diff --git a/Runtime/Transports/UNET/UNetTransport.cs b/Runtime/Transports/UNET/UNetTransport.cs index 2d6051e..992a791 100644 --- a/Runtime/Transports/UNET/UNetTransport.cs +++ b/Runtime/Transports/UNET/UNetTransport.cs @@ -205,8 +205,8 @@ public override bool StartClient() public override bool StartServer() { var topology = new HostTopology(GetConfig(), MaxConnections); - UnityEngine.Networking.NetworkTransport.AddHost(topology, ServerListenPort, null); - return true; + // Undocumented, but AddHost returns -1 in case of any type of failure. See UNET::NetLibraryManager::AddHost + return -1 != UnityEngine.Networking.NetworkTransport.AddHost(topology, ServerListenPort, null); } public override void DisconnectRemoteClient(ulong clientId) diff --git a/Samples~/Bootstrap/Scenes/Bootstrap.unity b/Samples~/Bootstrap/Scenes/Bootstrap.unity index b253454..170f510 100644 --- a/Samples~/Bootstrap/Scenes/Bootstrap.unity +++ b/Samples~/Bootstrap/Scenes/Bootstrap.unity @@ -152,8 +152,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 6960e84d07fb87f47956e7a81d71c4e6, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_ProtocolType: 0 m_MessageBufferSize: 6144 m_ReciveQueueSize: 128 @@ -171,8 +171,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: DontDestroy: 1 RunInBackground: 1 LogLevel: 1 @@ -185,7 +185,7 @@ MonoBehaviour: TickRate: 30 ClientConnectionBufferTimeout: 10 ConnectionApproval: 0 - ConnectionData: + ConnectionData: EnableTimeResync: 0 TimeResyncInterval: 30 EnsureNetworkVariableLengthSafety: 0 @@ -195,7 +195,7 @@ MonoBehaviour: NetworkIdRecycleDelay: 120 RpcHashSize: 0 LoadSceneTimeOut: 120 - MessageBufferTimeout: 20 + SpawnTimeout: 1 EnableNetworkLogs: 1 --- !u!114 &1114774668 MonoBehaviour: @@ -207,8 +207,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5fed568ebf6c14b11928f16219b5675b, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: --- !u!4 &1114774669 Transform: m_ObjectHideFlags: 0 diff --git a/Samples~/ClientNetworkTransform.meta b/Samples~/ClientNetworkTransform.meta deleted file mode 100644 index 2ffd543..0000000 --- a/Samples~/ClientNetworkTransform.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 3106ae882c6ec416d855a44c97eeaeef -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/.sample.json b/Samples~/ClientNetworkTransform/.sample.json deleted file mode 100644 index 0c7e9fc..0000000 --- a/Samples~/ClientNetworkTransform/.sample.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "displayName": "ClientNetworkTransform", - "description": "A sample to demonstrate how client-driven NetworkTransform can be implemented" -} \ No newline at end of file diff --git a/Samples~/ClientNetworkTransform/Prefabs.meta b/Samples~/ClientNetworkTransform/Prefabs.meta deleted file mode 100644 index 875e321..0000000 --- a/Samples~/ClientNetworkTransform/Prefabs.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 6b1ef235ca94b4bbd9a6456f44c69188 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Prefabs/.gitkeep b/Samples~/ClientNetworkTransform/Prefabs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Samples~/ClientNetworkTransform/Scenes.meta b/Samples~/ClientNetworkTransform/Scenes.meta deleted file mode 100644 index cc143c0..0000000 --- a/Samples~/ClientNetworkTransform/Scenes.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 03def738b58f746408d456f1f8c99264 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Scenes/.gitkeep b/Samples~/ClientNetworkTransform/Scenes/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Samples~/ClientNetworkTransform/Scripts.meta b/Samples~/ClientNetworkTransform/Scripts.meta deleted file mode 100644 index d0f7f18..0000000 --- a/Samples~/ClientNetworkTransform/Scripts.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 749af92bd75b44951b56ea583f3f10b5 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef deleted file mode 100644 index 26e8209..0000000 --- a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "ClientNetworkTransform", - "rootNamespace": "Unity.Netcode.Samples", - "references": [ - "Unity.Netcode.Runtime", - "Unity.Netcode.Components" - ] -} diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef.meta b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef.meta deleted file mode 100644 index 977e18f..0000000 --- a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 78ac2a8d1365141f68da5d0a9e10dbc6 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs deleted file mode 100644 index 824f1a3..0000000 --- a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Unity.Netcode.Components; -using UnityEngine; - -namespace Unity.Netcode.Samples -{ - /// - /// Used for syncing a transform with client side changes. This includes host. Pure server as owner isn't supported by this. Please use NetworkTransform - /// for transforms that'll always be owned by the server. - /// - [DisallowMultipleComponent] - public class ClientNetworkTransform : NetworkTransform - { - /// - /// Used to determine who can write to this transform. Owner client only. - /// Changing this value alone will not allow you to create a NetworkTransform which can be written to by clients. - /// We're using RPCs to send updated values from client to server. Netcode doesn't support client side network variable writing. - /// This imposes state to the server. This is putting trust on your clients. Make sure no security-sensitive features use this transform. - /// - // This is public to make sure that users don't depend on this IsClient && IsOwner check in their code. If this logic changes in the future, we can make it invisible here - - public override void OnNetworkSpawn() - { - base.OnNetworkSpawn(); - CanCommitToTransform = IsOwner; - } - - protected override void Update() - { - CanCommitToTransform = IsOwner; - base.Update(); - if (NetworkManager.Singleton != null && (NetworkManager.Singleton.IsConnectedClient || NetworkManager.Singleton.IsListening)) - { - if (CanCommitToTransform) - { - TryCommitTransformToServer(transform, NetworkManager.LocalTime.Time); - } - } - } - } -} diff --git a/TestHelpers/Runtime/Transport/MessageHooks.cs b/TestHelpers/Runtime/MessageHooks.cs similarity index 96% rename from TestHelpers/Runtime/Transport/MessageHooks.cs rename to TestHelpers/Runtime/MessageHooks.cs index 5d71a0b..dceceb3 100644 --- a/TestHelpers/Runtime/Transport/MessageHooks.cs +++ b/TestHelpers/Runtime/MessageHooks.cs @@ -50,7 +50,7 @@ public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelive return true; } - public bool OnVerifyCanReceive(ulong senderId, Type messageType) + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) { return true; } diff --git a/TestHelpers/Runtime/Transport/MessageHooks.cs.meta b/TestHelpers/Runtime/MessageHooks.cs.meta similarity index 100% rename from TestHelpers/Runtime/Transport/MessageHooks.cs.meta rename to TestHelpers/Runtime/MessageHooks.cs.meta diff --git a/TestHelpers/Runtime/Transport/MessageHooksConditional.cs b/TestHelpers/Runtime/MessageHooksConditional.cs similarity index 100% rename from TestHelpers/Runtime/Transport/MessageHooksConditional.cs rename to TestHelpers/Runtime/MessageHooksConditional.cs diff --git a/TestHelpers/Runtime/Transport/MessageHooksConditional.cs.meta b/TestHelpers/Runtime/MessageHooksConditional.cs.meta similarity index 100% rename from TestHelpers/Runtime/Transport/MessageHooksConditional.cs.meta rename to TestHelpers/Runtime/MessageHooksConditional.cs.meta diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index e846914..7318e46 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -79,6 +79,15 @@ public static void DeregisterNetworkObject(ulong localClientId, ulong networkObj protected const uint k_DefaultTickRate = 30; protected abstract int NumberOfClients { get; } + /// + /// Set this to false to create the clients first. + /// Note: If you are using scene placed NetworkObjects or doing any form of scene testing and + /// get prefab hash id "soft synchronization" errors, then set this to false and run your test + /// again. This is a work-around until we can resolve some issues with NetworkManagerOwner and + /// NetworkManager.Singleton. + /// + protected bool m_CreateServerFirst = true; + public enum NetworkManagerInstatiationMode { PerTest, // This will create and destroy new NetworkManagers for each test within a child derived class @@ -108,8 +117,6 @@ public enum HostOrServer protected bool m_UseHost = true; protected int m_TargetFrameRate = 60; - protected NetcodeIntegrationTestHelpers.InstanceTransport m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.SIP; - private NetworkManagerInstatiationMode m_NetworkManagerInstatiationMode; private bool m_EnableVerboseDebug; @@ -252,7 +259,7 @@ protected void CreateServerAndClients(int numberOfClients) CreatePlayerPrefab(); // Create multiple NetworkManager instances - if (!NetcodeIntegrationTestHelpers.Create(numberOfClients, out NetworkManager server, out NetworkManager[] clients, m_TargetFrameRate, m_NetworkTransport)) + if (!NetcodeIntegrationTestHelpers.Create(numberOfClients, out NetworkManager server, out NetworkManager[] clients, m_TargetFrameRate, m_CreateServerFirst)) { Debug.LogError("Failed to create instances"); Assert.Fail("Failed to create instances"); @@ -558,6 +565,7 @@ protected void DestroySceneNetworkObjects() } if (CanDestroyNetworkObject(networkObject)) { + networkObject.NetworkManagerOwner = m_ServerNetworkManager; // Destroy the GameObject that holds the NetworkObject component Object.DestroyImmediate(networkObject.gameObject); } @@ -668,6 +676,7 @@ protected GameObject CreateNetworkObjectPrefab(string baseName) var gameObject = new GameObject(); gameObject.name = baseName; var networkObject = gameObject.AddComponent(); + networkObject.NetworkManagerOwner = m_ServerNetworkManager; NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); var networkPrefab = new NetworkPrefab() { Prefab = gameObject }; m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab); diff --git a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs index ff733d7..571558d 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs @@ -30,10 +30,16 @@ internal class MessageHandleCheckWithResult public MessageHandleCheck Check; public bool Result; } + internal class MessageReceiveCheckWithResult + { + public Type CheckType; + public bool Result; + } private class MultiInstanceHooks : INetworkHooks { public Dictionary> HandleChecks = new Dictionary>(); + public List ReceiveChecks = new List(); public static bool CheckForMessageOfType(object receivedMessage) where T : INetworkMessage { @@ -50,6 +56,15 @@ public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) { + foreach (var check in ReceiveChecks) + { + if (check.CheckType == messageType) + { + check.Result = true; + ReceiveChecks.Remove(check); + break; + } + } } public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) @@ -77,7 +92,7 @@ public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelive return true; } - public bool OnVerifyCanReceive(ulong senderId, Type messageType) + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) { return true; } @@ -107,12 +122,6 @@ public void OnAfterHandleMessage(ref T message, ref NetworkContext context) w public static List NetworkManagerInstances => s_NetworkManagerInstances; - public enum InstanceTransport - { - SIP, - UTP - } - internal static IntegrationTestSceneHandler ClientSceneHandler = null; /// @@ -162,20 +171,32 @@ public static void RegisterHandlers(NetworkManager networkManager, bool serverSi } } - /// - /// Create the correct NetworkTransport, attach it to the game object and return it. - /// Default value is SIPTransport. - /// - internal static NetworkTransport CreateInstanceTransport(InstanceTransport instanceTransport, GameObject go) + public static NetworkManager CreateServer() { - switch (instanceTransport) + // Create gameObject + var go = new GameObject("NetworkManager - Server"); + + // Create networkManager component + var server = go.AddComponent(); + NetworkManagerInstances.Insert(0, server); + + // Create transport + var unityTransport = go.AddComponent(); + // We need to increase this buffer size for tests that spawn a bunch of things + unityTransport.MaxPayloadSize = 256000; + unityTransport.MaxSendQueueSize = 1024 * 1024; + + // Allow 4 connection attempts that each will time out after 500ms + unityTransport.MaxConnectAttempts = 4; + unityTransport.ConnectTimeoutMS = 500; + + // Set the NetworkConfig + server.NetworkConfig = new NetworkConfig() { - case InstanceTransport.SIP: - return go.AddComponent(); - default: - case InstanceTransport.UTP: - return go.AddComponent(); - } + // Set transport + NetworkTransport = unityTransport + }; + return server; } /// @@ -185,24 +206,22 @@ internal static NetworkTransport CreateInstanceTransport(InstanceTransport insta /// The server NetworkManager /// The clients NetworkManagers /// The targetFrameRate of the Unity engine to use while the multi instance helper is running. Will be reset on shutdown. - public static bool Create(int clientCount, out NetworkManager server, out NetworkManager[] clients, int targetFrameRate = 60, InstanceTransport instanceTransport = InstanceTransport.SIP) + /// This determines if the server or clients will be instantiated first (defaults to server first) + public static bool Create(int clientCount, out NetworkManager server, out NetworkManager[] clients, int targetFrameRate = 60, bool serverFirst = true) { s_NetworkManagerInstances = new List(); - CreateNewClients(clientCount, out clients, instanceTransport); - - // Create gameObject - var go = new GameObject("NetworkManager - Server"); + server = null; + if (serverFirst) + { + server = CreateServer(); + } - // Create networkManager component - server = go.AddComponent(); - NetworkManagerInstances.Insert(0, server); + CreateNewClients(clientCount, out clients); - // Set the NetworkConfig - server.NetworkConfig = new NetworkConfig() + if (!serverFirst) { - // Set transport - NetworkTransport = CreateInstanceTransport(instanceTransport, go) - }; + server = CreateServer(); + } s_OriginalTargetFrameRate = Application.targetFrameRate; Application.targetFrameRate = targetFrameRate; @@ -215,7 +234,7 @@ public static bool Create(int clientCount, out NetworkManager server, out Networ /// /// The amount of clients /// - public static bool CreateNewClients(int clientCount, out NetworkManager[] clients, InstanceTransport instanceTransport = InstanceTransport.SIP) + public static bool CreateNewClients(int clientCount, out NetworkManager[] clients) { clients = new NetworkManager[clientCount]; var activeSceneName = SceneManager.GetActiveScene().name; @@ -226,11 +245,14 @@ public static bool CreateNewClients(int clientCount, out NetworkManager[] client // Create networkManager component clients[i] = go.AddComponent(); + // Create transport + var unityTransport = go.AddComponent(); + // Set the NetworkConfig clients[i].NetworkConfig = new NetworkConfig() { // Set transport - NetworkTransport = CreateInstanceTransport(instanceTransport, go) + NetworkTransport = unityTransport }; } @@ -273,7 +295,10 @@ public static void Destroy() // Destroy the network manager instances foreach (var networkManager in NetworkManagerInstances) { - Object.DestroyImmediate(networkManager.gameObject); + if (networkManager.gameObject != null) + { + Object.Destroy(networkManager.gameObject); + } } NetworkManagerInstances.Clear(); @@ -697,7 +722,35 @@ public static IEnumerator WaitForCondition(Func predicate, ResultWrapper /// The result. If null, it will fail if the predicate is not met /// The max time in seconds to wait for - internal static IEnumerator WaitForMessageOfType(NetworkManager toBeReceivedBy, ResultWrapper result = null, float timeout = 0.5f) where T : INetworkMessage + internal static IEnumerator WaitForMessageOfTypeReceived(NetworkManager toBeReceivedBy, ResultWrapper result = null, float timeout = 0.5f) where T : INetworkMessage + { + var hooks = s_Hooks[toBeReceivedBy]; + var check = new MessageReceiveCheckWithResult { CheckType = typeof(T) }; + hooks.ReceiveChecks.Add(check); + if (result == null) + { + result = new ResultWrapper(); + } + + var startTime = Time.realtimeSinceStartup; + + while (!check.Result && Time.realtimeSinceStartup - startTime < timeout) + { + yield return null; + } + + var res = check.Result; + result.Result = res; + + Assert.True(result.Result, $"Expected message {typeof(T).Name} was not received within {timeout}s."); + } + + /// + /// Waits for a message of the given type to be received + /// + /// The result. If null, it will fail if the predicate is not met + /// The max time in seconds to wait for + internal static IEnumerator WaitForMessageOfTypeHandled(NetworkManager toBeReceivedBy, ResultWrapper result = null, float timeout = 0.5f) where T : INetworkMessage { var hooks = s_Hooks[toBeReceivedBy]; if (!hooks.HandleChecks.ContainsKey(typeof(T))) @@ -712,7 +765,7 @@ internal static IEnumerator WaitForMessageOfType(NetworkManager toBeReceivedB } yield return ExecuteWaitForHook(check, result, timeout); - Assert.True(result.Result, $"Expected message {typeof(T).Name} was not received within {timeout}s."); + Assert.True(result.Result, $"Expected message {typeof(T).Name} was not handled within {timeout}s."); } /// @@ -721,7 +774,7 @@ internal static IEnumerator WaitForMessageOfType(NetworkManager toBeReceivedB /// Called for each received message to check if it's the right one /// The result. If null, it will fail if the predicate is not met /// The max time in seconds to wait for - internal static IEnumerator WaitForMessageMeetingRequirement(NetworkManager toBeReceivedBy, MessageHandleCheck requirement, ResultWrapper result = null, float timeout = DefaultTimeout) + internal static IEnumerator WaitForMessageMeetingRequirementHandled(NetworkManager toBeReceivedBy, MessageHandleCheck requirement, ResultWrapper result = null, float timeout = DefaultTimeout) { var hooks = s_Hooks[toBeReceivedBy]; if (!hooks.HandleChecks.ContainsKey(typeof(T))) @@ -736,7 +789,7 @@ internal static IEnumerator WaitForMessageMeetingRequirement(NetworkManager t } yield return ExecuteWaitForHook(check, result, timeout); - Assert.True(result.Result, $"Expected message meeting user requirements was not received within {timeout}s."); + Assert.True(result.Result, $"Expected message meeting user requirements was not handled within {timeout}s."); } private static IEnumerator ExecuteWaitForHook(MessageHandleCheckWithResult check, ResultWrapper result, float timeout) diff --git a/TestHelpers/Runtime/NetworkManagerHelper.cs b/TestHelpers/Runtime/NetworkManagerHelper.cs index 7ac99ac..a45af70 100644 --- a/TestHelpers/Runtime/NetworkManagerHelper.cs +++ b/TestHelpers/Runtime/NetworkManagerHelper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using UnityEngine; using NUnit.Framework; +using Unity.Netcode.Transports.UTP; namespace Unity.Netcode.TestHelpers.Runtime { @@ -67,11 +68,7 @@ public static bool StartNetworkManager(out NetworkManager networkManager, Networ Debug.Log($"{nameof(NetworkManager)} Instantiated."); - // NOTE: For now we only use SIPTransport for tests until UnityTransport - // has been verified working in nightly builds - // TODO-MTT-2486: Provide support for other transports once tested and verified - // working on consoles. - var sipTransport = NetworkManagerGameObject.AddComponent(); + var unityTransport = NetworkManagerGameObject.AddComponent(); if (networkConfig == null) { networkConfig = new NetworkConfig @@ -81,7 +78,7 @@ public static bool StartNetworkManager(out NetworkManager networkManager, Networ } NetworkManagerObject.NetworkConfig = networkConfig; - NetworkManagerObject.NetworkConfig.NetworkTransport = sipTransport; + NetworkManagerObject.NetworkConfig.NetworkTransport = unityTransport; // Starts the network manager in the mode specified StartNetworkManagerMode(managerMode); diff --git a/TestHelpers/Runtime/Transport.meta b/TestHelpers/Runtime/Transport.meta deleted file mode 100644 index b909388..0000000 --- a/TestHelpers/Runtime/Transport.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: d764f651f0e54e8281952933cc49be97 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/TestHelpers/Runtime/Transport/SIPTransport.cs b/TestHelpers/Runtime/Transport/SIPTransport.cs deleted file mode 100644 index 654afb3..0000000 --- a/TestHelpers/Runtime/Transport/SIPTransport.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace Unity.Netcode.TestHelpers.Runtime -{ - /// - /// SIPTransport (SIngleProcessTransport) - /// is a NetworkTransport designed to be used with multiple network instances in a single process - /// it's designed for the netcode in a way where no networking stack has to be available - /// it's designed for testing purposes and it's not designed with speed in mind - /// - public class SIPTransport : TestingNetworkTransport - { - private struct Event - { - public NetworkEvent Type; - public ulong ConnectionId; - public ArraySegment Data; - } - - private class Peer - { - public ulong ConnectionId; - public SIPTransport Transport; - public Queue IncomingBuffer = new Queue(); - } - - private readonly Dictionary m_Peers = new Dictionary(); - private ulong m_ClientsCounter = 1; - - private static Peer s_Server; - private Peer m_LocalConnection; - - public override ulong ServerClientId => 0; - public ulong LocalClientId; - - public override void DisconnectLocalClient() - { - if (m_LocalConnection != null) - { - // Inject local disconnect - m_LocalConnection.IncomingBuffer.Enqueue(new Event - { - Type = NetworkEvent.Disconnect, - ConnectionId = m_LocalConnection.ConnectionId, - Data = new ArraySegment() - }); - - if (s_Server != null && m_LocalConnection != null) - { - // Remove the connection - s_Server.Transport.m_Peers.Remove(m_LocalConnection.ConnectionId); - } - - if (m_LocalConnection.ConnectionId == ServerClientId) - { - StopServer(); - } - - // Remove the local connection - m_LocalConnection = null; - } - } - - // Called by server - public override void DisconnectRemoteClient(ulong clientId) - { - if (m_Peers.ContainsKey(clientId)) - { - // Inject disconnect into remote - m_Peers[clientId].IncomingBuffer.Enqueue(new Event - { - Type = NetworkEvent.Disconnect, - ConnectionId = clientId, - Data = new ArraySegment() - }); - - // Inject local disconnect - m_LocalConnection.IncomingBuffer.Enqueue(new Event - { - Type = NetworkEvent.Disconnect, - ConnectionId = clientId, - Data = new ArraySegment() - }); - - // Remove the local connection on remote - m_Peers[clientId].Transport.m_LocalConnection = null; - - // Remove connection on server - m_Peers.Remove(clientId); - } - } - - public override ulong GetCurrentRtt(ulong clientId) - { - // Always returns 50ms - return 50; - } - - public override void Initialize(NetworkManager networkManager = null) - { - } - - private void StopServer() - { - s_Server = null; - m_Peers.Remove(ServerClientId); - m_LocalConnection = null; - } - - public override void Shutdown() - { - // Inject disconnects to all the remotes - foreach (KeyValuePair onePeer in m_Peers) - { - onePeer.Value.IncomingBuffer.Enqueue(new Event - { - Type = NetworkEvent.Disconnect, - ConnectionId = LocalClientId, - Data = new ArraySegment() - }); - } - - if (m_LocalConnection != null && m_LocalConnection.ConnectionId == ServerClientId) - { - StopServer(); - } - - - // TODO: Cleanup - } - - public override bool StartClient() - { - if (s_Server == null) - { - // No server - Debug.LogError("No server"); - return false; - } - - if (m_LocalConnection != null) - { - // Already connected - Debug.LogError("Already connected"); - return false; - } - - // Generate an Id for the server that represents this client - ulong serverConnectionId = ++s_Server.Transport.m_ClientsCounter; - LocalClientId = serverConnectionId; - - // Create local connection - m_LocalConnection = new Peer() - { - ConnectionId = serverConnectionId, - Transport = this, - IncomingBuffer = new Queue() - }; - - // Add the server as a local connection - m_Peers.Add(ServerClientId, s_Server); - - // Add local connection as a connection on the server - s_Server.Transport.m_Peers.Add(serverConnectionId, m_LocalConnection); - - // Sends a connect message to the server - s_Server.Transport.m_LocalConnection.IncomingBuffer.Enqueue(new Event() - { - Type = NetworkEvent.Connect, - ConnectionId = serverConnectionId, - Data = new ArraySegment() - }); - - // Send a local connect message - m_LocalConnection.IncomingBuffer.Enqueue(new Event - { - Type = NetworkEvent.Connect, - ConnectionId = ServerClientId, - Data = new ArraySegment() - }); - - return true; - } - - public override bool StartServer() - { - if (s_Server != null) - { - // Can only have one server - Debug.LogError("Server already started"); - return false; - } - - if (m_LocalConnection != null) - { - // Already connected - Debug.LogError("Already connected"); - return false; - } - - // Create local connection - m_LocalConnection = new Peer() - { - ConnectionId = ServerClientId, - Transport = this, - IncomingBuffer = new Queue() - }; - - // Set the local connection as the server - s_Server = m_LocalConnection; - - m_Peers.Add(ServerClientId, s_Server); - - return true; - } - - public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) - { - if (m_LocalConnection != null) - { - // Create copy since netcode wants the byte array back straight after the method call. - // Hard on GC. - byte[] copy = new byte[payload.Count]; - Buffer.BlockCopy(payload.Array, payload.Offset, copy, 0, payload.Count); - - if (m_Peers.ContainsKey(clientId)) - { - m_Peers[clientId].IncomingBuffer.Enqueue(new Event - { - Type = NetworkEvent.Data, - ConnectionId = m_LocalConnection.ConnectionId, - Data = new ArraySegment(copy) - }); - } - } - } - - public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime) - { - if (m_LocalConnection != null) - { - if (m_LocalConnection.IncomingBuffer.Count == 0) - { - clientId = 0; - payload = new ArraySegment(); - receiveTime = 0; - return NetworkEvent.Nothing; - } - - var peerEvent = m_LocalConnection.IncomingBuffer.Dequeue(); - - clientId = peerEvent.ConnectionId; - payload = peerEvent.Data; - receiveTime = 0; - - return peerEvent.Type; - } - - clientId = 0; - payload = new ArraySegment(); - receiveTime = 0; - return NetworkEvent.Nothing; - } - } -} diff --git a/TestHelpers/Runtime/Transport/SIPTransport.cs.meta b/TestHelpers/Runtime/Transport/SIPTransport.cs.meta deleted file mode 100644 index 8c84114..0000000 --- a/TestHelpers/Runtime/Transport/SIPTransport.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 1fd1b14eba874a189f13f12d343c331c -timeCreated: 1620145176 \ No newline at end of file diff --git a/Tests/Editor/Build/BuildTestScene.unity b/Tests/Editor/Build/BuildTestScene.unity index 0b80327..efc0871 100644 --- a/Tests/Editor/Build/BuildTestScene.unity +++ b/Tests/Editor/Build/BuildTestScene.unity @@ -340,8 +340,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: DontDestroy: 1 RunInBackground: 1 LogLevel: 1 @@ -356,7 +356,7 @@ MonoBehaviour: TickRate: 30 ClientConnectionBufferTimeout: 10 ConnectionApproval: 0 - ConnectionData: + ConnectionData: EnableTimeResync: 0 TimeResyncInterval: 30 EnableNetworkVariable: 1 @@ -367,5 +367,5 @@ MonoBehaviour: NetworkIdRecycleDelay: 120 RpcHashSize: 0 LoadSceneTimeOut: 120 - MessageBufferTimeout: 20 + SpawnTimeout: 20 EnableNetworkLogs: 1 diff --git a/Tests/Editor/Build/BuildTests.cs b/Tests/Editor/Build/BuildTests.cs index 0862d80..66a2b13 100644 --- a/Tests/Editor/Build/BuildTests.cs +++ b/Tests/Editor/Build/BuildTests.cs @@ -4,6 +4,7 @@ using UnityEditor; using UnityEditor.Build.Reporting; using UnityEngine; +using UnityEngine.TestTools; namespace Unity.Netcode.EditorTests { @@ -16,13 +17,25 @@ public void BasicBuildTest() { var execAssembly = Assembly.GetExecutingAssembly(); var packagePath = UnityEditor.PackageManager.PackageInfo.FindForAssembly(execAssembly).assetPath; + var buildTarget = EditorUserBuildSettings.activeBuildTarget; + var buildTargetGroup = BuildPipeline.GetBuildTargetGroup(buildTarget); + var buildTargetSupported = BuildPipeline.IsBuildTargetSupported(buildTargetGroup, buildTarget); + var buildReport = BuildPipeline.BuildPlayer( new[] { Path.Combine(packagePath, DefaultBuildScenePath) }, Path.Combine(Path.GetDirectoryName(Application.dataPath), "Builds", nameof(BuildTests)), - EditorUserBuildSettings.activeBuildTarget, + buildTarget, BuildOptions.None ); - Assert.AreEqual(BuildResult.Succeeded, buildReport.summary.result); + + if (buildTargetSupported) + { + Assert.AreEqual(BuildResult.Succeeded, buildReport.summary.result); + } + else + { + LogAssert.Expect(LogType.Error, "Error building player because build target was unsupported"); + } } } } diff --git a/Tests/Editor/Messaging/MessageReceivingTests.cs b/Tests/Editor/Messaging/MessageReceivingTests.cs index 862a236..d8b6f37 100644 --- a/Tests/Editor/Messaging/MessageReceivingTests.cs +++ b/Tests/Editor/Messaging/MessageReceivingTests.cs @@ -9,7 +9,7 @@ namespace Unity.Netcode.EditorTests { public class MessageReceivingTests { - private struct TestMessage : INetworkMessage + private struct TestMessage : INetworkMessage, INetworkSerializeByMemcpy { public int A; public int B; diff --git a/Tests/Editor/Messaging/MessageRegistrationTests.cs b/Tests/Editor/Messaging/MessageRegistrationTests.cs index 3a6cd31..b606fd5 100644 --- a/Tests/Editor/Messaging/MessageRegistrationTests.cs +++ b/Tests/Editor/Messaging/MessageRegistrationTests.cs @@ -5,7 +5,7 @@ namespace Unity.Netcode.EditorTests { public class MessageRegistrationTests { - private struct TestMessageOne : INetworkMessage + private struct TestMessageOne : INetworkMessage, INetworkSerializeByMemcpy { public int A; public int B; @@ -25,7 +25,7 @@ public void Handle(ref NetworkContext context) } } - private struct TestMessageTwo : INetworkMessage + private struct TestMessageTwo : INetworkMessage, INetworkSerializeByMemcpy { public int A; public int B; @@ -64,7 +64,7 @@ private class TestMessageProviderOne : IMessageProvider } } - private struct TestMessageThree : INetworkMessage + private struct TestMessageThree : INetworkMessage, INetworkSerializeByMemcpy { public int A; public int B; @@ -97,7 +97,7 @@ private class TestMessageProviderTwo : IMessageProvider }; } } - private struct TestMessageFour : INetworkMessage + private struct TestMessageFour : INetworkMessage, INetworkSerializeByMemcpy { public int A; public int B; @@ -173,8 +173,6 @@ public void WhenCreatingMessageSystem_BoundTypeMessageHandlersAreRegistered() MessagingSystem.MessageHandler handlerThree = MessagingSystem.ReceiveMessage; MessagingSystem.MessageHandler handlerFour = MessagingSystem.ReceiveMessage; - var foundHandlerOne = systemOne.MessageHandlers[systemOne.GetMessageType(typeof(TestMessageOne))]; - Assert.AreEqual(handlerOne, systemOne.MessageHandlers[systemOne.GetMessageType(typeof(TestMessageOne))]); Assert.AreEqual(handlerTwo, systemOne.MessageHandlers[systemOne.GetMessageType(typeof(TestMessageTwo))]); Assert.AreEqual(handlerThree, systemTwo.MessageHandlers[systemTwo.GetMessageType(typeof(TestMessageThree))]); diff --git a/Tests/Editor/Messaging/MessageSendingTests.cs b/Tests/Editor/Messaging/MessageSendingTests.cs index 79c5629..dd1d3eb 100644 --- a/Tests/Editor/Messaging/MessageSendingTests.cs +++ b/Tests/Editor/Messaging/MessageSendingTests.cs @@ -8,7 +8,7 @@ namespace Unity.Netcode.EditorTests { public class MessageSendingTests { - private struct TestMessage : INetworkMessage + private struct TestMessage : INetworkMessage, INetworkSerializeByMemcpy { public int A; public int B; diff --git a/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs index acc4f5f..3bd0662 100644 --- a/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs +++ b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs @@ -58,7 +58,7 @@ protected enum ULongEnum : ulong C }; - protected struct TestStruct + protected struct TestStruct : INetworkSerializeByMemcpy { public byte A; public short B; @@ -80,7 +80,6 @@ public enum WriteType } #endregion - protected abstract void RunTypeTest(T valueToTest) where T : unmanaged; protected abstract void RunTypeTestSafe(T valueToTest) where T : unmanaged; @@ -114,143 +113,144 @@ protected TestStruct GetTestStruct() #endregion - public void BaseTypeTest(Type testType, WriteType writeType) - { - var random = new Random(); - void RunTypeTestLocal(T val, WriteType wt) where T : unmanaged + private void RunTestWithWriteType(T val, WriteType wt, FastBufferWriter.ForPrimitives _ = default) where T : unmanaged + { + switch (wt) { - switch (wt) - { - case WriteType.WriteDirect: - RunTypeTest(val); - break; - case WriteType.WriteSafe: - RunTypeTestSafe(val); - break; - } + case WriteType.WriteDirect: + RunTypeTest(val); + break; + case WriteType.WriteSafe: + RunTypeTestSafe(val); + break; } + } + + public void BaseTypeTest(Type testType, WriteType writeType) + { + var random = new Random(); if (testType == typeof(byte)) { - RunTypeTestLocal((byte)random.Next(), writeType); + RunTestWithWriteType((byte)random.Next(), writeType); } else if (testType == typeof(sbyte)) { - RunTypeTestLocal((sbyte)random.Next(), writeType); + RunTestWithWriteType((sbyte)random.Next(), writeType); } else if (testType == typeof(short)) { - RunTypeTestLocal((short)random.Next(), writeType); + RunTestWithWriteType((short)random.Next(), writeType); } else if (testType == typeof(ushort)) { - RunTypeTestLocal((ushort)random.Next(), writeType); + RunTestWithWriteType((ushort)random.Next(), writeType); } else if (testType == typeof(int)) { - RunTypeTestLocal((int)random.Next(), writeType); + RunTestWithWriteType((int)random.Next(), writeType); } else if (testType == typeof(uint)) { - RunTypeTestLocal((uint)random.Next(), writeType); + RunTestWithWriteType((uint)random.Next(), writeType); } else if (testType == typeof(long)) { - RunTypeTestLocal(((long)random.Next() << 32) + random.Next(), writeType); + RunTestWithWriteType(((long)random.Next() << 32) + random.Next(), writeType); } else if (testType == typeof(ulong)) { - RunTypeTestLocal(((ulong)random.Next() << 32) + (ulong)random.Next(), writeType); + RunTestWithWriteType(((ulong)random.Next() << 32) + (ulong)random.Next(), writeType); } else if (testType == typeof(bool)) { - RunTypeTestLocal(true, writeType); + RunTestWithWriteType(true, writeType); } else if (testType == typeof(char)) { - RunTypeTestLocal('a', writeType); - RunTypeTestLocal('\u263a', writeType); + RunTestWithWriteType('a', writeType); + RunTestWithWriteType('\u263a', writeType); } else if (testType == typeof(float)) { - RunTypeTestLocal((float)random.NextDouble(), writeType); + RunTestWithWriteType((float)random.NextDouble(), writeType); } else if (testType == typeof(double)) { - RunTypeTestLocal(random.NextDouble(), writeType); + RunTestWithWriteType(random.NextDouble(), writeType); } else if (testType == typeof(ByteEnum)) { - RunTypeTestLocal(ByteEnum.C, writeType); + RunTestWithWriteType(ByteEnum.C, writeType); } else if (testType == typeof(SByteEnum)) { - RunTypeTestLocal(SByteEnum.C, writeType); + RunTestWithWriteType(SByteEnum.C, writeType); } else if (testType == typeof(ShortEnum)) { - RunTypeTestLocal(ShortEnum.C, writeType); + RunTestWithWriteType(ShortEnum.C, writeType); } else if (testType == typeof(UShortEnum)) { - RunTypeTestLocal(UShortEnum.C, writeType); + RunTestWithWriteType(UShortEnum.C, writeType); } else if (testType == typeof(IntEnum)) { - RunTypeTestLocal(IntEnum.C, writeType); + RunTestWithWriteType(IntEnum.C, writeType); } else if (testType == typeof(UIntEnum)) { - RunTypeTestLocal(UIntEnum.C, writeType); + RunTestWithWriteType(UIntEnum.C, writeType); } else if (testType == typeof(LongEnum)) { - RunTypeTestLocal(LongEnum.C, writeType); + RunTestWithWriteType(LongEnum.C, writeType); } else if (testType == typeof(ULongEnum)) { - RunTypeTestLocal(ULongEnum.C, writeType); + RunTestWithWriteType(ULongEnum.C, writeType); } else if (testType == typeof(Vector2)) { - RunTypeTestLocal(new Vector2((float)random.NextDouble(), (float)random.NextDouble()), writeType); + RunTestWithWriteType(new Vector2((float)random.NextDouble(), (float)random.NextDouble()), writeType); } else if (testType == typeof(Vector3)) { - RunTypeTestLocal(new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + RunTestWithWriteType(new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); } else if (testType == typeof(Vector4)) { - RunTypeTestLocal(new Vector4((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + RunTestWithWriteType(new Vector4((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); } else if (testType == typeof(Quaternion)) { - RunTypeTestLocal(new Quaternion((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + RunTestWithWriteType(new Quaternion((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); } else if (testType == typeof(Color)) { - RunTypeTestLocal(new Color((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + RunTestWithWriteType(new Color((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); } else if (testType == typeof(Color32)) { - RunTypeTestLocal(new Color32((byte)random.Next(), (byte)random.Next(), (byte)random.Next(), (byte)random.Next()), writeType); + RunTestWithWriteType(new Color32((byte)random.Next(), (byte)random.Next(), (byte)random.Next(), (byte)random.Next()), writeType); } else if (testType == typeof(Ray)) { - RunTypeTestLocal(new Ray( + RunTestWithWriteType(new Ray( new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble())), writeType); } else if (testType == typeof(Ray2D)) { - RunTypeTestLocal(new Ray2D( + RunTestWithWriteType(new Ray2D( new Vector2((float)random.NextDouble(), (float)random.NextDouble()), new Vector2((float)random.NextDouble(), (float)random.NextDouble())), writeType); } else if (testType == typeof(TestStruct)) { - RunTypeTestLocal(GetTestStruct(), writeType); + RunTestWithWriteType(GetTestStruct(), writeType); } else { diff --git a/Tests/Editor/Serialization/FastBufferReaderTests.cs b/Tests/Editor/Serialization/FastBufferReaderTests.cs index 0c469a2..9b9ca3b 100644 --- a/Tests/Editor/Serialization/FastBufferReaderTests.cs +++ b/Tests/Editor/Serialization/FastBufferReaderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using NUnit.Framework; using Unity.Collections; using UnityEngine; @@ -51,6 +52,184 @@ private FastBufferReader CommonChecks(FastBufferWriter writer, T valueToTest, return reader; } + + private void RunWriteMethod(string methodName, FastBufferWriter writer, in T value) where T : unmanaged + { + MethodInfo method = typeof(FastBufferWriter).GetMethod(methodName, new[] { typeof(T).MakeByRefType() }); + if (method == null) + { + foreach (var candidateMethod in typeof(FastBufferWriter).GetMethods()) + { + if (candidateMethod.Name == methodName && candidateMethod.IsGenericMethodDefinition) + { + if (candidateMethod.GetParameters().Length == 0 || (candidateMethod.GetParameters().Length > 1 && !candidateMethod.GetParameters()[1].HasDefaultValue)) + { + continue; + } + if (candidateMethod.GetParameters()[0].ParameterType.IsArray) + { + continue; + } + try + { + method = candidateMethod.MakeGenericMethod(typeof(T)); + break; + } + catch (ArgumentException) + { + continue; + } + } + } + } + + Assert.NotNull(method); + + object[] args = new object[method.GetParameters().Length]; + args[0] = value; + for (var i = 1; i < args.Length; ++i) + { + args[i] = method.GetParameters()[i].DefaultValue; + } + method.Invoke(writer, args); + } + + private void RunWriteMethod(string methodName, FastBufferWriter writer, in T[] value) where T : unmanaged + { + MethodInfo method = typeof(FastBufferWriter).GetMethod(methodName, new[] { typeof(T[]) }); + if (method == null) + { + foreach (var candidateMethod in typeof(FastBufferWriter).GetMethods()) + { + if (candidateMethod.Name == methodName && candidateMethod.IsGenericMethodDefinition) + { + if (candidateMethod.GetParameters().Length == 0 || (candidateMethod.GetParameters().Length > 1 && !candidateMethod.GetParameters()[1].HasDefaultValue)) + { + continue; + } + if (!candidateMethod.GetParameters()[0].ParameterType.IsArray) + { + continue; + } + try + { + method = candidateMethod.MakeGenericMethod(typeof(T)); + break; + } + catch (ArgumentException) + { + continue; + } + } + } + } + + Assert.NotNull(method); + + object[] args = new object[method.GetParameters().Length]; + args[0] = value; + for (var i = 1; i < args.Length; ++i) + { + args[i] = method.GetParameters()[i].DefaultValue; + } + method.Invoke(writer, args); + } + + private void RunReadMethod(string methodName, FastBufferReader reader, out T value) where T : unmanaged + { + MethodInfo method = typeof(FastBufferReader).GetMethod(methodName, new[] { typeof(T).MakeByRefType() }); + if (method == null) + { + foreach (var candidateMethod in typeof(FastBufferReader).GetMethods()) + { + if (candidateMethod.Name == methodName && candidateMethod.IsGenericMethodDefinition) + { + if (candidateMethod.GetParameters().Length == 0 || (candidateMethod.GetParameters().Length > 1 && !candidateMethod.GetParameters()[1].HasDefaultValue)) + { + continue; + } + if (candidateMethod.GetParameters()[0].ParameterType.IsArray) + { + continue; + } + try + { + method = candidateMethod.MakeGenericMethod(typeof(T)); + break; + } + catch (ArgumentException) + { + continue; + } + } + } + } + value = new T(); + + Assert.NotNull(method); + + object[] args = new object[method.GetParameters().Length]; + args[0] = value; + for (var i = 1; i < args.Length; ++i) + { + args[i] = method.GetParameters()[i].DefaultValue; + } + method.Invoke(reader, args); + value = (T)args[0]; + } + + private void RunReadMethod(string methodName, FastBufferReader reader, out T[] value) where T : unmanaged + { + MethodInfo method = null; + + try + { + method = typeof(FastBufferReader).GetMethod(methodName, new[] { typeof(T[]).MakeByRefType() }); + } + catch (AmbiguousMatchException) + { + // skip. + } + if (method == null) + { + foreach (var candidateMethod in typeof(FastBufferReader).GetMethods()) + { + if (candidateMethod.Name == methodName && candidateMethod.IsGenericMethodDefinition) + { + if (candidateMethod.GetParameters().Length == 0 || (candidateMethod.GetParameters().Length > 1 && !candidateMethod.GetParameters()[1].HasDefaultValue)) + { + continue; + } + if (!candidateMethod.GetParameters()[0].ParameterType.HasElementType || !candidateMethod.GetParameters()[0].ParameterType.GetElementType().IsArray) + { + continue; + } + try + { + method = candidateMethod.MakeGenericMethod(typeof(T)); + break; + } + catch (ArgumentException) + { + continue; + } + } + } + } + + Assert.NotNull(method); + + value = new T[] { }; + + object[] args = new object[method.GetParameters().Length]; + args[0] = value; + for (var i = 1; i < args.Length; ++i) + { + args[i] = method.GetParameters()[i].DefaultValue; + } + method.Invoke(reader, args); + value = (T[])args[0]; + } #endregion #region Generic Checks @@ -66,14 +245,14 @@ protected override unsafe void RunTypeTest(T valueToTest) var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; - writer.WriteValue(valueToTest); + RunWriteMethod(nameof(FastBufferWriter.WriteValue), writer, valueToTest); var reader = CommonChecks(writer, valueToTest, writeSize, failMessage); using (reader) { Assert.IsTrue(reader.TryBeginRead(FastBufferWriter.GetWriteSize())); - reader.ReadValue(out T result); + RunReadMethod(nameof(FastBufferReader.ReadValue), reader, out T result); Assert.AreEqual(valueToTest, result); } } @@ -89,14 +268,14 @@ protected override unsafe void RunTypeTestSafe(T valueToTest) var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; - writer.WriteValueSafe(valueToTest); + RunWriteMethod(nameof(FastBufferWriter.WriteValueSafe), writer, valueToTest); var reader = CommonChecks(writer, valueToTest, writeSize, failMessage); using (reader) { - reader.ReadValueSafe(out T result); + RunReadMethod(nameof(FastBufferReader.ReadValueSafe), reader, out T result); Assert.AreEqual(valueToTest, result); } } @@ -121,7 +300,7 @@ protected override unsafe void RunTypeArrayTest(T[] valueToTest) Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); - writer.WriteValue(valueToTest); + RunWriteMethod(nameof(FastBufferWriter.WriteValue), writer, valueToTest); WriteCheckBytes(writer, writeSize); @@ -131,7 +310,7 @@ protected override unsafe void RunTypeArrayTest(T[] valueToTest) VerifyPositionAndLength(reader, writer.Length); Assert.IsTrue(reader.TryBeginRead(writeSize)); - reader.ReadValue(out T[] result); + RunReadMethod(nameof(FastBufferReader.ReadValue), reader, out T[] result); VerifyArrayEquality(valueToTest, result, 0); VerifyCheckBytes(reader, writeSize); @@ -147,7 +326,7 @@ protected override unsafe void RunTypeArrayTestSafe(T[] valueToTest) { Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); - writer.WriteValueSafe(valueToTest); + RunWriteMethod(nameof(FastBufferWriter.WriteValueSafe), writer, valueToTest); WriteCheckBytes(writer, writeSize); @@ -156,7 +335,7 @@ protected override unsafe void RunTypeArrayTestSafe(T[] valueToTest) { VerifyPositionAndLength(reader, writer.Length); - reader.ReadValueSafe(out T[] result); + RunReadMethod(nameof(FastBufferReader.ReadValueSafe), reader, out T[] result); VerifyArrayEquality(valueToTest, result, 0); VerifyCheckBytes(reader, writeSize); diff --git a/Tests/Editor/Serialization/FastBufferWriterTests.cs b/Tests/Editor/Serialization/FastBufferWriterTests.cs index 1512c50..7adad15 100644 --- a/Tests/Editor/Serialization/FastBufferWriterTests.cs +++ b/Tests/Editor/Serialization/FastBufferWriterTests.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using NUnit.Framework; using Unity.Collections; using UnityEngine; @@ -10,6 +11,7 @@ public class FastBufferWriterTests : BaseFastBufferReaderWriterTest { #region Common Checks + private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "") { Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission"); @@ -63,9 +65,94 @@ private unsafe void CommonChecks(FastBufferWriter writer, T valueToTest, int VerifyTypedEquality(valueToTest, writer.GetUnsafePtr()); } + #endregion #region Generic Checks + + private void RunMethod(string methodName, FastBufferWriter writer, in T value) where T : unmanaged + { + MethodInfo method = typeof(FastBufferWriter).GetMethod(methodName, new[] { typeof(T).MakeByRefType() }); + if (method == null) + { + foreach (var candidateMethod in typeof(FastBufferWriter).GetMethods()) + { + if (candidateMethod.Name == methodName && candidateMethod.IsGenericMethodDefinition) + { + if (candidateMethod.GetParameters().Length == 0 || (candidateMethod.GetParameters().Length > 1 && !candidateMethod.GetParameters()[1].HasDefaultValue)) + { + continue; + } + if (candidateMethod.GetParameters()[0].ParameterType.IsArray) + { + continue; + } + try + { + method = candidateMethod.MakeGenericMethod(typeof(T)); + break; + } + catch (ArgumentException) + { + continue; + } + } + } + } + + Assert.NotNull(method); + + object[] args = new object[method.GetParameters().Length]; + args[0] = value; + for (var i = 1; i < args.Length; ++i) + { + args[i] = method.GetParameters()[i].DefaultValue; + } + method.Invoke(writer, args); + } + + private void RunMethod(string methodName, FastBufferWriter writer, in T[] value) where T : unmanaged + { + MethodInfo method = typeof(FastBufferWriter).GetMethod(methodName, new[] { typeof(T[]) }); + if (method == null) + { + foreach (var candidateMethod in typeof(FastBufferWriter).GetMethods()) + { + if (candidateMethod.Name == methodName && candidateMethod.IsGenericMethodDefinition) + { + if (candidateMethod.GetParameters().Length == 0 || (candidateMethod.GetParameters().Length > 1 && !candidateMethod.GetParameters()[1].HasDefaultValue)) + { + continue; + } + if (!candidateMethod.GetParameters()[0].ParameterType.IsArray) + { + continue; + } + try + { + method = candidateMethod.MakeGenericMethod(typeof(T)); + break; + } + catch (ArgumentException) + { + continue; + } + } + } + } + + Assert.NotNull(method); + + object[] args = new object[method.GetParameters().Length]; + args[0] = value; + for (var i = 1; i < args.Length; ++i) + { + args[i] = method.GetParameters()[i].DefaultValue; + } + method.Invoke(writer, args); + } + + protected override unsafe void RunTypeTest(T valueToTest) { var writeSize = FastBufferWriter.GetWriteSize(valueToTest); @@ -82,7 +169,7 @@ protected override unsafe void RunTypeTest(T valueToTest) var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; - writer.WriteValue(valueToTest); + RunMethod(nameof(FastBufferWriter.WriteValue), writer, valueToTest); CommonChecks(writer, valueToTest, writeSize, failMessage); } @@ -98,7 +185,7 @@ protected override unsafe void RunTypeTestSafe(T valueToTest) var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; - writer.WriteValueSafe(valueToTest); + RunMethod(nameof(FastBufferWriter.WriteValueSafe), writer, valueToTest); CommonChecks(writer, valueToTest, writeSize, failMessage); } @@ -129,7 +216,7 @@ protected override unsafe void RunTypeArrayTest(T[] valueToTest) Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); - writer.WriteValue(valueToTest); + RunMethod(nameof(FastBufferWriter.WriteValue), writer, valueToTest); VerifyPositionAndLength(writer, writeSize); WriteCheckBytes(writer, writeSize); @@ -150,7 +237,7 @@ protected override unsafe void RunTypeArrayTestSafe(T[] valueToTest) Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); - writer.WriteValueSafe(valueToTest); + RunMethod(nameof(FastBufferWriter.WriteValueSafe), writer, valueToTest); VerifyPositionAndLength(writer, writeSize); WriteCheckBytes(writer, writeSize); diff --git a/Tests/Editor/Transports/UNetTransportTests.cs b/Tests/Editor/Transports/UNetTransportTests.cs new file mode 100644 index 0000000..f8d7a46 --- /dev/null +++ b/Tests/Editor/Transports/UNetTransportTests.cs @@ -0,0 +1,49 @@ +#if UNITY_UNET_PRESENT +#pragma warning disable 618 // disable is obsolete +using NUnit.Framework; +using Unity.Netcode.Transports.UNET; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.EditorTests +{ + public class UNetTransportTests + { + [Test] + public void StartServerReturnsFalseOnFailure() + { + UNetTransport unet1 = null; + UNetTransport unet2 = null; + + try + { + // Arrange + + // We're expecting an error from UNET, but don't care to validate the specific message + LogAssert.ignoreFailingMessages = true; + + var go = new GameObject(); + unet1 = go.AddComponent(); + unet1.ServerListenPort = 1; + unet1.Initialize(); + unet1.StartServer(); + unet2 = go.AddComponent(); + unet2.ServerListenPort = 1; + unet2.Initialize(); + + // Act + var result = unet2.StartServer(); + + // Assert + Assert.IsFalse(result, "UNET fails to initialize against port already in use"); + } + finally + { + unet1?.Shutdown(); + unet2?.Shutdown(); + } + } + } +} +#pragma warning restore 618 +#endif diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs.meta b/Tests/Editor/Transports/UNetTransportTests.cs.meta similarity index 83% rename from Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs.meta rename to Tests/Editor/Transports/UNetTransportTests.cs.meta index a19c929..99744ac 100644 --- a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs.meta +++ b/Tests/Editor/Transports/UNetTransportTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 54c9647dc784a46bca664910f182491e +guid: 6e328ef8f7c9b46538253a1b39dc8a97 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Tests/Editor/com.unity.netcode.editortests.asmdef b/Tests/Editor/com.unity.netcode.editortests.asmdef index 49fd726..36642d5 100644 --- a/Tests/Editor/com.unity.netcode.editortests.asmdef +++ b/Tests/Editor/com.unity.netcode.editortests.asmdef @@ -24,6 +24,11 @@ "name": "com.unity.multiplayer.tools", "expression": "", "define": "MULTIPLAYER_TOOLS" + }, + { + "name": "Unity", + "expression": "(0,2022.2.0a5)", + "define": "UNITY_UNET_PRESENT" } ] } \ No newline at end of file diff --git a/Tests/Runtime/ClientOnlyConnectionTests.cs b/Tests/Runtime/ClientOnlyConnectionTests.cs index 97ea137..cef0899 100644 --- a/Tests/Runtime/ClientOnlyConnectionTests.cs +++ b/Tests/Runtime/ClientOnlyConnectionTests.cs @@ -3,11 +3,7 @@ using UnityEngine; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; -#if UNITY_UNET_PRESENT -using Unity.Netcode.Transports.UNET; -#else using Unity.Netcode.Transports.UTP; -#endif namespace Unity.Netcode.RuntimeTests { @@ -26,15 +22,13 @@ public void Setup() m_NetworkManagerGameObject = new GameObject(); m_ClientNetworkManager = m_NetworkManagerGameObject.AddComponent(); m_ClientNetworkManager.NetworkConfig = new NetworkConfig(); -#if UNITY_UNET_PRESENT - m_TimeoutHelper = new TimeoutHelper(30); - m_ClientNetworkManager.NetworkConfig.NetworkTransport = m_NetworkManagerGameObject.AddComponent(); -#else // Default is 1000ms per connection attempt and 60 connection attempts (60s) // Currently there is no easy way to set these values other than in-editor - m_TimeoutHelper = new TimeoutHelper(70); - m_ClientNetworkManager.NetworkConfig.NetworkTransport = m_NetworkManagerGameObject.AddComponent(); -#endif + var unityTransport = m_NetworkManagerGameObject.AddComponent(); + unityTransport.ConnectTimeoutMS = 1000; + unityTransport.MaxConnectAttempts = 1; + m_TimeoutHelper = new TimeoutHelper(2); + m_ClientNetworkManager.NetworkConfig.NetworkTransport = unityTransport; } [UnityTest] @@ -46,10 +40,9 @@ public IEnumerator ClientFailsToConnect() // Only start the client (so it will timeout) m_ClientNetworkManager.StartClient(); -#if !UNITY_UNET_PRESENT // Unity Transport throws an error when it times out LogAssert.Expect(LogType.Error, "Failed to connect to server."); -#endif + yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => m_WasDisconnected, m_TimeoutHelper); Assert.False(m_TimeoutHelper.TimedOut, "Timed out waiting for client to timeout waiting to connect!"); diff --git a/Tests/Runtime/DeferredMessagingTests.cs b/Tests/Runtime/DeferredMessagingTests.cs new file mode 100644 index 0000000..6ac05fb --- /dev/null +++ b/Tests/Runtime/DeferredMessagingTests.cs @@ -0,0 +1,1225 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode.TestHelpers.Runtime; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + internal class TestDeferredMessageManager : DeferredMessageManager + { + public bool DeferMessageCalled; + public bool ProcessTriggersCalled; + + public delegate void BeforePurgeDelegate(TestDeferredMessageManager manager, ulong key); + public delegate void BeforeDeferDelegate(TestDeferredMessageManager manager, ulong key); + + public BeforePurgeDelegate OnBeforePurge; + public BeforeDeferDelegate OnBeforeDefer; + + // The way that OnBeforePurge gets used in these tests involves counting things that are still in the + // deferred message queue. This is something the base class doesn't support so the way it purges individual + // messages does not make it safe to access structures during purging. This set contains a list of + // keys that are unsafe to access because they've been purged/disposed, but not removed from the dictionary + // yet, so that we can avoid accessing those and record them instead as 0 (which is what they will be once + // the purge completes) + private HashSet m_PurgedKeys = new HashSet(); + + public TestDeferredMessageManager(NetworkManager networkManager) : + base(networkManager) + { + } + + public void ClearCallFlags() + { + DeferMessageCalled = false; + ProcessTriggersCalled = false; + } + + public int DeferredMessageCountTotal() + { + int ret = 0; + foreach (var kvp in m_Triggers) + { + ret += DeferredMessageCountForType(kvp.Key); + } + + return ret; + } + + public int DeferredMessageCountForType(IDeferredMessageManager.TriggerType trigger) + { + var count = 0; + if (m_Triggers.TryGetValue(trigger, out var dict)) + { + foreach (var kvp in dict) + { + if (m_PurgedKeys.Contains(kvp.Key)) + { + continue; + } + count += kvp.Value.TriggerData.Length; + } + } + + return count; + } + + public int DeferredMessageCountForKey(IDeferredMessageManager.TriggerType trigger, ulong key) + { + if (m_PurgedKeys.Contains(key)) + { + return 0; + } + if (m_Triggers.TryGetValue(trigger, out var dict)) + { + if (dict.TryGetValue(key, out var subdict)) + { + return subdict.TriggerData.Length; + } + } + + return 0; + } + + public override void DeferMessage(IDeferredMessageManager.TriggerType trigger, ulong key, FastBufferReader reader, ref NetworkContext context) + { + OnBeforeDefer?.Invoke(this, key); + DeferMessageCalled = true; + base.DeferMessage(trigger, key, reader, ref context); + } + + public override void ProcessTriggers(IDeferredMessageManager.TriggerType trigger, ulong key) + { + ProcessTriggersCalled = true; + base.ProcessTriggers(trigger, key); + } + + protected override void PurgeTrigger(IDeferredMessageManager.TriggerType triggerType, ulong key, TriggerInfo triggerInfo) + { + OnBeforePurge?.Invoke(this, key); + base.PurgeTrigger(triggerType, key, triggerInfo); + m_PurgedKeys.Add(key); + } + + public override void CleanupStaleTriggers() + { + base.CleanupStaleTriggers(); + m_PurgedKeys.Clear(); + } + } + + internal class SpawnCatcher : INetworkHooks + { + public struct TriggerData + { + public FastBufferReader Reader; + public MessageHeader Header; + public ulong SenderId; + public float Timestamp; + public int SerializedHeaderSize; + } + public readonly List CaughtMessages = new List(); + + public void OnBeforeSendMessage(ulong clientId, ref T message, NetworkDelivery delivery) where T : INetworkMessage + { + } + + public void OnAfterSendMessage(ulong clientId, ref T message, NetworkDelivery delivery, int messageSizeBytes) where T : INetworkMessage + { + } + + public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) + { + return true; + } + + public bool OnVerifyCanReceive(ulong senderId, Type messageType, FastBufferReader messageContent, ref NetworkContext context) + { + if (messageType == typeof(CreateObjectMessage)) + { + CaughtMessages.Add(new TriggerData + { + Reader = new FastBufferReader(messageContent, Allocator.Persistent), + Header = context.Header, + Timestamp = context.Timestamp, + SenderId = context.SenderId, + SerializedHeaderSize = context.SerializedHeaderSize + }); + return false; + } + + return true; + } + + public void OnBeforeHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage + { + } + + public void OnAfterHandleMessage(ref T message, ref NetworkContext context) where T : INetworkMessage + { + } + } + + public class DeferredMessageTestRpcComponent : NetworkBehaviour + { + public bool ClientRpcCalled; + + [ClientRpc] + public void SendTestClientRpc() + { + ClientRpcCalled = true; + } + } + + public class DeferredMessageTestNetworkVariableComponent : NetworkBehaviour + { + public NetworkVariable TestNetworkVariable = new NetworkVariable(); + } + + public class DeferredMessageTestRpcAndNetworkVariableComponent : NetworkBehaviour + { + public bool ClientRpcCalled; + + [ClientRpc] + public void SendTestClientRpc() + { + ClientRpcCalled = true; + } + + public NetworkVariable TestNetworkVariable = new NetworkVariable(); + } + + public class DeferredMessagingTest : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + private List m_ClientSpawnCatchers = new List(); + + private GameObject m_RpcPrefab; + private GameObject m_NetworkVariablePrefab; + private GameObject m_RpcAndNetworkVariablePrefab; + + protected override IEnumerator OnSetup() + { + // Host is irrelevant, messages don't get sent to the host "client" + m_UseHost = false; + + m_RpcPrefab = new GameObject("Object With RPC"); + var networkObject = m_RpcPrefab.AddComponent(); + m_RpcPrefab.AddComponent(); + + // Make it a prefab + NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); + + m_NetworkVariablePrefab = new GameObject("Object With NetworkVariable"); + networkObject = m_NetworkVariablePrefab.AddComponent(); + m_NetworkVariablePrefab.AddComponent(); + + // Make it a prefab + NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); + + m_RpcAndNetworkVariablePrefab = new GameObject("Object With NetworkVariable And RPC"); + networkObject = m_RpcAndNetworkVariablePrefab.AddComponent(); + m_RpcAndNetworkVariablePrefab.AddComponent(); + + // Make it a prefab + NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); + + // Replace the IDeferredMessageManager component with our test one in the component factory + ComponentFactory.Register(networkManager => new TestDeferredMessageManager(networkManager)); + yield return null; + } + + protected override IEnumerator OnTearDown() + { + // Revert the IDeferredMessageManager component to its default (DeferredMessageManager) + ComponentFactory.Deregister(); + m_ClientSpawnCatchers.Clear(); + yield return null; + } + + protected override void OnServerAndClientsCreated() + { + m_ServerNetworkManager.AddNetworkPrefab(m_RpcPrefab); + m_ServerNetworkManager.AddNetworkPrefab(m_NetworkVariablePrefab); + m_ServerNetworkManager.AddNetworkPrefab(m_RpcAndNetworkVariablePrefab); + m_ServerNetworkManager.NetworkConfig.ForceSamePrefabs = false; + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.ForceSamePrefabs = false; + } + } + + private T GetComponentForClient(ulong clientId) where T : NetworkBehaviour + { + foreach (var component in Object.FindObjectsOfType()) + { + if (component.IsSpawned && component.NetworkManager.LocalClientId == clientId) + { + return component; + } + } + + return null; + } + + private void CatchSpawns() + { + foreach (var client in m_ClientNetworkManagers) + { + var catcher = new SpawnCatcher(); + m_ClientSpawnCatchers.Add(catcher); + client.MessagingSystem.Hook(catcher); + } + } + + private void RegisterClientPrefabs(bool clearTestDeferredMessageManagerCallFlags = true) + { + foreach (var client in m_ClientNetworkManagers) + { + client.AddNetworkPrefab(m_RpcPrefab); + client.AddNetworkPrefab(m_NetworkVariablePrefab); + client.AddNetworkPrefab(m_RpcAndNetworkVariablePrefab); + } + + if (clearTestDeferredMessageManagerCallFlags) + { + ClearTestDeferredMessageManagerCallFlags(); + } + } + + private void ReleaseSpawns() + { + for (var i = 0; i < m_ClientNetworkManagers.Length; ++i) + { + // Unhook first so the spawn catcher stops catching spawns + m_ClientNetworkManagers[i].MessagingSystem.Unhook(m_ClientSpawnCatchers[i]); + foreach (var caughtSpawn in m_ClientSpawnCatchers[i].CaughtMessages) + { + // Reader will be disposed within HandleMessage + m_ClientNetworkManagers[i].MessagingSystem.HandleMessage(caughtSpawn.Header, caughtSpawn.Reader, caughtSpawn.SenderId, caughtSpawn.Timestamp, caughtSpawn.SerializedHeaderSize); + } + } + m_ClientSpawnCatchers.Clear(); + } + + protected override IEnumerator OnServerAndClientsConnected() + { + // Clear out these values from whatever might have set them during the initial startup. + ClearTestDeferredMessageManagerCallFlags(); + yield return null; + } + + private IEnumerator WaitForClientsToCatchSpawns(int count = 1) + { + yield return WaitForConditionOrTimeOut(() => + { + foreach (var catcher in m_ClientSpawnCatchers) + { + if (catcher.CaughtMessages.Count != count) + { + return false; + } + } + + return true; + }); + } + + private void ClearTestDeferredMessageManagerCallFlags() + { + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.ClearCallFlags(); + } + } + + private void AssertSpawnTriggerCountForObject(TestDeferredMessageManager manager, GameObject serverObject, int expectedCount = 1) + { + Assert.AreEqual(expectedCount, manager.DeferredMessageCountTotal()); + Assert.AreEqual(expectedCount, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(expectedCount, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); + } + + private static CoroutineRunner s_CoroutineRunner; + + private Coroutine Run(IEnumerator enumerator) + { + if (s_CoroutineRunner == null) + { + s_CoroutineRunner = new GameObject(nameof(CoroutineRunner)).AddComponent(); + } + + return s_CoroutineRunner.StartCoroutine(enumerator); + } + + private IEnumerator RunMultiple(List waitFor) + { + yield return WaitMultiple(StartMultiple(waitFor)); + } + + private List StartMultiple(List waitFor) + { + var runningCoroutines = new List(); + foreach (var enumerator in waitFor) + { + runningCoroutines.Add(Run(enumerator)); + } + + return runningCoroutines; + } + + private IEnumerator WaitMultiple(List runningCoroutines) + { + foreach (var coroutine in runningCoroutines) + { + yield return coroutine; + } + } + + private List WaitForAllClientsToReceive() where T : INetworkMessage + { + var waiters = new List(); + foreach (var client in m_ClientNetworkManagers) + { + waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); + } + + return waiters; + } + + private List WaitForAllClientsToReceive() + where TFirstMessage : INetworkMessage + where TSecondMessage : INetworkMessage + { + var waiters = new List(); + foreach (var client in m_ClientNetworkManagers) + { + waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); + waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); + } + + return waiters; + } + + private List WaitForAllClientsToReceive() + where TFirstMessage : INetworkMessage + where TSecondMessage : INetworkMessage + where TThirdMessage : INetworkMessage + where TFourthMessage : INetworkMessage + { + var waiters = new List(); + foreach (var client in m_ClientNetworkManagers) + { + waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); + waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); + waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); + waiters.Add(NetcodeIntegrationTestHelpers.WaitForMessageOfTypeReceived(client)); + } + + return waiters; + } + + [UnityTest] + public IEnumerator WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred() + { + RegisterClientPrefabs(); + CatchSpawns(); + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + serverObject.GetComponent().SendTestClientRpc(); + + yield return RunMultiple(WaitForAllClientsToReceive()); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + AssertSpawnTriggerCountForObject(manager, serverObject); + } + } + + [UnityTest] + public IEnumerator WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred() + { + RegisterClientPrefabs(); + CatchSpawns(); + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + serverObject.GetComponent().Despawn(false); + + yield return RunMultiple(WaitForAllClientsToReceive()); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + AssertSpawnTriggerCountForObject(manager, serverObject); + } + } + + [UnityTest] + public IEnumerator WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred() + { + RegisterClientPrefabs(); + CatchSpawns(); + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + yield return RunMultiple(WaitForAllClientsToReceive()); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + AssertSpawnTriggerCountForObject(manager, serverObject); + } + } + + [UnityTest] + public IEnumerator WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred() + { + RegisterClientPrefabs(); + CatchSpawns(); + + // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too + // Depending on timing, if we start this after spawning, we may end up missing the first one. + var waiters = WaitForAllClientsToReceive(); + var coroutines = StartMultiple(waiters); + + var serverObject = Object.Instantiate(m_NetworkVariablePrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + serverObject.GetComponent().TestNetworkVariable.Value = 1; + + yield return WaitMultiple(coroutines); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + // TODO: Network Variables generate an extra message immediately at spawn for some reason... + // Seems like a bug since the network variable data is in the spawn message already. + AssertSpawnTriggerCountForObject(manager, serverObject, 2); + } + } + + [UnityTest] + public IEnumerator WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred() + { + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + + yield return RunMultiple(WaitForAllClientsToReceive()); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + Assert.AreEqual(1, manager.DeferredMessageCountTotal()); + Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnAddPrefab, serverObject.GetComponent().GlobalObjectIdHash)); + + var component = GetComponentForClient(client.LocalClientId); + Assert.IsNull(component); + } + } + + [UnityTest] + public IEnumerator WhenAnRpcIsDeferred_ItIsProcessedOnSpawn() + { + yield return WhenAnRpcArrivesBeforeASpawnArrives_ItIsDeferred(); + ReleaseSpawns(); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + Assert.IsTrue(manager.ProcessTriggersCalled); + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + + var component = GetComponentForClient(client.LocalClientId); + Assert.IsTrue(component.ClientRpcCalled); + } + } + + [UnityTest] + public IEnumerator WhenADespawnIsDeferred_ItIsProcessedOnSpawn() + { + yield return WhenADespawnArrivesBeforeASpawnArrives_ItIsDeferred(); + ReleaseSpawns(); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + Assert.IsTrue(manager.ProcessTriggersCalled); + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + + // Should have been immediately despawned. + var component = GetComponentForClient(client.LocalClientId); + Assert.IsTrue(!component); + } + } + + [UnityTest] + public IEnumerator WhenAChangeOwnershipMessageIsDeferred_ItIsProcessedOnSpawn() + { + yield return WhenAChangeOwnershipMessageArrivesBeforeASpawnArrives_ItIsDeferred(); + ReleaseSpawns(); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.ProcessTriggersCalled); + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + + var component = GetComponentForClient(client.LocalClientId); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, component.OwnerClientId); + } + } + + [UnityTest] + public IEnumerator WhenANetworkVariableDeltaMessageIsDeferred_ItIsProcessedOnSpawn() + { + yield return WhenANetworkVariableDeltaMessageArrivesBeforeASpawnArrives_ItIsDeferred(); + ReleaseSpawns(); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.ProcessTriggersCalled); + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + + var component = GetComponentForClient(client.LocalClientId); + Assert.AreEqual(1, component.TestNetworkVariable.Value); + } + } + + [UnityTest] + public IEnumerator WhenASpawnMessageIsDeferred_ItIsProcessedOnAddPrefab() + { + yield return WhenASpawnMessageArrivesBeforeThePrefabIsAvailable_ItIsDeferred(); + RegisterClientPrefabs(false); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.ProcessTriggersCalled); + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + + var component = GetComponentForClient(client.LocalClientId); + Assert.IsNotNull(component); + } + } + + [UnityTest] + public IEnumerator WhenMultipleSpawnTriggeredMessagesAreDeferred_TheyAreAllProcessedOnSpawn() + { + RegisterClientPrefabs(); + CatchSpawns(); + + // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too + // Depending on timing, if we start this after spawning, we may end up missing the first one. + var waiters = WaitForAllClientsToReceive(); + var coroutines = StartMultiple(waiters); + + var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + serverObject.GetComponent().SendTestClientRpc(); + serverObject.GetComponent().TestNetworkVariable.Value = 1; + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + // Should be received in order so we'll wait for the last one. + yield return WaitMultiple(coroutines); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + + Assert.AreEqual(4, manager.DeferredMessageCountTotal()); + Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); + } + ReleaseSpawns(); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.ProcessTriggersCalled); + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + + var component = GetComponentForClient(client.LocalClientId); + Assert.IsTrue(component.ClientRpcCalled); + Assert.AreEqual(1, component.TestNetworkVariable.Value); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, component.OwnerClientId); + } + } + + [UnityTest] + public IEnumerator WhenMultipleAddPrefabTriggeredMessagesAreDeferred_TheyAreAllProcessedOnAddNetworkPrefab() + { + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + + var serverObject2 = Object.Instantiate(m_RpcPrefab); + serverObject2.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject2.GetComponent().Spawn(); + + yield return RunMultiple(WaitForAllClientsToReceive()); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + + Assert.AreEqual(2, manager.DeferredMessageCountTotal()); + Assert.AreEqual(0, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); + Assert.AreEqual(2, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnAddPrefab, serverObject.GetComponent().GlobalObjectIdHash)); + } + + RegisterClientPrefabs(false); + + foreach (var client in m_ClientNetworkManagers) + { + var found1 = false; + var found2 = false; + foreach (var component in Object.FindObjectsOfType()) + { + if (component.IsSpawned && component.NetworkManager.LocalClientId == client.LocalClientId) + { + if (component.NetworkObjectId == serverObject.GetComponent().NetworkObjectId) + { + found1 = true; + } + else if (component.NetworkObjectId == serverObject2.GetComponent().NetworkObjectId) + { + found2 = true; + } + } + } + + Assert.IsTrue(found1); + Assert.IsTrue(found2); + + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.ProcessTriggersCalled); + + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + } + } + + [UnityTest] + public IEnumerator WhenSpawnTriggeredMessagesAreDeferredBeforeThePrefabIsAdded_AddingThePrefabCausesThemToBeProcessed() + { + // Because we're not waiting for the client to receive the spawn before we change the network variable value, + // there's only one NetworkVariableDeltaMessage this time. + var waiters = WaitForAllClientsToReceive(); + var coroutines = StartMultiple(waiters); + + var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + + serverObject.GetComponent().SendTestClientRpc(); + serverObject.GetComponent().TestNetworkVariable.Value = 1; + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + yield return WaitMultiple(coroutines); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.DeferMessageCalled); + Assert.IsFalse(manager.ProcessTriggersCalled); + Assert.AreEqual(4, manager.DeferredMessageCountTotal()); + Assert.AreEqual(3, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(3, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnAddPrefab)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnAddPrefab, serverObject.GetComponent().GlobalObjectIdHash)); + } + + RegisterClientPrefabs(false); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.IsTrue(manager.ProcessTriggersCalled); + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + + var component = GetComponentForClient(client.LocalClientId); + Assert.NotNull(component); + Assert.IsTrue(component.ClientRpcCalled); + Assert.AreEqual(1, component.TestNetworkVariable.Value); + Assert.AreEqual(m_ClientNetworkManagers[0].LocalClientId, component.OwnerClientId); + } + } + + [UnityTest] + public IEnumerator WhenAMessageIsDeferredForMoreThanTheConfiguredTime_ItIsRemoved([Values(1, 2, 3)] int timeout) + { + RegisterClientPrefabs(); + CatchSpawns(); + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.SpawnTimeout = timeout; + } + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + var start = 0f; + + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforeDeferDelegate beforeDefer = (manager, key) => + { + if (start == 0) + { + start = Time.realtimeSinceStartup; + } + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforeDefer = beforeDefer; + } + + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + yield return WaitForAllClientsToReceive(); + + foreach (var unused in m_ClientNetworkManagers) + { + LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); + } + + int purgeCount = 0; + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => + { + ++purgeCount; + var elapsed = Time.realtimeSinceStartup - start; + Assert.GreaterOrEqual(elapsed, timeout - 0.05f); + Assert.AreEqual(1, manager.DeferredMessageCountTotal()); + Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); + Assert.AreEqual(serverObject.GetComponent().NetworkObjectId, key); + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforePurge = beforePurge; + } + + yield return new WaitForSeconds(timeout + 0.1f); + + Assert.AreEqual(NumberOfClients, purgeCount); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + } + } + + [UnityTest] + public IEnumerator WhenMultipleMessagesForTheSameObjectAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout) + { + RegisterClientPrefabs(); + CatchSpawns(); + // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too + // Depending on timing, if we start this after spawning, we may end up missing the first one. + var waiters = WaitForAllClientsToReceive(); + var coroutines = StartMultiple(waiters); + + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.SpawnTimeout = timeout; + } + var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + var start = 0f; + + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforeDeferDelegate beforeDefer = (manager, key) => + { + if (start == 0) + { + start = Time.realtimeSinceStartup; + } + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforeDefer = beforeDefer; + } + + serverObject.GetComponent().SendTestClientRpc(); + serverObject.GetComponent().TestNetworkVariable.Value = 1; + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + yield return WaitMultiple(coroutines); + + foreach (var unused in m_ClientNetworkManagers) + { + LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); + } + + int purgeCount = 0; + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => + { + ++purgeCount; + var elapsed = Time.realtimeSinceStartup - start; + Assert.GreaterOrEqual(elapsed, timeout - 0.05f); + Assert.AreEqual(4, manager.DeferredMessageCountTotal()); + Assert.AreEqual(4, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); + Assert.AreEqual(serverObject.GetComponent().NetworkObjectId, key); + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforePurge = beforePurge; + } + + yield return new WaitForSeconds(timeout + 0.1f); + + Assert.AreEqual(NumberOfClients, purgeCount); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + } + } + + [UnityTest] + public IEnumerator WhenMultipleMessagesForDifferentObjectsAreDeferredForMoreThanTheConfiguredTime_TheyAreAllRemoved([Values(1, 2, 3)] int timeout) + { + RegisterClientPrefabs(); + CatchSpawns(); + + // Have to start these before spawning here because spawning sends a NetworkVariableDeltaMessage, too + // Depending on timing, if we start this after spawning, we may end up missing the first one. + var waiters = WaitForAllClientsToReceive(); + waiters.AddRange(WaitForAllClientsToReceive()); + var coroutines = StartMultiple(waiters); + + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.SpawnTimeout = timeout; + } + var serverObject = Object.Instantiate(m_RpcAndNetworkVariablePrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + + var serverObject2 = Object.Instantiate(m_RpcAndNetworkVariablePrefab); + serverObject2.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject2.GetComponent().Spawn(); + + yield return WaitForClientsToCatchSpawns(2); + + var start = 0f; + + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforeDeferDelegate beforeDefer = (manager, key) => + { + if (start == 0) + { + start = Time.realtimeSinceStartup; + } + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforeDefer = beforeDefer; + } + + serverObject.GetComponent().SendTestClientRpc(); + serverObject.GetComponent().TestNetworkVariable.Value = 1; + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + serverObject2.GetComponent().SendTestClientRpc(); + serverObject2.GetComponent().TestNetworkVariable.Value = 1; + serverObject2.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + yield return WaitMultiple(coroutines); + + foreach (var unused in m_ClientNetworkManagers) + { + LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); + LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject2.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); + } + + int purgeCount = 0; + foreach (var client in m_ClientNetworkManagers) + { + var remainingMessagesTotalThisClient = 8; + TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => + { + ++purgeCount; + var elapsed = Time.realtimeSinceStartup - start; + Assert.GreaterOrEqual(elapsed, timeout - 0.05f); + Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountTotal()); + Assert.AreEqual(remainingMessagesTotalThisClient, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(4, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); + remainingMessagesTotalThisClient -= 4; + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforePurge = beforePurge; + } + + yield return new WaitForSeconds(timeout + 0.1f); + + Assert.AreEqual(NumberOfClients * 2, purgeCount); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + } + } + + [UnityTest] + public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForSameObjectAreRemoved([Values(1, 2, 3)] int timeout) + { + RegisterClientPrefabs(); + CatchSpawns(); + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.SpawnTimeout = timeout; + } + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(); + + var start = 0f; + + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforeDeferDelegate beforeDefer = (manager, key) => + { + if (start == 0) + { + start = Time.realtimeSinceStartup; + } + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforeDefer = beforeDefer; + } + + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + yield return RunMultiple(WaitForAllClientsToReceive()); + + yield return new WaitForSeconds(timeout - 0.5f); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(1, manager.DeferredMessageCountTotal()); + Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + } + + serverObject.GetComponent().ChangeOwnership(m_ServerNetworkManager.LocalClientId); + yield return RunMultiple(WaitForAllClientsToReceive()); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(2, manager.DeferredMessageCountTotal()); + Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(2, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + } + + foreach (var unused in m_ClientNetworkManagers) + { + LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); + } + + int purgeCount = 0; + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => + { + ++purgeCount; + var elapsed = Time.realtimeSinceStartup - start; + Assert.GreaterOrEqual(elapsed, timeout - 0.05f); + Assert.AreEqual(2, manager.DeferredMessageCountTotal()); + Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(2, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, key)); + Assert.AreEqual(serverObject.GetComponent().NetworkObjectId, key); + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforePurge = beforePurge; + } + + yield return new WaitForSeconds(0.6f); + + Assert.AreEqual(NumberOfClients, purgeCount); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(0, manager.DeferredMessageCountTotal()); + } + } + + [UnityTest] + public IEnumerator WhenADeferredMessageIsRemoved_OtherMessagesForDifferentObjectsAreNotRemoved([Values(1, 2, 3)] int timeout) + { + RegisterClientPrefabs(); + CatchSpawns(); + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.SpawnTimeout = timeout; + } + var serverObject = Object.Instantiate(m_RpcPrefab); + serverObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject.GetComponent().Spawn(); + var serverObject2 = Object.Instantiate(m_RpcPrefab); + serverObject2.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; + serverObject2.GetComponent().Spawn(); + yield return WaitForClientsToCatchSpawns(2); + + var start = 0f; + + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforeDeferDelegate beforeDefer = (manager, key) => + { + if (start == 0) + { + start = Time.realtimeSinceStartup; + } + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforeDefer = beforeDefer; + } + + serverObject.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + yield return RunMultiple(WaitForAllClientsToReceive()); + + yield return new WaitForSeconds(timeout - 0.5f); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(1, manager.DeferredMessageCountTotal()); + Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(0, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject2.GetComponent().NetworkObjectId)); + } + + serverObject2.GetComponent().ChangeOwnership(m_ServerNetworkManager.LocalClientId); + yield return RunMultiple(WaitForAllClientsToReceive()); + + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(2, manager.DeferredMessageCountTotal()); + Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject2.GetComponent().NetworkObjectId)); + } + + foreach (var unused in m_ClientNetworkManagers) + { + LogAssert.Expect(LogType.Warning, $"[Netcode] Deferred messages were received for a trigger of type {IDeferredMessageManager.TriggerType.OnSpawn} with key {serverObject.GetComponent().NetworkObjectId}, but that trigger was not received within within {timeout} second(s)."); + } + + int purgeCount = 0; + foreach (var client in m_ClientNetworkManagers) + { + TestDeferredMessageManager.BeforePurgeDelegate beforePurge = (manager, key) => + { + ++purgeCount; + var elapsed = Time.realtimeSinceStartup - start; + Assert.GreaterOrEqual(elapsed, timeout - 0.05f); + Assert.AreEqual(2, manager.DeferredMessageCountTotal()); + Assert.AreEqual(2, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject2.GetComponent().NetworkObjectId)); + + Assert.AreEqual(serverObject.GetComponent().NetworkObjectId, key); + }; + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforePurge = beforePurge; + } + + yield return new WaitForSeconds(0.6f); + + Assert.AreEqual(NumberOfClients, purgeCount); + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + Assert.AreEqual(1, manager.DeferredMessageCountTotal()); + Assert.AreEqual(1, manager.DeferredMessageCountForType(IDeferredMessageManager.TriggerType.OnSpawn)); + Assert.AreEqual(0, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject.GetComponent().NetworkObjectId)); + Assert.AreEqual(1, manager.DeferredMessageCountForKey(IDeferredMessageManager.TriggerType.OnSpawn, serverObject2.GetComponent().NetworkObjectId)); + } + foreach (var client in m_ClientNetworkManagers) + { + var manager = (TestDeferredMessageManager)client.DeferredMessageManager; + manager.OnBeforePurge = null; + } + } + } +} diff --git a/Tests/Runtime/DeferredMessagingTests.cs.meta b/Tests/Runtime/DeferredMessagingTests.cs.meta new file mode 100644 index 0000000..7dff7f8 --- /dev/null +++ b/Tests/Runtime/DeferredMessagingTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b3d771dc2a334464ad96e8c66ac776cc +timeCreated: 1650295945 \ No newline at end of file diff --git a/Tests/Runtime/DisconnectTests.cs b/Tests/Runtime/DisconnectTests.cs index 2fb7541..7590769 100644 --- a/Tests/Runtime/DisconnectTests.cs +++ b/Tests/Runtime/DisconnectTests.cs @@ -9,6 +9,8 @@ namespace Unity.Netcode.RuntimeTests { public class DisconnectTests { + + private bool m_ClientDisconnected; [UnityTest] public IEnumerator RemoteDisconnectPlayerObjectCleanup() { @@ -38,11 +40,14 @@ public IEnumerator RemoteDisconnectPlayerObjectCleanup() yield return NetcodeIntegrationTestHelpers.WaitForClientConnectedToServer(server); // disconnect the remote client + m_ClientDisconnected = false; server.DisconnectClient(clients[0].LocalClientId); + clients[0].OnClientDisconnectCallback += OnClientDisconnectCallback; + var timeoutHelper = new TimeoutHelper(); + yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => m_ClientDisconnected, timeoutHelper); - // wait 1 frame because destroys are delayed - var nextFrameNumber = Time.frameCount + 1; - yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + // We need to do this to remove other associated client properties/values from NetcodeIntegrationTestHelpers + NetcodeIntegrationTestHelpers.StopOneClient(clients[0]); // ensure the object was destroyed Assert.False(server.SpawnManager.SpawnedObjects.Any(x => x.Value.IsPlayerObject && x.Value.OwnerClientId == clients[0].LocalClientId)); @@ -50,5 +55,10 @@ public IEnumerator RemoteDisconnectPlayerObjectCleanup() // cleanup NetcodeIntegrationTestHelpers.Destroy(); } + + private void OnClientDisconnectCallback(ulong obj) + { + m_ClientDisconnected = true; + } } } diff --git a/Tests/Runtime/Messaging/NamedMessageTests.cs b/Tests/Runtime/Messaging/NamedMessageTests.cs index 63dd7d3..f2cde70 100644 --- a/Tests/Runtime/Messaging/NamedMessageTests.cs +++ b/Tests/Runtime/Messaging/NamedMessageTests.cs @@ -27,7 +27,7 @@ protected override NetworkManagerInstatiationMode OnSetIntegrationTestMode() public IEnumerator NamedMessageIsReceivedOnClientWithContent() { var messageName = Guid.NewGuid().ToString(); - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { @@ -39,7 +39,7 @@ public IEnumerator NamedMessageIsReceivedOnClientWithContent() } ulong receivedMessageSender = 0; - var receivedMessageContent = new Guid(); + var receivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); FirstClient.CustomMessagingManager.RegisterNamedMessageHandler( messageName, (ulong sender, FastBufferReader reader) => @@ -51,7 +51,7 @@ public IEnumerator NamedMessageIsReceivedOnClientWithContent() yield return new WaitForSeconds(0.2f); - Assert.AreEqual(messageContent, receivedMessageContent); + Assert.AreEqual(messageContent.Value, receivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, receivedMessageSender); } @@ -59,7 +59,7 @@ public IEnumerator NamedMessageIsReceivedOnClientWithContent() public IEnumerator NamedMessageIsReceivedOnMultipleClientsWithContent() { var messageName = Guid.NewGuid().ToString(); - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { @@ -71,7 +71,7 @@ public IEnumerator NamedMessageIsReceivedOnMultipleClientsWithContent() } ulong firstReceivedMessageSender = 0; - var firstReceivedMessageContent = new Guid(); + var firstReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); FirstClient.CustomMessagingManager.RegisterNamedMessageHandler( messageName, (ulong sender, FastBufferReader reader) => @@ -82,7 +82,7 @@ public IEnumerator NamedMessageIsReceivedOnMultipleClientsWithContent() }); ulong secondReceivedMessageSender = 0; - var secondReceivedMessageContent = new Guid(); + var secondReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); SecondClient.CustomMessagingManager.RegisterNamedMessageHandler( messageName, (ulong sender, FastBufferReader reader) => @@ -94,10 +94,10 @@ public IEnumerator NamedMessageIsReceivedOnMultipleClientsWithContent() yield return new WaitForSeconds(0.2f); - Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(messageContent.Value, firstReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); - Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(messageContent.Value, secondReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); } @@ -105,7 +105,7 @@ public IEnumerator NamedMessageIsReceivedOnMultipleClientsWithContent() public IEnumerator WhenSendingNamedMessageToAll_AllClientsReceiveIt() { var messageName = Guid.NewGuid().ToString(); - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { @@ -114,7 +114,7 @@ public IEnumerator WhenSendingNamedMessageToAll_AllClientsReceiveIt() } ulong firstReceivedMessageSender = 0; - var firstReceivedMessageContent = new Guid(); + var firstReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); FirstClient.CustomMessagingManager.RegisterNamedMessageHandler( messageName, (ulong sender, FastBufferReader reader) => @@ -125,7 +125,7 @@ public IEnumerator WhenSendingNamedMessageToAll_AllClientsReceiveIt() }); ulong secondReceivedMessageSender = 0; - var secondReceivedMessageContent = new Guid(); + var secondReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); SecondClient.CustomMessagingManager.RegisterNamedMessageHandler( messageName, (ulong sender, FastBufferReader reader) => @@ -137,10 +137,10 @@ public IEnumerator WhenSendingNamedMessageToAll_AllClientsReceiveIt() yield return new WaitForSeconds(0.2f); - Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(messageContent.Value, firstReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); - Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(messageContent.Value, secondReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); } @@ -148,7 +148,7 @@ public IEnumerator WhenSendingNamedMessageToAll_AllClientsReceiveIt() public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrown() { var messageName = Guid.NewGuid().ToString(); - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { diff --git a/Tests/Runtime/Messaging/UnnamedMessageTests.cs b/Tests/Runtime/Messaging/UnnamedMessageTests.cs index 569c5ff..711e7cf 100644 --- a/Tests/Runtime/Messaging/UnnamedMessageTests.cs +++ b/Tests/Runtime/Messaging/UnnamedMessageTests.cs @@ -19,7 +19,7 @@ public class UnnamedMessageTests : NetcodeIntegrationTest [UnityTest] public IEnumerator UnnamedMessageIsReceivedOnClientWithContent() { - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { @@ -30,7 +30,7 @@ public IEnumerator UnnamedMessageIsReceivedOnClientWithContent() } ulong receivedMessageSender = 0; - var receivedMessageContent = new Guid(); + var receivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); FirstClient.CustomMessagingManager.OnUnnamedMessage += (ulong sender, FastBufferReader reader) => { @@ -41,14 +41,14 @@ public IEnumerator UnnamedMessageIsReceivedOnClientWithContent() yield return new WaitForSeconds(0.2f); - Assert.AreEqual(messageContent, receivedMessageContent); + Assert.AreEqual(messageContent.Value, receivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, receivedMessageSender); } [UnityTest] public IEnumerator UnnamedMessageIsReceivedOnMultipleClientsWithContent() { - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { @@ -59,7 +59,7 @@ public IEnumerator UnnamedMessageIsReceivedOnMultipleClientsWithContent() } ulong firstReceivedMessageSender = 0; - var firstReceivedMessageContent = new Guid(); + var firstReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); FirstClient.CustomMessagingManager.OnUnnamedMessage += (ulong sender, FastBufferReader reader) => { @@ -69,7 +69,7 @@ public IEnumerator UnnamedMessageIsReceivedOnMultipleClientsWithContent() }; ulong secondReceivedMessageSender = 0; - var secondReceivedMessageContent = new Guid(); + var secondReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); SecondClient.CustomMessagingManager.OnUnnamedMessage += (ulong sender, FastBufferReader reader) => { @@ -80,17 +80,17 @@ public IEnumerator UnnamedMessageIsReceivedOnMultipleClientsWithContent() yield return new WaitForSeconds(0.2f); - Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(messageContent.Value, firstReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); - Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(messageContent.Value, secondReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); } [UnityTest] public IEnumerator WhenSendingUnnamedMessageToAll_AllClientsReceiveIt() { - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { @@ -99,7 +99,7 @@ public IEnumerator WhenSendingUnnamedMessageToAll_AllClientsReceiveIt() } ulong firstReceivedMessageSender = 0; - var firstReceivedMessageContent = new Guid(); + var firstReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); FirstClient.CustomMessagingManager.OnUnnamedMessage += (ulong sender, FastBufferReader reader) => { @@ -109,7 +109,7 @@ public IEnumerator WhenSendingUnnamedMessageToAll_AllClientsReceiveIt() }; ulong secondReceivedMessageSender = 0; - var secondReceivedMessageContent = new Guid(); + var secondReceivedMessageContent = new ForceNetworkSerializeByMemcpy(new Guid()); SecondClient.CustomMessagingManager.OnUnnamedMessage += (ulong sender, FastBufferReader reader) => { @@ -120,17 +120,17 @@ public IEnumerator WhenSendingUnnamedMessageToAll_AllClientsReceiveIt() yield return new WaitForSeconds(0.2f); - Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(messageContent.Value, firstReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); - Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(messageContent.Value, secondReceivedMessageContent.Value); Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); } [Test] public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrown() { - var messageContent = Guid.NewGuid(); + var messageContent = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { diff --git a/Tests/Runtime/Metrics/MessagingMetricsTests.cs b/Tests/Runtime/Metrics/MessagingMetricsTests.cs index 082c6b6..06b51c1 100644 --- a/Tests/Runtime/Metrics/MessagingMetricsTests.cs +++ b/Tests/Runtime/Metrics/MessagingMetricsTests.cs @@ -27,12 +27,12 @@ public IEnumerator TrackNetworkMessageSentMetric() { var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NetworkMessageSent); - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), FirstClient.LocalClientId, writer); } yield return waitForMetricValues.WaitForMetricsReceived(); @@ -47,12 +47,12 @@ public IEnumerator TrackNetworkMessageSentMetric() public IEnumerator TrackNetworkMessageSentMetricToMultipleClients() { var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NetworkMessageSent); - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, writer); } @@ -65,10 +65,10 @@ public IEnumerator TrackNetworkMessageSentMetricToMultipleClients() [UnityTest] public IEnumerator TrackNetworkMessageReceivedMetric() { - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); LogAssert.Expect(LogType.Log, $"Received from {Server.LocalClientId}"); - FirstClient.CustomMessagingManager.RegisterNamedMessageHandler(messageName.ToString(), (ulong sender, FastBufferReader payload) => + FirstClient.CustomMessagingManager.RegisterNamedMessageHandler(messageName.Value.ToString(), (ulong sender, FastBufferReader payload) => { Debug.Log($"Received from {sender}"); }); @@ -79,7 +79,7 @@ public IEnumerator TrackNetworkMessageReceivedMetric() { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), FirstClient.LocalClientId, writer); } yield return waitForMetricValues.WaitForMetricsReceived(); @@ -94,12 +94,12 @@ public IEnumerator TrackNamedMessageSentMetric() { var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NamedMessageSent); - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), FirstClient.LocalClientId, writer); } @@ -109,7 +109,7 @@ public IEnumerator TrackNamedMessageSentMetric() Assert.AreEqual(1, namedMessageSentMetricValues.Count); var namedMessageSent = namedMessageSentMetricValues.First(); - Assert.AreEqual(messageName.ToString(), namedMessageSent.Name); + Assert.AreEqual(messageName.Value.ToString(), namedMessageSent.Name); Assert.AreEqual(FirstClient.LocalClientId, namedMessageSent.Connection.Id); Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + k_NamedMessageOverhead, namedMessageSent.BytesCount); } @@ -118,12 +118,12 @@ public IEnumerator TrackNamedMessageSentMetric() public IEnumerator TrackNamedMessageSentMetricToMultipleClients() { var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NamedMessageSent); - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, writer); } @@ -131,7 +131,7 @@ public IEnumerator TrackNamedMessageSentMetricToMultipleClients() var namedMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); Assert.AreEqual(2, namedMessageSentMetricValues.Count); - Assert.That(namedMessageSentMetricValues.Select(x => x.Name), Has.All.EqualTo(messageName.ToString())); + Assert.That(namedMessageSentMetricValues.Select(x => x.Name), Has.All.EqualTo(messageName.Value.ToString())); Assert.That(namedMessageSentMetricValues.Select(x => x.BytesCount), Has.All.EqualTo(FastBufferWriter.GetWriteSize(messageName) + k_NamedMessageOverhead)); } @@ -139,12 +139,12 @@ public IEnumerator TrackNamedMessageSentMetricToMultipleClients() public IEnumerator TrackNamedMessageSentMetricToSelf() { var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NamedMessageSent); - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), Server.LocalClientId, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), Server.LocalClientId, writer); } yield return waitForMetricValues.WaitForMetricsReceived(); @@ -157,10 +157,10 @@ public IEnumerator TrackNamedMessageReceivedMetric() { var waitForMetricValues = new WaitForEventMetricValues(FirstClientMetrics.Dispatcher, NetworkMetricTypes.NamedMessageReceived); - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); LogAssert.Expect(LogType.Log, $"Received from {Server.LocalClientId}"); - FirstClient.CustomMessagingManager.RegisterNamedMessageHandler(messageName.ToString(), (ulong sender, FastBufferReader payload) => + FirstClient.CustomMessagingManager.RegisterNamedMessageHandler(messageName.Value.ToString(), (ulong sender, FastBufferReader payload) => { Debug.Log($"Received from {sender}"); }); @@ -169,7 +169,7 @@ public IEnumerator TrackNamedMessageReceivedMetric() { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), FirstClient.LocalClientId, writer); } @@ -179,7 +179,7 @@ public IEnumerator TrackNamedMessageReceivedMetric() Assert.AreEqual(1, namedMessageReceivedValues.Count); var namedMessageReceived = namedMessageReceivedValues.First(); - Assert.AreEqual(messageName.ToString(), namedMessageReceived.Name); + Assert.AreEqual(messageName.Value.ToString(), namedMessageReceived.Name); Assert.AreEqual(Server.LocalClientId, namedMessageReceived.Connection.Id); Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + k_NamedMessageOverhead, namedMessageReceived.BytesCount); } @@ -187,7 +187,7 @@ public IEnumerator TrackNamedMessageReceivedMetric() [UnityTest] public IEnumerator TrackUnnamedMessageSentMetric() { - var message = Guid.NewGuid(); + var message = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { writer.WriteValueSafe(message); @@ -211,7 +211,7 @@ public IEnumerator TrackUnnamedMessageSentMetric() [UnityTest] public IEnumerator TrackUnnamedMessageSentMetricToMultipleClients() { - var message = Guid.NewGuid(); + var message = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.UnnamedMessageSent); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { @@ -236,7 +236,7 @@ public IEnumerator TrackUnnamedMessageSentMetricToMultipleClients() public IEnumerator TrackUnnamedMessageSentMetricToSelf() { var waitForMetricValues = new WaitForEventMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.UnnamedMessageSent); - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { writer.WriteValueSafe(messageName); @@ -252,7 +252,7 @@ public IEnumerator TrackUnnamedMessageSentMetricToSelf() [UnityTest] public IEnumerator TrackUnnamedMessageReceivedMetric() { - var message = Guid.NewGuid(); + var message = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var waitForMetricValues = new WaitForEventMetricValues(FirstClientMetrics.Dispatcher, NetworkMetricTypes.UnnamedMessageReceived); using (var writer = new FastBufferWriter(1300, Allocator.Temp)) { diff --git a/Tests/Runtime/Metrics/PacketLossMetricsTests.cs b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs index e838832..24ff300 100644 --- a/Tests/Runtime/Metrics/PacketLossMetricsTests.cs +++ b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs @@ -24,11 +24,6 @@ public PacketLossMetricsTests() : base(HostOrServer.Server) {} - protected override void OnOneTimeSetup() - { - m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.UTP; - } - protected override void OnServerAndClientsCreated() { var clientTransport = (UnityTransport)m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport; diff --git a/Tests/Runtime/Metrics/PacketMetricsTests.cs b/Tests/Runtime/Metrics/PacketMetricsTests.cs index 6a0003a..a9e4cc0 100644 --- a/Tests/Runtime/Metrics/PacketMetricsTests.cs +++ b/Tests/Runtime/Metrics/PacketMetricsTests.cs @@ -12,13 +12,6 @@ namespace Unity.Netcode.RuntimeTests.Metrics { internal class PacketMetricsTests : SingleClientMetricTestBase { - - protected override void OnOneTimeSetup() - { - m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.UTP; - base.OnOneTimeSetup(); - } - [UnityTest] public IEnumerator TrackPacketSentMetric() { diff --git a/Tests/Runtime/Metrics/RttMetricsTests.cs b/Tests/Runtime/Metrics/RttMetricsTests.cs index aad9ae5..2c77c66 100644 --- a/Tests/Runtime/Metrics/RttMetricsTests.cs +++ b/Tests/Runtime/Metrics/RttMetricsTests.cs @@ -40,15 +40,6 @@ public RttMetricsTests(ClientCount numberOfClients) m_ClientCount = numberOfClients == ClientCount.OneClient ? 1 : 2; } - /// - /// Note: We are using the OnOneTimeSetup to select the transport to use for - /// this test set. - /// - protected override void OnOneTimeSetup() - { - m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.UTP; - } - [UnityTest] public IEnumerator TrackRttMetricServerToClient() { diff --git a/Tests/Runtime/Metrics/ServerLogsMetricTests.cs b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs index 77a5a5f..78d70bc 100644 --- a/Tests/Runtime/Metrics/ServerLogsMetricTests.cs +++ b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs @@ -16,6 +16,12 @@ internal class ServerLogsMetricTests : SingleClientMetricTestBase private static readonly int k_ServerLogSentMessageOverhead = 2 + k_MessageHeaderSize; private static readonly int k_ServerLogReceivedMessageOverhead = 2; + protected override IEnumerator OnSetup() + { + m_CreateServerFirst = false; + return base.OnSetup(); + } + [UnityTest] public IEnumerator TrackServerLogSentMetric() { diff --git a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs index 3f649f9..a6ded69 100644 --- a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs +++ b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs @@ -20,14 +20,14 @@ internal class TransportBytesMetricsTests : SingleClientMetricTestBase [UnityTest] public IEnumerator TrackTotalNumberOfBytesSent() { - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); var observer = new TotalBytesObserver(ClientMetrics.Dispatcher, NetworkMetricTypes.TotalBytesReceived); try { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), Client.LocalClientId, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), Client.LocalClientId, writer); } finally { @@ -48,14 +48,14 @@ public IEnumerator TrackTotalNumberOfBytesSent() [UnityTest] public IEnumerator TrackTotalNumberOfBytesReceived() { - var messageName = Guid.NewGuid(); + var messageName = new ForceNetworkSerializeByMemcpy(Guid.NewGuid()); var writer = new FastBufferWriter(1300, Allocator.Temp); var observer = new TotalBytesObserver(ClientMetrics.Dispatcher, NetworkMetricTypes.TotalBytesReceived); try { writer.WriteValueSafe(messageName); - Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), Client.LocalClientId, writer); + Server.CustomMessagingManager.SendNamedMessage(messageName.Value.ToString(), Client.LocalClientId, writer); } finally { diff --git a/Tests/Runtime/NestedNetworkManagerTests.cs b/Tests/Runtime/NestedNetworkManagerTests.cs index ff99617..afbea13 100644 --- a/Tests/Runtime/NestedNetworkManagerTests.cs +++ b/Tests/Runtime/NestedNetworkManagerTests.cs @@ -1,7 +1,7 @@ using UnityEngine; using NUnit.Framework; using UnityEngine.TestTools; -using Unity.Netcode.TestHelpers.Runtime; +using Unity.Netcode.Transports.UTP; using Object = UnityEngine.Object; namespace Unity.Netcode.RuntimeTests @@ -14,9 +14,9 @@ public void CheckNestedNetworkManager() var parent = new GameObject("ParentObject"); var networkManagerObject = new GameObject(nameof(CheckNestedNetworkManager)); - var transport = networkManagerObject.AddComponent(); + var unityTransport = networkManagerObject.AddComponent(); var networkManager = networkManagerObject.AddComponent(); - networkManager.NetworkConfig = new NetworkConfig() { NetworkTransport = transport }; + networkManager.NetworkConfig = new NetworkConfig() { NetworkTransport = unityTransport }; // Make our NetworkManager's GameObject nested networkManagerObject.transform.parent = parent.transform; diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs index c989d90..fa45a5c 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs @@ -38,7 +38,7 @@ public IEnumerator TestNetworkObjectServerDestroy() // destroy the server player Object.Destroy(serverClientPlayerResult.Result.gameObject); - yield return NetcodeIntegrationTestHelpers.WaitForMessageOfType(m_ClientNetworkManagers[0]); + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); Assert.IsTrue(serverClientPlayerResult.Result == null); // Assert.IsNull doesn't work here Assert.IsTrue(clientClientPlayerResult.Result == null); diff --git a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs index dbfc24d..fcdbfd8 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs @@ -51,6 +51,8 @@ public IEnumerator ChangeOwnershipOwnedObjectsAddTest() yield return s_DefaultWaitForTick; // Ensure it's now added to the list + yield return WaitForConditionOrTimeOut(() => m_ClientNetworkManagers[0].SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for client to gain ownership!"); Assert.True(m_ClientNetworkManagers[0].SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); Assert.True(m_ServerNetworkManager.SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); } diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs index e2d2fe4..9e10d16 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs @@ -67,7 +67,7 @@ public IEnumerator TestOwnershipCallbacks() m_OwnershipObject = SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager); m_OwnershipNetworkObject = m_OwnershipObject.GetComponent(); - yield return NetcodeIntegrationTestHelpers.WaitForMessageOfType(m_ClientNetworkManagers[0]); + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); var ownershipNetworkObjectId = m_OwnershipNetworkObject.NetworkObjectId; Assert.That(ownershipNetworkObjectId, Is.GreaterThan(0)); @@ -95,11 +95,14 @@ public IEnumerator TestOwnershipCallbacks() Assert.That(m_ServerNetworkManager.ConnectedClients.ContainsKey(m_ClientNetworkManagers[0].LocalClientId)); - serverObject.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + serverObject.ChangeOwnership(clientComponent.NetworkManager.LocalClientId); + yield return s_DefaultWaitForTick; Assert.That(serverComponent.OnLostOwnershipFired); Assert.That(serverComponent.OwnerClientId, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); + + yield return WaitForConditionOrTimeOut(() => clientComponent.OnGainedOwnershipFired); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for client to gain ownership!"); Assert.That(clientComponent.OnGainedOwnershipFired); Assert.That(clientComponent.OwnerClientId, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); @@ -107,10 +110,13 @@ public IEnumerator TestOwnershipCallbacks() clientComponent.ResetFlags(); serverObject.ChangeOwnership(NetworkManager.ServerClientId); - yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + yield return s_DefaultWaitForTick; Assert.That(serverComponent.OnGainedOwnershipFired); Assert.That(serverComponent.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); + + yield return WaitForConditionOrTimeOut(() => clientComponent.OnLostOwnershipFired); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for client to lose ownership!"); Assert.That(clientComponent.OnLostOwnershipFired); Assert.That(clientComponent.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); } @@ -151,11 +157,22 @@ public IEnumerator TestOwnershipCallbacksSeveralClients() var ownershipNetworkObjectId = m_OwnershipNetworkObject.NetworkObjectId; Assert.That(ownershipNetworkObjectId, Is.GreaterThan(0)); Assert.That(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId)); - foreach (var clientNetworkManager in m_ClientNetworkManagers) + + bool WaitForClientsToSpawnNetworkObject() { - Assert.That(clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId)); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + if (!clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId)) + { + return false; + } + } + return true; } + yield return WaitForConditionOrTimeOut(WaitForClientsToSpawnNetworkObject); + Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for all clients to change ownership!"); + // Verifies that removing the ownership when the default (server) is already set does not cause a Key Not Found Exception m_ServerNetworkManager.SpawnManager.RemoveOwnership(m_OwnershipNetworkObject); var serverObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId]; @@ -237,7 +254,10 @@ public IEnumerator TestOwnershipCallbacksSeveralClients() Assert.That(serverComponent.OnGainedOwnershipFired); Assert.That(serverComponent.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); - Assert.That(previousClientComponent.OnLostOwnershipFired); + + yield return WaitForConditionOrTimeOut(() => previousClientComponent.OnLostOwnershipFired); + + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for {previousClientComponent.name} to lose ownership!"); // Make sure all client-side versions of the object is once again owned by the server for (int i = 0; i < NumberOfClients; i++) diff --git a/Tests/Runtime/NetworkSpawnManagerTests.cs b/Tests/Runtime/NetworkSpawnManagerTests.cs index a84e64f..a8a4b43 100644 --- a/Tests/Runtime/NetworkSpawnManagerTests.cs +++ b/Tests/Runtime/NetworkSpawnManagerTests.cs @@ -91,6 +91,8 @@ public void TestClientCanUseGetLocalPlayerObject() Assert.AreEqual(clientSideClientId, clientSideClientPlayerObject.OwnerClientId); } + private bool m_ClientDisconnected; + [UnityTest] public IEnumerator TestConnectAndDisconnect() { @@ -120,11 +122,22 @@ public IEnumerator TestConnectAndDisconnect() // test when client disconnects, player object no longer available. var nbConnectedClients = m_ServerNetworkManager.ConnectedClients.Count; + m_ClientDisconnected = false; + newClientNetworkManager.OnClientDisconnectCallback += ClientNetworkManager_OnClientDisconnectCallback; + m_ServerNetworkManager.DisconnectClient(newClientLocalClientId); + yield return WaitForConditionOrTimeOut(() => m_ClientDisconnected); + Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client to disconnect"); + // Call this to clean up NetcodeIntegrationTestHelpers NetcodeIntegrationTestHelpers.StopOneClient(newClientNetworkManager); - yield return WaitForConditionOrTimeOut(() => m_ServerNetworkManager.ConnectedClients.Count == nbConnectedClients - 1); + Assert.AreEqual(m_ServerNetworkManager.ConnectedClients.Count, nbConnectedClients - 1); serverSideNewClientPlayer = m_ServerNetworkManager.SpawnManager.GetPlayerNetworkObject(newClientLocalClientId); Assert.Null(serverSideNewClientPlayer); } + + private void ClientNetworkManager_OnClientDisconnectCallback(ulong obj) + { + m_ClientDisconnected = true; + } } } diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs new file mode 100644 index 0000000..1defdf1 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs @@ -0,0 +1,277 @@ +#if COM_UNITY_MODULES_PHYSICS +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Unity.Netcode.Components; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode.TestHelpers.Runtime; +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkTransformOwnershipTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + private GameObject m_ClientNetworkTransformPrefab; + private GameObject m_NetworkTransformPrefab; + + protected override void OnServerAndClientsCreated() + { + VerifyObjectIsSpawnedOnClient.ResetObjectTable(); + m_ClientNetworkTransformPrefab = CreateNetworkObjectPrefab("OwnerAuthorityTest"); + var clientNetworkTransform = m_ClientNetworkTransformPrefab.AddComponent(); + clientNetworkTransform.Interpolate = false; + var rigidBody = m_ClientNetworkTransformPrefab.AddComponent(); + rigidBody.useGravity = false; + m_ClientNetworkTransformPrefab.AddComponent(); + m_ClientNetworkTransformPrefab.AddComponent(); + m_ClientNetworkTransformPrefab.AddComponent(); + + m_NetworkTransformPrefab = CreateNetworkObjectPrefab("ServerAuthorityTest"); + var networkTransform = m_NetworkTransformPrefab.AddComponent(); + rigidBody = m_NetworkTransformPrefab.AddComponent(); + rigidBody.useGravity = false; + m_NetworkTransformPrefab.AddComponent(); + m_NetworkTransformPrefab.AddComponent(); + m_NetworkTransformPrefab.AddComponent(); + networkTransform.Interpolate = false; + + base.OnServerAndClientsCreated(); + } + + public enum StartingOwnership + { + HostStartsAsOwner, + ClientStartsAsOwner, + } + + /// + /// This verifies that when authority is owner authoritative the owner's + /// Rigidbody is kinematic and the non-owner's is not. + /// This also verifies that we can switch between owners and that only the + /// owner can update the transform while non-owners cannot. + /// + /// determines who starts as the owner (true): host | (false): client + [UnityTest] + public IEnumerator OwnerAuthoritativeTest([Values] StartingOwnership startingOwnership) + { + // Get the current ownership layout + var networkManagerOwner = startingOwnership == StartingOwnership.HostStartsAsOwner ? m_ServerNetworkManager : m_ClientNetworkManagers[0]; + var networkManagerNonOwner = startingOwnership == StartingOwnership.HostStartsAsOwner ? m_ClientNetworkManagers[0] : m_ServerNetworkManager; + + // Spawn the m_ClientNetworkTransformPrefab and wait for the client-side to spawn the object + var serverSideInstance = SpawnObject(m_ClientNetworkTransformPrefab, networkManagerOwner); + yield return WaitForConditionOrTimeOut(() => VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(m_ClientNetworkManagers[0].LocalClientId)); + + // Get owner relative instances + var ownerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(networkManagerOwner.LocalClientId); + var nonOwnerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(networkManagerNonOwner.LocalClientId); + Assert.NotNull(ownerInstance); + Assert.NotNull(nonOwnerInstance); + + // Make sure the owner is not kinematic and the non-owner(s) are kinematic + Assert.True(nonOwnerInstance.GetComponent().isKinematic, $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} is not kinematic when it should be!"); + Assert.False(ownerInstance.GetComponent().isKinematic, $"{networkManagerOwner.name}'s object instance {ownerInstance.name} is kinematic when it should not be!"); + + // Owner changes transform values + var valueSetByOwner = Vector3.one * 2; + ownerInstance.transform.position = valueSetByOwner; + ownerInstance.transform.localScale = valueSetByOwner; + var rotation = new Quaternion(); + rotation.eulerAngles = valueSetByOwner; + ownerInstance.transform.rotation = rotation; + var transformToTest = nonOwnerInstance.transform; + yield return WaitForConditionOrTimeOut(() => transformToTest.position == valueSetByOwner && transformToTest.localScale == valueSetByOwner && transformToTest.rotation == rotation); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for {networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} to change its transform!\n" + + $"Expected Position: {valueSetByOwner} | Current Position: {transformToTest.position}\n" + + $"Expected Rotation: {valueSetByOwner} | Current Rotation: {transformToTest.rotation.eulerAngles}\n" + + $"Expected Scale: {valueSetByOwner} | Current Scale: {transformToTest.localScale}"); + + // Verify non-owners cannot change transform values + nonOwnerInstance.transform.position = Vector3.zero; + yield return s_DefaultWaitForTick; + Assert.True(nonOwnerInstance.transform.position == valueSetByOwner, $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} was allowed to change its position! Expected: {Vector3.one} Is Currently:{nonOwnerInstance.transform.position}"); + + // Change ownership and wait for the non-owner to reflect the change + VerifyObjectIsSpawnedOnClient.ResetObjectTable(); + m_ServerNetworkManager.SpawnManager.ChangeOwnership(serverSideInstance.GetComponent(), networkManagerNonOwner.LocalClientId); + yield return WaitForConditionOrTimeOut(() => nonOwnerInstance.GetComponent().OwnerClientId == networkManagerNonOwner.LocalClientId); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for {networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} to change ownership!"); + + // Re-assign the ownership references and wait for the non-owner instance to be notified of ownership change + networkManagerOwner = startingOwnership == StartingOwnership.HostStartsAsOwner ? m_ClientNetworkManagers[0] : m_ServerNetworkManager; + networkManagerNonOwner = startingOwnership == StartingOwnership.HostStartsAsOwner ? m_ServerNetworkManager : m_ClientNetworkManagers[0]; + ownerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(networkManagerOwner.LocalClientId); + Assert.NotNull(ownerInstance); + yield return WaitForConditionOrTimeOut(() => VerifyObjectIsSpawnedOnClient.GetClientInstance(networkManagerNonOwner.LocalClientId) != null); + nonOwnerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(networkManagerNonOwner.LocalClientId); + Assert.NotNull(nonOwnerInstance); + + // Make sure the owner is not kinematic and the non-owner(s) are kinematic + Assert.False(ownerInstance.GetComponent().isKinematic, $"{networkManagerOwner.name}'s object instance {ownerInstance.name} is kinematic when it should not be!"); + Assert.True(nonOwnerInstance.GetComponent().isKinematic, $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} is not kinematic when it should be!"); + + // Have the new owner change transform values and wait for those values to be applied on the non-owner side. + valueSetByOwner = Vector3.one * 50; + ownerInstance.transform.position = valueSetByOwner; + ownerInstance.transform.localScale = valueSetByOwner; + rotation.eulerAngles = valueSetByOwner; + ownerInstance.transform.rotation = rotation; + transformToTest = nonOwnerInstance.transform; + yield return WaitForConditionOrTimeOut(() => transformToTest.position == valueSetByOwner && transformToTest.localScale == valueSetByOwner && transformToTest.rotation == rotation); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for {networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} to change its transform!\n" + + $"Expected Position: {valueSetByOwner} | Current Position: {transformToTest.position}\n" + + $"Expected Rotation: {valueSetByOwner} | Current Rotation: {transformToTest.rotation.eulerAngles}\n" + + $"Expected Scale: {valueSetByOwner} | Current Scale: {transformToTest.localScale}"); + + // The last check is to verify non-owners cannot change transform values after ownership has changed + nonOwnerInstance.transform.position = Vector3.zero; + yield return s_DefaultWaitForTick; + Assert.True(nonOwnerInstance.transform.position == valueSetByOwner, $"{networkManagerNonOwner.name}'s object instance {nonOwnerInstance.name} was allowed to change its position! Expected: {Vector3.one} Is Currently:{nonOwnerInstance.transform.position}"); + } + + /// + /// This verifies that when authority is server authoritative the + /// client's Rigidbody is kinematic and the server is not. + /// This also verifies only the server can apply updates to the + /// transform while the clients cannot. + /// + [UnityTest] + public IEnumerator ServerAuthoritativeTest() + { + // Spawn the m_NetworkTransformPrefab and wait for the client-side to spawn the object + var serverSideInstance = SpawnObject(m_NetworkTransformPrefab, m_ServerNetworkManager); + yield return WaitForConditionOrTimeOut(() => VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(m_ClientNetworkManagers[0].LocalClientId)); + + var ownerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ServerNetworkManager.LocalClientId); + var nonOwnerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ClientNetworkManagers[0].LocalClientId); + + // Make sure the owner is not kinematic and the non-owner(s) are kinematic + Assert.False(ownerInstance.GetComponent().isKinematic, $"{m_ServerNetworkManager.name}'s object instance {ownerInstance.name} is kinematic when it should not be!"); + Assert.True(nonOwnerInstance.GetComponent().isKinematic, $"{m_ClientNetworkManagers[0].name}'s object instance {nonOwnerInstance.name} is not kinematic when it should be!"); + + // Server changes transform values + var valueSetByOwner = Vector3.one * 2; + ownerInstance.transform.position = valueSetByOwner; + ownerInstance.transform.localScale = valueSetByOwner; + var rotation = new Quaternion(); + rotation.eulerAngles = valueSetByOwner; + ownerInstance.transform.rotation = rotation; + var transformToTest = nonOwnerInstance.transform; + yield return WaitForConditionOrTimeOut(() => transformToTest.position == valueSetByOwner && transformToTest.localScale == valueSetByOwner && transformToTest.rotation == rotation); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for {m_ClientNetworkManagers[0].name}'s object instance {nonOwnerInstance.name} to change its transform!\n" + + $"Expected Position: {valueSetByOwner} | Current Position: {transformToTest.position}\n" + + $"Expected Rotation: {valueSetByOwner} | Current Rotation: {transformToTest.rotation.eulerAngles}\n" + + $"Expected Scale: {valueSetByOwner} | Current Scale: {transformToTest.localScale}"); + + // The last check is to verify clients cannot change transform values + nonOwnerInstance.transform.position = Vector3.zero; + yield return s_DefaultWaitForTick; + Assert.True(nonOwnerInstance.transform.position == valueSetByOwner, $"{m_ClientNetworkManagers[0].name}'s object instance {nonOwnerInstance.name} was allowed to change its position! Expected: {Vector3.one} Is Currently:{nonOwnerInstance.transform.position}"); + } + + /// + /// NetworkTransformOwnershipTests helper behaviour + /// + public class VerifyObjectIsSpawnedOnClient : NetworkBehaviour + { + private static Dictionary s_NetworkManagerRelativeSpawnedObjects = new Dictionary(); + + public static void ResetObjectTable() + { + s_NetworkManagerRelativeSpawnedObjects.Clear(); + } + + public override void OnGainedOwnership() + { + if (!s_NetworkManagerRelativeSpawnedObjects.ContainsKey(NetworkManager.LocalClientId)) + { + s_NetworkManagerRelativeSpawnedObjects.Add(NetworkManager.LocalClientId, this); + } + base.OnGainedOwnership(); + } + + public override void OnLostOwnership() + { + if (!s_NetworkManagerRelativeSpawnedObjects.ContainsKey(NetworkManager.LocalClientId)) + { + s_NetworkManagerRelativeSpawnedObjects.Add(NetworkManager.LocalClientId, this); + } + base.OnLostOwnership(); + } + + public static List GetClientsThatSpawnedThisPrefab() + { + return s_NetworkManagerRelativeSpawnedObjects.Keys.ToList(); + } + + public static VerifyObjectIsSpawnedOnClient GetClientInstance(ulong clientId) + { + if (s_NetworkManagerRelativeSpawnedObjects.ContainsKey(clientId)) + { + return s_NetworkManagerRelativeSpawnedObjects[clientId]; + } + return null; + } + + public override void OnNetworkSpawn() + { + // This makes sure that the NetworkManager relative NetworkObject instances don't collide with each other + // and skew the expected changes to the transforms + foreach (var entry in s_NetworkManagerRelativeSpawnedObjects) + { + Physics.IgnoreCollision(entry.Value.GetComponent(), GetComponent()); + } + + if (!s_NetworkManagerRelativeSpawnedObjects.ContainsKey(NetworkManager.LocalClientId)) + { + s_NetworkManagerRelativeSpawnedObjects.Add(NetworkManager.LocalClientId, this); + } + base.OnNetworkSpawn(); + } + + public override void OnNetworkDespawn() + { + if (s_NetworkManagerRelativeSpawnedObjects.ContainsKey(NetworkManager.LocalClientId)) + { + s_NetworkManagerRelativeSpawnedObjects.Remove(NetworkManager.LocalClientId); + } + base.OnNetworkDespawn(); + } + } + + /// + /// Until we can better locate the ClientNetworkTransform + /// This will have to be used to verify the ownership authority + /// + [DisallowMultipleComponent] + public class TestClientNetworkTransform : NetworkTransform + { + public override void OnNetworkSpawn() + { + base.OnNetworkSpawn(); + CanCommitToTransform = IsOwner; + } + + protected override void Update() + { + CanCommitToTransform = IsOwner; + base.Update(); + if (NetworkManager.Singleton != null && (NetworkManager.Singleton.IsConnectedClient || NetworkManager.Singleton.IsListening)) + { + if (CanCommitToTransform) + { + TryCommitTransformToServer(transform, NetworkManager.LocalTime.Time); + } + } + } + + protected override bool OnIsServerAuthoritatitive() + { + return false; + } + } + } +} +#endif diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs.meta b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs.meta new file mode 100644 index 0000000..b3b6e8c --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2f8cf0e06334cb4f9f7d4ca3c2d19e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index e2f215f..0d6c58f 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -22,6 +22,11 @@ public override void OnNetworkSpawn() ReadyToReceivePositionUpdate = true; } + + public (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyState() + { + return ApplyLocalNetworkState(transform); + } } // [TestFixture(true, true)] @@ -172,6 +177,80 @@ public IEnumerator TestCantChangeTransformFromOtherSideAuthority([Values] bool t #endif } + + /// + /// Validates that rotation checks don't produce false positive + /// results when rolling over between 0 and 360 degrees + /// + [UnityTest] + public IEnumerator TestRotationThresholdDeltaCheck() + { + // Get the client player's NetworkTransform for both instances + var authoritativeNetworkTransform = m_ServerSideClientPlayer.GetComponent(); + var otherSideNetworkTransform = m_ClientSideClientPlayer.GetComponent(); + otherSideNetworkTransform.RotAngleThreshold = authoritativeNetworkTransform.RotAngleThreshold = 5.0f; + + var halfThreshold = authoritativeNetworkTransform.RotAngleThreshold * 0.5001f; + var serverRotation = authoritativeNetworkTransform.transform.rotation; + var serverEulerRotation = serverRotation.eulerAngles; + + // Verify rotation is not marked dirty when rotated by half of the threshold + serverEulerRotation.y += halfThreshold; + serverRotation.eulerAngles = serverEulerRotation; + authoritativeNetworkTransform.transform.rotation = serverRotation; + var results = authoritativeNetworkTransform.ApplyState(); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {authoritativeNetworkTransform.RotAngleThreshold} degrees and only adjusted by {halfThreshold} degrees!"); + yield return s_DefaultWaitForTick; + + // Verify rotation is marked dirty when rotated by another half threshold value + serverEulerRotation.y += halfThreshold; + serverRotation.eulerAngles = serverEulerRotation; + authoritativeNetworkTransform.transform.rotation = serverRotation; + results = authoritativeNetworkTransform.ApplyState(); + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by the threshold value: {authoritativeNetworkTransform.RotAngleThreshold} degrees!"); + yield return s_DefaultWaitForTick; + + //Reset rotation back to zero on all axis + serverRotation.eulerAngles = serverEulerRotation = Vector3.zero; + authoritativeNetworkTransform.transform.rotation = serverRotation; + yield return s_DefaultWaitForTick; + + // Rotate by 360 minus halfThreshold (which is really just negative halfThreshold) and verify rotation is not marked dirty + serverEulerRotation.y = 360 - halfThreshold; + serverRotation.eulerAngles = serverEulerRotation; + authoritativeNetworkTransform.transform.rotation = serverRotation; + results = authoritativeNetworkTransform.ApplyState(); + + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {authoritativeNetworkTransform.RotAngleThreshold} degrees and only adjusted by " + + $"{Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + + serverEulerRotation.y -= halfThreshold; + serverRotation.eulerAngles = serverEulerRotation; + authoritativeNetworkTransform.transform.rotation = serverRotation; + results = authoritativeNetworkTransform.ApplyState(); + + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + + //Reset rotation back to zero on all axis + serverRotation.eulerAngles = serverEulerRotation = Vector3.zero; + authoritativeNetworkTransform.transform.rotation = serverRotation; + yield return s_DefaultWaitForTick; + + serverEulerRotation.y -= halfThreshold; + serverRotation.eulerAngles = serverEulerRotation; + authoritativeNetworkTransform.transform.rotation = serverRotation; + results = authoritativeNetworkTransform.ApplyState(); + Assert.IsFalse(results.isRotationDirty, $"Rotation is dirty when rotation threshold is {authoritativeNetworkTransform.RotAngleThreshold} degrees and only adjusted by " + + $"{Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + + serverEulerRotation.y -= halfThreshold; + serverRotation.eulerAngles = serverEulerRotation; + authoritativeNetworkTransform.transform.rotation = serverRotation; + results = authoritativeNetworkTransform.ApplyState(); + + Assert.IsTrue(results.isRotationDirty, $"Rotation was not dirty when rotated by {Mathf.DeltaAngle(0, serverEulerRotation.y)} degrees!"); + } + /* * ownership change * test teleport with interpolation diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs index dd9936a..6c7eb17 100644 --- a/Tests/Runtime/NetworkVariableTests.cs +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -273,9 +273,9 @@ public class NetworkVariableTest : NetworkBehaviour { public readonly NetworkVariable TheScalar = new NetworkVariable(); public readonly NetworkList TheList = new NetworkList(); - public readonly NetworkList TheLargeList = new NetworkList(); + public readonly NetworkList> TheLargeList = new NetworkList>(); - public readonly NetworkVariable FixedString32 = new NetworkVariable(); + public readonly NetworkVariable> FixedString32 = new NetworkVariable>(); private void ListChanged(NetworkListEvent e) { @@ -306,7 +306,8 @@ public override void OnNetworkSpawn() [TestFixture(false)] public class NetworkVariableTests : NetcodeIntegrationTest { - private const string k_FixedStringTestValue = "abcdefghijklmnopqrstuvwxyz"; + private const string k_StringTestValue = "abcdefghijklmnopqrstuvwxyz"; + private static readonly FixedString32Bytes k_FixedStringTestValue = k_StringTestValue; protected override int NumberOfClients => 2; private const uint k_TestUInt = 0x12345678; diff --git a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs index 02ae245..910a096 100644 --- a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs +++ b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs @@ -8,29 +8,16 @@ namespace Unity.Netcode.RuntimeTests { - public class NetworkRigidbodyDynamicTest : NetworkRigidbodyTestBase - { - public override bool Kinematic => false; - } - - public class NetworkRigidbodyKinematicTest : NetworkRigidbodyTestBase - { - public override bool Kinematic => true; - } - - public abstract class NetworkRigidbodyTestBase : NetcodeIntegrationTest + public class NetworkRigidbodyTest : NetcodeIntegrationTest { protected override int NumberOfClients => 1; - public abstract bool Kinematic { get; } - protected override void OnCreatePlayerPrefab() { m_PlayerPrefab.AddComponent(); m_PlayerPrefab.AddComponent(); m_PlayerPrefab.AddComponent(); m_PlayerPrefab.GetComponent().interpolation = RigidbodyInterpolation.Interpolate; - m_PlayerPrefab.GetComponent().isKinematic = Kinematic; } /// @@ -55,8 +42,8 @@ public IEnumerator TestRigidbodyKinematicEnableDisable() yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 3); - // server rigidbody has authority and should have a kinematic mode of false - Assert.True(serverPlayer.GetComponent().isKinematic == Kinematic, "serverPlayer kinematic"); + // server rigidbody has authority and should not be kinematic + Assert.True(serverPlayer.GetComponent().isKinematic == false, "serverPlayer kinematic"); Assert.AreEqual(RigidbodyInterpolation.Interpolate, serverPlayer.GetComponent().interpolation, "server equal interpolate"); // client rigidbody has no authority and should have a kinematic mode of true @@ -68,7 +55,8 @@ public IEnumerator TestRigidbodyKinematicEnableDisable() yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 3); - Assert.IsTrue(serverPlayer.GetComponent().isKinematic == Kinematic, "serverPlayer second kinematic"); + // When despawned, we should always be kinematic (i.e. don't apply physics when despawned) + Assert.IsTrue(serverPlayer.GetComponent().isKinematic == true, "serverPlayer second kinematic"); yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 3); diff --git a/Tests/Runtime/RpcTests.cs b/Tests/Runtime/RpcTests.cs index f68e512..746d001 100644 --- a/Tests/Runtime/RpcTests.cs +++ b/Tests/Runtime/RpcTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; using Debug = UnityEngine.Debug; namespace Unity.Netcode.RuntimeTests @@ -13,6 +14,7 @@ public class RpcTests : NetcodeIntegrationTest public class RpcTestNB : NetworkBehaviour { public event Action OnServer_Rpc; + public event Action OnTypedServer_Rpc; public event Action OnClient_Rpc; [ServerRpc] @@ -26,6 +28,12 @@ public void MyClientRpc() { OnClient_Rpc(); } + + [ServerRpc] + public void MyTypedServerRpc(Vector3 param1, Vector3[] param2) + { + OnTypedServer_Rpc(param1, param2); + } } protected override int NumberOfClients => 1; @@ -46,9 +54,13 @@ public IEnumerator TestRpcs() // Setup state bool hasReceivedServerRpc = false; + bool hasReceivedTypedServerRpc = false; bool hasReceivedClientRpcRemotely = false; bool hasReceivedClientRpcLocally = false; + var vector3 = new Vector3(1, 2, 3); + Vector3[] vector3s = new[] { new Vector3(4, 5, 6), new Vector3(7, 8, 9) }; + localClienRpcTestNB.OnClient_Rpc += () => { Debug.Log("ClientRpc received on client object"); @@ -75,9 +87,22 @@ public IEnumerator TestRpcs() hasReceivedClientRpcLocally = true; }; + serverClientRpcTestNB.OnTypedServer_Rpc += (param1, param2) => + { + Debug.Log("TypedServerRpc received on server object"); + Assert.AreEqual(param1, vector3); + Assert.AreEqual(param2.Length, vector3s.Length); + Assert.AreEqual(param2[0], vector3s[0]); + Assert.AreEqual(param2[1], vector3s[1]); + hasReceivedTypedServerRpc = true; + }; + // Send ServerRpc localClienRpcTestNB.MyServerRpc(m_ClientNetworkManagers[0].LocalClientId); + // Send TypedServerRpc + localClienRpcTestNB.MyTypedServerRpc(vector3, vector3s); + // Send ClientRpc serverClientRpcTestNB.MyClientRpc(); @@ -86,6 +111,11 @@ public IEnumerator TestRpcs() var serverMessageHookEntry = new MessageHookEntry(m_ServerNetworkManager); serverMessageHookEntry.AssignMessageType(); messageHookList.Add(serverMessageHookEntry); + + var typedServerMessageHookEntry = new MessageHookEntry(m_ServerNetworkManager); + typedServerMessageHookEntry.AssignMessageType(); + messageHookList.Add(typedServerMessageHookEntry); + foreach (var client in m_ClientNetworkManagers) { var clientMessageHookEntry = new MessageHookEntry(client); @@ -97,9 +127,10 @@ public IEnumerator TestRpcs() Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for messages: {rpcMessageHooks.GetHooksStillWaiting()}"); // Make sure RPCs propagated all the way up and were called on the relative destination class instance - yield return WaitForConditionOrTimeOut(() => hasReceivedServerRpc && hasReceivedClientRpcLocally && hasReceivedClientRpcRemotely); + yield return WaitForConditionOrTimeOut(() => hasReceivedServerRpc && hasReceivedClientRpcLocally && hasReceivedClientRpcRemotely && hasReceivedTypedServerRpc); Assert.True(hasReceivedServerRpc, "ServerRpc was not received"); + Assert.True(hasReceivedTypedServerRpc, "TypedServerRpc was not received"); Assert.True(hasReceivedClientRpcLocally, "ClientRpc was not locally received on the server"); Assert.True(hasReceivedClientRpcRemotely, "ClientRpc was not remotely received on the client"); } diff --git a/Tests/Runtime/StopStartRuntimeTests.cs b/Tests/Runtime/StopStartRuntimeTests.cs index 3326dbb..421746e 100644 --- a/Tests/Runtime/StopStartRuntimeTests.cs +++ b/Tests/Runtime/StopStartRuntimeTests.cs @@ -6,69 +6,75 @@ namespace Unity.Netcode.RuntimeTests { - public class StopStartRuntimeTests + public class StopStartRuntimeTests : NetcodeIntegrationTest { + protected override int NumberOfClients => 1; + + protected override void OnOneTimeSetup() + { + m_UseHost = false; + base.OnOneTimeSetup(); + } + [UnityTest] public IEnumerator WhenShuttingDownAndRestarting_SDKRestartsSuccessfullyAndStaysRunning() - { // create server and client instances - NetcodeIntegrationTestHelpers.Create(1, out NetworkManager server, out NetworkManager[] clients); + { + // shutdown the server + m_ServerNetworkManager.Shutdown(); - try - { + // wait 1 frame because shutdowns are delayed + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); - // create prefab - var gameObject = new GameObject("PlayerObject"); - var networkObject = gameObject.AddComponent(); - networkObject.DontDestroyWithOwner = true; - NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); + // Verify the shutdown occurred + Assert.IsFalse(m_ServerNetworkManager.IsServer); + Assert.IsFalse(m_ServerNetworkManager.IsListening); + Assert.IsFalse(m_ServerNetworkManager.IsHost); + Assert.IsFalse(m_ServerNetworkManager.IsClient); - server.NetworkConfig.PlayerPrefab = gameObject; + m_ServerNetworkManager.StartServer(); + // Verify the server started + Assert.IsTrue(m_ServerNetworkManager.IsServer); + Assert.IsTrue(m_ServerNetworkManager.IsListening); - for (int i = 0; i < clients.Length; i++) - { - clients[i].NetworkConfig.PlayerPrefab = gameObject; - } + // Wait several frames / one full network tick + yield return s_DefaultWaitForTick; - // start server and connect clients - NetcodeIntegrationTestHelpers.Start(false, server, clients); - - // wait for connection on client side - yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(clients); + // Verify the server is still running + Assert.IsTrue(m_ServerNetworkManager.IsServer); + Assert.IsTrue(m_ServerNetworkManager.IsListening); + } - // wait for connection on server side - yield return NetcodeIntegrationTestHelpers.WaitForClientConnectedToServer(server); + [UnityTest] + public IEnumerator WhenShuttingDownTwiceAndRestarting_SDKRestartsSuccessfullyAndStaysRunning() + { + // shutdown the server + m_ServerNetworkManager.Shutdown(); - // shutdown the server - server.Shutdown(); + // wait 1 frame because shutdowns are delayed + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); - // wait 1 frame because shutdowns are delayed - var nextFrameNumber = Time.frameCount + 1; - yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + // Verify the shutdown occurred + Assert.IsFalse(m_ServerNetworkManager.IsServer); + Assert.IsFalse(m_ServerNetworkManager.IsListening); + Assert.IsFalse(m_ServerNetworkManager.IsHost); + Assert.IsFalse(m_ServerNetworkManager.IsClient); - // Verify the shutdown occurred - Assert.IsFalse(server.IsServer); - Assert.IsFalse(server.IsListening); - Assert.IsFalse(server.IsHost); - Assert.IsFalse(server.IsClient); + // Shutdown the server again. + m_ServerNetworkManager.Shutdown(); - server.StartServer(); - // Verify the server started - Assert.IsTrue(server.IsServer); - Assert.IsTrue(server.IsListening); + m_ServerNetworkManager.StartServer(); + // Verify the server started + Assert.IsTrue(m_ServerNetworkManager.IsServer); + Assert.IsTrue(m_ServerNetworkManager.IsListening); - // Wait several frames - nextFrameNumber = Time.frameCount + 10; - yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + // Wait several frames / one full network tick + yield return s_DefaultWaitForTick; - // Verify the server is still running - Assert.IsTrue(server.IsServer); - Assert.IsTrue(server.IsListening); - } - finally - { - // cleanup - NetcodeIntegrationTestHelpers.Destroy(); - } + // Verify the server is still running + Assert.IsTrue(m_ServerNetworkManager.IsServer); + Assert.IsTrue(m_ServerNetworkManager.IsListening); } } } diff --git a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs index c08a16b..b8622e2 100644 --- a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs +++ b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs @@ -89,8 +89,13 @@ public IEnumerator TestTimeIntegrationTest(int targetFrameRate, uint tickRate) // compares the two client times, only difference should be based on buffering. m_Client1State.AssertCheckDifference(m_Client2State, 0.2 - tickInterval, (0.1 - tickInterval), tickInterval * 2 + frameInterval * 2 + k_AdditionalTimeTolerance); } + } + protected override IEnumerator OnTearDown() + { + // Always "shutdown in a tear-down" otherwise you can cause all proceeding tests to fail ShutdownAndCleanUp(); + yield return base.OnTearDown(); } // This is from NetcodeIntegrationTest but we need a custom version of this to modifiy the config diff --git a/Tests/Runtime/TransformInterpolationTests.cs b/Tests/Runtime/TransformInterpolationTests.cs index 2f61a35..166e243 100644 --- a/Tests/Runtime/TransformInterpolationTests.cs +++ b/Tests/Runtime/TransformInterpolationTests.cs @@ -57,12 +57,15 @@ public class TransformInterpolationTests : NetcodeIntegrationTest { protected override int NumberOfClients => 1; - private ulong m_ClientId0; private GameObject m_PrefabToSpawn; - private NetworkObject m_AsNetworkObject; + private NetworkObject m_SpawnedAsNetworkObject; private NetworkObject m_SpawnedObjectOnClient; + private NetworkObject m_BaseAsNetworkObject; + private NetworkObject m_BaseOnClient; + + protected override void OnServerAndClientsCreated() { m_PrefabToSpawn = CreateNetworkObjectPrefab("InterpTestObject"); @@ -74,41 +77,59 @@ private IEnumerator RefreshNetworkObjects() { var clientId = m_ClientNetworkManagers[0].LocalClientId; yield return WaitForConditionOrTimeOut(() => s_GlobalNetworkObjects.ContainsKey(clientId) && - s_GlobalNetworkObjects[clientId].ContainsKey(m_AsNetworkObject.NetworkObjectId)); + s_GlobalNetworkObjects[clientId].ContainsKey(m_BaseAsNetworkObject.NetworkObjectId) && + s_GlobalNetworkObjects[clientId].ContainsKey(m_SpawnedAsNetworkObject.NetworkObjectId)); - Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for client side {nameof(NetworkObject)} ID of {m_AsNetworkObject.NetworkObjectId}"); - m_SpawnedObjectOnClient = s_GlobalNetworkObjects[clientId][m_AsNetworkObject.NetworkObjectId]; + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for client side {nameof(NetworkObject)} ID of {m_SpawnedAsNetworkObject.NetworkObjectId}"); + + m_BaseOnClient = s_GlobalNetworkObjects[clientId][m_BaseAsNetworkObject.NetworkObjectId]; + // make sure the objects are set with the right network manager + m_BaseOnClient.NetworkManagerOwner = m_ClientNetworkManagers[0]; + + m_SpawnedObjectOnClient = s_GlobalNetworkObjects[clientId][m_SpawnedAsNetworkObject.NetworkObjectId]; // make sure the objects are set with the right network manager m_SpawnedObjectOnClient.NetworkManagerOwner = m_ClientNetworkManagers[0]; + + } [UnityTest] public IEnumerator TransformInterpolationTest() { - m_ClientId0 = m_ClientNetworkManagers[0].LocalClientId; - // create an object var spawnedObject = Object.Instantiate(m_PrefabToSpawn); var baseObject = Object.Instantiate(m_PrefabToSpawn); baseObject.GetComponent().NetworkManagerOwner = m_ServerNetworkManager; baseObject.GetComponent().Spawn(); - m_AsNetworkObject = spawnedObject.GetComponent(); - m_AsNetworkObject.NetworkManagerOwner = m_ServerNetworkManager; + m_SpawnedAsNetworkObject = spawnedObject.GetComponent(); + m_SpawnedAsNetworkObject.NetworkManagerOwner = m_ServerNetworkManager; + + m_BaseAsNetworkObject = baseObject.GetComponent(); + m_BaseAsNetworkObject.NetworkManagerOwner = m_ServerNetworkManager; + - m_AsNetworkObject.TrySetParent(baseObject); + m_SpawnedAsNetworkObject.TrySetParent(baseObject); - m_AsNetworkObject.Spawn(); + m_SpawnedAsNetworkObject.Spawn(); yield return RefreshNetworkObjects(); - m_AsNetworkObject.TrySetParent(baseObject); + m_SpawnedAsNetworkObject.TrySetParent(baseObject); baseObject.GetComponent().IsFixed = true; spawnedObject.GetComponent().IsMoving = true; - // Give two seconds for the object to settle - yield return new WaitForSeconds(2.0f); + const float maxPlacementError = 0.01f; + + // Wait for the base object to place itself on both instances + while (m_BaseOnClient.transform.position.y < 1000 - maxPlacementError || + m_BaseOnClient.transform.position.y > 1000 + maxPlacementError || + baseObject.transform.position.y < 1000 - maxPlacementError || + baseObject.transform.position.y > 1000 + maxPlacementError) + { + yield return new WaitForSeconds(0.01f); + } m_SpawnedObjectOnClient.GetComponent().CheckPosition = true; diff --git a/Tests/Runtime/Transports/SIPTransportTests.cs b/Tests/Runtime/Transports/SIPTransportTests.cs deleted file mode 100644 index 38f02ea..0000000 --- a/Tests/Runtime/Transports/SIPTransportTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Text; -using NUnit.Framework; -using UnityEngine; -using Unity.Netcode.TestHelpers.Runtime; - -namespace Unity.Netcode.RuntimeTests -{ - public class SIPTransportTests - { - [Test] - public void SendReceiveData() - { - SIPTransport server = new GameObject("Server").AddComponent(); - SIPTransport client = new GameObject("Client").AddComponent(); - - server.Initialize(); - server.StartServer(); - - client.Initialize(); - client.StartClient(); - - NetworkEvent serverEvent = server.PollEvent(out ulong clientId, out _, out _); - NetworkEvent clientEvent = client.PollEvent(out ulong serverId, out _, out _); - - // Make sure both connected - Assert.True(serverEvent == NetworkEvent.Connect); - Assert.True(clientEvent == NetworkEvent.Connect); - - // Send data - server.Send(clientId, new ArraySegment(Encoding.ASCII.GetBytes("Hello Client")), NetworkDelivery.ReliableSequenced); - client.Send(serverId, new ArraySegment(Encoding.ASCII.GetBytes("Hello Server")), NetworkDelivery.ReliableSequenced); - - serverEvent = server.PollEvent(out ulong newClientId, out ArraySegment serverPayload, out _); - clientEvent = client.PollEvent(out ulong newServerId, out ArraySegment clientPayload, out _); - - // Make sure we got data - Assert.True(serverEvent == NetworkEvent.Data); - Assert.True(clientEvent == NetworkEvent.Data); - - // Make sure the ID is correct - Assert.True(newClientId == clientId); - Assert.True(newServerId == serverId); - - // Make sure the payload was correct - Assert.That(serverPayload, Is.EquivalentTo(Encoding.ASCII.GetBytes("Hello Server"))); - Assert.That(clientPayload, Is.EquivalentTo(Encoding.ASCII.GetBytes("Hello Client"))); - - server.Shutdown(); - client.Shutdown(); - } - } -} diff --git a/Tests/Runtime/Transports/SIPTransportTests.cs.meta b/Tests/Runtime/Transports/SIPTransportTests.cs.meta deleted file mode 100644 index 5730f2c..0000000 --- a/Tests/Runtime/Transports/SIPTransportTests.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: e3fe3777ca6a4f4392d6281d148d0d3c -timeCreated: 1620385694 \ No newline at end of file diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef index 59b5f84..45ae434 100644 --- a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef +++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef @@ -31,6 +31,11 @@ "name": "com.unity.multiplayer.tools", "expression": "1.0.0-pre.7", "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_7" + }, + { + "name": "com.unity.modules.physics", + "expression": "", + "define": "COM_UNITY_MODULES_PHYSICS" } ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index b65d2f1..e6f580d 100644 --- a/package.json +++ b/package.json @@ -2,33 +2,28 @@ "name": "com.unity.netcode.gameobjects", "displayName": "Netcode for GameObjects", "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", - "version": "1.0.0-pre.7", + "version": "1.0.0-pre.8", "unity": "2020.3", "dependencies": { "com.unity.nuget.mono-cecil": "1.10.1", "com.unity.transport": "1.0.0" }, - "upm": { - "changelog": "### Added\n\n- Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828)\n- Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823)\n- Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762)\n- `UnityTransport` settings can now be set programmatically. (#1845)\n- `FastBufferWriter` and Reader IsInitialized property. (#1859)\n\n### Changed\n\n- Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849)\n\n### Removed\n\n- Removed `SnapshotSystem` (#1852)\n- Removed `com.unity.modules.animation`, `com.unity.modules.physics` and `com.unity.modules.physics2d` dependencies from the package (#1812)\n- Removed `com.unity.collections` dependency from the package (#1849)\n\n### Fixed\n- Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850)\n- Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847)\n- Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841)\n- Fixed issue where entries were not being removed from the NetworkSpawnManager.OwnershipToObjectsTable. (#1838)\n- Fixed ClientRpcs would always send to all connected clients by default as opposed to only sending to the NetworkObject's Observers list by default. (#1836)\n- Fixed clarity for NetworkSceneManager client side notification when it receives a scene hash value that does not exist in its local hash table. (#1828)\n- Fixed client throws a key not found exception when it times out using UNet or UTP. (#1821)\n- Fixed network variable updates are no longer limited to 32,768 bytes when NetworkConfig.EnsureNetworkVariableLengthSafety is enabled. The limits are now determined by what the transport can send in a message. (#1811)\n- Fixed in-scene NetworkObjects get destroyed if a client fails to connect and shuts down the NetworkManager. (#1809)\n- Fixed user never being notified in the editor that a NetworkBehaviour requires a NetworkObject to function properly. (#1808)\n- Fixed PlayerObjects and dynamically spawned NetworkObjects not being added to the NetworkClient's OwnedObjects (#1801)\n- Fixed issue where NetworkManager would continue starting even if the NetworkTransport selected failed. (#1780)\n- Fixed issue when spawning new player if an already existing player exists it does not remove IsPlayer from the previous player (#1779)\n- Fixed lack of notification that NetworkManager and NetworkObject cannot be added to the same GameObject with in-editor notifications (#1777)\n- Fixed parenting warning printing for false positives (#1855)" + "_upm": { + "changelog": "### Changed\n\n- `unmanaged` structs are no longer universally accepted as RPC parameters because some structs (i.e., structs with pointers in them, such as `NativeList`) can't be supported by the default memcpy struct serializer. Structs that are intended to be serialized across the network must add `INetworkSerializeByMemcpy` to the interface list (i.e., `struct Foo : INetworkSerializeByMemcpy`). This interface is empty and just serves to mark the struct as compatible with memcpy serialization. For external structs you can't edit, you can pass them to RPCs by wrapping them in `ForceNetworkSerializeByMemcpy`. (#1901)\n\n### Removed\n- Removed `SIPTransport` (#1870)\n\n- Removed `ClientNetworkTransform` from the package samples and moved to Boss Room's Utilities package which can be found [here](https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop/blob/main/Packages/com.unity.multiplayer.samples.coop/Utilities/Net/ClientAuthority/ClientNetworkTransform.cs).\n\n### Fixed\n\n- Fixed `NetworkTransform` generating false positive rotation delta checks when rolling over between 0 and 360 degrees. (#1890)\n- Fixed client throwing an exception if it has messages in the outbound queue when processing the `NetworkEvent.Disconnect` event and is using UTP. (#1884)\n- Fixed issue during client synchronization if 'ValidateSceneBeforeLoading' returned false it would halt the client synchronization process resulting in a client that was approved but not synchronized or fully connected with the server. (#1883)\n- Fixed an issue where UNetTransport.StartServer would return success even if the underlying transport failed to start (#854)\n- Passing generic types to RPCs no longer causes a native crash (#1901)\n- Fixed an issue where calling `Shutdown` on a `NetworkManager` that was already shut down would cause an immediate shutdown the next time it was started (basically the fix makes `Shutdown` idempotent). (#1877)" }, "upmCi": { - "footprint": "98c91bebf56ec13b76dfa5cf9d5d22946f7529cd" + "footprint": "7593dccd41bcdc7887dea63f675cdee7400ee3dc" }, "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "16420648a7790c64f917e000f7d0cdbe57350835" + "revision": "437d82ad0f11faa22e9f2539dddd3c5dda798a36" }, "samples": [ { "displayName": "Bootstrap", "description": "A lightweight sample to get started", "path": "Samples~/Bootstrap" - }, - { - "displayName": "ClientNetworkTransform", - "description": "A sample to demonstrate how client-driven NetworkTransform can be implemented", - "path": "Samples~/ClientNetworkTransform" } ] }