From 158f26b913b4b8f8fc75eddf9a5175b7f54a6133 Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Thu, 18 Apr 2024 00:00:00 +0000 Subject: [PATCH] com.unity.netcode.gameobjects@1.9.1 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [1.9.1] - 2024-04-18 ### Added - Added AnticipatedNetworkVariable, which adds support for client anticipation of NetworkVariable values, allowing for more responsive gameplay (#2820) - Added AnticipatedNetworkTransform, which adds support for client anticipation of NetworkTransforms (#2820) - Added NetworkVariableBase.ExceedsDirtinessThreshold to allow network variables to throttle updates by only sending updates when the difference between the current and previous values exceeds a threshold. (This is exposed in NetworkVariable with the callback NetworkVariable.CheckExceedsDirtinessThreshold) (#2820) - Added NetworkVariableUpdateTraits, which add additional throttling support: MinSecondsBetweenUpdates will prevent the NetworkVariable from sending updates more often than the specified time period (even if it exceeds the dirtiness threshold), while MaxSecondsBetweenUpdates will force a dirty NetworkVariable to send an update after the specified time period even if it has not yet exceeded the dirtiness threshold. (#2820) - Added virtual method NetworkVariableBase.OnInitialize() which can be used by NetworkVariable subclasses to add initialization code (#2820) - Added virtual method NetworkVariableBase.Update(), which is called once per frame to support behaviors such as interpolation between an anticipated value and an authoritative one. (#2820) - Added NetworkTime.TickWithPartial, which represents the current tick as a double that includes the fractional/partial tick value. (#2820) - Added NetworkTickSystem.AnticipationTick, which can be helpful with implementation of client anticipation. This value represents the tick the current local client was at at the beginning of the most recent network round trip, which enables it to correlate server update ticks with the client tick that may have triggered them. (#2820) - `NetworkVariable` now includes built-in support for `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, and `Dictionary` (#2813) - `NetworkVariable` now includes delta compression for collection values (`NativeList`, `NativeArray`, `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, `Dictionary`, and `FixedString` types) to save bandwidth by only sending the values that changed. (Note: For `NativeList`, `NativeArray`, and `List`, this algorithm works differently than that used in `NetworkList`. This algorithm will use less bandwidth for "set" and "add" operations, but `NetworkList` is more bandwidth-efficient if you are performing frequent "insert" operations.) (#2813) - `UserNetworkVariableSerialization` now has optional callbacks for `WriteDelta` and `ReadDelta`. If both are provided, they will be used for all serialization operations on NetworkVariables of that type except for the first one for each client. If either is missing, the existing `Write` and `Read` will always be used. (#2813) - Network variables wrapping `INetworkSerializable` types can perform delta serialization by setting `UserNetworkVariableSerialization.WriteDelta` and `UserNetworkVariableSerialization.ReadDelta` for those types. The built-in `INetworkSerializable` serializer will continue to be used for all other serialization operations, but if those callbacks are set, it will call into them on all but the initial serialization to perform delta serialization. (This could be useful if you have a large struct where most values do not change regularly and you want to send only the fields that did change.) (#2813) ### Fixed - Fixed issue where NetworkTransformEditor would throw and exception if you excluded the physics package. (#2871) - Fixed issue where `NetworkTransform` could not properly synchronize its base position when using half float precision. (#2845) - Fixed issue where the host was not invoking `OnClientDisconnectCallback` for its own local client when internally shutting down. (#2822) - Fixed issue where NetworkTransform could potentially attempt to "unregister" a named message prior to it being registered. (#2807) - Fixed issue where in-scene placed `NetworkObject`s with complex nested children `NetworkObject`s (more than one child in depth) would not synchronize properly if WorldPositionStays was set to true. (#2796) ### Changed - Changed `NetworkObjectReference` and `NetworkBehaviourReference` to allow null references when constructing and serializing. (#2874) - Changed `NetworkAnimator` no longer requires the `Animator` component to exist on the same `GameObject`. (#2872) - Changed `NetworkTransform` to now use `NetworkTransformMessage` as opposed to named messages for NetworkTransformState updates. (#2810) - Changed `CustomMessageManager` so it no longer attempts to register or "unregister" a null or empty string and will log an error if this condition occurs. (#2807) --- CHANGELOG.md | 31 + Components/AnticipatedNetworkTransform.cs | 500 +++ .../AnticipatedNetworkTransform.cs.meta | 3 + Components/Messages.meta | 8 + .../Messages/NetworkTransformMessage.cs | 116 + .../Messages/NetworkTransformMessage.cs.meta | 11 + Components/NetworkAnimator.cs | 1 - Components/NetworkRigidbody.cs | 22 +- Components/NetworkRigidbody2D.cs | 5 + Components/NetworkTransform.cs | 152 +- Editor/AnticipatedNetworkTransformEditor.cs | 14 + .../AnticipatedNetworkTransformEditor.cs.meta | 3 + Editor/CodeGen/CodeGenHelpers.cs | 1 + Editor/CodeGen/INetworkMessageILPP.cs | 2 +- Editor/CodeGen/NetworkBehaviourILPP.cs | 154 +- Editor/NetworkManagerEditor.cs | 8 +- Editor/NetworkTransformEditor.cs | 11 +- .../Connection/NetworkConnectionManager.cs | 9 + Runtime/Core/NetworkBehaviour.cs | 56 +- Runtime/Core/NetworkBehaviourUpdater.cs | 6 +- Runtime/Core/NetworkManager.cs | 39 + Runtime/Core/NetworkObject.cs | 26 +- Runtime/Core/NetworkUpdateLoop.cs | 21 + Runtime/Messaging/CustomMessageManager.cs | 18 + .../Messages/AnticipationCounterSync.cs | 70 + .../Messages/AnticipationCounterSync.cs.meta | 3 + .../Messages/NetworkVariableDeltaMessage.cs | 25 +- .../Messaging/RpcTargets/NotMeRpcTarget.cs | 4 + .../Messaging/RpcTargets/NotOwnerRpcTarget.cs | 8 + .../AnticipatedNetworkVariable.cs | 392 ++ .../AnticipatedNetworkVariable.cs.meta | 3 + .../CollectionSerializationUtility.cs | 746 ++++ .../CollectionSerializationUtility.cs.meta | 3 + Runtime/NetworkVariable/NetworkVariable.cs | 30 +- .../NetworkVariable/NetworkVariableBase.cs | 81 + .../NetworkVariableSerialization.cs | 921 ++++- Runtime/NetworkVariable/ResizableBitVector.cs | 107 + .../ResizableBitVector.cs.meta | 3 + Runtime/SceneManagement/SceneEventData.cs | 105 +- Runtime/Serialization/FastBufferReader.cs | 30 + Runtime/Serialization/FastBufferWriter.cs | 25 + .../NetworkBehaviourReference.cs | 9 +- .../Serialization/NetworkObjectReference.cs | 21 +- Runtime/Timing/AnticipationSystem.cs | 100 + Runtime/Timing/AnticipationSystem.cs.meta | 3 + Runtime/Timing/NetworkTime.cs | 5 + Runtime/Timing/NetworkTimeSystem.cs | 4 +- .../Runtime/IntegrationTestSceneHandler.cs | 29 +- TestHelpers/Runtime/MockTransport.cs | 147 +- TestHelpers/Runtime/NetcodeIntegrationTest.cs | 128 +- .../Messaging/MessageCorruptionTests.cs | 15 +- Tests/Runtime/DisconnectTests.cs | 53 + Tests/Runtime/Messaging/NamedMessageTests.cs | 18 + Tests/Runtime/NetworkManagerEventsTests.cs | 17 +- .../NetworkTransformPacketLossTests.cs | 106 +- .../NetworkTransform/NetworkTransformTests.cs | 20 + .../NetworkTransformAnticipationTests.cs | 521 +++ .../NetworkTransformAnticipationTests.cs.meta | 3 + .../NetworkVariableAnticipationTests.cs | 420 ++ .../NetworkVariableAnticipationTests.cs.meta | 3 + Tests/Runtime/NetworkVariableTests.cs | 3556 ++++++++++++++--- .../NetworkVariableTestsHelperTypes.cs | 936 +++++ .../NetworkVariableTestsHelperTypes.cs.meta | 3 + Tests/Runtime/NetworkVariableTraitsTests.cs | 138 + .../NetworkVariableTraitsTests.cs.meta | 3 + ...tworkVariableUserSerializableTypesTests.cs | 41 + Tests/Runtime/RpcTypeSerializationTests.cs | 312 +- .../NetworkBehaviourReferenceTests.cs | 48 +- .../NetworkObjectReferenceTests.cs | 74 +- Tests/Runtime/UniversalRpcTests.cs | 709 ++-- .../com.unity.netcode.runtimetests.asmdef | 19 +- package.json | 10 +- 72 files changed, 9955 insertions(+), 1289 deletions(-) create mode 100644 Components/AnticipatedNetworkTransform.cs create mode 100644 Components/AnticipatedNetworkTransform.cs.meta create mode 100644 Components/Messages.meta create mode 100644 Components/Messages/NetworkTransformMessage.cs create mode 100644 Components/Messages/NetworkTransformMessage.cs.meta create mode 100644 Editor/AnticipatedNetworkTransformEditor.cs create mode 100644 Editor/AnticipatedNetworkTransformEditor.cs.meta create mode 100644 Runtime/Messaging/Messages/AnticipationCounterSync.cs create mode 100644 Runtime/Messaging/Messages/AnticipationCounterSync.cs.meta create mode 100644 Runtime/NetworkVariable/AnticipatedNetworkVariable.cs create mode 100644 Runtime/NetworkVariable/AnticipatedNetworkVariable.cs.meta create mode 100644 Runtime/NetworkVariable/CollectionSerializationUtility.cs create mode 100644 Runtime/NetworkVariable/CollectionSerializationUtility.cs.meta create mode 100644 Runtime/NetworkVariable/ResizableBitVector.cs create mode 100644 Runtime/NetworkVariable/ResizableBitVector.cs.meta create mode 100644 Runtime/Timing/AnticipationSystem.cs create mode 100644 Runtime/Timing/AnticipationSystem.cs.meta create mode 100644 Tests/Runtime/NetworkTransformAnticipationTests.cs create mode 100644 Tests/Runtime/NetworkTransformAnticipationTests.cs.meta create mode 100644 Tests/Runtime/NetworkVariableAnticipationTests.cs create mode 100644 Tests/Runtime/NetworkVariableAnticipationTests.cs.meta create mode 100644 Tests/Runtime/NetworkVariableTestsHelperTypes.cs create mode 100644 Tests/Runtime/NetworkVariableTestsHelperTypes.cs.meta create mode 100644 Tests/Runtime/NetworkVariableTraitsTests.cs create mode 100644 Tests/Runtime/NetworkVariableTraitsTests.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c3955b..3980348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,37 @@ 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.9.1] - 2024-04-18 + +### Added +- Added AnticipatedNetworkVariable, which adds support for client anticipation of NetworkVariable values, allowing for more responsive gameplay (#2820) +- Added AnticipatedNetworkTransform, which adds support for client anticipation of NetworkTransforms (#2820) +- Added NetworkVariableBase.ExceedsDirtinessThreshold to allow network variables to throttle updates by only sending updates when the difference between the current and previous values exceeds a threshold. (This is exposed in NetworkVariable with the callback NetworkVariable.CheckExceedsDirtinessThreshold) (#2820) +- Added NetworkVariableUpdateTraits, which add additional throttling support: MinSecondsBetweenUpdates will prevent the NetworkVariable from sending updates more often than the specified time period (even if it exceeds the dirtiness threshold), while MaxSecondsBetweenUpdates will force a dirty NetworkVariable to send an update after the specified time period even if it has not yet exceeded the dirtiness threshold. (#2820) +- Added virtual method NetworkVariableBase.OnInitialize() which can be used by NetworkVariable subclasses to add initialization code (#2820) +- Added virtual method NetworkVariableBase.Update(), which is called once per frame to support behaviors such as interpolation between an anticipated value and an authoritative one. (#2820) +- Added NetworkTime.TickWithPartial, which represents the current tick as a double that includes the fractional/partial tick value. (#2820) +- Added NetworkTickSystem.AnticipationTick, which can be helpful with implementation of client anticipation. This value represents the tick the current local client was at at the beginning of the most recent network round trip, which enables it to correlate server update ticks with the client tick that may have triggered them. (#2820) +- `NetworkVariable` now includes built-in support for `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, and `Dictionary` (#2813) +- `NetworkVariable` now includes delta compression for collection values (`NativeList`, `NativeArray`, `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, `Dictionary`, and `FixedString` types) to save bandwidth by only sending the values that changed. (Note: For `NativeList`, `NativeArray`, and `List`, this algorithm works differently than that used in `NetworkList`. This algorithm will use less bandwidth for "set" and "add" operations, but `NetworkList` is more bandwidth-efficient if you are performing frequent "insert" operations.) (#2813) +- `UserNetworkVariableSerialization` now has optional callbacks for `WriteDelta` and `ReadDelta`. If both are provided, they will be used for all serialization operations on NetworkVariables of that type except for the first one for each client. If either is missing, the existing `Write` and `Read` will always be used. (#2813) +- Network variables wrapping `INetworkSerializable` types can perform delta serialization by setting `UserNetworkVariableSerialization.WriteDelta` and `UserNetworkVariableSerialization.ReadDelta` for those types. The built-in `INetworkSerializable` serializer will continue to be used for all other serialization operations, but if those callbacks are set, it will call into them on all but the initial serialization to perform delta serialization. (This could be useful if you have a large struct where most values do not change regularly and you want to send only the fields that did change.) (#2813) + +### Fixed + +- Fixed issue where NetworkTransformEditor would throw and exception if you excluded the physics package. (#2871) +- Fixed issue where `NetworkTransform` could not properly synchronize its base position when using half float precision. (#2845) +- Fixed issue where the host was not invoking `OnClientDisconnectCallback` for its own local client when internally shutting down. (#2822) +- Fixed issue where NetworkTransform could potentially attempt to "unregister" a named message prior to it being registered. (#2807) +- Fixed issue where in-scene placed `NetworkObject`s with complex nested children `NetworkObject`s (more than one child in depth) would not synchronize properly if WorldPositionStays was set to true. (#2796) + +### Changed + +- Changed `NetworkObjectReference` and `NetworkBehaviourReference` to allow null references when constructing and serializing. (#2874) +- Changed `NetworkAnimator` no longer requires the `Animator` component to exist on the same `GameObject`. (#2872) +- Changed `NetworkTransform` to now use `NetworkTransformMessage` as opposed to named messages for NetworkTransformState updates. (#2810) +- Changed `CustomMessageManager` so it no longer attempts to register or "unregister" a null or empty string and will log an error if this condition occurs. (#2807) + ## [1.8.1] - 2024-02-05 ### Fixed diff --git a/Components/AnticipatedNetworkTransform.cs b/Components/AnticipatedNetworkTransform.cs new file mode 100644 index 0000000..f39bda4 --- /dev/null +++ b/Components/AnticipatedNetworkTransform.cs @@ -0,0 +1,500 @@ +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Netcode.Components +{ + +#pragma warning disable IDE0001 + /// + /// A subclass of that supports basic client anticipation - the client + /// can set a value on the belief that the server will update it to reflect the same value in a future update + /// (i.e., as the result of an RPC call). This value can then be adjusted as new updates from the server come in, + /// in three basic modes: + /// + /// + /// + /// Snap: In this mode (with set to + /// and no callback), + /// the moment a more up-to-date value is received from the authority, it will simply replace the anticipated value, + /// resulting in a "snap" to the new value if it is different from the anticipated value. + /// + /// Smooth: In this mode (with set to + /// and an callback that calls + /// from the anticipated value to the authority value with an appropriate + /// -style smooth function), when a more up-to-date value is received from the authority, + /// it will interpolate over time from an incorrect anticipated value to the correct authoritative value. + /// + /// Constant Reanticipation: In this mode (with set to + /// and an that calculates a + /// new anticipated value based on the current authoritative value), when a more up-to-date value is received from + /// the authority, user code calculates a new anticipated value, possibly calling to interpolate + /// between the previous anticipation and the new anticipation. This is useful for values that change frequently and + /// need to constantly be re-evaluated, as opposed to values that change only in response to user action and simply + /// need a one-time anticipation when the user performs that action. + /// + /// + /// + /// Note that these three modes may be combined. For example, if an callback + /// does not call either or one of the Anticipate methods, the result will be a snap to the + /// authoritative value, enabling for a callback that may conditionally call when the + /// difference between the anticipated and authoritative values is within some threshold, but fall back to + /// snap behavior if the difference is too large. + /// +#pragma warning restore IDE0001 + [DisallowMultipleComponent] + [AddComponentMenu("Netcode/Anticipated Network Transform")] + [DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts + public class AnticipatedNetworkTransform : NetworkTransform + { + public struct TransformState + { + public Vector3 Position; + public Quaternion Rotation; + public Vector3 Scale; + } + + private TransformState m_AuthoritativeTransform = new TransformState(); + private TransformState m_AnticipatedTransform = new TransformState(); + private TransformState m_PreviousAnticipatedTransform = new TransformState(); + private ulong m_LastAnticipaionCounter; + private double m_LastAnticipationTime; + private ulong m_LastAuthorityUpdateCounter; + + private TransformState m_SmoothFrom; + private TransformState m_SmoothTo; + private float m_SmoothDuration; + private float m_CurrentSmoothTime; + + private bool m_OutstandingAuthorityChange = false; + +#if UNITY_EDITOR + private void Reset() + { + // Anticipation + smoothing is a form of interpolation, and adding NetworkTransform's buffered interpolation + // makes the anticipation get weird, so we default it to false. + Interpolate = false; + } +#endif + +#pragma warning disable IDE0001 + /// + /// Defines what the behavior should be if we receive a value from the server with an earlier associated + /// time value than the anticipation time value. + ///

+ /// If this is , the stale data will be ignored and the authoritative + /// value will not replace the anticipated value until the anticipation time is reached. + /// and will also not be invoked for this stale data. + ///

+ /// If this is , the stale data will replace the anticipated data and + /// and will be invoked. + /// In this case, the authoritativeTime value passed to will be lower than + /// the anticipationTime value, and that callback can be used to calculate a new anticipated value. + ///
+#pragma warning restore IDE0001 + public StaleDataHandling StaleDataHandling = StaleDataHandling.Reanticipate; + + /// + /// Contains the current state of this transform on the server side. + /// Note that, on the server side, this gets updated at the end of the frame, and will not immediately reflect + /// changes to the transform. + /// + public TransformState AuthoritativeState => m_AuthoritativeTransform; + + /// + /// Contains the current anticipated state, which will match the values of this object's + /// actual . When a server + /// update arrives, this value will be overwritten by the new + /// server value (unless stale data handling is set to "Ignore" + /// and the update is determined to be stale). This value will + /// be duplicated in , which + /// will NOT be overwritten in server updates. + /// + public TransformState AnticipatedState => m_AnticipatedTransform; + + /// + /// Indicates whether this transform currently needs + /// reanticipation. If this is true, the anticipated value + /// has been overwritten by the authoritative value from the + /// server; the previous anticipated value is stored in + /// + public bool ShouldReanticipate + { + get; + private set; + } + + /// + /// Holds the most recent anticipated state, whatever was + /// most recently set using the Anticipate methods. Unlike + /// , this does not get overwritten + /// when a server update arrives. + /// + public TransformState PreviousAnticipatedState => m_PreviousAnticipatedTransform; + + /// + /// Anticipate that, at the end of one round trip to the server, this transform will be in the given + /// + /// + /// + public void AnticipateMove(Vector3 newPosition) + { + if (NetworkManager.ShutdownInProgress || !NetworkManager.IsListening) + { + return; + } + transform.position = newPosition; + m_AnticipatedTransform.Position = newPosition; + if (CanCommitToTransform) + { + m_AuthoritativeTransform.Position = newPosition; + } + + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + + m_LastAnticipaionCounter = NetworkManager.AnticipationSystem.AnticipationCounter; + m_LastAnticipationTime = NetworkManager.LocalTime.Time; + + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + } + + /// + /// Anticipate that, at the end of one round trip to the server, this transform will have the given + /// + /// + /// + public void AnticipateRotate(Quaternion newRotation) + { + if (NetworkManager.ShutdownInProgress || !NetworkManager.IsListening) + { + return; + } + transform.rotation = newRotation; + m_AnticipatedTransform.Rotation = newRotation; + if (CanCommitToTransform) + { + m_AuthoritativeTransform.Rotation = newRotation; + } + + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + + m_LastAnticipaionCounter = NetworkManager.AnticipationSystem.AnticipationCounter; + m_LastAnticipationTime = NetworkManager.LocalTime.Time; + + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + } + + /// + /// Anticipate that, at the end of one round trip to the server, this transform will have the given + /// + /// + /// + public void AnticipateScale(Vector3 newScale) + { + if (NetworkManager.ShutdownInProgress || !NetworkManager.IsListening) + { + return; + } + transform.localScale = newScale; + m_AnticipatedTransform.Scale = newScale; + if (CanCommitToTransform) + { + m_AuthoritativeTransform.Scale = newScale; + } + + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + + m_LastAnticipaionCounter = NetworkManager.AnticipationSystem.AnticipationCounter; + m_LastAnticipationTime = NetworkManager.LocalTime.Time; + + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + } + + /// + /// Anticipate that, at the end of one round trip to the server, the transform will have the given + /// + /// + /// + public void AnticipateState(TransformState newState) + { + if (NetworkManager.ShutdownInProgress || !NetworkManager.IsListening) + { + return; + } + var transform_ = transform; + transform_.position = newState.Position; + transform_.rotation = newState.Rotation; + transform_.localScale = newState.Scale; + m_AnticipatedTransform = newState; + if (CanCommitToTransform) + { + m_AuthoritativeTransform = newState; + } + + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + + m_LastAnticipaionCounter = NetworkManager.AnticipationSystem.AnticipationCounter; + m_LastAnticipationTime = NetworkManager.LocalTime.Time; + + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + } + + protected override void Update() + { + // If not spawned or this instance has authority, exit early + if (!IsSpawned) + { + return; + } + // Do not call the base class implementation... + // AnticipatedNetworkTransform applies its authoritative state immediately rather than waiting for update + // This is because AnticipatedNetworkTransforms may need to reference each other in reanticipating + // and we will want all reanticipation done before anything else wants to reference the transform in + // Update() + //base.Update(); + + if (m_CurrentSmoothTime < m_SmoothDuration) + { + m_CurrentSmoothTime += NetworkManager.RealTimeProvider.DeltaTime; + var transform_ = transform; + var pct = math.min(m_CurrentSmoothTime / m_SmoothDuration, 1f); + + m_AnticipatedTransform = new TransformState + { + Position = Vector3.Lerp(m_SmoothFrom.Position, m_SmoothTo.Position, pct), + Rotation = Quaternion.Slerp(m_SmoothFrom.Rotation, m_SmoothTo.Rotation, pct), + Scale = Vector3.Lerp(m_SmoothFrom.Scale, m_SmoothTo.Scale, pct) + }; + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + if (!CanCommitToTransform) + { + transform_.position = m_AnticipatedTransform.Position; + transform_.localScale = m_AnticipatedTransform.Scale; + transform_.rotation = m_AnticipatedTransform.Rotation; + } + } + } + + internal class AnticipatedObject : IAnticipationEventReceiver, IAnticipatedObject + { + public AnticipatedNetworkTransform Transform; + + + public void SetupForRender() + { + if (Transform.CanCommitToTransform) + { + var transform_ = Transform.transform; + Transform.m_AuthoritativeTransform = new TransformState + { + Position = transform_.position, + Rotation = transform_.rotation, + Scale = transform_.localScale + }; + if (Transform.m_CurrentSmoothTime >= Transform.m_SmoothDuration) + { + // If we've had a call to Smooth() we'll continue interpolating. + // Otherwise we'll go ahead and make the visual and actual locations + // match. + Transform.m_AnticipatedTransform = Transform.m_AuthoritativeTransform; + } + + transform_.position = Transform.m_AnticipatedTransform.Position; + transform_.rotation = Transform.m_AnticipatedTransform.Rotation; + transform_.localScale = Transform.m_AnticipatedTransform.Scale; + } + } + + public void SetupForUpdate() + { + if (Transform.CanCommitToTransform) + { + var transform_ = Transform.transform; + transform_.position = Transform.m_AuthoritativeTransform.Position; + transform_.rotation = Transform.m_AuthoritativeTransform.Rotation; + transform_.localScale = Transform.m_AuthoritativeTransform.Scale; + } + } + + public void Update() + { + // No need to do this, it's handled by NetworkBehaviour.Update + } + + public void ResetAnticipation() + { + Transform.ShouldReanticipate = false; + } + + public NetworkObject OwnerObject => Transform.NetworkObject; + } + + private AnticipatedObject m_AnticipatedObject = null; + + private void ResetAnticipatedState() + { + var transform_ = transform; + m_AuthoritativeTransform = new TransformState + { + Position = transform_.position, + Rotation = transform_.rotation, + Scale = transform_.localScale + }; + m_AnticipatedTransform = m_AuthoritativeTransform; + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + } + + protected override void OnSynchronize(ref BufferSerializer serializer) + { + base.OnSynchronize(ref serializer); + if (!CanCommitToTransform) + { + m_OutstandingAuthorityChange = true; + ApplyAuthoritativeState(); + ResetAnticipatedState(); + } + } + + public override void OnNetworkSpawn() + { + base.OnNetworkSpawn(); + m_OutstandingAuthorityChange = true; + ApplyAuthoritativeState(); + ResetAnticipatedState(); + + m_AnticipatedObject = new AnticipatedObject { Transform = this }; + NetworkManager.AnticipationSystem.RegisterForAnticipationEvents(m_AnticipatedObject); + NetworkManager.AnticipationSystem.AllAnticipatedObjects.Add(m_AnticipatedObject); + } + + public override void OnNetworkDespawn() + { + if (m_AnticipatedObject != null) + { + NetworkManager.AnticipationSystem.DeregisterForAnticipationEvents(m_AnticipatedObject); + NetworkManager.AnticipationSystem.AllAnticipatedObjects.Remove(m_AnticipatedObject); + NetworkManager.AnticipationSystem.ObjectsToReanticipate.Remove(m_AnticipatedObject); + m_AnticipatedObject = null; + } + ResetAnticipatedState(); + + base.OnNetworkDespawn(); + } + + public override void OnDestroy() + { + if (m_AnticipatedObject != null) + { + NetworkManager.AnticipationSystem.DeregisterForAnticipationEvents(m_AnticipatedObject); + NetworkManager.AnticipationSystem.AllAnticipatedObjects.Remove(m_AnticipatedObject); + NetworkManager.AnticipationSystem.ObjectsToReanticipate.Remove(m_AnticipatedObject); + m_AnticipatedObject = null; + } + + base.OnDestroy(); + } + + /// + /// Interpolate between the transform represented by to the transform represented by + /// over of real time. The duration uses + /// , so it is affected by . + /// + /// + /// + /// + public void Smooth(TransformState from, TransformState to, float durationSeconds) + { + var transform_ = transform; + if (durationSeconds <= 0) + { + m_AnticipatedTransform = to; + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + transform_.position = to.Position; + transform_.rotation = to.Rotation; + transform_.localScale = to.Scale; + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + return; + } + m_AnticipatedTransform = from; + m_PreviousAnticipatedTransform = m_AnticipatedTransform; + + if (!CanCommitToTransform) + { + transform_.position = from.Position; + transform_.rotation = from.Rotation; + transform_.localScale = from.Scale; + } + + m_SmoothFrom = from; + m_SmoothTo = to; + m_SmoothDuration = durationSeconds; + m_CurrentSmoothTime = 0; + } + + protected override void OnBeforeUpdateTransformState() + { + // this is called when new data comes from the server + m_LastAuthorityUpdateCounter = NetworkManager.AnticipationSystem.LastAnticipationAck; + m_OutstandingAuthorityChange = true; + } + + protected override void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState) + { + base.OnNetworkTransformStateUpdated(ref oldState, ref newState); + ApplyAuthoritativeState(); + } + + protected override void OnTransformUpdated() + { + if (CanCommitToTransform || m_AnticipatedObject == null) + { + return; + } + // this is called pretty much every frame and will change the transform + // If we've overridden the transform with an anticipated state, we need to be able to change it back + // to the anticipated state (while updating the authority state accordingly) or else + // mark this transform for reanticipation + var transform_ = transform; + + var previousAnticipatedTransform = m_AnticipatedTransform; + + // Update authority state to catch any possible interpolation data + m_AuthoritativeTransform.Position = transform_.position; + m_AuthoritativeTransform.Rotation = transform_.rotation; + m_AuthoritativeTransform.Scale = transform_.localScale; + + if (!m_OutstandingAuthorityChange) + { + // Keep the anticipated value unchanged, we have no updates from the server at all. + transform_.position = previousAnticipatedTransform.Position; + transform_.localScale = previousAnticipatedTransform.Scale; + transform_.rotation = previousAnticipatedTransform.Rotation; + return; + } + + if (StaleDataHandling == StaleDataHandling.Ignore && m_LastAnticipaionCounter > m_LastAuthorityUpdateCounter) + { + // Keep the anticipated value unchanged because it is more recent than the authoritative one. + transform_.position = previousAnticipatedTransform.Position; + transform_.localScale = previousAnticipatedTransform.Scale; + transform_.rotation = previousAnticipatedTransform.Rotation; + return; + } + + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + m_OutstandingAuthorityChange = false; + m_AnticipatedTransform = m_AuthoritativeTransform; + + ShouldReanticipate = true; + NetworkManager.AnticipationSystem.ObjectsToReanticipate.Add(m_AnticipatedObject); + } + } +} diff --git a/Components/AnticipatedNetworkTransform.cs.meta b/Components/AnticipatedNetworkTransform.cs.meta new file mode 100644 index 0000000..1adaf33 --- /dev/null +++ b/Components/AnticipatedNetworkTransform.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 97616b67982a4be48d957d421e422433 +timeCreated: 1705597211 \ No newline at end of file diff --git a/Components/Messages.meta b/Components/Messages.meta new file mode 100644 index 0000000..fcf8b73 --- /dev/null +++ b/Components/Messages.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9db1d18fa0117f4da5e8e65386b894a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/Messages/NetworkTransformMessage.cs b/Components/Messages/NetworkTransformMessage.cs new file mode 100644 index 0000000..1cdd9fa --- /dev/null +++ b/Components/Messages/NetworkTransformMessage.cs @@ -0,0 +1,116 @@ +using Unity.Netcode.Components; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// NetworkTransform State Update Message + /// + internal struct NetworkTransformMessage : INetworkMessage + { + public int Version => 0; + public ulong NetworkObjectId; + public int NetworkBehaviourId; + public NetworkTransform.NetworkTransformState State; + + private NetworkTransform m_ReceiverNetworkTransform; + private FastBufferReader m_CurrentReader; + + private unsafe void CopyPayload(ref FastBufferWriter writer) + { + writer.WriteBytesSafe(m_CurrentReader.GetUnsafePtrAtCurrentPosition(), m_CurrentReader.Length - m_CurrentReader.Position); + } + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + if (m_CurrentReader.IsInitialized) + { + CopyPayload(ref writer); + } + else + { + BytePacker.WriteValueBitPacked(writer, NetworkObjectId); + BytePacker.WriteValueBitPacked(writer, NetworkBehaviourId); + writer.WriteNetworkSerializable(State); + } + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + var networkManager = context.SystemOwner as NetworkManager; + if (networkManager == null) + { + Debug.LogError($"[{nameof(NetworkTransformMessage)}] System owner context was not of type {nameof(NetworkManager)}!"); + return false; + } + var currentPosition = reader.Position; + ByteUnpacker.ReadValueBitPacked(reader, out NetworkObjectId); + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId)) + { + networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context); + return false; + } + // Get the behaviour index + ByteUnpacker.ReadValueBitPacked(reader, out NetworkBehaviourId); + + // Deserialize the state + reader.ReadNetworkSerializable(out State); + + var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId]; + + // Get the target NetworkTransform + m_ReceiverNetworkTransform = networkObject.ChildNetworkBehaviours[NetworkBehaviourId] as NetworkTransform; + + var isServerAuthoritative = m_ReceiverNetworkTransform.IsServerAuthoritative(); + var ownerAuthoritativeServerSide = !isServerAuthoritative && networkManager.IsServer; + if (ownerAuthoritativeServerSide) + { + var ownerClientId = networkObject.OwnerClientId; + if (ownerClientId == NetworkManager.ServerClientId) + { + // Ownership must have changed, ignore any additional pending messages that might have + // come from a previous owner client. + return true; + } + + var networkDelivery = State.IsReliableStateUpdate() ? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced; + + // Forward the state update if there are any remote clients to foward it to + if (networkManager.ConnectionManager.ConnectedClientsList.Count > (networkManager.IsHost ? 2 : 1)) + { + // This is only to copy the existing and already serialized struct for forwarding purposes only. + // This will not include any changes made to this struct at this particular stage of processing the message. + var currentMessage = this; + // Create a new reader that replicates this message + currentMessage.m_CurrentReader = new FastBufferReader(reader, Collections.Allocator.None); + // Rewind the new reader to the beginning of the message's payload + currentMessage.m_CurrentReader.Seek(currentPosition); + // Forward the message to all connected clients that are observers of the associated NetworkObject + var clientCount = networkManager.ConnectionManager.ConnectedClientsList.Count; + for (int i = 0; i < clientCount; i++) + { + var clientId = networkManager.ConnectionManager.ConnectedClientsList[i].ClientId; + if (NetworkManager.ServerClientId == clientId || (!isServerAuthoritative && clientId == ownerClientId) || !networkObject.Observers.Contains(clientId)) + { + continue; + } + networkManager.MessageManager.SendMessage(ref currentMessage, networkDelivery, clientId); + } + // Dispose of the reader used for forwarding + currentMessage.m_CurrentReader.Dispose(); + } + } + return true; + } + + public void Handle(ref NetworkContext context) + { + if (m_ReceiverNetworkTransform == null) + { + Debug.LogError($"[{nameof(NetworkTransformMessage)}][Dropped] Reciever {nameof(NetworkTransform)} was not set!"); + return; + } + m_ReceiverNetworkTransform.TransformStateUpdate(ref State); + } + } +} diff --git a/Components/Messages/NetworkTransformMessage.cs.meta b/Components/Messages/NetworkTransformMessage.cs.meta new file mode 100644 index 0000000..640ec3b --- /dev/null +++ b/Components/Messages/NetworkTransformMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcfc8ac43fef97e42adb19b998d70c37 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs index 79030b7..8aaea1e 100644 --- a/Components/NetworkAnimator.cs +++ b/Components/NetworkAnimator.cs @@ -164,7 +164,6 @@ namespace Unity.Netcode.Components /// NetworkAnimator enables remote synchronization of state for on network objects. /// [AddComponentMenu("Netcode/Network Animator")] - [RequireComponent(typeof(Animator))] public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver { [Serializable] diff --git a/Components/NetworkRigidbody.cs b/Components/NetworkRigidbody.cs index 0569aa9..bebaf79 100644 --- a/Components/NetworkRigidbody.cs +++ b/Components/NetworkRigidbody.cs @@ -29,15 +29,25 @@ namespace Unity.Netcode.Components m_NetworkTransform = GetComponent(); m_IsServerAuthoritative = m_NetworkTransform.IsServerAuthoritative(); + SetupRigidBody(); + } + + /// + /// If the current has authority, + /// then use the interpolation strategy, + /// if the is handling interpolation, + /// set interpolation to none on the + ///
+ /// Turn off physics for the rigid body until spawned, otherwise + /// clients can run fixed update before the first + /// full update + ///
+ private void SetupRigidBody() + { 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.interpolation = m_IsAuthority ? m_OriginalInterpolation : (m_NetworkTransform.Interpolate ? RigidbodyInterpolation.None : m_OriginalInterpolation); m_Rigidbody.isKinematic = true; } diff --git a/Components/NetworkRigidbody2D.cs b/Components/NetworkRigidbody2D.cs index 246519c..3a37b34 100644 --- a/Components/NetworkRigidbody2D.cs +++ b/Components/NetworkRigidbody2D.cs @@ -30,6 +30,11 @@ namespace Unity.Netcode.Components { m_Rigidbody = GetComponent(); m_NetworkTransform = GetComponent(); + + // 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; } private void FixedUpdate() diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs index c463d39..96d3f72 100644 --- a/Components/NetworkTransform.cs +++ b/Components/NetworkTransform.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using Unity.Collections; using Unity.Mathematics; using UnityEngine; @@ -1330,9 +1329,6 @@ namespace Unity.Netcode.Components private Quaternion m_CurrentRotation; private Vector3 m_TargetRotation; - // Used to for each instance to uniquely identify the named message - private string m_MessageName; - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void UpdatePositionInterpolator(Vector3 position, double time, bool resetInterpolator = false) @@ -1428,6 +1424,7 @@ namespace Unity.Netcode.Components /// the clientId being synchronized (both reading and writing) protected override void OnSynchronize(ref BufferSerializer serializer) { + m_CachedNetworkManager = NetworkManager; var targetClientId = m_TargetIdBeingSynchronized; var synchronizationState = new NetworkTransformState() { @@ -2051,10 +2048,15 @@ namespace Unity.Netcode.Components return isDirty; } + protected virtual void OnTransformUpdated() + { + + } + /// /// Applies the authoritative state to the transform /// - private void ApplyAuthoritativeState() + protected internal void ApplyAuthoritativeState() { var networkState = m_LocalAuthoritativeNetworkState; // The m_CurrentPosition, m_CurrentRotation, and m_CurrentScale values are continually updated @@ -2225,6 +2227,7 @@ namespace Unity.Netcode.Components } transform.localScale = m_CurrentScale; } + OnTransformUpdated(); } /// @@ -2422,6 +2425,7 @@ namespace Unity.Netcode.Components { AddLogEntry(ref newState, NetworkObject.OwnerClientId); } + OnTransformUpdated(); } /// @@ -2590,6 +2594,11 @@ namespace Unity.Netcode.Components } + protected virtual void OnBeforeUpdateTransformState() + { + + } + private NetworkTransformState m_OldState = new NetworkTransformState(); /// @@ -2613,6 +2622,8 @@ namespace Unity.Netcode.Components // Get the time when this new state was sent newState.SentTime = new NetworkTime(m_CachedNetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time; + OnBeforeUpdateTransformState(); + // Apply the new state ApplyUpdatedState(newState); @@ -2753,21 +2764,17 @@ namespace Unity.Netcode.Components // Started using this again to avoid the getter processing cost of NetworkBehaviour.NetworkManager m_CachedNetworkManager = NetworkManager; - // Register a custom named message specifically for this instance - m_MessageName = $"NTU_{NetworkObjectId}_{NetworkBehaviourId}"; - m_CachedNetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(m_MessageName, TransformStateUpdate); Initialize(); + + if (CanCommitToTransform && UseHalfFloatPrecision) + { + SetState(GetSpaceRelativePosition(), GetSpaceRelativeRotation(), GetScale(), false); + } } /// public override void OnNetworkDespawn() { - // During destroy, use NetworkBehaviour.NetworkManager as opposed to m_CachedNetworkManager - if (!NetworkManager.ShutdownInProgress && NetworkManager.CustomMessagingManager != null) - { - NetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(m_MessageName); - } - DeregisterForTickUpdate(this); CanCommitToTransform = false; @@ -3156,79 +3163,23 @@ namespace Unity.Netcode.Components } /// - /// Receives the named message updates + /// Invoked by to update the transform state /// - /// authority of the transform - /// serialzied - private void TransformStateUpdate(ulong senderId, FastBufferReader messagePayload) + /// + internal void TransformStateUpdate(ref NetworkTransformState networkTransformState) { - var ownerAuthoritativeServerSide = !OnIsServerAuthoritative() && IsServer; - if (ownerAuthoritativeServerSide && OwnerClientId == NetworkManager.ServerClientId) - { - // Ownership must have changed, ignore any additional pending messages that might have - // come from a previous owner client. - return; - } - // Store the previous/old state m_OldState = m_LocalAuthoritativeNetworkState; - // Save the current payload stream position - var currentPosition = messagePayload.Position; + // Assign the new incoming state + m_LocalAuthoritativeNetworkState = networkTransformState; - // Deserialize the message (and determine network delivery) - messagePayload.ReadNetworkSerializableInPlace(ref m_LocalAuthoritativeNetworkState); - - // Rewind back prior to serialization - messagePayload.Seek(currentPosition); - - // Get the network delivery method used to send this state update - var networkDelivery = m_LocalAuthoritativeNetworkState.ReliableSequenced ? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced; - - // Forward owner authoritative messages before doing anything else - if (ownerAuthoritativeServerSide) - { - // Forward the state update if there are any remote clients to foward it to - if (m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count > (IsHost ? 2 : 1)) - { - ForwardStateUpdateMessage(messagePayload, networkDelivery); - } - } - - // Apply the message + // Apply the state update OnNetworkStateChanged(m_OldState, m_LocalAuthoritativeNetworkState); } /// - /// Forwards owner authoritative state updates when received by the server - /// - /// the owner state message payload - private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload, NetworkDelivery networkDelivery) - { - var serverAuthoritative = OnIsServerAuthoritative(); - var currentPosition = messagePayload.Position; - var messageSize = messagePayload.Length - currentPosition; - var writer = new FastBufferWriter(messageSize, Allocator.Temp); - using (writer) - { - writer.WriteBytesSafe(messagePayload.GetUnsafePtr(), messageSize, currentPosition); - - var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count; - for (int i = 0; i < clientCount; i++) - { - var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; - if (NetworkManager.ServerClientId == clientId || (!serverAuthoritative && clientId == OwnerClientId)) - { - continue; - } - m_CachedNetworkManager.CustomMessagingManager.SendNamedMessage(m_MessageName, clientId, writer, networkDelivery); - } - } - messagePayload.Seek(currentPosition); - } - - /// - /// Sends named message updates by the authority of the transform + /// Invoked by the authoritative instance to sends a containing the /// private void UpdateTransformState() { @@ -3248,7 +3199,12 @@ namespace Unity.Netcode.Components } var customMessageManager = m_CachedNetworkManager.CustomMessagingManager; - var writer = new FastBufferWriter(128, Allocator.Temp); + var networkTransformMessage = new NetworkTransformMessage() + { + NetworkObjectId = NetworkObjectId, + NetworkBehaviourId = NetworkBehaviourId, + State = m_LocalAuthoritativeNetworkState + }; // Determine what network delivery method to use: // When to send reliable packets: @@ -3259,32 +3215,32 @@ namespace Unity.Netcode.Components | m_LocalAuthoritativeNetworkState.UnreliableFrameSync | m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat ? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced; - using (writer) + // Server-host always sends updates to all clients (but itself) + if (IsServer) { - writer.WriteNetworkSerializable(m_LocalAuthoritativeNetworkState); - // Server-host always sends updates to all clients (but itself) - if (IsServer) + var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count; + for (int i = 0; i < clientCount; i++) { - var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count; - for (int i = 0; i < clientCount; i++) + var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; + if (NetworkManager.ServerClientId == clientId) { - var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId; - if (NetworkManager.ServerClientId == clientId) - { - continue; - } - customMessageManager.SendNamedMessage(m_MessageName, clientId, writer, networkDelivery); + continue; } + if (!NetworkObject.Observers.Contains(clientId)) + { + continue; + } + NetworkManager.MessageManager.SendMessage(ref networkTransformMessage, networkDelivery, clientId); } - else - { - // Clients (owner authoritative) send messages to the server-host - customMessageManager.SendNamedMessage(m_MessageName, NetworkManager.ServerClientId, writer, networkDelivery); - } + } + else + { + // Clients (owner authoritative) send messages to the server-host + NetworkManager.MessageManager.SendMessage(ref networkTransformMessage, networkDelivery, NetworkManager.ServerClientId); } } - + #region Network Tick Registration and Handling private static Dictionary s_NetworkTickRegistration = new Dictionary(); private static void RemoveTickUpdate(NetworkManager networkManager) @@ -3379,7 +3335,7 @@ namespace Unity.Netcode.Components /// /// If a NetworkTransformTickRegistration exists for the NetworkManager instance, then this will - /// remove the NetworkTransform instance from the single tick update entry point. + /// remove the NetworkTransform instance from the single tick update entry point. /// /// private static void DeregisterForTickUpdate(NetworkTransform networkTransform) @@ -3394,6 +3350,8 @@ namespace Unity.Netcode.Components } } } + + #endregion } internal interface INetworkTransformLogStateEntry diff --git a/Editor/AnticipatedNetworkTransformEditor.cs b/Editor/AnticipatedNetworkTransformEditor.cs new file mode 100644 index 0000000..0986808 --- /dev/null +++ b/Editor/AnticipatedNetworkTransformEditor.cs @@ -0,0 +1,14 @@ +using Unity.Netcode.Components; +using UnityEditor; + +namespace Unity.Netcode.Editor +{ + /// + /// The for + /// + [CustomEditor(typeof(AnticipatedNetworkTransform), true)] + public class AnticipatedNetworkTransformEditor : NetworkTransformEditor + { + public override bool HideInterpolateValue => true; + } +} diff --git a/Editor/AnticipatedNetworkTransformEditor.cs.meta b/Editor/AnticipatedNetworkTransformEditor.cs.meta new file mode 100644 index 0000000..3e98b97 --- /dev/null +++ b/Editor/AnticipatedNetworkTransformEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 34bc168605014eeeadf97b12080e11fa +timeCreated: 1707514321 \ No newline at end of file diff --git a/Editor/CodeGen/CodeGenHelpers.cs b/Editor/CodeGen/CodeGenHelpers.cs index 6368e5e..75387fa 100644 --- a/Editor/CodeGen/CodeGenHelpers.cs +++ b/Editor/CodeGen/CodeGenHelpers.cs @@ -21,6 +21,7 @@ namespace Unity.Netcode.Editor.CodeGen public const string NetcodeModuleName = "Unity.Netcode.Runtime.dll"; public const string RuntimeAssemblyName = "Unity.Netcode.Runtime"; + public const string ComponentsAssemblyName = "Unity.Netcode.Components"; public static readonly string NetworkBehaviour_FullName = typeof(NetworkBehaviour).FullName; public static readonly string INetworkMessage_FullName = typeof(INetworkMessage).FullName; diff --git a/Editor/CodeGen/INetworkMessageILPP.cs b/Editor/CodeGen/INetworkMessageILPP.cs index 23436bf..d74b737 100644 --- a/Editor/CodeGen/INetworkMessageILPP.cs +++ b/Editor/CodeGen/INetworkMessageILPP.cs @@ -17,7 +17,7 @@ namespace Unity.Netcode.Editor.CodeGen { public override ILPPInterface GetInstance() => this; - public override bool WillProcess(ICompiledAssembly compiledAssembly) => compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName; + public override bool WillProcess(ICompiledAssembly compiledAssembly) => compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName || compiledAssembly.Name == CodeGenHelpers.ComponentsAssemblyName; private readonly List m_Diagnostics = new List(); diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs b/Editor/CodeGen/NetworkBehaviourILPP.cs index 55a195f..1734d02 100644 --- a/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -31,6 +31,43 @@ namespace Unity.Netcode.Editor.CodeGen private readonly List m_Diagnostics = new List(); + public void AddWrappedType(TypeReference wrappedType) + { + if (!m_WrappedNetworkVariableTypes.Contains(wrappedType)) + { + m_WrappedNetworkVariableTypes.Add(wrappedType); + + var resolved = wrappedType.Resolve(); + if (resolved != null) + { + if (resolved.FullName == "System.Collections.Generic.List`1") + { + AddWrappedType(((GenericInstanceType)wrappedType).GenericArguments[0]); + } + if (resolved.FullName == "System.Collections.Generic.HashSet`1") + { + AddWrappedType(((GenericInstanceType)wrappedType).GenericArguments[0]); + } + else if (resolved.FullName == "System.Collections.Generic.Dictionary`2") + { + AddWrappedType(((GenericInstanceType)wrappedType).GenericArguments[0]); + AddWrappedType(((GenericInstanceType)wrappedType).GenericArguments[1]); + } +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + else if (resolved.FullName == "Unity.Collections.NativeHashSet`1") + { + AddWrappedType(((GenericInstanceType)wrappedType).GenericArguments[0]); + } + else if (resolved.FullName == "Unity.Collections.NativeHashMap`2") + { + AddWrappedType(((GenericInstanceType)wrappedType).GenericArguments[0]); + AddWrappedType(((GenericInstanceType)wrappedType).GenericArguments[1]); + } +#endif + } + } + } + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) { if (!WillProcess(compiledAssembly)) @@ -87,10 +124,7 @@ namespace Unity.Netcode.Editor.CodeGen if (attribute.AttributeType.Name == nameof(GenerateSerializationForTypeAttribute)) { var wrappedType = mainModule.ImportReference((TypeReference)attribute.ConstructorArguments[0].Value); - if (!m_WrappedNetworkVariableTypes.Contains(wrappedType)) - { - m_WrappedNetworkVariableTypes.Add(wrappedType); - } + AddWrappedType(wrappedType); } } @@ -101,10 +135,7 @@ namespace Unity.Netcode.Editor.CodeGen if (attribute.AttributeType.Name == nameof(GenerateSerializationForTypeAttribute)) { var wrappedType = mainModule.ImportReference((TypeReference)attribute.ConstructorArguments[0].Value); - if (!m_WrappedNetworkVariableTypes.Contains(wrappedType)) - { - m_WrappedNetworkVariableTypes.Add(wrappedType); - } + AddWrappedType(wrappedType); } } } @@ -241,6 +272,36 @@ namespace Unity.Netcode.Editor.CodeGen serializeMethod?.GenericArguments.Add(wrappedType); equalityMethod.GenericArguments.Add(wrappedType); } + else if (type.Resolve().FullName == "System.Collections.Generic.List`1") + { + var wrappedType = ((GenericInstanceType)type).GenericArguments[0]; + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_List_MethodRef); + + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_List_MethodRef); + serializeMethod.GenericArguments.Add(wrappedType); + equalityMethod.GenericArguments.Add(wrappedType); + } + else if (type.Resolve().FullName == "System.Collections.Generic.HashSet`1") + { + var wrappedType = ((GenericInstanceType)type).GenericArguments[0]; + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_HashSet_MethodRef); + + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_HashSet_MethodRef); + serializeMethod.GenericArguments.Add(wrappedType); + equalityMethod.GenericArguments.Add(wrappedType); + } + else if (type.Resolve().FullName == "System.Collections.Generic.Dictionary`2") + { + var wrappedKeyType = ((GenericInstanceType)type).GenericArguments[0]; + var wrappedValType = ((GenericInstanceType)type).GenericArguments[1]; + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_Dictionary_MethodRef); + + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_Dictionary_MethodRef); + serializeMethod.GenericArguments.Add(wrappedKeyType); + serializeMethod.GenericArguments.Add(wrappedValType); + equalityMethod.GenericArguments.Add(wrappedKeyType); + equalityMethod.GenericArguments.Add(wrappedValType); + } #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT else if (type.Resolve().FullName == "Unity.Collections.NativeList`1") { @@ -267,12 +328,30 @@ namespace Unity.Netcode.Editor.CodeGen equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEqualsList_MethodRef); } - if (serializeMethod != null) - { - serializeMethod.GenericArguments.Add(wrappedType); - } + serializeMethod?.GenericArguments.Add(wrappedType); equalityMethod.GenericArguments.Add(wrappedType); } + else if (type.Resolve().FullName == "Unity.Collections.NativeHashSet`1") + { + var wrappedType = ((GenericInstanceType)type).GenericArguments[0]; + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_NativeHashSet_MethodRef); + + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_NativeHashSet_MethodRef); + serializeMethod.GenericArguments.Add(wrappedType); + equalityMethod.GenericArguments.Add(wrappedType); + } + else if (type.Resolve().FullName == "Unity.Collections.NativeHashMap`2") + { + var wrappedKeyType = ((GenericInstanceType)type).GenericArguments[0]; + var wrappedValType = ((GenericInstanceType)type).GenericArguments[1]; + serializeMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeSerializer_NativeHashMap_MethodRef); + + equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_NativeHashMap_MethodRef); + serializeMethod.GenericArguments.Add(wrappedKeyType); + serializeMethod.GenericArguments.Add(wrappedValType); + equalityMethod.GenericArguments.Add(wrappedKeyType); + equalityMethod.GenericArguments.Add(wrappedValType); + } #endif else if (type.IsValueType) { @@ -329,6 +408,7 @@ namespace Unity.Netcode.Editor.CodeGen } else { + m_Diagnostics.AddError($"{type}: Managed type in NetworkVariable must implement IEquatable<{type}>"); equalityMethod = new GenericInstanceMethod(m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef); } @@ -398,7 +478,12 @@ namespace Unity.Netcode.Editor.CodeGen private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedINetworkSerializableArray_MethodRef; #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedINetworkSerializableList_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_NativeHashSet_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_NativeHashMap_MethodRef; #endif + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_List_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_HashSet_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_Dictionary_MethodRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_ManagedINetworkSerializable_MethodRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_FixedString_MethodRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_FixedStringArray_MethodRef; @@ -415,7 +500,12 @@ namespace Unity.Netcode.Editor.CodeGen private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEqualsArray_MethodRef; #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEqualsList_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_NativeHashSet_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_NativeHashMap_MethodRef; #endif + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_List_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_HashSet_MethodRef; + private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_Dictionary_MethodRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef; private MethodReference m_RuntimeInitializeOnLoadAttribute_Ctor; @@ -940,7 +1030,22 @@ namespace Unity.Netcode.Editor.CodeGen case nameof(NetworkVariableSerializationTypes.InitializeSerializer_UnmanagedINetworkSerializableList): m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedINetworkSerializableList_MethodRef = method; break; + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_NativeHashSet): + m_NetworkVariableSerializationTypes_InitializeSerializer_NativeHashSet_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_NativeHashMap): + m_NetworkVariableSerializationTypes_InitializeSerializer_NativeHashMap_MethodRef = method; + break; #endif + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_List): + m_NetworkVariableSerializationTypes_InitializeSerializer_List_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_HashSet): + m_NetworkVariableSerializationTypes_InitializeSerializer_HashSet_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeSerializer_Dictionary): + m_NetworkVariableSerializationTypes_InitializeSerializer_Dictionary_MethodRef = method; + break; case nameof(NetworkVariableSerializationTypes.InitializeSerializer_ManagedINetworkSerializable): m_NetworkVariableSerializationTypes_InitializeSerializer_ManagedINetworkSerializable_MethodRef = method; break; @@ -971,7 +1076,22 @@ namespace Unity.Netcode.Editor.CodeGen case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_UnmanagedIEquatableList): m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedIEquatableList_MethodRef = method; break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_NativeHashSet): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_NativeHashSet_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_NativeHashMap): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_NativeHashMap_MethodRef = method; + break; #endif + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_List): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_List_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_HashSet): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_HashSet_MethodRef = method; + break; + case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_Dictionary): + m_NetworkVariableSerializationTypes_InitializeEqualityChecker_Dictionary_MethodRef = method; + break; case nameof(NetworkVariableSerializationTypes.InitializeEqualityChecker_UnmanagedValueEquals): m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEquals_MethodRef = method; break; @@ -1246,10 +1366,7 @@ namespace Unity.Netcode.Editor.CodeGen continue; } var wrappedType = genericInstanceType.GenericArguments[idx]; - if (!m_WrappedNetworkVariableTypes.Contains(wrappedType)) - { - m_WrappedNetworkVariableTypes.Add(wrappedType); - } + AddWrappedType(wrappedType); } } } @@ -1282,10 +1399,7 @@ namespace Unity.Netcode.Editor.CodeGen continue; } var wrappedType = genericInstanceType.GenericArguments[idx]; - if (!m_WrappedNetworkVariableTypes.Contains(wrappedType)) - { - m_WrappedNetworkVariableTypes.Add(wrappedType); - } + AddWrappedType(wrappedType); } } } diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index 1713ba2..75c5765 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/Editor/NetworkManagerEditor.cs @@ -222,7 +222,7 @@ namespace Unity.Netcode.Editor private void DrawTransportField() { #if RELAY_INTEGRATION_AVAILABLE - var useRelay = EditorPrefs.GetBool(k_UseEasyRelayIntegrationKey, false); + var useRelay = EditorPrefs.GetBool(m_UseEasyRelayIntegrationKey, false); #else var useRelay = false; #endif @@ -257,7 +257,7 @@ namespace Unity.Netcode.Editor } #if RELAY_INTEGRATION_AVAILABLE - private readonly string k_UseEasyRelayIntegrationKey = "NetworkManagerUI_UseRelay_" + Application.dataPath.GetHashCode(); + private readonly string m_UseEasyRelayIntegrationKey = "NetworkManagerUI_UseRelay_" + Application.dataPath.GetHashCode(); private string m_JoinCode = ""; private string m_StartConnectionError = null; private string m_Region = ""; @@ -272,7 +272,7 @@ namespace Unity.Netcode.Editor #if RELAY_INTEGRATION_AVAILABLE // use editor prefs to persist the setting when entering / leaving play mode / exiting Unity - var useRelay = EditorPrefs.GetBool(k_UseEasyRelayIntegrationKey, false); + var useRelay = EditorPrefs.GetBool(m_UseEasyRelayIntegrationKey, false); GUILayout.BeginHorizontal(); useRelay = GUILayout.Toggle(useRelay, "Try Relay in the Editor"); @@ -284,7 +284,7 @@ namespace Unity.Netcode.Editor } GUILayout.EndHorizontal(); - EditorPrefs.SetBool(k_UseEasyRelayIntegrationKey, useRelay); + EditorPrefs.SetBool(m_UseEasyRelayIntegrationKey, useRelay); if (useRelay && !Application.isPlaying && !CloudProjectSettings.projectBound) { EditorGUILayout.HelpBox("To use relay, you need to setup your project in the Project Settings in the Services section.", MessageType.Warning); diff --git a/Editor/NetworkTransformEditor.cs b/Editor/NetworkTransformEditor.cs index 4e7831d..a5abeb0 100644 --- a/Editor/NetworkTransformEditor.cs +++ b/Editor/NetworkTransformEditor.cs @@ -37,6 +37,8 @@ namespace Unity.Netcode.Editor private static GUIContent s_RotationLabel = EditorGUIUtility.TrTextContent("Rotation"); private static GUIContent s_ScaleLabel = EditorGUIUtility.TrTextContent("Scale"); + public virtual bool HideInterpolateValue => false; + /// public void OnEnable() { @@ -137,7 +139,11 @@ namespace Unity.Netcode.Editor EditorGUILayout.Space(); EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel); EditorGUILayout.PropertyField(m_InLocalSpaceProperty); - EditorGUILayout.PropertyField(m_InterpolateProperty); + if (!HideInterpolateValue) + { + EditorGUILayout.PropertyField(m_InterpolateProperty); + } + EditorGUILayout.PropertyField(m_SlerpPosition); EditorGUILayout.PropertyField(m_UseQuaternionSynchronization); if (m_UseQuaternionSynchronization.boolValue) @@ -150,9 +156,10 @@ namespace Unity.Netcode.Editor } EditorGUILayout.PropertyField(m_UseHalfFloatPrecision); -#if COM_UNITY_MODULES_PHYSICS // if rigidbody is present but network rigidbody is not present var go = ((NetworkTransform)target).gameObject; + +#if COM_UNITY_MODULES_PHYSICS if (go.TryGetComponent(out _) && go.TryGetComponent(out _) == false) { EditorGUILayout.HelpBox("This GameObject contains a Rigidbody but no NetworkRigidbody.\n" + diff --git a/Runtime/Connection/NetworkConnectionManager.cs b/Runtime/Connection/NetworkConnectionManager.cs index d737598..db753bd 100644 --- a/Runtime/Connection/NetworkConnectionManager.cs +++ b/Runtime/Connection/NetworkConnectionManager.cs @@ -510,6 +510,15 @@ namespace Unity.Netcode // as the client ID is no longer valid. NetworkManager.Shutdown(true); } + + if (NetworkManager.IsServer) + { + MessageManager.ClientDisconnected(clientId); + } + else + { + MessageManager.ClientDisconnected(NetworkManager.ServerClientId); + } #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportDisconnect.End(); #endif diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index 6d099c0..8801950 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -505,12 +505,14 @@ namespace Unity.Netcode { get { + if (m_NetworkObject != null) + { + return m_NetworkObject; + } + try { - if (m_NetworkObject == null) - { - m_NetworkObject = GetComponentInParent(); - } + m_NetworkObject = GetComponentInParent(); } catch (Exception) { @@ -813,7 +815,16 @@ namespace Unity.Netcode // during OnNetworkSpawn has been sent and needs to be cleared for (int i = 0; i < NetworkVariableFields.Count; i++) { - NetworkVariableFields[i].ResetDirty(); + var networkVariable = NetworkVariableFields[i]; + if (networkVariable.IsDirty()) + { + if (networkVariable.CanSend()) + { + networkVariable.UpdateLastSentTime(); + networkVariable.ResetDirty(); + networkVariable.SetDirty(false); + } + } } } else @@ -821,11 +832,18 @@ namespace Unity.Netcode // mark any variables we wrote as no longer dirty for (int i = 0; i < NetworkVariableIndexesToReset.Count; i++) { - NetworkVariableFields[NetworkVariableIndexesToReset[i]].ResetDirty(); + var networkVariable = NetworkVariableFields[NetworkVariableIndexesToReset[i]]; + if (networkVariable.IsDirty()) + { + if (networkVariable.CanSend()) + { + networkVariable.UpdateLastSentTime(); + networkVariable.ResetDirty(); + networkVariable.SetDirty(false); + } + } } } - - MarkVariablesDirty(false); } internal void PreVariableUpdate() @@ -834,7 +852,6 @@ namespace Unity.Netcode { InitializeVariables(); } - PreNetworkVariableWrite(); } @@ -861,7 +878,10 @@ namespace Unity.Netcode var networkVariable = NetworkVariableFields[k]; if (networkVariable.IsDirty() && networkVariable.CanClientRead(targetClientId)) { - shouldSend = true; + if (networkVariable.CanSend()) + { + shouldSend = true; + } break; } } @@ -902,9 +922,16 @@ namespace Unity.Netcode // TODO: There should be a better way by reading one dirty variable vs. 'n' for (int i = 0; i < NetworkVariableFields.Count; i++) { - if (NetworkVariableFields[i].IsDirty()) + var networkVariable = NetworkVariableFields[i]; + if (networkVariable.IsDirty()) { - return true; + if (networkVariable.CanSend()) + { + return true; + } + // If it's dirty but can't be sent yet, we have to keep monitoring it until one of the + // conditions blocking its send changes. + NetworkManager.BehaviourUpdater.AddForUpdate(NetworkObject); } } @@ -1061,6 +1088,11 @@ namespace Unity.Netcode } + public virtual void OnReanticipate(double lastRoundTripTime) + { + + } + /// /// The relative client identifier targeted for the serialization of this instance. /// diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs index e6bcfc5..67487ce 100644 --- a/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/Runtime/Core/NetworkBehaviourUpdater.cs @@ -11,6 +11,7 @@ namespace Unity.Netcode private NetworkManager m_NetworkManager; private NetworkConnectionManager m_ConnectionManager; private HashSet m_DirtyNetworkObjects = new HashSet(); + private HashSet m_PendingDirtyNetworkObjects = new HashSet(); #if DEVELOPMENT_BUILD || UNITY_EDITOR private ProfilerMarker m_NetworkBehaviourUpdate = new ProfilerMarker($"{nameof(NetworkBehaviour)}.{nameof(NetworkBehaviourUpdate)}"); @@ -18,7 +19,7 @@ namespace Unity.Netcode internal void AddForUpdate(NetworkObject networkObject) { - m_DirtyNetworkObjects.Add(networkObject); + m_PendingDirtyNetworkObjects.Add(networkObject); } internal void NetworkBehaviourUpdate() @@ -28,6 +29,9 @@ namespace Unity.Netcode #endif try { + m_DirtyNetworkObjects.UnionWith(m_PendingDirtyNetworkObjects); + m_PendingDirtyNetworkObjects.Clear(); + // NetworkObject references can become null, when hidden or despawned. Once NUll, there is no point // trying to process them, even if they were previously marked as dirty. m_DirtyNetworkObjects.RemoveWhere((sobj) => sobj == null); diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index 4fd66e3..2faaa82 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -45,15 +45,25 @@ namespace Unity.Netcode DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnNextFrame, 0); + AnticipationSystem.SetupForUpdate(); MessageManager.ProcessIncomingMessageQueue(); MessageManager.CleanupDisconnectedClients(); + + AnticipationSystem.ProcessReanticipation(); } break; case NetworkUpdateStage.PreUpdate: { NetworkTimeSystem.UpdateTime(); + AnticipationSystem.Update(); } break; + case NetworkUpdateStage.PostScriptLateUpdate: + + AnticipationSystem.Sync(); + AnticipationSystem.SetupForRender(); + break; + case NetworkUpdateStage.PostLateUpdate: { // This should be invoked just prior to the MessageManager processes its outbound queue. @@ -274,6 +284,25 @@ namespace Unity.Netcode remove => ConnectionManager.OnTransportFailure -= value; } + public delegate void ReanticipateDelegate(double lastRoundTripTime); + + /// + /// This callback is called after all individual OnReanticipate calls on AnticipatedNetworkVariable + /// and AnticipatedNetworkTransform values have been invoked. The first parameter is a hash set of + /// all the variables that have been changed on this frame (you can detect a particular variable by + /// checking if the set contains it), while the second parameter is a set of all anticipated network + /// transforms that have been changed. Both are passed as their base class type. + /// + /// The third parameter is the local time corresponding to the current authoritative server state + /// (i.e., to determine the amount of time that needs to be re-simulated, you will use + /// NetworkManager.LocalTime.Time - authorityTime). + /// + public event ReanticipateDelegate OnReanticipate + { + add => AnticipationSystem.OnReanticipate += value; + remove => AnticipationSystem.OnReanticipate -= value; + } + /// /// The callback to invoke during connection approval. Allows client code to decide whether or not to allow incoming client connection /// @@ -518,6 +547,8 @@ namespace Unity.Netcode /// public NetworkTickSystem NetworkTickSystem { get; private set; } + internal AnticipationSystem AnticipationSystem { get; private set; } + /// /// Used for time mocking in tests /// @@ -813,6 +844,7 @@ namespace Unity.Netcode this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); + this.RegisterNetworkUpdate(NetworkUpdateStage.PostScriptLateUpdate); this.RegisterNetworkUpdate(NetworkUpdateStage.PostLateUpdate); // ComponentFactory needs to set its defaults next @@ -845,6 +877,7 @@ namespace Unity.Netcode // The remaining systems can then be initialized NetworkTimeSystem = server ? NetworkTimeSystem.ServerTimeSystem() : new NetworkTimeSystem(1.0 / NetworkConfig.TickRate); NetworkTickSystem = NetworkTimeSystem.Initialize(this); + AnticipationSystem = new AnticipationSystem(this); // Create spawn manager instance SpawnManager = new NetworkSpawnManager(this); @@ -1185,6 +1218,12 @@ namespace Unity.Netcode IsListening = false; m_ShuttingDown = false; + // Generate a local notification that the host client is disconnected + if (IsHost) + { + ConnectionManager.InvokeOnClientDisconnectCallback(LocalClientId); + } + if (ConnectionManager.LocalClient.IsClient) { // If we were a client, we want to know if we were a host diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index 4c47753..9dea536 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -992,6 +992,11 @@ namespace Unity.Netcode m_CachedParent = parentTransform; } + internal Transform GetCachedParent() + { + return m_CachedParent; + } + internal ulong? GetNetworkParenting() => m_LatestParent; internal void SetNetworkParenting(ulong? latestParent, bool worldPositionStays) @@ -1236,7 +1241,7 @@ namespace Unity.Netcode // we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one. internal static HashSet OrphanChildren = new HashSet(); - internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false) + internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false) { if (!AutoObjectParentSync) { @@ -1324,9 +1329,17 @@ namespace Unity.Netcode // If we made it here, then parent this instance under the parentObject var parentObject = NetworkManager.SpawnManager.SpawnedObjects[m_LatestParent.Value]; + // If we are handling an orphaned child and its parent is orphaned too, then don't parent yet. + if (orphanedChildPass) + { + if (OrphanChildren.Contains(parentObject)) + { + return false; + } + } + m_CachedParent = parentObject.transform; transform.SetParent(parentObject.transform, m_CachedWorldPositionStays); - InvokeBehaviourOnNetworkObjectParentChanged(parentObject); return true; } @@ -1336,7 +1349,7 @@ namespace Unity.Netcode var objectsToRemove = new List(); foreach (var orphanObject in OrphanChildren) { - if (orphanObject.ApplyNetworkParenting()) + if (orphanObject.ApplyNetworkParenting(orphanedChildPass: true)) { objectsToRemove.Add(orphanObject); } @@ -1797,6 +1810,12 @@ namespace Unity.Netcode var syncRotationPositionLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays; var syncScaleLocalSpaceRelative = obj.HasParent && !m_CachedWorldPositionStays; + // Always synchronize in-scene placed object's scale using local space + if (obj.IsSceneObject) + { + syncScaleLocalSpaceRelative = obj.HasParent; + } + // If auto object synchronization is turned off if (!AutoObjectParentSync) { @@ -1808,7 +1827,6 @@ namespace Unity.Netcode syncScaleLocalSpaceRelative = obj.HasParent; } - obj.Transform = new SceneObject.TransformData { // If we are parented and we have the m_CachedWorldPositionStays disabled, then use local space diff --git a/Runtime/Core/NetworkUpdateLoop.cs b/Runtime/Core/NetworkUpdateLoop.cs index cd47065..3b44715 100644 --- a/Runtime/Core/NetworkUpdateLoop.cs +++ b/Runtime/Core/NetworkUpdateLoop.cs @@ -54,7 +54,14 @@ namespace Unity.Netcode /// PreLateUpdate = 6, /// + /// Updated after Monobehaviour.LateUpdate, but BEFORE rendering + /// + // Yes, these numbers are out of order due to backward compatibility requirements. + // The enum values are listed in the order they will be called. + PostScriptLateUpdate = 8, + /// /// Updated after the Monobehaviour.LateUpdate for all components is invoked + /// and all rendering is complete /// PostLateUpdate = 7 } @@ -258,6 +265,18 @@ namespace Unity.Netcode } } + internal struct NetworkPostScriptLateUpdate + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkPostScriptLateUpdate), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.PostScriptLateUpdate) + }; + } + } + internal struct NetworkPostLateUpdate { public static PlayerLoopSystem CreateLoopSystem() @@ -399,6 +418,7 @@ namespace Unity.Netcode else if (currentSystem.type == typeof(PreLateUpdate)) { TryAddLoopSystem(ref currentSystem, NetworkPreLateUpdate.CreateLoopSystem(), typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate), LoopSystemPosition.Before); + TryAddLoopSystem(ref currentSystem, NetworkPostScriptLateUpdate.CreateLoopSystem(), typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate), LoopSystemPosition.After); } else if (currentSystem.type == typeof(PostLateUpdate)) { @@ -440,6 +460,7 @@ namespace Unity.Netcode else if (currentSystem.type == typeof(PreLateUpdate)) { TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPreLateUpdate)); + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPostScriptLateUpdate)); } else if (currentSystem.type == typeof(PostLateUpdate)) { diff --git a/Runtime/Messaging/CustomMessageManager.cs b/Runtime/Messaging/CustomMessageManager.cs index 4e15ec6..620e5bb 100644 --- a/Runtime/Messaging/CustomMessageManager.cs +++ b/Runtime/Messaging/CustomMessageManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Unity.Collections; +using UnityEngine; namespace Unity.Netcode { @@ -199,6 +200,14 @@ namespace Unity.Netcode /// The callback to run when a named message is received. public void RegisterNamedMessageHandler(string name, HandleNamedMessageDelegate callback) { + if (string.IsNullOrEmpty(name)) + { + if (m_NetworkManager.LogLevel <= LogLevel.Error) + { + Debug.LogError($"[{nameof(RegisterNamedMessageHandler)}] Cannot register a named message of type null or empty!"); + } + return; + } var hash32 = XXHash.Hash32(name); var hash64 = XXHash.Hash64(name); @@ -215,6 +224,15 @@ namespace Unity.Netcode /// The name of the message. public void UnregisterNamedMessageHandler(string name) { + if (string.IsNullOrEmpty(name)) + { + if (m_NetworkManager.LogLevel <= LogLevel.Error) + { + Debug.LogError($"[{nameof(UnregisterNamedMessageHandler)}] Cannot unregister a named message of type null or empty!"); + } + return; + } + var hash32 = XXHash.Hash32(name); var hash64 = XXHash.Hash64(name); diff --git a/Runtime/Messaging/Messages/AnticipationCounterSync.cs b/Runtime/Messaging/Messages/AnticipationCounterSync.cs new file mode 100644 index 0000000..b9cdfe7 --- /dev/null +++ b/Runtime/Messaging/Messages/AnticipationCounterSync.cs @@ -0,0 +1,70 @@ +namespace Unity.Netcode +{ + internal struct AnticipationCounterSyncPingMessage : INetworkMessage + { + public int Version => 0; + + public ulong Counter; + public double Time; + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + BytePacker.WriteValuePacked(writer, Counter); + writer.WriteValueSafe(Time); + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsServer) + { + return false; + } + ByteUnpacker.ReadValuePacked(reader, out Counter); + reader.ReadValueSafe(out Time); + return true; + } + + public void Handle(ref NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (networkManager.IsListening && !networkManager.ShutdownInProgress && networkManager.ConnectedClients.ContainsKey(context.SenderId)) + { + var message = new AnticipationCounterSyncPongMessage { Counter = Counter, Time = Time }; + networkManager.MessageManager.SendMessage(ref message, NetworkDelivery.Reliable, context.SenderId); + } + } + } + internal struct AnticipationCounterSyncPongMessage : INetworkMessage + { + public int Version => 0; + + public ulong Counter; + public double Time; + + public void Serialize(FastBufferWriter writer, int targetVersion) + { + BytePacker.WriteValuePacked(writer, Counter); + writer.WriteValueSafe(Time); + } + + public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return false; + } + ByteUnpacker.ReadValuePacked(reader, out Counter); + reader.ReadValueSafe(out Time); + return true; + } + + public void Handle(ref NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + networkManager.AnticipationSystem.LastAnticipationAck = Counter; + networkManager.AnticipationSystem.LastAnticipationAckTime = Time; + } + } +} diff --git a/Runtime/Messaging/Messages/AnticipationCounterSync.cs.meta b/Runtime/Messaging/Messages/AnticipationCounterSync.cs.meta new file mode 100644 index 0000000..93d99ab --- /dev/null +++ b/Runtime/Messaging/Messages/AnticipationCounterSync.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: db5034828a9741ce9bc8ec9a64d5a5b6 +timeCreated: 1706042908 \ No newline at end of file diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs index 99970c4..9652869 100644 --- a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs +++ b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs @@ -30,6 +30,9 @@ namespace Unity.Netcode throw new OverflowException($"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}"); } + var obj = NetworkBehaviour.NetworkObject; + var networkManager = obj.NetworkManagerOwner; + BytePacker.WriteValueBitPacked(writer, NetworkObjectId); BytePacker.WriteValueBitPacked(writer, NetworkBehaviourIndex); @@ -38,7 +41,7 @@ namespace Unity.Netcode if (!DeliveryMappedNetworkVariableIndex.Contains(i)) { // This var does not belong to the currently iterating delivery group. - if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { BytePacker.WriteValueBitPacked(writer, (ushort)0); } @@ -52,9 +55,11 @@ namespace Unity.Netcode var startingSize = writer.Length; var networkVariable = NetworkBehaviour.NetworkVariableFields[i]; + var shouldWrite = networkVariable.IsDirty() && networkVariable.CanClientRead(TargetClientId) && - (NetworkBehaviour.NetworkManager.IsServer || networkVariable.CanClientWrite(NetworkBehaviour.NetworkManager.LocalClientId)); + (networkManager.IsServer || networkVariable.CanClientWrite(networkManager.LocalClientId)) && + networkVariable.CanSend(); // 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 @@ -67,14 +72,14 @@ namespace Unity.Netcode // The object containing the behaviour we're about to process is about to be shown to this client // As a result, the client will get the fully serialized NetworkVariable and would be confused by // an extraneous delta - if (NetworkBehaviour.NetworkManager.SpawnManager.ObjectsToShowToClient.ContainsKey(TargetClientId) && - NetworkBehaviour.NetworkManager.SpawnManager.ObjectsToShowToClient[TargetClientId] - .Contains(NetworkBehaviour.NetworkObject)) + if (networkManager.SpawnManager.ObjectsToShowToClient.ContainsKey(TargetClientId) && + networkManager.SpawnManager.ObjectsToShowToClient[TargetClientId] + .Contains(obj)) { shouldWrite = false; } - if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { if (!shouldWrite) { @@ -88,9 +93,9 @@ namespace Unity.Netcode if (shouldWrite) { - if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { - var tempWriter = new FastBufferWriter(NetworkBehaviour.NetworkManager.MessageManager.NonFragmentedMessageMaxSize, Allocator.Temp, NetworkBehaviour.NetworkManager.MessageManager.FragmentedMessageMaxSize); + var tempWriter = new FastBufferWriter(networkManager.MessageManager.NonFragmentedMessageMaxSize, Allocator.Temp, networkManager.MessageManager.FragmentedMessageMaxSize); NetworkBehaviour.NetworkVariableFields[i].WriteDelta(tempWriter); BytePacker.WriteValueBitPacked(writer, tempWriter.Length); @@ -105,9 +110,9 @@ namespace Unity.Netcode { networkVariable.WriteDelta(writer); } - NetworkBehaviour.NetworkManager.NetworkMetrics.TrackNetworkVariableDeltaSent( + networkManager.NetworkMetrics.TrackNetworkVariableDeltaSent( TargetClientId, - NetworkBehaviour.NetworkObject, + obj, networkVariable.Name, NetworkBehaviour.__getTypeName(), writer.Length - startingSize); diff --git a/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs b/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs index 50f81f2..da8a0ed 100644 --- a/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs +++ b/Runtime/Messaging/RpcTargets/NotMeRpcTarget.cs @@ -49,6 +49,10 @@ namespace Unity.Netcode { continue; } + if (clientId == NetworkManager.ServerClientId) + { + continue; + } m_GroupSendTarget.Add(clientId); } } diff --git a/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs b/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs index ece1ded..e4f64f0 100644 --- a/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs +++ b/Runtime/Messaging/RpcTargets/NotOwnerRpcTarget.cs @@ -40,6 +40,10 @@ namespace Unity.Netcode { continue; } + if (clientId == NetworkManager.ServerClientId) + { + continue; + } if (clientId == behaviour.NetworkManager.LocalClientId) { m_LocalSendRpcTarget.Send(behaviour, ref message, delivery, rpcParams); @@ -57,6 +61,10 @@ namespace Unity.Netcode { continue; } + if (clientId == NetworkManager.ServerClientId) + { + continue; + } if (clientId == behaviour.NetworkManager.LocalClientId) { m_LocalSendRpcTarget.Send(behaviour, ref message, delivery, rpcParams); diff --git a/Runtime/NetworkVariable/AnticipatedNetworkVariable.cs b/Runtime/NetworkVariable/AnticipatedNetworkVariable.cs new file mode 100644 index 0000000..2a08293 --- /dev/null +++ b/Runtime/NetworkVariable/AnticipatedNetworkVariable.cs @@ -0,0 +1,392 @@ +using System; +using Unity.Mathematics; +using UnityEngine; + +namespace Unity.Netcode +{ + + public enum StaleDataHandling + { + Ignore, + Reanticipate + } + +#pragma warning disable IDE0001 + /// + /// A variable that can be synchronized over the network. + /// This version supports basic client anticipation - the client can set a value on the belief that the server + /// will update it to reflect the same value in a future update (i.e., as the result of an RPC call). + /// This value can then be adjusted as new updates from the server come in, in three basic modes: + /// + /// + /// + /// Snap: In this mode (with set to + /// and no callback), + /// the moment a more up-to-date value is received from the authority, it will simply replace the anticipated value, + /// resulting in a "snap" to the new value if it is different from the anticipated value. + /// + /// Smooth: In this mode (with set to + /// and an callback that calls + /// from the anticipated value to the authority value with an appropriate + /// -style smooth function), when a more up-to-date value is received from the authority, + /// it will interpolate over time from an incorrect anticipated value to the correct authoritative value. + /// + /// Constant Reanticipation: In this mode (with set to + /// and an that calculates a + /// new anticipated value based on the current authoritative value), when a more up-to-date value is received from + /// the authority, user code calculates a new anticipated value, possibly calling to interpolate + /// between the previous anticipation and the new anticipation. This is useful for values that change frequently and + /// need to constantly be re-evaluated, as opposed to values that change only in response to user action and simply + /// need a one-time anticipation when the user performs that action. + /// + /// + /// + /// Note that these three modes may be combined. For example, if an callback + /// does not call either or , the result will be a snap to the + /// authoritative value, enabling for a callback that may conditionally call when the + /// difference between the anticipated and authoritative values is within some threshold, but fall back to + /// snap behavior if the difference is too large. + /// + /// the unmanaged type for +#pragma warning restore IDE0001 + [Serializable] + [GenerateSerializationForGenericParameter(0)] + public class AnticipatedNetworkVariable : NetworkVariableBase + { + [SerializeField] + private NetworkVariable m_AuthoritativeValue; + private T m_AnticipatedValue; + private T m_PreviousAnticipatedValue; + private ulong m_LastAuthorityUpdateCounter = 0; + private ulong m_LastAnticipationCounter = 0; + private bool m_IsDisposed = false; + private bool m_SettingAuthoritativeValue = false; + + private T m_SmoothFrom; + private T m_SmoothTo; + private float m_SmoothDuration; + private float m_CurrentSmoothTime; + private bool m_HasSmoothValues; + +#pragma warning disable IDE0001 + /// + /// Defines what the behavior should be if we receive a value from the server with an earlier associated + /// time value than the anticipation time value. + ///

+ /// If this is , the stale data will be ignored and the authoritative + /// value will not replace the anticipated value until the anticipation time is reached. + /// and will also not be invoked for this stale data. + ///

+ /// If this is , the stale data will replace the anticipated data and + /// and will be invoked. + /// In this case, the authoritativeTime value passed to will be lower than + /// the anticipationTime value, and that callback can be used to calculate a new anticipated value. + ///
+#pragma warning restore IDE0001 + public StaleDataHandling StaleDataHandling; + + public delegate void OnAuthoritativeValueChangedDelegate(AnticipatedNetworkVariable variable, in T previousValue, in T newValue); + + /// + /// Invoked any time the authoritative value changes, even when the data is stale or has been changed locally. + /// + public OnAuthoritativeValueChangedDelegate OnAuthoritativeValueChanged = null; + + /// + /// Determines if the difference between the last serialized value and the current value is large enough + /// to serialize it again. + /// + public event NetworkVariable.CheckExceedsDirtinessThresholdDelegate CheckExceedsDirtinessThreshold + { + add => m_AuthoritativeValue.CheckExceedsDirtinessThreshold += value; + remove => m_AuthoritativeValue.CheckExceedsDirtinessThreshold -= value; + } + + private class AnticipatedObject : IAnticipatedObject + { + public AnticipatedNetworkVariable Variable; + + public void Update() + { + Variable.Update(); + } + + public void ResetAnticipation() + { + Variable.ShouldReanticipate = false; + } + + public NetworkObject OwnerObject => Variable.m_NetworkBehaviour.NetworkObject; + } + + private AnticipatedObject m_AnticipatedObject; + + public override void OnInitialize() + { + m_AuthoritativeValue.Initialize(m_NetworkBehaviour); + NetworkVariableSerialization.Duplicate(m_AuthoritativeValue.Value, ref m_AnticipatedValue); + NetworkVariableSerialization.Duplicate(m_AnticipatedValue, ref m_PreviousAnticipatedValue); + if (m_NetworkBehaviour != null && m_NetworkBehaviour.NetworkManager != null && m_NetworkBehaviour.NetworkManager.AnticipationSystem != null) + { + m_AnticipatedObject = new AnticipatedObject { Variable = this }; + m_NetworkBehaviour.NetworkManager.AnticipationSystem.AllAnticipatedObjects.Add(m_AnticipatedObject); + } + } + + public override bool ExceedsDirtinessThreshold() + { + return m_AuthoritativeValue.ExceedsDirtinessThreshold(); + } + + /// + /// Retrieves the current value for the variable. + /// This is the "display value" for this variable, and is affected by and + /// , as well as by updates from the authority, depending on + /// and the behavior of any callbacks. + ///

+ /// When a server update arrives, this value will be overwritten + /// by the new server value (unless stale data handling is set + /// to "Ignore" and the update is determined to be stale). + /// This value will be duplicated in + /// , which + /// will NOT be overwritten in server updates. + ///
+ public T Value => m_AnticipatedValue; + + /// + /// Indicates whether this variable currently needs + /// reanticipation. If this is true, the anticipated value + /// has been overwritten by the authoritative value from the + /// server; the previous anticipated value is stored in + /// + public bool ShouldReanticipate + { + get; + private set; + } + + /// + /// Holds the most recent anticipated value, whatever was + /// most recently set using . Unlike + /// , this does not get overwritten + /// when a server update arrives. + /// + public T PreviousAnticipatedValue => m_PreviousAnticipatedValue; + + /// + /// Sets the current value of the variable on the expectation that the authority will set the variable + /// to the same value within one network round trip (i.e., in response to an RPC). + /// + /// + public void Anticipate(T value) + { + if (m_NetworkBehaviour.NetworkManager.ShutdownInProgress || !m_NetworkBehaviour.NetworkManager.IsListening) + { + return; + } + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + m_LastAnticipationCounter = m_NetworkBehaviour.NetworkManager.AnticipationSystem.AnticipationCounter; + m_AnticipatedValue = value; + NetworkVariableSerialization.Duplicate(m_AnticipatedValue, ref m_PreviousAnticipatedValue); + if (CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) + { + AuthoritativeValue = value; + } + } + +#pragma warning disable IDE0001 + /// + /// Retrieves or sets the underlying authoritative value. + /// Note that only a client or server with write permissions to this variable may set this value. + /// When this variable has been anticipated, this value will alawys return the most recent authoritative + /// state, which is updated even if is . + /// +#pragma warning restore IDE0001 + public T AuthoritativeValue + { + get => m_AuthoritativeValue.Value; + set + { + m_SettingAuthoritativeValue = true; + try + { + m_AuthoritativeValue.Value = value; + m_AnticipatedValue = value; + NetworkVariableSerialization.Duplicate(m_AnticipatedValue, ref m_PreviousAnticipatedValue); + } + finally + { + m_SettingAuthoritativeValue = false; + } + } + } + + /// + /// A function to interpolate between two values based on a percentage. + /// See , , , and so on + /// for examples. + /// + public delegate T SmoothDelegate(T authoritativeValue, T anticipatedValue, float amount); + + private SmoothDelegate m_SmoothDelegate = null; + + public AnticipatedNetworkVariable(T value = default, + StaleDataHandling staleDataHandling = StaleDataHandling.Ignore) + : base() + { + StaleDataHandling = staleDataHandling; + m_AuthoritativeValue = new NetworkVariable(value) + { + OnValueChanged = OnValueChangedInternal + }; + } + + public void Update() + { + if (m_CurrentSmoothTime < m_SmoothDuration) + { + m_CurrentSmoothTime += m_NetworkBehaviour.NetworkManager.RealTimeProvider.DeltaTime; + var pct = math.min(m_CurrentSmoothTime / m_SmoothDuration, 1f); + m_AnticipatedValue = m_SmoothDelegate(m_SmoothFrom, m_SmoothTo, pct); + NetworkVariableSerialization.Duplicate(m_AnticipatedValue, ref m_PreviousAnticipatedValue); + } + } + + public override void Dispose() + { + if (m_IsDisposed) + { + return; + } + + if (m_NetworkBehaviour != null && m_NetworkBehaviour.NetworkManager != null && m_NetworkBehaviour.NetworkManager.AnticipationSystem != null) + { + if (m_AnticipatedObject != null) + { + m_NetworkBehaviour.NetworkManager.AnticipationSystem.AllAnticipatedObjects.Remove(m_AnticipatedObject); + m_NetworkBehaviour.NetworkManager.AnticipationSystem.ObjectsToReanticipate.Remove(m_AnticipatedObject); + m_AnticipatedObject = null; + } + } + + m_IsDisposed = true; + + m_AuthoritativeValue.Dispose(); + if (m_AnticipatedValue is IDisposable anticipatedValueDisposable) + { + anticipatedValueDisposable.Dispose(); + } + + m_AnticipatedValue = default; + if (m_PreviousAnticipatedValue is IDisposable previousValueDisposable) + { + previousValueDisposable.Dispose(); + m_PreviousAnticipatedValue = default; + } + + if (m_HasSmoothValues) + { + if (m_SmoothFrom is IDisposable smoothFromDisposable) + { + smoothFromDisposable.Dispose(); + m_SmoothFrom = default; + } + if (m_SmoothTo is IDisposable smoothToDisposable) + { + smoothToDisposable.Dispose(); + m_SmoothTo = default; + } + + m_HasSmoothValues = false; + } + } + + ~AnticipatedNetworkVariable() + { + Dispose(); + } + + private void OnValueChangedInternal(T previousValue, T newValue) + { + if (!m_SettingAuthoritativeValue) + { + m_LastAuthorityUpdateCounter = m_NetworkBehaviour.NetworkManager.AnticipationSystem.LastAnticipationAck; + if (StaleDataHandling == StaleDataHandling.Ignore && m_LastAnticipationCounter > m_LastAuthorityUpdateCounter) + { + // Keep the anticipated value unchanged because it is more recent than the authoritative one. + return; + } + + + ShouldReanticipate = true; + m_NetworkBehaviour.NetworkManager.AnticipationSystem.ObjectsToReanticipate.Add(m_AnticipatedObject); + } + + NetworkVariableSerialization.Duplicate(AuthoritativeValue, ref m_AnticipatedValue); + + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + OnAuthoritativeValueChanged?.Invoke(this, previousValue, newValue); + } + + /// + /// Interpolate this variable from to over of + /// real time. The duration uses , so it is affected by . + /// + /// + /// + /// + /// + public void Smooth(in T from, in T to, float durationSeconds, SmoothDelegate how) + { + if (durationSeconds <= 0) + { + NetworkVariableSerialization.Duplicate(to, ref m_AnticipatedValue); + m_SmoothDuration = 0; + m_CurrentSmoothTime = 0; + m_SmoothDelegate = null; + return; + } + NetworkVariableSerialization.Duplicate(from, ref m_AnticipatedValue); + NetworkVariableSerialization.Duplicate(from, ref m_SmoothFrom); + NetworkVariableSerialization.Duplicate(to, ref m_SmoothTo); + m_SmoothDuration = durationSeconds; + m_CurrentSmoothTime = 0; + m_SmoothDelegate = how; + m_HasSmoothValues = true; + } + + public override bool IsDirty() + { + return m_AuthoritativeValue.IsDirty(); + } + + public override void ResetDirty() + { + m_AuthoritativeValue.ResetDirty(); + } + + public override void WriteDelta(FastBufferWriter writer) + { + m_AuthoritativeValue.WriteDelta(writer); + } + + public override void WriteField(FastBufferWriter writer) + { + m_AuthoritativeValue.WriteField(writer); + } + + public override void ReadField(FastBufferReader reader) + { + m_AuthoritativeValue.ReadField(reader); + NetworkVariableSerialization.Duplicate(m_AuthoritativeValue.Value, ref m_AnticipatedValue); + NetworkVariableSerialization.Duplicate(m_AnticipatedValue, ref m_PreviousAnticipatedValue); + } + + public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) + { + m_AuthoritativeValue.ReadDelta(reader, keepDirtyDelta); + } + } +} diff --git a/Runtime/NetworkVariable/AnticipatedNetworkVariable.cs.meta b/Runtime/NetworkVariable/AnticipatedNetworkVariable.cs.meta new file mode 100644 index 0000000..1bcf680 --- /dev/null +++ b/Runtime/NetworkVariable/AnticipatedNetworkVariable.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 10f5188736b742d1993a2aad46a03e78 +timeCreated: 1705595868 \ No newline at end of file diff --git a/Runtime/NetworkVariable/CollectionSerializationUtility.cs b/Runtime/NetworkVariable/CollectionSerializationUtility.cs new file mode 100644 index 0000000..096b2a4 --- /dev/null +++ b/Runtime/NetworkVariable/CollectionSerializationUtility.cs @@ -0,0 +1,746 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace Unity.Netcode +{ + internal static class CollectionSerializationUtility + { + public static void WriteNativeArrayDelta(FastBufferWriter writer, ref NativeArray value, ref NativeArray previousValue) where T : unmanaged + { + // This bit vector serializes the list of which fields have changed using 1 bit per field. + // This will always be 1 bit per field of the whole array (rounded up to the nearest 8 bits) + // even if there is only one change, so as compared to serializing the index with each item, + // this will use more bandwidth when the overall bandwidth usage is small and the array is large, + // but less when the overall bandwidth usage is large. So it optimizes for the worst case while accepting + // some reduction in efficiency in the best case. + using var changes = new ResizableBitVector(Allocator.Temp); + int minLength = math.min(value.Length, previousValue.Length); + var numChanges = 0; + // Iterate the array, checking which values have changed and marking that in the bit vector + for (var i = 0; i < minLength; ++i) + { + var val = value[i]; + var prevVal = previousValue[i]; + if (!NetworkVariableSerialization.AreEqual(ref val, ref prevVal)) + { + ++numChanges; + changes.Set(i); + } + } + + // Mark any newly added items as well + // We don't need to mark removed items because they are captured by serializing the length + for (var i = previousValue.Length; i < value.Length; ++i) + { + ++numChanges; + changes.Set(i); + } + + // If the size of serializing the dela is greater than the size of serializing the whole array (i.e., + // because almost the entire array has changed and the overhead of the change set increases bandwidth), + // then we just do a normal full serialization instead of a delta. + if (changes.GetSerializedSize() + FastBufferWriter.GetWriteSize() * numChanges > FastBufferWriter.GetWriteSize() * value.Length) + { + // 1 = full serialization + writer.WriteByteSafe(1); + writer.WriteValueSafe(value); + return; + } + // 0 = delta serialization + writer.WriteByte(0); + // Write the length, which will be used on the read side to resize the array + BytePacker.WriteValuePacked(writer, value.Length); + writer.WriteValueSafe(changes); + unsafe + { + var ptr = (T*)value.GetUnsafePtr(); + var prevPtr = (T*)previousValue.GetUnsafePtr(); + for (int i = 0; i < value.Length; ++i) + { + if (changes.IsSet(i)) + { + if (i < previousValue.Length) + { + // If we have an item in the previous array for this index, we can do nested deltas! + NetworkVariableSerialization.WriteDelta(writer, ref ptr[i], ref prevPtr[i]); + } + else + { + // If not, just write it normally + NetworkVariableSerialization.Write(writer, ref ptr[i]); + } + } + } + } + } + public static void ReadNativeArrayDelta(FastBufferReader reader, ref NativeArray value) where T : unmanaged + { + // 1 = full serialization, 0 = delta serialization + reader.ReadByteSafe(out byte full); + if (full == 1) + { + // If we're doing full serialization, we fall back on reading the whole array. + value.Dispose(); + reader.ReadValueSafe(out value, Allocator.Persistent); + return; + } + // If not, first read the length and the change bits + ByteUnpacker.ReadValuePacked(reader, out int length); + var changes = new ResizableBitVector(Allocator.Temp); + using var toDispose = changes; + { + reader.ReadNetworkSerializableInPlace(ref changes); + + // If the length has changed, we need to resize. + // NativeArray is not resizeable, so we have to dispose and allocate a new one. + var previousLength = value.Length; + if (length != value.Length) + { + var newArray = new NativeArray(length, Allocator.Persistent); + unsafe + { + UnsafeUtility.MemCpy(newArray.GetUnsafePtr(), value.GetUnsafePtr(), math.min(newArray.Length * sizeof(T), value.Length * sizeof(T))); + } + value.Dispose(); + value = newArray; + } + + unsafe + { + var ptr = (T*)value.GetUnsafePtr(); + for (var i = 0; i < value.Length; ++i) + { + if (changes.IsSet(i)) + { + if (i < previousLength) + { + // If we have an item to read a delta into, read it as a delta + NetworkVariableSerialization.ReadDelta(reader, ref ptr[i]); + } + else + { + // If not, read as a standard element + NetworkVariableSerialization.Read(reader, ref ptr[i]); + } + } + } + } + } + } + public static void WriteListDelta(FastBufferWriter writer, ref List value, ref List previousValue) + { + // Lists can be null, so we have to handle that case. + // We do that by marking this as a full serialization and using the existing null handling logic + // in NetworkVariableSerialization> + if (value == null || previousValue == null) + { + writer.WriteByteSafe(1); + NetworkVariableSerialization>.Write(writer, ref value); + return; + } + // This bit vector serializes the list of which fields have changed using 1 bit per field. + // This will always be 1 bit per field of the whole array (rounded up to the nearest 8 bits) + // even if there is only one change, so as compared to serializing the index with each item, + // this will use more bandwidth when the overall bandwidth usage is small and the array is large, + // but less when the overall bandwidth usage is large. So it optimizes for the worst case while accepting + // some reduction in efficiency in the best case. + using var changes = new ResizableBitVector(Allocator.Temp); + int minLength = math.min(value.Count, previousValue.Count); + var numChanges = 0; + // Iterate the list, checking which values have changed and marking that in the bit vector + for (var i = 0; i < minLength; ++i) + { + var val = value[i]; + var prevVal = previousValue[i]; + if (!NetworkVariableSerialization.AreEqual(ref val, ref prevVal)) + { + ++numChanges; + changes.Set(i); + } + } + + // Mark any newly added items as well + // We don't need to mark removed items because they are captured by serializing the length + for (var i = previousValue.Count; i < value.Count; ++i) + { + ++numChanges; + changes.Set(i); + } + + // If the size of serializing the dela is greater than the size of serializing the whole array (i.e., + // because almost the entire array has changed and the overhead of the change set increases bandwidth), + // then we just do a normal full serialization instead of a delta. + // In the case of List, it's difficult to know exactly what the serialized size is going to be before + // we serialize it, so we fudge it. + if (numChanges >= value.Count * 0.9) + { + // 1 = full serialization + writer.WriteByteSafe(1); + NetworkVariableSerialization>.Write(writer, ref value); + return; + } + + // 0 = delta serialization + writer.WriteByteSafe(0); + // Write the length, which will be used on the read side to resize the list + BytePacker.WriteValuePacked(writer, value.Count); + writer.WriteValueSafe(changes); + for (int i = 0; i < value.Count; ++i) + { + if (changes.IsSet(i)) + { + var reffable = value[i]; + if (i < previousValue.Count) + { + // If we have an item in the previous array for this index, we can do nested deltas! + var prevReffable = previousValue[i]; + NetworkVariableSerialization.WriteDelta(writer, ref reffable, ref prevReffable); + } + else + { + // If not, just write it normally. + NetworkVariableSerialization.Write(writer, ref reffable); + } + } + } + } + public static void ReadListDelta(FastBufferReader reader, ref List value) + { + // 1 = full serialization, 0 = delta serialization + reader.ReadByteSafe(out byte full); + if (full == 1) + { + // If we're doing full serialization, we fall back on reading the whole list. + NetworkVariableSerialization>.Read(reader, ref value); + return; + } + // If not, first read the length and the change bits + ByteUnpacker.ReadValuePacked(reader, out int length); + var changes = new ResizableBitVector(Allocator.Temp); + using var toDispose = changes; + { + reader.ReadNetworkSerializableInPlace(ref changes); + + // If the list shrank, we need to resize it down. + // List has no method to reserve space for future elements, + // so if we have to grow it, we just do that using Add() below. + if (length < value.Count) + { + value.RemoveRange(length, value.Count - length); + } + + for (var i = 0; i < length; ++i) + { + if (changes.IsSet(i)) + { + if (i < value.Count) + { + // If we have an item to read a delta into, read it as a delta + T item = value[i]; + NetworkVariableSerialization.ReadDelta(reader, ref item); + value[i] = item; + } + else + { + // If not, just read it as a standard item. + T item = default; + NetworkVariableSerialization.Read(reader, ref item); + value.Add(item); + } + } + } + } + } + + // For HashSet and Dictionary, we need to have some local space to hold lists we need to serialize. + // We don't want to do allocations all the time and we know each one needs a maximum of three lists, + // so we're going to keep static lists that we can reuse in these methods. + private static class ListCache + { + private static List s_AddedList = new List(); + private static List s_RemovedList = new List(); + private static List s_ChangedList = new List(); + + public static List GetAddedList() + { + s_AddedList.Clear(); + return s_AddedList; + } + public static List GetRemovedList() + { + s_RemovedList.Clear(); + return s_RemovedList; + } + public static List GetChangedList() + { + s_ChangedList.Clear(); + return s_ChangedList; + } + } + + public static void WriteHashSetDelta(FastBufferWriter writer, ref HashSet value, ref HashSet previousValue) where T : IEquatable + { + // HashSets can be null, so we have to handle that case. + // We do that by marking this as a full serialization and using the existing null handling logic + // in NetworkVariableSerialization> + if (value == null || previousValue == null) + { + writer.WriteByteSafe(1); + NetworkVariableSerialization>.Write(writer, ref value); + return; + } + // No changed array because a set can't have a "changed" element, only added and removed. + var added = ListCache.GetAddedList(); + var removed = ListCache.GetRemovedList(); + // collect the new elements + foreach (var item in value) + { + if (!previousValue.Contains(item)) + { + added.Add(item); + } + } + + // collect the removed elements + foreach (var item in previousValue) + { + if (!value.Contains(item)) + { + removed.Add(item); + } + } + + // If we've got more changes than total items, we just do a full serialization + if (added.Count + removed.Count >= value.Count) + { + writer.WriteByteSafe(1); + NetworkVariableSerialization>.Write(writer, ref value); + return; + } + + writer.WriteByteSafe(0); + // Write out the added and removed arrays. + writer.WriteValueSafe(added.Count); + for (var i = 0; i < added.Count; ++i) + { + var item = added[i]; + NetworkVariableSerialization.Write(writer, ref item); + } + writer.WriteValueSafe(removed.Count); + for (var i = 0; i < removed.Count; ++i) + { + var item = removed[i]; + NetworkVariableSerialization.Write(writer, ref item); + } + } + + public static void ReadHashSetDelta(FastBufferReader reader, ref HashSet value) where T : IEquatable + { + // 1 = full serialization, 0 = delta serialization + reader.ReadByteSafe(out byte full); + if (full != 0) + { + NetworkVariableSerialization>.Read(reader, ref value); + return; + } + // Read in the added and removed values + reader.ReadValueSafe(out int addedCount); + for (var i = 0; i < addedCount; ++i) + { + T item = default; + NetworkVariableSerialization.Read(reader, ref item); + value.Add(item); + } + reader.ReadValueSafe(out int removedCount); + for (var i = 0; i < removedCount; ++i) + { + T item = default; + NetworkVariableSerialization.Read(reader, ref item); + value.Remove(item); + } + } + public static void WriteDictionaryDelta(FastBufferWriter writer, ref Dictionary value, ref Dictionary previousValue) + where TKey : IEquatable + { + if (value == null || previousValue == null) + { + writer.WriteByteSafe(1); + NetworkVariableSerialization>.Write(writer, ref value); + return; + } + var added = ListCache>.GetAddedList(); + var changed = ListCache>.GetRemovedList(); + var removed = ListCache>.GetChangedList(); + // Collect items that have been added or have changed + foreach (var item in value) + { + var val = item.Value; + var hasPrevVal = previousValue.TryGetValue(item.Key, out var prevVal); + if (!hasPrevVal) + { + added.Add(item); + } + else if (!NetworkVariableSerialization.AreEqual(ref val, ref prevVal)) + { + changed.Add(item); + } + } + + // collect the items that have been removed + foreach (var item in previousValue) + { + if (!value.ContainsKey(item.Key)) + { + removed.Add(item); + } + } + + // If there are more changes than total values, just do a full serialization + if (added.Count + removed.Count + changed.Count >= value.Count) + { + writer.WriteByteSafe(1); + NetworkVariableSerialization>.Write(writer, ref value); + return; + } + + writer.WriteByteSafe(0); + // Else, write out the added, removed, and changed arrays + writer.WriteValueSafe(added.Count); + for (var i = 0; i < added.Count; ++i) + { + (var key, var val) = (added[i].Key, added[i].Value); + NetworkVariableSerialization.Write(writer, ref key); + NetworkVariableSerialization.Write(writer, ref val); + } + writer.WriteValueSafe(removed.Count); + for (var i = 0; i < removed.Count; ++i) + { + var key = removed[i].Key; + NetworkVariableSerialization.Write(writer, ref key); + } + writer.WriteValueSafe(changed.Count); + for (var i = 0; i < changed.Count; ++i) + { + (var key, var val) = (changed[i].Key, changed[i].Value); + NetworkVariableSerialization.Write(writer, ref key); + NetworkVariableSerialization.Write(writer, ref val); + } + } + + public static void ReadDictionaryDelta(FastBufferReader reader, ref Dictionary value) + where TKey : IEquatable + { + // 1 = full serialization, 0 = delta serialization + reader.ReadByteSafe(out byte full); + if (full != 0) + { + NetworkVariableSerialization>.Read(reader, ref value); + return; + } + // Added + reader.ReadValueSafe(out int length); + for (var i = 0; i < length; ++i) + { + (TKey key, TVal val) = (default, default); + NetworkVariableSerialization.Read(reader, ref key); + NetworkVariableSerialization.Read(reader, ref val); + value.Add(key, val); + } + // Removed + reader.ReadValueSafe(out length); + for (var i = 0; i < length; ++i) + { + TKey key = default; + NetworkVariableSerialization.Read(reader, ref key); + value.Remove(key); + } + // Changed + reader.ReadValueSafe(out length); + for (var i = 0; i < length; ++i) + { + (TKey key, TVal val) = (default, default); + NetworkVariableSerialization.Read(reader, ref key); + NetworkVariableSerialization.Read(reader, ref val); + value[key] = val; + } + } + +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public static void WriteNativeListDelta(FastBufferWriter writer, ref NativeList value, ref NativeList previousValue) where T : unmanaged + { + // See WriteListDelta and WriteNativeArrayDelta to understand most of this. It's basically the same, + // just adjusted for the NativeList API + using var changes = new ResizableBitVector(Allocator.Temp); + int minLength = math.min(value.Length, previousValue.Length); + var numChanges = 0; + for (var i = 0; i < minLength; ++i) + { + var val = value[i]; + var prevVal = previousValue[i]; + if (!NetworkVariableSerialization.AreEqual(ref val, ref prevVal)) + { + ++numChanges; + changes.Set(i); + } + } + + for (var i = previousValue.Length; i < value.Length; ++i) + { + ++numChanges; + changes.Set(i); + } + + if (changes.GetSerializedSize() + FastBufferWriter.GetWriteSize() * numChanges > FastBufferWriter.GetWriteSize() * value.Length) + { + writer.WriteByteSafe(1); + writer.WriteValueSafe(value); + return; + } + + writer.WriteByte(0); + BytePacker.WriteValuePacked(writer, value.Length); + writer.WriteValueSafe(changes); + unsafe + { + var ptr = (T*)value.GetUnsafePtr(); + var prevPtr = (T*)previousValue.GetUnsafePtr(); + for (int i = 0; i < value.Length; ++i) + { + if (changes.IsSet(i)) + { + if (i < previousValue.Length) + { + NetworkVariableSerialization.WriteDelta(writer, ref ptr[i], ref prevPtr[i]); + } + else + { + NetworkVariableSerialization.Write(writer, ref ptr[i]); + } + } + } + } + } + public static void ReadNativeListDelta(FastBufferReader reader, ref NativeList value) where T : unmanaged + { + // See ReadListDelta and ReadNativeArrayDelta to understand most of this. It's basically the same, + // just adjusted for the NativeList API + reader.ReadByteSafe(out byte full); + if (full == 1) + { + reader.ReadValueSafeInPlace(ref value); + return; + } + ByteUnpacker.ReadValuePacked(reader, out int length); + var changes = new ResizableBitVector(Allocator.Temp); + using var toDispose = changes; + { + reader.ReadNetworkSerializableInPlace(ref changes); + + var previousLength = value.Length; + // The one big difference between this and NativeArray/List is that NativeList supports + // easy and fast resizing and reserving space. + if (length != value.Length) + { + value.Resize(length, NativeArrayOptions.UninitializedMemory); + } + + unsafe + { + var ptr = (T*)value.GetUnsafePtr(); + for (var i = 0; i < value.Length; ++i) + { + if (changes.IsSet(i)) + { + if (i < previousLength) + { + NetworkVariableSerialization.ReadDelta(reader, ref ptr[i]); + } + else + { + NetworkVariableSerialization.Read(reader, ref ptr[i]); + } + } + } + } + } + } + + public static unsafe void WriteNativeHashSetDelta(FastBufferWriter writer, ref NativeHashSet value, ref NativeHashSet previousValue) where T : unmanaged, IEquatable + { + // See WriteHashSet; this is the same algorithm, adjusted for the NativeHashSet API + var added = stackalloc T[value.Count()]; + var removed = stackalloc T[previousValue.Count()]; + var addedCount = 0; + var removedCount = 0; + foreach (var item in value) + { + if (!previousValue.Contains(item)) + { + added[addedCount] = item; + ++addedCount; + } + } + + foreach (var item in previousValue) + { + if (!value.Contains(item)) + { + removed[removedCount] = item; + ++removedCount; + } + } + + if (addedCount + removedCount >= value.Count()) + { + writer.WriteByteSafe(1); + writer.WriteValueSafe(value); + return; + } + + writer.WriteByteSafe(0); + writer.WriteValueSafe(addedCount); + for (var i = 0; i < addedCount; ++i) + { + NetworkVariableSerialization.Write(writer, ref added[i]); + } + writer.WriteValueSafe(removedCount); + for (var i = 0; i < removedCount; ++i) + { + NetworkVariableSerialization.Write(writer, ref removed[i]); + } + } + + public static void ReadNativeHashSetDelta(FastBufferReader reader, ref NativeHashSet value) where T : unmanaged, IEquatable + { + // See ReadHashSet; this is the same algorithm, adjusted for the NativeHashSet API + reader.ReadByteSafe(out byte full); + if (full != 0) + { + reader.ReadValueSafeInPlace(ref value); + return; + } + reader.ReadValueSafe(out int addedCount); + for (var i = 0; i < addedCount; ++i) + { + T item = default; + NetworkVariableSerialization.Read(reader, ref item); + value.Add(item); + } + reader.ReadValueSafe(out int removedCount); + for (var i = 0; i < removedCount; ++i) + { + T item = default; + NetworkVariableSerialization.Read(reader, ref item); + value.Remove(item); + } + } + + public static unsafe void WriteNativeHashMapDelta(FastBufferWriter writer, ref NativeHashMap value, ref NativeHashMap previousValue) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + // See WriteDictionary; this is the same algorithm, adjusted for the NativeHashMap API + var added = stackalloc KeyValue[value.Count()]; + var changed = stackalloc KeyValue[value.Count()]; + var removed = stackalloc KeyValue[previousValue.Count()]; + var addedCount = 0; + var changedCount = 0; + var removedCount = 0; + foreach (var item in value) + { + var hasPrevVal = previousValue.TryGetValue(item.Key, out var prevVal); + if (!hasPrevVal) + { + added[addedCount] = item; + ++addedCount; + } + else if (!NetworkVariableSerialization.AreEqual(ref item.Value, ref prevVal)) + { + changed[changedCount] = item; + ++changedCount; + } + } + + foreach (var item in previousValue) + { + if (!value.ContainsKey(item.Key)) + { + removed[removedCount] = item; + ++removedCount; + } + } + + if (addedCount + removedCount + changedCount >= value.Count()) + { + writer.WriteByteSafe(1); + writer.WriteValueSafe(value); + return; + } + + writer.WriteByteSafe(0); + writer.WriteValueSafe(addedCount); + for (var i = 0; i < addedCount; ++i) + { + (var key, var val) = (added[i].Key, added[i].Value); + NetworkVariableSerialization.Write(writer, ref key); + NetworkVariableSerialization.Write(writer, ref val); + } + writer.WriteValueSafe(removedCount); + for (var i = 0; i < removedCount; ++i) + { + var key = removed[i].Key; + NetworkVariableSerialization.Write(writer, ref key); + } + writer.WriteValueSafe(changedCount); + for (var i = 0; i < changedCount; ++i) + { + (var key, var val) = (changed[i].Key, changed[i].Value); + NetworkVariableSerialization.Write(writer, ref key); + NetworkVariableSerialization.Write(writer, ref val); + } + } + + public static void ReadNativeHashMapDelta(FastBufferReader reader, ref NativeHashMap value) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + // See ReadDictionary; this is the same algorithm, adjusted for the NativeHashMap API + reader.ReadByteSafe(out byte full); + if (full != 0) + { + reader.ReadValueSafeInPlace(ref value); + return; + } + // Added + reader.ReadValueSafe(out int length); + for (var i = 0; i < length; ++i) + { + (TKey key, TVal val) = (default, default); + NetworkVariableSerialization.Read(reader, ref key); + NetworkVariableSerialization.Read(reader, ref val); + value.Add(key, val); + } + // Removed + reader.ReadValueSafe(out length); + for (var i = 0; i < length; ++i) + { + TKey key = default; + NetworkVariableSerialization.Read(reader, ref key); + value.Remove(key); + } + // Changed + reader.ReadValueSafe(out length); + for (var i = 0; i < length; ++i) + { + (TKey key, TVal val) = (default, default); + NetworkVariableSerialization.Read(reader, ref key); + NetworkVariableSerialization.Read(reader, ref val); + value[key] = val; + } + } +#endif + } +} diff --git a/Runtime/NetworkVariable/CollectionSerializationUtility.cs.meta b/Runtime/NetworkVariable/CollectionSerializationUtility.cs.meta new file mode 100644 index 0000000..2d25365 --- /dev/null +++ b/Runtime/NetworkVariable/CollectionSerializationUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c822ece4e24f4676861e07288a7f8526 +timeCreated: 1705437250 \ No newline at end of file diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index db33b7b..a17b703 100644 --- a/Runtime/NetworkVariable/NetworkVariable.cs +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -22,6 +22,28 @@ namespace Unity.Netcode ///
public OnValueChangedDelegate OnValueChanged; + public delegate bool CheckExceedsDirtinessThresholdDelegate(in T previousValue, in T newValue); + + public CheckExceedsDirtinessThresholdDelegate CheckExceedsDirtinessThreshold; + + public override bool ExceedsDirtinessThreshold() + { + if (CheckExceedsDirtinessThreshold != null && m_HasPreviousValue) + { + return CheckExceedsDirtinessThreshold(m_PreviousValue, m_InternalValue); + } + + return true; + } + + public override void OnInitialize() + { + base.OnInitialize(); + + m_HasPreviousValue = true; + NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); + } + /// /// Constructor for /// @@ -142,16 +164,16 @@ namespace Unity.Netcode /// public override void ResetDirty() { - base.ResetDirty(); // Resetting the dirty value declares that the current value is not dirty // Therefore, we set the m_PreviousValue field to a duplicate of the current // field, so that our next dirty check is made against the current "not dirty" // value. - if (!m_HasPreviousValue || !NetworkVariableSerialization.AreEqual(ref m_InternalValue, ref m_PreviousValue)) + if (IsDirty()) { m_HasPreviousValue = true; NetworkVariableSerialization.Duplicate(m_InternalValue, ref m_PreviousValue); } + base.ResetDirty(); } /// @@ -173,7 +195,7 @@ namespace Unity.Netcode /// The stream to write the value to public override void WriteDelta(FastBufferWriter writer) { - WriteField(writer); + NetworkVariableSerialization.WriteDelta(writer, ref m_InternalValue, ref m_PreviousValue); } /// @@ -189,7 +211,7 @@ namespace Unity.Netcode // would be stored in different fields T previousValue = m_InternalValue; - NetworkVariableSerialization.Read(reader, ref m_InternalValue); + NetworkVariableSerialization.ReadDelta(reader, ref m_InternalValue); if (keepDirtyDelta) { diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index 927687c..22028dd 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -3,11 +3,26 @@ using UnityEngine; namespace Unity.Netcode { + public struct NetworkVariableUpdateTraits + { + [Tooltip("The minimum amount of time that must pass between sending updates. If this amount of time has not passed since the last update, dirtiness will be ignored.")] + public float MinSecondsBetweenUpdates; + + [Tooltip("The maximum amount of time that a variable can be dirty without sending an update. If this amount of time has passed since the last update, an update will be sent even if the dirtiness threshold has not been met.")] + public float MaxSecondsBetweenUpdates; + } + /// /// Interface for network value containers /// public abstract class NetworkVariableBase : IDisposable { + [SerializeField] + internal NetworkVariableUpdateTraits UpdateTraits = default; + + [NonSerialized] + internal double LastUpdateSent; + /// /// The delivery type (QoS) to send data with /// @@ -30,6 +45,43 @@ namespace Unity.Netcode public void Initialize(NetworkBehaviour networkBehaviour) { m_NetworkBehaviour = networkBehaviour; + if (m_NetworkBehaviour.NetworkManager) + { + if (m_NetworkBehaviour.NetworkManager.NetworkTimeSystem != null) + { + UpdateLastSentTime(); + } + } + + OnInitialize(); + } + + /// + /// Called on initialization + /// + public virtual void OnInitialize() + { + + } + + /// + /// Sets the update traits for this network variable to determine how frequently it will send updates. + /// + /// + public void SetUpdateTraits(NetworkVariableUpdateTraits traits) + { + UpdateTraits = traits; + } + + /// + /// Check whether or not this variable has changed significantly enough to send an update. + /// If not, no update will be sent even if the variable is dirty, unless the time since last update exceeds + /// the ' . + /// + /// + public virtual bool ExceedsDirtinessThreshold() + { + return true; } /// @@ -92,6 +144,25 @@ namespace Unity.Netcode } } + internal bool CanSend() + { + var timeSinceLastUpdate = m_NetworkBehaviour.NetworkManager.NetworkTimeSystem.LocalTime - LastUpdateSent; + return + ( + UpdateTraits.MaxSecondsBetweenUpdates > 0 && + timeSinceLastUpdate >= UpdateTraits.MaxSecondsBetweenUpdates + ) || + ( + timeSinceLastUpdate >= UpdateTraits.MinSecondsBetweenUpdates && + ExceedsDirtinessThreshold() + ); + } + + internal void UpdateLastSentTime() + { + LastUpdateSent = m_NetworkBehaviour.NetworkManager.NetworkTimeSystem.LocalTime; + } + protected void MarkNetworkBehaviourDirty() { if (m_NetworkBehaviour == null) @@ -109,6 +180,16 @@ namespace Unity.Netcode } return; } + + if (!m_NetworkBehaviour.NetworkManager.IsListening) + { + if (m_NetworkBehaviour.NetworkManager.LogLevel <= LogLevel.Developer) + { + Debug.LogWarning($"NetworkVariable is written to after the NetworkManager has already shutdown! " + + "Are you modifying a NetworkVariable within a NetworkBehaviour.OnDestroy or NetworkBehaviour.OnDespawn method?"); + } + return; + } m_NetworkBehaviour.NetworkManager.BehaviourUpdater.AddForUpdate(m_NetworkBehaviour.NetworkObject); } diff --git a/Runtime/NetworkVariable/NetworkVariableSerialization.cs b/Runtime/NetworkVariable/NetworkVariableSerialization.cs index fc900e6..c2d2d3e 100644 --- a/Runtime/NetworkVariable/NetworkVariableSerialization.cs +++ b/Runtime/NetworkVariable/NetworkVariableSerialization.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; using UnityEditor; using UnityEngine; @@ -20,6 +22,8 @@ namespace Unity.Netcode // of it to pass it as a ref parameter. public void Write(FastBufferWriter writer, ref T value); public void Read(FastBufferReader reader, ref T value); + public void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue); + public void ReadDelta(FastBufferReader reader, ref T value); internal void ReadWithAllocator(FastBufferReader reader, out T value, Allocator allocator); public void Duplicate(in T value, ref T duplicatedValue); } @@ -38,6 +42,15 @@ namespace Unity.Netcode ByteUnpacker.ReadValueBitPacked(reader, out value); } + public void WriteDelta(FastBufferWriter writer, ref short value, ref short previousValue) + { + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref short value) + { + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out short value, Allocator allocator) { throw new NotImplementedException(); @@ -63,6 +76,15 @@ namespace Unity.Netcode ByteUnpacker.ReadValueBitPacked(reader, out value); } + public void WriteDelta(FastBufferWriter writer, ref ushort value, ref ushort previousValue) + { + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref ushort value) + { + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out ushort value, Allocator allocator) { throw new NotImplementedException(); @@ -88,6 +110,15 @@ namespace Unity.Netcode ByteUnpacker.ReadValueBitPacked(reader, out value); } + public void WriteDelta(FastBufferWriter writer, ref int value, ref int previousValue) + { + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref int value) + { + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out int value, Allocator allocator) { throw new NotImplementedException(); @@ -113,6 +144,15 @@ namespace Unity.Netcode ByteUnpacker.ReadValueBitPacked(reader, out value); } + public void WriteDelta(FastBufferWriter writer, ref uint value, ref uint previousValue) + { + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref uint value) + { + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out uint value, Allocator allocator) { throw new NotImplementedException(); @@ -138,6 +178,15 @@ namespace Unity.Netcode ByteUnpacker.ReadValueBitPacked(reader, out value); } + public void WriteDelta(FastBufferWriter writer, ref long value, ref long previousValue) + { + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref long value) + { + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out long value, Allocator allocator) { throw new NotImplementedException(); @@ -163,6 +212,15 @@ namespace Unity.Netcode ByteUnpacker.ReadValueBitPacked(reader, out value); } + public void WriteDelta(FastBufferWriter writer, ref ulong value, ref ulong previousValue) + { + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref ulong value) + { + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out ulong value, Allocator allocator) { throw new NotImplementedException(); @@ -193,6 +251,15 @@ namespace Unity.Netcode reader.ReadUnmanagedSafe(out value); } + public void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue) + { + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref T value) + { + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out T value, Allocator allocator) { throw new NotImplementedException(); @@ -204,6 +271,237 @@ namespace Unity.Netcode } } + internal class ListSerializer : INetworkVariableSerializer> + { + public void Write(FastBufferWriter writer, ref List value) + { + bool isNull = value == null; + writer.WriteValueSafe(isNull); + if (!isNull) + { + BytePacker.WriteValuePacked(writer, value.Count); + foreach (var item in value) + { + var reffable = item; + NetworkVariableSerialization.Write(writer, ref reffable); + } + } + } + public void Read(FastBufferReader reader, ref List value) + { + reader.ReadValueSafe(out bool isNull); + if (isNull) + { + value = null; + } + else + { + if (value == null) + { + value = new List(); + } + + ByteUnpacker.ReadValuePacked(reader, out int len); + if (len < value.Count) + { + value.RemoveRange(len, value.Count - len); + } + for (var i = 0; i < len; ++i) + { + // Read in place where possible + if (i < value.Count) + { + T item = value[i]; + NetworkVariableSerialization.Read(reader, ref item); + value[i] = item; + } + else + { + T item = default; + NetworkVariableSerialization.Read(reader, ref item); + value.Add(item); + } + } + } + } + + public void WriteDelta(FastBufferWriter writer, ref List value, ref List previousValue) + { + CollectionSerializationUtility.WriteListDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref List value) + { + CollectionSerializationUtility.ReadListDelta(reader, ref value); + } + + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out List value, Allocator allocator) + { + throw new NotImplementedException(); + } + + public void Duplicate(in List value, ref List duplicatedValue) + { + if (duplicatedValue == null) + { + duplicatedValue = new List(); + } + + duplicatedValue.Clear(); + foreach (var item in value) + { + duplicatedValue.Add(item); + } + } + } + + internal class HashSetSerializer : INetworkVariableSerializer> where T : IEquatable + { + public void Write(FastBufferWriter writer, ref HashSet value) + { + bool isNull = value == null; + writer.WriteValueSafe(isNull); + if (!isNull) + { + writer.WriteValueSafe(value.Count); + foreach (var item in value) + { + var reffable = item; + NetworkVariableSerialization.Write(writer, ref reffable); + } + } + } + public void Read(FastBufferReader reader, ref HashSet value) + { + reader.ReadValueSafe(out bool isNull); + if (isNull) + { + value = null; + } + else + { + if (value == null) + { + value = new HashSet(); + } + else + { + value.Clear(); + } + reader.ReadValueSafe(out int len); + for (var i = 0; i < len; ++i) + { + T item = default; + NetworkVariableSerialization.Read(reader, ref item); + value.Add(item); + } + } + } + + public void WriteDelta(FastBufferWriter writer, ref HashSet value, ref HashSet previousValue) + { + CollectionSerializationUtility.WriteHashSetDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref HashSet value) + { + CollectionSerializationUtility.ReadHashSetDelta(reader, ref value); + } + + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out HashSet value, Allocator allocator) + { + throw new NotImplementedException(); + } + + public void Duplicate(in HashSet value, ref HashSet duplicatedValue) + { + if (duplicatedValue == null) + { + duplicatedValue = new HashSet(); + } + + duplicatedValue.Clear(); + foreach (var item in value) + { + duplicatedValue.Add(item); + } + } + } + + + internal class DictionarySerializer : INetworkVariableSerializer> + where TKey : IEquatable + { + public void Write(FastBufferWriter writer, ref Dictionary value) + { + bool isNull = value == null; + writer.WriteValueSafe(isNull); + if (!isNull) + { + writer.WriteValueSafe(value.Count); + foreach (var item in value) + { + (var key, var val) = (item.Key, item.Value); + NetworkVariableSerialization.Write(writer, ref key); + NetworkVariableSerialization.Write(writer, ref val); + } + } + } + public void Read(FastBufferReader reader, ref Dictionary value) + { + reader.ReadValueSafe(out bool isNull); + if (isNull) + { + value = null; + } + else + { + if (value == null) + { + value = new Dictionary(); + } + else + { + value.Clear(); + } + reader.ReadValueSafe(out int len); + for (var i = 0; i < len; ++i) + { + (TKey key, TVal val) = (default, default); + NetworkVariableSerialization.Read(reader, ref key); + NetworkVariableSerialization.Read(reader, ref val); + value.Add(key, val); + } + } + } + + public void WriteDelta(FastBufferWriter writer, ref Dictionary value, ref Dictionary previousValue) + { + CollectionSerializationUtility.WriteDictionaryDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref Dictionary value) + { + CollectionSerializationUtility.ReadDictionaryDelta(reader, ref value); + } + + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out Dictionary value, Allocator allocator) + { + throw new NotImplementedException(); + } + + public void Duplicate(in Dictionary value, ref Dictionary duplicatedValue) + { + if (duplicatedValue == null) + { + duplicatedValue = new Dictionary(); + } + + duplicatedValue.Clear(); + foreach (var item in value) + { + duplicatedValue.Add(item.Key, item.Value); + } + } + } + internal class UnmanagedArraySerializer : INetworkVariableSerializer> where T : unmanaged { public void Write(FastBufferWriter writer, ref NativeArray value) @@ -216,6 +514,15 @@ namespace Unity.Netcode reader.ReadUnmanagedSafe(out value, Allocator.Persistent); } + public void WriteDelta(FastBufferWriter writer, ref NativeArray value, ref NativeArray previousValue) + { + CollectionSerializationUtility.WriteNativeArrayDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeArray value) + { + CollectionSerializationUtility.ReadNativeArrayDelta(reader, ref value); + } + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeArray value, Allocator allocator) { reader.ReadUnmanagedSafe(out value, allocator); @@ -249,6 +556,15 @@ namespace Unity.Netcode reader.ReadUnmanagedSafeInPlace(ref value); } + public void WriteDelta(FastBufferWriter writer, ref NativeList value, ref NativeList previousValue) + { + CollectionSerializationUtility.WriteNativeListDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeList value) + { + CollectionSerializationUtility.ReadNativeListDelta(reader, ref value); + } + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeList value, Allocator allocator) { throw new NotImplementedException(); @@ -268,6 +584,90 @@ namespace Unity.Netcode duplicatedValue.CopyFrom(value); } } + + + internal class NativeHashSetSerializer : INetworkVariableSerializer> where T : unmanaged, IEquatable + { + public void Write(FastBufferWriter writer, ref NativeHashSet value) + { + writer.WriteValueSafe(value); + } + public void Read(FastBufferReader reader, ref NativeHashSet value) + { + reader.ReadValueSafeInPlace(ref value); + } + + public void WriteDelta(FastBufferWriter writer, ref NativeHashSet value, ref NativeHashSet previousValue) + { + CollectionSerializationUtility.WriteNativeHashSetDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeHashSet value) + { + CollectionSerializationUtility.ReadNativeHashSetDelta(reader, ref value); + } + + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeHashSet value, Allocator allocator) + { + throw new NotImplementedException(); + } + + public void Duplicate(in NativeHashSet value, ref NativeHashSet duplicatedValue) + { + if (!duplicatedValue.IsCreated) + { + duplicatedValue = new NativeHashSet(value.Capacity, Allocator.Persistent); + } + + duplicatedValue.Clear(); + foreach (var item in value) + { + duplicatedValue.Add(item); + } + } + } + + + internal class NativeHashMapSerializer : INetworkVariableSerializer> + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + public void Write(FastBufferWriter writer, ref NativeHashMap value) + { + writer.WriteValueSafe(value); + } + public void Read(FastBufferReader reader, ref NativeHashMap value) + { + reader.ReadValueSafeInPlace(ref value); + } + + public void WriteDelta(FastBufferWriter writer, ref NativeHashMap value, ref NativeHashMap previousValue) + { + CollectionSerializationUtility.WriteNativeHashMapDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeHashMap value) + { + CollectionSerializationUtility.ReadNativeHashMapDelta(reader, ref value); + } + + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeHashMap value, Allocator allocator) + { + throw new NotImplementedException(); + } + + public void Duplicate(in NativeHashMap value, ref NativeHashMap duplicatedValue) + { + if (!duplicatedValue.IsCreated) + { + duplicatedValue = new NativeHashMap(value.Capacity, Allocator.Persistent); + } + + duplicatedValue.Clear(); + foreach (var item in value) + { + duplicatedValue.Add(item.Key, item.Value); + } + } + } #endif /// @@ -285,6 +685,93 @@ namespace Unity.Netcode reader.ReadValueSafeInPlace(ref value); } + // Because of how strings are generally used, it is likely that most strings will still write as full strings + // instead of deltas. This actually adds one byte to the data to encode that it was serialized in full. + // But the potential savings from a small change to a large string are valuable enough to be worth that extra + // byte. + public unsafe void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue) + { + using var changes = new ResizableBitVector(Allocator.Temp); + int minLength = math.min(value.Length, previousValue.Length); + var numChanges = 0; + for (var i = 0; i < minLength; ++i) + { + var val = value[i]; + var prevVal = previousValue[i]; + if (!NetworkVariableSerialization.AreEqual(ref val, ref prevVal)) + { + ++numChanges; + changes.Set(i); + } + } + + for (var i = previousValue.Length; i < value.Length; ++i) + { + ++numChanges; + changes.Set(i); + } + + if (changes.GetSerializedSize() + FastBufferWriter.GetWriteSize() * numChanges > FastBufferWriter.GetWriteSize() * value.Length) + { + writer.WriteByteSafe(1); + writer.WriteValueSafe(value); + return; + } + writer.WriteByte(0); + BytePacker.WriteValuePacked(writer, value.Length); + writer.WriteValueSafe(changes); + unsafe + { + byte* ptr = value.GetUnsafePtr(); + byte* prevPtr = previousValue.GetUnsafePtr(); + for (int i = 0; i < value.Length; ++i) + { + if (changes.IsSet(i)) + { + if (i < previousValue.Length) + { + NetworkVariableSerialization.WriteDelta(writer, ref ptr[i], ref prevPtr[i]); + } + else + { + NetworkVariableSerialization.Write(writer, ref ptr[i]); + } + } + } + } + } + public unsafe void ReadDelta(FastBufferReader reader, ref T value) + { + // Writing can use the NativeArray logic as it is, but reading is a little different. + // Using the NativeArray logic for reading would result in length changes allocating a new NativeArray, + // which is not what we want for FixedString. With FixedString, the actual size of the data does not change, + // only an in-memory "length" value - so if the length changes, the only thing we want to do is change + // that value, and otherwise read everything in-place. + reader.ReadByteSafe(out byte full); + if (full == 1) + { + reader.ReadValueSafeInPlace(ref value); + return; + } + ByteUnpacker.ReadValuePacked(reader, out int length); + var changes = new ResizableBitVector(Allocator.Temp); + using var toDispose = changes; + { + reader.ReadNetworkSerializableInPlace(ref changes); + + value.Length = length; + + byte* ptr = value.GetUnsafePtr(); + for (var i = 0; i < value.Length; ++i) + { + if (changes.IsSet(i)) + { + reader.ReadByte(out ptr[i]); + } + } + } + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out T value, Allocator allocator) { throw new NotImplementedException(); @@ -312,6 +799,16 @@ namespace Unity.Netcode reader.ReadValueSafe(out value, Allocator.Persistent); } + + public void WriteDelta(FastBufferWriter writer, ref NativeArray value, ref NativeArray previousValue) + { + CollectionSerializationUtility.WriteNativeArrayDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeArray value) + { + CollectionSerializationUtility.ReadNativeArrayDelta(reader, ref value); + } + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeArray value, Allocator allocator) { reader.ReadValueSafe(out value, allocator); @@ -349,6 +846,15 @@ namespace Unity.Netcode reader.ReadValueSafeInPlace(ref value); } + public void WriteDelta(FastBufferWriter writer, ref NativeList value, ref NativeList previousValue) + { + CollectionSerializationUtility.WriteNativeListDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeList value) + { + CollectionSerializationUtility.ReadNativeListDelta(reader, ref value); + } + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeList value, Allocator allocator) { throw new NotImplementedException(); @@ -387,6 +893,25 @@ namespace Unity.Netcode value.NetworkSerialize(bufferSerializer); } + public void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue) + { + if (UserNetworkVariableSerialization.WriteDelta != null && UserNetworkVariableSerialization.ReadDelta != null) + { + UserNetworkVariableSerialization.WriteDelta(writer, value, previousValue); + return; + } + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref T value) + { + if (UserNetworkVariableSerialization.WriteDelta != null && UserNetworkVariableSerialization.ReadDelta != null) + { + UserNetworkVariableSerialization.ReadDelta(reader, ref value); + return; + } + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out T value, Allocator allocator) { throw new NotImplementedException(); @@ -414,6 +939,16 @@ namespace Unity.Netcode reader.ReadNetworkSerializable(out value, Allocator.Persistent); } + + public void WriteDelta(FastBufferWriter writer, ref NativeArray value, ref NativeArray previousValue) + { + CollectionSerializationUtility.WriteNativeArrayDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeArray value) + { + CollectionSerializationUtility.ReadNativeArrayDelta(reader, ref value); + } + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeArray value, Allocator allocator) { reader.ReadNetworkSerializable(out value, allocator); @@ -451,6 +986,15 @@ namespace Unity.Netcode reader.ReadNetworkSerializableInPlace(ref value); } + public void WriteDelta(FastBufferWriter writer, ref NativeList value, ref NativeList previousValue) + { + CollectionSerializationUtility.WriteNativeListDelta(writer, ref value, ref previousValue); + } + public void ReadDelta(FastBufferReader reader, ref NativeList value) + { + CollectionSerializationUtility.ReadNativeListDelta(reader, ref value); + } + void INetworkVariableSerializer>.ReadWithAllocator(FastBufferReader reader, out NativeList value, Allocator allocator) { throw new NotImplementedException(); @@ -507,6 +1051,25 @@ namespace Unity.Netcode } } + public void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue) + { + if (UserNetworkVariableSerialization.WriteDelta != null && UserNetworkVariableSerialization.ReadDelta != null) + { + UserNetworkVariableSerialization.WriteDelta(writer, value, previousValue); + return; + } + Write(writer, ref value); + } + public void ReadDelta(FastBufferReader reader, ref T value) + { + if (UserNetworkVariableSerialization.WriteDelta != null && UserNetworkVariableSerialization.ReadDelta != null) + { + UserNetworkVariableSerialization.ReadDelta(reader, ref value); + return; + } + Read(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out T value, Allocator allocator) { throw new NotImplementedException(); @@ -539,6 +1102,13 @@ namespace Unity.Netcode /// The value of type `T` to be written public delegate void WriteValueDelegate(FastBufferWriter writer, in T value); + /// + /// The write value delegate handler definition + /// + /// The to write the value of type `T` + /// The value of type `T` to be written + public delegate void WriteDeltaDelegate(FastBufferWriter writer, in T value, in T previousValue); + /// /// The read value delegate handler definition /// @@ -546,6 +1116,13 @@ namespace Unity.Netcode /// The value of type `T` to be read public delegate void ReadValueDelegate(FastBufferReader reader, out T value); + /// + /// The read value delegate handler definition + /// + /// The to read the value of type `T` + /// The value of type `T` to be read + public delegate void ReadDeltaDelegate(FastBufferReader reader, ref T value); + /// /// The read value delegate handler definition /// @@ -563,6 +1140,17 @@ namespace Unity.Netcode /// public static ReadValueDelegate ReadValue; + /// + /// Callback to write a delta between two values, based on computing the difference between the previous and + /// current values. + /// + public static WriteDeltaDelegate WriteDelta; + + /// + /// Callback to read a delta, applying only select changes to the current value. + /// + public static ReadDeltaDelegate ReadDelta; + /// /// Callback to create a duplicate of a value, used to check for dirty status. /// @@ -602,6 +1190,36 @@ namespace Unity.Netcode UserNetworkVariableSerialization.ReadValue(reader, out value); } + public void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue) + { + if (UserNetworkVariableSerialization.ReadValue == null || UserNetworkVariableSerialization.WriteValue == null || UserNetworkVariableSerialization.DuplicateValue == null) + { + ThrowArgumentError(); + } + + if (UserNetworkVariableSerialization.WriteDelta == null || UserNetworkVariableSerialization.ReadDelta == null) + { + UserNetworkVariableSerialization.WriteValue(writer, value); + return; + } + UserNetworkVariableSerialization.WriteDelta(writer, value, previousValue); + } + + public void ReadDelta(FastBufferReader reader, ref T value) + { + if (UserNetworkVariableSerialization.ReadValue == null || UserNetworkVariableSerialization.WriteValue == null || UserNetworkVariableSerialization.DuplicateValue == null) + { + ThrowArgumentError(); + } + + if (UserNetworkVariableSerialization.WriteDelta == null || UserNetworkVariableSerialization.ReadDelta == null) + { + UserNetworkVariableSerialization.ReadValue(reader, out value); + return; + } + UserNetworkVariableSerialization.ReadDelta(reader, ref value); + } + void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, out T value, Allocator allocator) { throw new NotImplementedException(); @@ -678,8 +1296,55 @@ namespace Unity.Netcode { NetworkVariableSerialization>.Serializer = new UnmanagedListSerializer(); } + + /// + /// Registeres a native hash set (this generic implementation works with all types) + /// + /// + public static void InitializeSerializer_NativeHashSet() where T : unmanaged, IEquatable + { + NetworkVariableSerialization>.Serializer = new NativeHashSetSerializer(); + } + + /// + /// Registeres a native hash set (this generic implementation works with all types) + /// + /// + public static void InitializeSerializer_NativeHashMap() + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + NetworkVariableSerialization>.Serializer = new NativeHashMapSerializer(); + } #endif + /// + /// Registeres a native hash set (this generic implementation works with all types) + /// + /// + public static void InitializeSerializer_List() + { + NetworkVariableSerialization>.Serializer = new ListSerializer(); + } + + /// + /// Registeres a native hash set (this generic implementation works with all types) + /// + /// + public static void InitializeSerializer_HashSet() where T : IEquatable + { + NetworkVariableSerialization>.Serializer = new HashSetSerializer(); + } + + /// + /// Registeres a native hash set (this generic implementation works with all types) + /// + /// + public static void InitializeSerializer_Dictionary() where TKey : IEquatable + { + NetworkVariableSerialization>.Serializer = new DictionarySerializer(); + } + /// /// Registers an unmanaged type that implements INetworkSerializable and will be serialized through a call to /// NetworkSerialize @@ -780,6 +1445,31 @@ namespace Unity.Netcode { NetworkVariableSerialization>.AreEqual = NetworkVariableSerialization.EqualityEqualsArray; } + /// + /// Registers an unmanaged type that will be checked for equality using T.Equals() + /// + /// + public static void InitializeEqualityChecker_List() + { + NetworkVariableSerialization>.AreEqual = NetworkVariableSerialization.EqualityEqualsList; + } + /// + /// Registers an unmanaged type that will be checked for equality using T.Equals() + /// + /// + public static void InitializeEqualityChecker_HashSet() where T : IEquatable + { + NetworkVariableSerialization>.AreEqual = NetworkVariableSerialization.EqualityEqualsHashSet; + } + /// + /// Registers an unmanaged type that will be checked for equality using T.Equals() + /// + /// + public static void InitializeEqualityChecker_Dictionary() + where TKey : IEquatable + { + NetworkVariableSerialization>.AreEqual = NetworkVariableDictionarySerialization.GenericEqualsDictionary; + } #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT /// @@ -788,7 +1478,25 @@ namespace Unity.Netcode /// public static void InitializeEqualityChecker_UnmanagedIEquatableList() where T : unmanaged, IEquatable { - NetworkVariableSerialization>.AreEqual = NetworkVariableSerialization.EqualityEqualsList; + NetworkVariableSerialization>.AreEqual = NetworkVariableSerialization.EqualityEqualsNativeList; + } + /// + /// Registers an unmanaged type that will be checked for equality using T.Equals() + /// + /// + public static void InitializeEqualityChecker_NativeHashSet() where T : unmanaged, IEquatable + { + NetworkVariableSerialization>.AreEqual = NetworkVariableSerialization.EqualityEqualsNativeHashSet; + } + /// + /// Registers an unmanaged type that will be checked for equality using T.Equals() + /// + /// + public static void InitializeEqualityChecker_NativeHashMap() + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + NetworkVariableSerialization>.AreEqual = NetworkVariableMapSerialization.GenericEqualsNativeHashMap; } #endif @@ -912,6 +1620,53 @@ namespace Unity.Netcode Serializer.Read(reader, ref value); } + /// + /// Serialize a value using the best-known serialization method for a generic value. + /// Will reliably serialize any value that is passed to it correctly with no boxing. + ///
+ ///
+ /// Note: If you are using this in a custom generic class, please make sure your class is + /// decorated with so that codegen can + /// initialize the serialization mechanisms correctly. If your class is NOT + /// generic, it is better to use FastBufferWriter directly. + ///
+ ///
+ /// If the codegen is unable to determine a serializer for a type, + /// . is called, which, by default, + /// will throw an exception, unless you have assigned a user serialization callback to it at runtime. + ///
+ /// + /// + public static void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue) + { + Serializer.WriteDelta(writer, ref value, ref previousValue); + } + + /// + /// Deserialize a value using the best-known serialization method for a generic value. + /// Will reliably deserialize any value that is passed to it correctly with no boxing. + /// For types whose deserialization can be determined by codegen (which is most types), + /// GC will only be incurred if the type is a managed type and the ref value passed in is `null`, + /// in which case a new value is created; otherwise, it will be deserialized in-place. + ///
+ ///
+ /// Note: If you are using this in a custom generic class, please make sure your class is + /// decorated with so that codegen can + /// initialize the serialization mechanisms correctly. If your class is NOT + /// generic, it is better to use FastBufferReader directly. + ///
+ ///
+ /// If the codegen is unable to determine a serializer for a type, + /// . is called, which, by default, + /// will throw an exception, unless you have assigned a user deserialization callback to it at runtime. + ///
+ /// + /// + public static void ReadDelta(FastBufferReader reader, ref T value) + { + Serializer.ReadDelta(reader, ref value); + } + /// /// Duplicates a value using the most efficient means of creating a complete copy. /// For most types this is a simple assignment or memcpy. @@ -1021,11 +1776,69 @@ namespace Unity.Netcode return a.Equals(b); } + internal static bool EqualityEqualsList(ref List a, ref List b) + { + if ((a == null) != (b == null)) + { + return false; + } + + if (a == null) + { + return true; + } + + if (a.Count != b.Count) + { + return false; + } + + for (var i = 0; i < a.Count; ++i) + { + var aItem = a[i]; + var bItem = b[i]; + if (!NetworkVariableSerialization.AreEqual(ref aItem, ref bItem)) + { + return false; + } + } + + return true; + } + + internal static bool EqualityEqualsHashSet(ref HashSet a, ref HashSet b) where TValueType : IEquatable + { + if ((a == null) != (b == null)) + { + return false; + } + + if (a == null) + { + return true; + } + + if (a.Count != b.Count) + { + return false; + } + + foreach (var item in a) + { + if (!b.Contains(item)) + { + return false; + } + } + + return true; + } + #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT // Compares two values of the same unmanaged type by underlying memory // Ignoring any overridden value checks // Size is fixed - internal static unsafe bool EqualityEqualsList(ref NativeList a, ref NativeList b) where TValueType : unmanaged, IEquatable + internal static unsafe bool EqualityEqualsNativeList(ref NativeList a, ref NativeList b) where TValueType : unmanaged, IEquatable { if (a.IsCreated != b.IsCreated) { @@ -1054,6 +1867,34 @@ namespace Unity.Netcode return true; } + + internal static bool EqualityEqualsNativeHashSet(ref NativeHashSet a, ref NativeHashSet b) where TValueType : unmanaged, IEquatable + { + if (a.IsCreated != b.IsCreated) + { + return false; + } + + if (!a.IsCreated) + { + return true; + } + + if (a.Count() != b.Count()) + { + return false; + } + + foreach (var item in a) + { + if (!b.Contains(item)) + { + return false; + } + } + + return true; + } #endif // Compares two values of the same unmanaged type by underlying memory @@ -1094,6 +1935,82 @@ namespace Unity.Netcode return a == b; } } + internal class NetworkVariableDictionarySerialization + where TKey : IEquatable + { + + internal static bool GenericEqualsDictionary(ref Dictionary a, ref Dictionary b) + { + if ((a == null) != (b == null)) + { + return false; + } + + if (a == null) + { + return true; + } + + if (a.Count != b.Count) + { + return false; + } + + foreach (var item in a) + { + var hasKey = b.TryGetValue(item.Key, out var val); + if (!hasKey) + { + return false; + } + + var bVal = item.Value; + if (!NetworkVariableSerialization.AreEqual(ref bVal, ref val)) + { + return false; + } + } + + return true; + } + } + +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + internal class NetworkVariableMapSerialization + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + + internal static bool GenericEqualsNativeHashMap(ref NativeHashMap a, ref NativeHashMap b) + { + if (a.IsCreated != b.IsCreated) + { + return false; + } + + if (!a.IsCreated) + { + return true; + } + + if (a.Count() != b.Count()) + { + return false; + } + + foreach (var item in a) + { + var hasKey = b.TryGetValue(item.Key, out var val); + if (!hasKey || !NetworkVariableSerialization.AreEqual(ref item.Value, ref val)) + { + return false; + } + } + + return true; + } + } +#endif // RuntimeAccessModifiersILPP will make this `public` // This is just pass-through to NetworkVariableSerialization but is here becaues I could not get ILPP diff --git a/Runtime/NetworkVariable/ResizableBitVector.cs b/Runtime/NetworkVariable/ResizableBitVector.cs new file mode 100644 index 0000000..5b3ec1e --- /dev/null +++ b/Runtime/NetworkVariable/ResizableBitVector.cs @@ -0,0 +1,107 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + /// + /// This is a simple resizable bit vector - i.e., a list of flags that use 1 bit each and can + /// grow to an indefinite size. This is backed by a NativeList<byte> instead of a single + /// integer value, allowing it to contain any size of memory. Contains built-in serialization support. + /// + internal struct ResizableBitVector : INetworkSerializable, IDisposable + { + private NativeList m_Bits; + private const int k_Divisor = sizeof(byte) * 8; + + public ResizableBitVector(Allocator allocator) + { + m_Bits = new NativeList(allocator); + } + + public void Dispose() + { + m_Bits.Dispose(); + } + + public int GetSerializedSize() + { + return sizeof(int) + m_Bits.Length; + } + + private (int, int) GetBitData(int i) + { + var index = i / k_Divisor; + var bitWithinIndex = i % k_Divisor; + return (index, bitWithinIndex); + } + + /// + /// Set bit 'i' - i.e., bit 0 is 00000001, bit 1 is 00000010, and so on. + /// There is no upper bound on i except for the memory available in the system. + /// + /// + public void Set(int i) + { + var (index, bitWithinIndex) = GetBitData(i); + if (index >= m_Bits.Length) + { + m_Bits.Resize(index + 1, NativeArrayOptions.ClearMemory); + } + + m_Bits[index] |= (byte)(1 << bitWithinIndex); + } + + /// + /// Unset bit 'i' - i.e., bit 0 is 00000001, bit 1 is 00000010, and so on. + /// There is no upper bound on i except for the memory available in the system. + /// Note that once a BitVector has grown to a certain size, it will not shrink back down, + /// so if you set and unset every bit, it will still serialize at its high watermark size. + /// + /// + public void Unset(int i) + { + var (index, bitWithinIndex) = GetBitData(i); + if (index >= m_Bits.Length) + { + return; + } + + m_Bits[index] &= (byte)~(1 << bitWithinIndex); + } + + /// + /// Check if bit 'i' is set - i.e., bit 0 is 00000001, bit 1 is 00000010, and so on. + /// There is no upper bound on i except for the memory available in the system. + /// + /// + public bool IsSet(int i) + { + var (index, bitWithinIndex) = GetBitData(i); + if (index >= m_Bits.Length) + { + return false; + } + + return (m_Bits[index] & (byte)(1 << bitWithinIndex)) != 0; + } + + public unsafe void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + var length = m_Bits.Length; + serializer.SerializeValue(ref length); + m_Bits.ResizeUninitialized(length); + var ptr = m_Bits.GetUnsafePtr(); + { + if (serializer.IsReader) + { + serializer.GetFastBufferReader().ReadBytesSafe((byte*)ptr, length); + } + else + { + serializer.GetFastBufferWriter().WriteBytesSafe((byte*)ptr, length); + } + } + } + } +} diff --git a/Runtime/NetworkVariable/ResizableBitVector.cs.meta b/Runtime/NetworkVariable/ResizableBitVector.cs.meta new file mode 100644 index 0000000..5b3238a --- /dev/null +++ b/Runtime/NetworkVariable/ResizableBitVector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 664696a622e244dfa43b26628c05e4a6 +timeCreated: 1705437231 \ No newline at end of file diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs index 351e49d..612b834 100644 --- a/Runtime/SceneManagement/SceneEventData.cs +++ b/Runtime/SceneManagement/SceneEventData.cs @@ -245,6 +245,73 @@ namespace Unity.Netcode } } + /// + /// Used with SortParentedNetworkObjects to sort the children of the root parent NetworkObject + /// + /// object to be sorted + /// object to be compared to for sorting the first object + /// + private int SortChildrenNetworkObjects(NetworkObject first, NetworkObject second) + { + var firstParent = first.GetCachedParent()?.GetComponent(); + // If the second is the first's parent then move the first down + if (firstParent != null && firstParent == second) + { + return 1; + } + + var secondParent = second.GetCachedParent()?.GetComponent(); + // If the first is the second's parent then move the first up + if (secondParent != null && secondParent == first) + { + return -1; + } + + // Otherwise, don't move the first at all + return 0; + } + + /// + /// Sorts the synchronization order of the NetworkObjects to be serialized + /// by parents before children order + /// + private void SortParentedNetworkObjects() + { + var networkObjectList = m_NetworkObjectsSync.ToList(); + foreach (var networkObject in networkObjectList) + { + // Find only the root parent NetworkObjects + if (networkObject.transform.childCount > 0 && networkObject.transform.parent == null) + { + // Get all child NetworkObjects of the root + var childNetworkObjects = networkObject.GetComponentsInChildren().ToList(); + + childNetworkObjects.Sort(SortChildrenNetworkObjects); + + // Remove the root from the children list + childNetworkObjects.Remove(networkObject); + + // Remove the root's children from the primary list + foreach (var childObject in childNetworkObjects) + { + m_NetworkObjectsSync.Remove(childObject); + } + // Insert or Add the sorted children list + var nextIndex = m_NetworkObjectsSync.IndexOf(networkObject) + 1; + if (nextIndex == m_NetworkObjectsSync.Count) + { + m_NetworkObjectsSync.AddRange(childNetworkObjects); + } + else + { + m_NetworkObjectsSync.InsertRange(nextIndex, childNetworkObjects); + } + } + } + } + + internal static bool LogSerializationOrder = false; + internal void AddSpawnedNetworkObjects() { m_NetworkObjectsSync.Clear(); @@ -256,22 +323,22 @@ namespace Unity.Netcode } } - // Sort by parents before children - m_NetworkObjectsSync.Sort(SortParentedNetworkObjects); - // Sort by INetworkPrefabInstanceHandler implementation before the // NetworkObjects spawned by the implementation m_NetworkObjectsSync.Sort(SortNetworkObjects); + // The last thing we sort is parents before children + SortParentedNetworkObjects(); + // This is useful to know what NetworkObjects a client is going to be synchronized with // as well as the order in which they will be deserialized - if (m_NetworkManager.LogLevel == LogLevel.Developer) + if (LogSerializationOrder && m_NetworkManager.LogLevel == LogLevel.Developer) { var messageBuilder = new System.Text.StringBuilder(0xFFFF); - messageBuilder.Append("[Server-Side Client-Synchronization] NetworkObject serialization order:"); + messageBuilder.AppendLine("[Server-Side Client-Synchronization] NetworkObject serialization order:"); foreach (var networkObject in m_NetworkObjectsSync) { - messageBuilder.Append($"{networkObject.name}"); + messageBuilder.AppendLine($"{networkObject.name}"); } NetworkLog.LogInfo(messageBuilder.ToString()); } @@ -362,32 +429,6 @@ namespace Unity.Netcode return 0; } - /// - /// Sorts the synchronization order of the NetworkObjects to be serialized - /// by parents before children. - /// - /// - /// This only handles late joining players. Spawning and nesting several children - /// dynamically is still handled by the orphaned child list when deserialized out of - /// hierarchical order (i.e. Spawn parent and child dynamically, parent message is - /// dropped and re-sent but child object is received and processed) - /// - private int SortParentedNetworkObjects(NetworkObject first, NetworkObject second) - { - // If the first has a parent, move the first down - if (first.transform.parent != null) - { - return 1; - } - else // If the second has a parent and the first does not, then move the first up - if (second.transform.parent != null) - { - return -1; - } - return 0; - } - - /// /// Client and Server Side: /// Serializes data based on the SceneEvent type () diff --git a/Runtime/Serialization/FastBufferReader.cs b/Runtime/Serialization/FastBufferReader.cs index f91a41e..8adcc98 100644 --- a/Runtime/Serialization/FastBufferReader.cs +++ b/Runtime/Serialization/FastBufferReader.cs @@ -1069,6 +1069,36 @@ namespace Unity.Netcode ReadUnmanagedSafeInPlace(ref value); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ReadValueSafeInPlace(ref NativeHashSet value) where T : unmanaged, IEquatable + { + ReadUnmanagedSafe(out int length); + value.Clear(); + for (var i = 0; i < length; ++i) + { + T val = default; + NetworkVariableSerialization.Read(this, ref val); + value.Add(val); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ReadValueSafeInPlace(ref NativeHashMap value) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + ReadUnmanagedSafe(out int length); + value.Clear(); + for (var i = 0; i < length; ++i) + { + TKey key = default; + TVal val = default; + NetworkVariableSerialization.Read(this, ref key); + NetworkVariableSerialization.Read(this, ref val); + value[key] = val; + } + } #endif /// diff --git a/Runtime/Serialization/FastBufferWriter.cs b/Runtime/Serialization/FastBufferWriter.cs index 475b00a..aa50ad6 100644 --- a/Runtime/Serialization/FastBufferWriter.cs +++ b/Runtime/Serialization/FastBufferWriter.cs @@ -1189,6 +1189,31 @@ namespace Unity.Netcode WriteUnmanaged(value); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteValueSafe(NativeHashSet value) where T : unmanaged, IEquatable + { + WriteUnmanagedSafe(value.Count()); + foreach (var item in value) + { + var iReffable = item; + NetworkVariableSerialization.Write(this, ref iReffable); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteValueSafe(NativeHashMap value) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + WriteUnmanagedSafe(value.Count()); + foreach (var item in value) + { + (var key, var val) = (item.Key, item.Value); + NetworkVariableSerialization.Write(this, ref key); + NetworkVariableSerialization.Write(this, ref val); + } + } #endif /// diff --git a/Runtime/Serialization/NetworkBehaviourReference.cs b/Runtime/Serialization/NetworkBehaviourReference.cs index 6df91b8..4bc978c 100644 --- a/Runtime/Serialization/NetworkBehaviourReference.cs +++ b/Runtime/Serialization/NetworkBehaviourReference.cs @@ -11,6 +11,7 @@ namespace Unity.Netcode { private NetworkObjectReference m_NetworkObjectReference; private ushort m_NetworkBehaviourId; + private static ushort s_NullId = ushort.MaxValue; /// /// Creates a new instance of the struct. @@ -21,7 +22,9 @@ namespace Unity.Netcode { if (networkBehaviour == null) { - throw new ArgumentNullException(nameof(networkBehaviour)); + m_NetworkObjectReference = new NetworkObjectReference((NetworkObject)null); + m_NetworkBehaviourId = s_NullId; + return; } if (networkBehaviour.NetworkObject == null) { @@ -60,6 +63,10 @@ namespace Unity.Netcode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static NetworkBehaviour GetInternal(NetworkBehaviourReference networkBehaviourRef, NetworkManager networkManager = null) { + if (networkBehaviourRef.m_NetworkBehaviourId == s_NullId) + { + return null; + } if (networkBehaviourRef.m_NetworkObjectReference.TryGet(out NetworkObject networkObject, networkManager)) { return networkObject.GetNetworkBehaviourAtOrderIndex(networkBehaviourRef.m_NetworkBehaviourId); diff --git a/Runtime/Serialization/NetworkObjectReference.cs b/Runtime/Serialization/NetworkObjectReference.cs index dc91044..c2f1ba4 100644 --- a/Runtime/Serialization/NetworkObjectReference.cs +++ b/Runtime/Serialization/NetworkObjectReference.cs @@ -10,6 +10,7 @@ namespace Unity.Netcode public struct NetworkObjectReference : INetworkSerializable, IEquatable { private ulong m_NetworkObjectId; + private static ulong s_NullId = ulong.MaxValue; /// /// The of the referenced . @@ -24,13 +25,13 @@ namespace Unity.Netcode /// Creates a new instance of the struct. /// /// The to reference. - /// /// public NetworkObjectReference(NetworkObject networkObject) { if (networkObject == null) { - throw new ArgumentNullException(nameof(networkObject)); + m_NetworkObjectId = s_NullId; + return; } if (networkObject.IsSpawned == false) @@ -45,16 +46,20 @@ namespace Unity.Netcode /// Creates a new instance of the struct. /// /// The GameObject from which the component will be referenced. - /// /// public NetworkObjectReference(GameObject gameObject) { if (gameObject == null) { - throw new ArgumentNullException(nameof(gameObject)); + m_NetworkObjectId = s_NullId; + return; } - var networkObject = gameObject.GetComponent() ?? throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component."); + var networkObject = gameObject.GetComponent(); + if (!networkObject) + { + throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component."); + } if (networkObject.IsSpawned == false) { throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s."); @@ -80,10 +85,14 @@ namespace Unity.Netcode /// /// The reference. /// The networkmanager. Uses to resolve if null. - /// The resolves . Returns null if the networkobject was not found + /// The resolved . Returns null if the networkobject was not found [MethodImpl(MethodImplOptions.AggressiveInlining)] private static NetworkObject Resolve(NetworkObjectReference networkObjectRef, NetworkManager networkManager = null) { + if (networkObjectRef.m_NetworkObjectId == s_NullId) + { + return null; + } networkManager = networkManager ?? NetworkManager.Singleton; networkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectRef.m_NetworkObjectId, out NetworkObject networkObject); diff --git a/Runtime/Timing/AnticipationSystem.cs b/Runtime/Timing/AnticipationSystem.cs new file mode 100644 index 0000000..b61afd4 --- /dev/null +++ b/Runtime/Timing/AnticipationSystem.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; + +namespace Unity.Netcode +{ + internal interface IAnticipationEventReceiver + { + public void SetupForUpdate(); + public void SetupForRender(); + } + + internal interface IAnticipatedObject + { + public void Update(); + public void ResetAnticipation(); + public NetworkObject OwnerObject { get; } + } + + internal class AnticipationSystem + { + internal ulong LastAnticipationAck; + internal double LastAnticipationAckTime; + + internal HashSet AllAnticipatedObjects = new HashSet(); + + internal ulong AnticipationCounter; + + private NetworkManager m_NetworkManager; + + public HashSet ObjectsToReanticipate = new HashSet(); + + public AnticipationSystem(NetworkManager manager) + { + m_NetworkManager = manager; + } + + public event NetworkManager.ReanticipateDelegate OnReanticipate; + + private HashSet m_AnticipationEventReceivers = new HashSet(); + + public void RegisterForAnticipationEvents(IAnticipationEventReceiver receiver) + { + m_AnticipationEventReceivers.Add(receiver); + } + public void DeregisterForAnticipationEvents(IAnticipationEventReceiver receiver) + { + m_AnticipationEventReceivers.Remove(receiver); + } + + public void SetupForUpdate() + { + foreach (var receiver in m_AnticipationEventReceivers) + { + receiver.SetupForUpdate(); + } + } + + public void SetupForRender() + { + foreach (var receiver in m_AnticipationEventReceivers) + { + receiver.SetupForRender(); + } + } + + public void ProcessReanticipation() + { + var lastRoundTripTime = m_NetworkManager.LocalTime.Time - LastAnticipationAckTime; + foreach (var item in ObjectsToReanticipate) + { + foreach (var behaviour in item.OwnerObject.ChildNetworkBehaviours) + { + behaviour.OnReanticipate(lastRoundTripTime); + } + item.ResetAnticipation(); + } + + ObjectsToReanticipate.Clear(); + OnReanticipate?.Invoke(lastRoundTripTime); + } + + public void Update() + { + foreach (var item in AllAnticipatedObjects) + { + item.Update(); + } + } + + public void Sync() + { + if (AllAnticipatedObjects.Count != 0 && !m_NetworkManager.ShutdownInProgress && !m_NetworkManager.ConnectionManager.LocalClient.IsServer && m_NetworkManager.ConnectionManager.LocalClient.IsConnected) + { + var message = new AnticipationCounterSyncPingMessage { Counter = AnticipationCounter, Time = m_NetworkManager.LocalTime.Time }; + m_NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.Reliable, NetworkManager.ServerClientId); + } + + ++AnticipationCounter; + } + } +} diff --git a/Runtime/Timing/AnticipationSystem.cs.meta b/Runtime/Timing/AnticipationSystem.cs.meta new file mode 100644 index 0000000..3bdf3f1 --- /dev/null +++ b/Runtime/Timing/AnticipationSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 21e6ae0a82f945458519aee4ee119cab +timeCreated: 1707845625 \ No newline at end of file diff --git a/Runtime/Timing/NetworkTime.cs b/Runtime/Timing/NetworkTime.cs index 7af210b..7c624c4 100644 --- a/Runtime/Timing/NetworkTime.cs +++ b/Runtime/Timing/NetworkTime.cs @@ -24,6 +24,11 @@ namespace Unity.Netcode /// public double TickOffset => m_CachedTickOffset; + /// + /// Gets the tick, including partial tick value passed since it started. + /// + public double TickWithPartial => Tick + (TickOffset / m_TickInterval); + /// /// Gets the current time. This is a non fixed time value and similar to . /// diff --git a/Runtime/Timing/NetworkTimeSystem.cs b/Runtime/Timing/NetworkTimeSystem.cs index 16a3c4e..4dbd85a 100644 --- a/Runtime/Timing/NetworkTimeSystem.cs +++ b/Runtime/Timing/NetworkTimeSystem.cs @@ -5,7 +5,9 @@ namespace Unity.Netcode { /// /// is a standalone system which can be used to run a network time simulation. - /// The network time system maintains both a local and a server time. The local time is based on + /// The network time system maintains both a local and a server time. The local time is based on the server time + /// as last received from the server plus an offset based on the current RTT - in other words, it is a best-guess + /// effort at predicting what the server tick will be when a given network action is processed on the server. /// public class NetworkTimeSystem { diff --git a/TestHelpers/Runtime/IntegrationTestSceneHandler.cs b/TestHelpers/Runtime/IntegrationTestSceneHandler.cs index d979c74..05998d7 100644 --- a/TestHelpers/Runtime/IntegrationTestSceneHandler.cs +++ b/TestHelpers/Runtime/IntegrationTestSceneHandler.cs @@ -165,13 +165,30 @@ namespace Unity.Netcode.TestHelpers.Runtime foreach (var sobj in inSceneNetworkObjects) { - if (sobj.NetworkManagerOwner != networkManager) + ProcessInSceneObject(sobj, networkManager); + } + } + + /// + /// Assures to apply an ObjectNameIdentifier to all children + /// + private static void ProcessInSceneObject(NetworkObject networkObject, NetworkManager networkManager) + { + if (networkObject.NetworkManagerOwner != networkManager) + { + networkObject.NetworkManagerOwner = networkManager; + } + if (networkObject.GetComponent() == null) + { + networkObject.gameObject.AddComponent(); + var networkObjects = networkObject.gameObject.GetComponentsInChildren(); + foreach (var child in networkObjects) { - sobj.NetworkManagerOwner = networkManager; - } - if (sobj.GetComponent() == null && sobj.GetComponentInChildren() == null) - { - sobj.gameObject.AddComponent(); + if (child == networkObject) + { + continue; + } + ProcessInSceneObject(child, networkManager); } } } diff --git a/TestHelpers/Runtime/MockTransport.cs b/TestHelpers/Runtime/MockTransport.cs index 1bfc130..0f29377 100644 --- a/TestHelpers/Runtime/MockTransport.cs +++ b/TestHelpers/Runtime/MockTransport.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using Unity.Collections; +using Random = UnityEngine.Random; namespace Unity.Netcode.TestHelpers.Runtime { @@ -8,37 +10,107 @@ namespace Unity.Netcode.TestHelpers.Runtime private struct MessageData { public ulong FromClientId; - public ArraySegment Payload; + public FastBufferReader Payload; public NetworkEvent Event; + public float AvailableTime; + public int Sequence; + public NetworkDelivery Delivery; } - private static Dictionary> s_MessageQueue = new Dictionary>(); + private static Dictionary> s_MessageQueue = new Dictionary>(); public override ulong ServerClientId { get; } = 0; public static ulong HighTransportId = 0; public ulong TransportId = 0; + public float SimulatedLatencySeconds; + public float PacketDropRate; + public float LatencyJitter; + + public Dictionary LastSentSequence = new Dictionary(); + public Dictionary LastReceivedSequence = new Dictionary(); + public NetworkManager NetworkManager; public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) { - var copy = new byte[payload.Array.Length]; - Array.Copy(payload.Array, copy, payload.Array.Length); - s_MessageQueue[clientId].Enqueue(new MessageData { FromClientId = TransportId, Payload = new ArraySegment(copy, payload.Offset, payload.Count), Event = NetworkEvent.Data }); + if ((networkDelivery == NetworkDelivery.Unreliable || networkDelivery == NetworkDelivery.UnreliableSequenced) && Random.Range(0, 1) < PacketDropRate) + { + return; + } + + if (!LastSentSequence.ContainsKey(clientId)) + { + LastSentSequence[clientId] = 1; + } + + var reader = new FastBufferReader(payload, Allocator.TempJob); + s_MessageQueue[clientId].Add(new MessageData + { + FromClientId = TransportId, + Payload = reader, + Event = NetworkEvent.Data, + AvailableTime = NetworkManager.RealTimeProvider.UnscaledTime + SimulatedLatencySeconds + Random.Range(-LatencyJitter, LatencyJitter), + Sequence = ++LastSentSequence[clientId], + Delivery = networkDelivery + }); + s_MessageQueue[clientId].Sort(((a, b) => a.AvailableTime.CompareTo(b.AvailableTime))); } public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime) { if (s_MessageQueue[TransportId].Count > 0) { - var data = s_MessageQueue[TransportId].Dequeue(); - clientId = data.FromClientId; - payload = data.Payload; + MessageData data; + for (; ; ) + { + data = s_MessageQueue[TransportId][0]; + if (data.AvailableTime > NetworkManager.RealTimeProvider.UnscaledTime) + { + clientId = 0; + payload = new ArraySegment(); + receiveTime = 0; + return NetworkEvent.Nothing; + } + + s_MessageQueue[TransportId].RemoveAt(0); + clientId = data.FromClientId; + if (data.Event == NetworkEvent.Data && data.Delivery == NetworkDelivery.UnreliableSequenced && LastReceivedSequence.ContainsKey(clientId) && data.Sequence <= LastReceivedSequence[clientId]) + { + continue; + } + + break; + } + + if (data.Delivery == NetworkDelivery.UnreliableSequenced) + { + LastReceivedSequence[clientId] = data.Sequence; + } + + payload = new ArraySegment(); + if (data.Event == NetworkEvent.Data) + { + payload = data.Payload.ToArray(); + data.Payload.Dispose(); + } + receiveTime = NetworkManager.RealTimeProvider.RealTimeSinceStartup; if (NetworkManager.IsServer && data.Event == NetworkEvent.Connect) { - s_MessageQueue[data.FromClientId].Enqueue(new MessageData { Event = NetworkEvent.Connect, FromClientId = ServerClientId, Payload = new ArraySegment() }); + if (!LastSentSequence.ContainsKey(data.FromClientId)) + { + LastSentSequence[data.FromClientId] = 1; + } + s_MessageQueue[data.FromClientId].Add( + new MessageData + { + Event = NetworkEvent.Connect, + FromClientId = ServerClientId, + AvailableTime = NetworkManager.RealTimeProvider.UnscaledTime + SimulatedLatencySeconds + Random.Range(-LatencyJitter, LatencyJitter), + Sequence = ++LastSentSequence[data.FromClientId] + }); } return data.Event; } @@ -51,30 +123,45 @@ namespace Unity.Netcode.TestHelpers.Runtime public override bool StartClient() { TransportId = ++HighTransportId; - s_MessageQueue[TransportId] = new Queue(); - s_MessageQueue[ServerClientId].Enqueue(new MessageData { Event = NetworkEvent.Connect, FromClientId = TransportId, Payload = new ArraySegment() }); + s_MessageQueue[TransportId] = new List(); + s_MessageQueue[ServerClientId].Add( + new MessageData + { + Event = NetworkEvent.Connect, + FromClientId = TransportId, + }); return true; } public override bool StartServer() { - s_MessageQueue[ServerClientId] = new Queue(); + s_MessageQueue[ServerClientId] = new List(); return true; } public override void DisconnectRemoteClient(ulong clientId) { - s_MessageQueue[clientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = TransportId, Payload = new ArraySegment() }); + s_MessageQueue[clientId].Add( + new MessageData + { + Event = NetworkEvent.Disconnect, + FromClientId = TransportId, + }); } public override void DisconnectLocalClient() { - s_MessageQueue[ServerClientId].Enqueue(new MessageData { Event = NetworkEvent.Disconnect, FromClientId = TransportId, Payload = new ArraySegment() }); + s_MessageQueue[ServerClientId].Add( + new MessageData + { + Event = NetworkEvent.Disconnect, + FromClientId = TransportId, + }); } public override ulong GetCurrentRtt(ulong clientId) { - return 0; + return (ulong)(SimulatedLatencySeconds * 1000); } public override void Shutdown() @@ -85,5 +172,35 @@ namespace Unity.Netcode.TestHelpers.Runtime { NetworkManager = networkManager; } + + protected static void DisposeQueueItems() + { + foreach (var kvp in s_MessageQueue) + { + foreach (var value in kvp.Value) + { + if (value.Event == NetworkEvent.Data) + { + value.Payload.Dispose(); + } + } + } + } + + public static void Reset() + { + DisposeQueueItems(); + s_MessageQueue.Clear(); + HighTransportId = 0; + } + + public static void ClearQueues() + { + DisposeQueueItems(); + foreach (var kvp in s_MessageQueue) + { + kvp.Value.Clear(); + } + } } } diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index f05ee46..a97a92b 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -308,6 +308,14 @@ namespace Unity.Netcode.TestHelpers.Runtime NetcodeLogAssert = new NetcodeLogAssert(); if (m_EnableTimeTravel) { + if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests) + { + MockTransport.ClearQueues(); + } + else + { + MockTransport.Reset(); + } // Setup the frames per tick for time travel advance to next tick ConfigureFramesPerTick(); } @@ -548,6 +556,33 @@ namespace Unity.Netcode.TestHelpers.Runtime Assert.True(WaitForConditionOrTimeOutWithTimeTravel(() => !networkManager.IsConnectedClient)); } + protected void SetTimeTravelSimulatedLatency(float latencySeconds) + { + ((MockTransport)m_ServerNetworkManager.NetworkConfig.NetworkTransport).SimulatedLatencySeconds = latencySeconds; + foreach (var client in m_ClientNetworkManagers) + { + ((MockTransport)client.NetworkConfig.NetworkTransport).SimulatedLatencySeconds = latencySeconds; + } + } + + protected void SetTimeTravelSimulatedDropRate(float dropRatePercent) + { + ((MockTransport)m_ServerNetworkManager.NetworkConfig.NetworkTransport).PacketDropRate = dropRatePercent; + foreach (var client in m_ClientNetworkManagers) + { + ((MockTransport)client.NetworkConfig.NetworkTransport).PacketDropRate = dropRatePercent; + } + } + + protected void SetTimeTravelSimulatedLatencyJitter(float jitterSeconds) + { + ((MockTransport)m_ServerNetworkManager.NetworkConfig.NetworkTransport).LatencyJitter = jitterSeconds; + foreach (var client in m_ClientNetworkManagers) + { + ((MockTransport)client.NetworkConfig.NetworkTransport).LatencyJitter = jitterSeconds; + } + } + /// /// Creates the server and clients /// @@ -1005,6 +1040,17 @@ namespace Unity.Netcode.TestHelpers.Runtime VerboseDebug($"Exiting {nameof(TearDown)}"); LogWaitForMessages(); NetcodeLogAssert.Dispose(); + if (m_EnableTimeTravel) + { + if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests) + { + MockTransport.ClearQueues(); + } + else + { + MockTransport.Reset(); + } + } } /// @@ -1554,8 +1600,17 @@ namespace Unity.Netcode.TestHelpers.Runtime /// /// /// - protected static void TimeTravel(double amountOfTimeInSeconds, int numFramesToSimulate) + protected static void TimeTravel(double amountOfTimeInSeconds, int numFramesToSimulate = -1) { + if (numFramesToSimulate < 0) + { + var frameRate = Application.targetFrameRate; + if (frameRate <= 0) + { + frameRate = 60; + } + numFramesToSimulate = Math.Max((int)(amountOfTimeInSeconds / frameRate), 1); + } var interval = amountOfTimeInSeconds / numFramesToSimulate; for (var i = 0; i < numFramesToSimulate; ++i) { @@ -1613,6 +1668,16 @@ namespace Unity.Netcode.TestHelpers.Runtime TimeTravel(timePassed, frames); } + private struct UpdateData + { + public MethodInfo Update; + public MethodInfo FixedUpdate; + public MethodInfo LateUpdate; + } + + private static object[] s_EmptyObjectArray = { }; + private static Dictionary s_UpdateFunctionCache = new Dictionary(); + /// /// Simulates one SDK frame. This can be used even without TimeTravel, though it's of somewhat less use /// without TimeTravel, as, without the mock transport, it will likely not provide enough time for any @@ -1620,33 +1685,50 @@ namespace Unity.Netcode.TestHelpers.Runtime /// public static void SimulateOneFrame() { - foreach (NetworkUpdateStage stage in Enum.GetValues(typeof(NetworkUpdateStage))) + foreach (NetworkUpdateStage updateStage in Enum.GetValues(typeof(NetworkUpdateStage))) { - NetworkUpdateLoop.RunNetworkUpdateStage(stage); - string methodName = string.Empty; - switch (stage) + var stage = updateStage; + // These two are out of order numerically due to backward compatibility + // requirements. We have to swap them to maintain correct execution + // order. + if (stage == NetworkUpdateStage.PostScriptLateUpdate) { - case NetworkUpdateStage.FixedUpdate: - methodName = "FixedUpdate"; // mapping NetworkUpdateStage.FixedUpdate to MonoBehaviour.FixedUpdate - break; - case NetworkUpdateStage.Update: - methodName = "Update"; // mapping NetworkUpdateStage.Update to MonoBehaviour.Update - break; - case NetworkUpdateStage.PreLateUpdate: - methodName = "LateUpdate"; // mapping NetworkUpdateStage.PreLateUpdate to MonoBehaviour.LateUpdate - break; + stage = NetworkUpdateStage.PostLateUpdate; } - - if (!string.IsNullOrEmpty(methodName)) + else if (stage == NetworkUpdateStage.PostLateUpdate) { -#if UNITY_2023_1_OR_NEWER - foreach (var behaviour in Object.FindObjectsByType(FindObjectsSortMode.InstanceID)) -#else - foreach (var behaviour in Object.FindObjectsOfType()) -#endif + stage = NetworkUpdateStage.PostScriptLateUpdate; + } + NetworkUpdateLoop.RunNetworkUpdateStage(stage); + + if (stage == NetworkUpdateStage.Update || stage == NetworkUpdateStage.FixedUpdate || stage == NetworkUpdateStage.PreLateUpdate) + { + foreach (var behaviour in Object.FindObjectsByType(FindObjectsSortMode.None)) { - var method = behaviour.GetType().GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - method?.Invoke(behaviour, new object[] { }); + var type = behaviour.GetType(); + if (!s_UpdateFunctionCache.TryGetValue(type, out var updateData)) + { + updateData = new UpdateData + { + Update = type.GetMethod("Update", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), + FixedUpdate = type.GetMethod("FixedUpdate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), + LateUpdate = type.GetMethod("LateUpdate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), + }; + s_UpdateFunctionCache[type] = updateData; + } + + switch (stage) + { + case NetworkUpdateStage.FixedUpdate: + updateData.FixedUpdate?.Invoke(behaviour, new object[] { }); + break; + case NetworkUpdateStage.Update: + updateData.Update?.Invoke(behaviour, new object[] { }); + break; + case NetworkUpdateStage.PreLateUpdate: + updateData.LateUpdate?.Invoke(behaviour, new object[] { }); + break; + } } } } diff --git a/Tests/Editor/Messaging/MessageCorruptionTests.cs b/Tests/Editor/Messaging/MessageCorruptionTests.cs index 96544ac..b37454f 100644 --- a/Tests/Editor/Messaging/MessageCorruptionTests.cs +++ b/Tests/Editor/Messaging/MessageCorruptionTests.cs @@ -90,11 +90,16 @@ namespace Unity.Netcode.EditorTests break; } case TypeOfCorruption.CorruptBytes: - batchData.Seek(batchData.Length - 2); - var currentByte = batchData.GetUnsafePtr()[0]; - batchData.WriteByteSafe((byte)(currentByte == 0 ? 1 : 0)); - MessageQueue.Add(batchData.ToArray()); - break; + { + batchData.Seek(batchData.Length - 4); + for (int i = 0; i < 4; i++) + { + var currentByte = batchData.GetUnsafePtr()[i]; + batchData.WriteByteSafe((byte)(currentByte == 0 ? 1 : 0)); + MessageQueue.Add(batchData.ToArray()); + } + break; + } case TypeOfCorruption.Truncated: batchData.Truncate(batchData.Length - 1); MessageQueue.Add(batchData.ToArray()); diff --git a/Tests/Runtime/DisconnectTests.cs b/Tests/Runtime/DisconnectTests.cs index c72cfdd..9c7a95f 100644 --- a/Tests/Runtime/DisconnectTests.cs +++ b/Tests/Runtime/DisconnectTests.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; @@ -37,7 +38,10 @@ namespace Unity.Netcode.RuntimeTests protected override int NumberOfClients => 1; private OwnerPersistence m_OwnerPersistence; + private ClientDisconnectType m_ClientDisconnectType; private bool m_ClientDisconnected; + private Dictionary m_DisconnectedEvent = new Dictionary(); + private ulong m_DisconnectEventClientId; private ulong m_TransportClientId; private ulong m_ClientId; @@ -89,6 +93,16 @@ namespace Unity.Netcode.RuntimeTests m_ClientDisconnected = true; } + private void OnConnectionEvent(NetworkManager networkManager, ConnectionEventData connectionEventData) + { + if (connectionEventData.EventType != ConnectionEvent.ClientDisconnected) + { + return; + } + + m_DisconnectedEvent.Add(networkManager, connectionEventData); + } + /// /// Conditional check to assure the transport to client (and vice versa) mappings are cleaned up /// @@ -126,19 +140,26 @@ namespace Unity.Netcode.RuntimeTests public IEnumerator ClientPlayerDisconnected([Values] ClientDisconnectType clientDisconnectType) { m_ClientId = m_ClientNetworkManagers[0].LocalClientId; + m_ClientDisconnectType = clientDisconnectType; var serverSideClientPlayer = m_ServerNetworkManager.ConnectionManager.ConnectedClients[m_ClientId].PlayerObject; m_TransportClientId = m_ServerNetworkManager.ConnectionManager.ClientIdToTransportId(m_ClientId); + var clientManager = m_ClientNetworkManagers[0]; + if (clientDisconnectType == ClientDisconnectType.ServerDisconnectsClient) { m_ClientNetworkManagers[0].OnClientDisconnectCallback += OnClientDisconnectCallback; + m_ClientNetworkManagers[0].OnConnectionEvent += OnConnectionEvent; + m_ServerNetworkManager.OnConnectionEvent += OnConnectionEvent; m_ServerNetworkManager.DisconnectClient(m_ClientId); } else { m_ServerNetworkManager.OnClientDisconnectCallback += OnClientDisconnectCallback; + m_ServerNetworkManager.OnConnectionEvent += OnConnectionEvent; + m_ClientNetworkManagers[0].OnConnectionEvent += OnConnectionEvent; yield return StopOneClient(m_ClientNetworkManagers[0]); } @@ -146,6 +167,23 @@ namespace Unity.Netcode.RuntimeTests yield return WaitForConditionOrTimeOut(() => m_ClientDisconnected); AssertOnTimeout("Timed out waiting for client to disconnect!"); + if (clientDisconnectType == ClientDisconnectType.ServerDisconnectsClient) + { + Assert.IsTrue(m_DisconnectedEvent.ContainsKey(m_ServerNetworkManager), $"Could not find the server {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_DisconnectedEvent[m_ServerNetworkManager].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the server {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_DisconnectedEvent.ContainsKey(clientManager), $"Could not find the client {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_DisconnectedEvent[clientManager].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the client {nameof(NetworkManager)} disconnect event entry!"); + // Unregister for this event otherwise it will be invoked during teardown + m_ServerNetworkManager.OnConnectionEvent -= OnConnectionEvent; + } + else + { + Assert.IsTrue(m_DisconnectedEvent.ContainsKey(m_ServerNetworkManager), $"Could not find the server {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_DisconnectedEvent[m_ServerNetworkManager].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the server {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_DisconnectedEvent.ContainsKey(clientManager), $"Could not find the client {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_DisconnectedEvent[clientManager].ClientId == m_ClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the client {nameof(NetworkManager)} disconnect event entry!"); + } + if (m_OwnerPersistence == OwnerPersistence.DestroyWithOwner) { // When we are destroying with the owner, validate the player object is destroyed on the server side @@ -161,6 +199,21 @@ namespace Unity.Netcode.RuntimeTests yield return WaitForConditionOrTimeOut(TransportIdCleanedUp); AssertOnTimeout("Timed out waiting for transport and client id mappings to be cleaned up!"); + + // Validate the host-client generates a OnClientDisconnected event when it shutsdown. + // Only test when the test run is the client disconnecting from the server (otherwise the server will be shutdown already) + if (clientDisconnectType == ClientDisconnectType.ClientDisconnectsFromServer) + { + m_DisconnectedEvent.Clear(); + m_ClientDisconnected = false; + m_ServerNetworkManager.Shutdown(); + + yield return WaitForConditionOrTimeOut(() => m_ClientDisconnected); + AssertOnTimeout("Timed out waiting for host-client to generate disconnect message!"); + + Assert.IsTrue(m_DisconnectedEvent.ContainsKey(m_ServerNetworkManager), $"Could not find the server {nameof(NetworkManager)} disconnect event entry!"); + Assert.IsTrue(m_DisconnectedEvent[m_ServerNetworkManager].ClientId == NetworkManager.ServerClientId, $"Expected ClientID {m_ClientId} but found ClientID {m_DisconnectedEvent[m_ServerNetworkManager].ClientId} for the server {nameof(NetworkManager)} disconnect event entry!"); + } } } } diff --git a/Tests/Runtime/Messaging/NamedMessageTests.cs b/Tests/Runtime/Messaging/NamedMessageTests.cs index 90eeb71..ae243cc 100644 --- a/Tests/Runtime/Messaging/NamedMessageTests.cs +++ b/Tests/Runtime/Messaging/NamedMessageTests.cs @@ -86,6 +86,24 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(m_ServerNetworkManager.LocalClientId, receivedMessageSender); } + private void MockNamedMessageCallback(ulong sender, FastBufferReader reader) + { + + } + + [Test] + public void NullOrEmptyNamedMessageDoesNotThrowException() + { + LogAssert.Expect(UnityEngine.LogType.Error, $"[{nameof(CustomMessagingManager.RegisterNamedMessageHandler)}] Cannot register a named message of type null or empty!"); + m_ServerNetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(string.Empty, MockNamedMessageCallback); + LogAssert.Expect(UnityEngine.LogType.Error, $"[{nameof(CustomMessagingManager.RegisterNamedMessageHandler)}] Cannot register a named message of type null or empty!"); + m_ServerNetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(null, MockNamedMessageCallback); + LogAssert.Expect(UnityEngine.LogType.Error, $"[{nameof(CustomMessagingManager.UnregisterNamedMessageHandler)}] Cannot unregister a named message of type null or empty!"); + m_ServerNetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(string.Empty); + LogAssert.Expect(UnityEngine.LogType.Error, $"[{nameof(CustomMessagingManager.UnregisterNamedMessageHandler)}] Cannot unregister a named message of type null or empty!"); + m_ServerNetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(null); + } + [UnityTest] public IEnumerator NamedMessageIsReceivedOnMultipleClientsWithContent() { diff --git a/Tests/Runtime/NetworkManagerEventsTests.cs b/Tests/Runtime/NetworkManagerEventsTests.cs index 3ae6828..4cebe20 100644 --- a/Tests/Runtime/NetworkManagerEventsTests.cs +++ b/Tests/Runtime/NetworkManagerEventsTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; using UnityEngine.TestTools; +using Object = UnityEngine.Object; namespace Unity.Netcode.RuntimeTests { @@ -35,7 +36,7 @@ namespace Unity.Netcode.RuntimeTests m_ServerManager.OnServerStopped += onServerStopped; m_ServerManager.Shutdown(); - UnityEngine.Object.DestroyImmediate(gameObject); + Object.DestroyImmediate(gameObject); yield return WaitUntilManagerShutsdown(); @@ -92,7 +93,7 @@ namespace Unity.Netcode.RuntimeTests m_ServerManager.OnServerStopped += onServerStopped; m_ServerManager.OnClientStopped += onClientStopped; m_ServerManager.Shutdown(); - UnityEngine.Object.DestroyImmediate(gameObject); + Object.DestroyImmediate(gameObject); yield return WaitUntilManagerShutsdown(); @@ -228,6 +229,18 @@ namespace Unity.Netcode.RuntimeTests public virtual IEnumerator Teardown() { NetcodeIntegrationTestHelpers.Destroy(); + if (m_ServerManager != null) + { + m_ServerManager.ShutdownInternal(); + Object.DestroyImmediate(m_ServerManager); + m_ServerManager = null; + } + if (m_ClientManager != null) + { + m_ClientManager.ShutdownInternal(); + Object.DestroyImmediate(m_ClientManager); + m_ClientManager = null; + } yield return null; } } diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs index 739fa4f..d382cfb 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs @@ -1,10 +1,8 @@ // TODO: Rewrite test to use the tools package. Debug simulator not available in UTP 2.X. #if !UTP_TRANSPORT_2_0_ABOVE -using System.Collections; using NUnit.Framework; using Unity.Netcode.Components; using UnityEngine; -using UnityEngine.TestTools; namespace Unity.Netcode.RuntimeTests { @@ -38,32 +36,38 @@ namespace Unity.Netcode.RuntimeTests base(testWithHost, authority, rotationCompression, rotation, precision) { } - protected override void OnServerAndClientsCreated() - { - base.OnServerAndClientsCreated(); + protected override bool m_EnableTimeTravel => true; + protected override bool m_SetupIsACoroutine => true; + protected override bool m_TearDownIsACoroutine => true; - var unityTransport = m_ServerNetworkManager.NetworkConfig.NetworkTransport as Transports.UTP.UnityTransport; - unityTransport.SetDebugSimulatorParameters(k_Latency, 0, k_PacketLoss); + protected override void OnTimeTravelServerAndClientsConnected() + { + base.OnTimeTravelServerAndClientsConnected(); + + SetTimeTravelSimulatedLatency(k_Latency * 0.001f); + SetTimeTravelSimulatedDropRate(k_PacketLoss * 0.01f); } /// /// Handles validating all children of the test objects have matching local and global space vaues. /// - private IEnumerator AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTransformCheckType checkType) + private void AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTransformCheckType checkType) { // We don't assert on timeout here because we want to log this information during PostAllChildrenLocalTransformValuesMatch - yield return WaitForConditionOrTimeOut(() => AllInstancesKeptLocalTransformValues(useSubChild)); + WaitForConditionOrTimeOutWithTimeTravel(() => AllInstancesKeptLocalTransformValues(useSubChild)); var success = true; - m_InfoMessage.AppendLine($"[{checkType}][{useSubChild}] Timed out waiting for all children to have the correct local space values:\n"); if (s_GlobalTimeoutHelper.TimedOut) { - var waitForMs = new WaitForSeconds(0.001f); + //var waitForMs = new WaitForSeconds(0.001f); // If we timed out, then wait for a full range of ticks to assure all data has been synchronized before declaring this a failed test. for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate; j++) { + m_InfoMessage.Clear(); + m_InfoMessage.AppendLine($"[{checkType}][{useSubChild}] Timed out waiting for all children to have the correct local space values:\n"); var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances; success = PostAllChildrenLocalTransformValuesMatch(useSubChild); - yield return waitForMs; + TimeTravel(0.001f); + //yield return waitForMs; } } @@ -78,8 +82,8 @@ namespace Unity.Netcode.RuntimeTests /// parented under another NetworkTransform under all of the possible axial conditions /// as well as when the parent has a varying scale. /// - [UnityTest] - public IEnumerator ParentedNetworkTransformTest([Values] Interpolation interpolation, [Values] bool worldPositionStays, [Values(0.5f, 1.0f, 5.0f)] float scale) + [Test] + public void ParentedNetworkTransformTest([Values] Interpolation interpolation, [Values] bool worldPositionStays, [Values(0.5f, 1.0f, 5.0f)] float scale) { ChildObjectComponent.EnableChildLog = m_EnableVerboseDebug; if (m_EnableVerboseDebug) @@ -101,7 +105,7 @@ namespace Unity.Netcode.RuntimeTests var serverSideSubChild = SpawnObject(m_SubChildObject.gameObject, authorityNetworkManager).GetComponent(); // Assure all of the child object instances are spawned before proceeding to parenting - yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned); + WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned); AssertOnTimeout("Timed out waiting for all child instances to be spawned!"); // Get the authority parent and child instances @@ -139,7 +143,7 @@ namespace Unity.Netcode.RuntimeTests // Allow one tick for authority to update these changes - yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches); + WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); AssertOnTimeout("All transform values did not match prior to parenting!"); @@ -150,37 +154,37 @@ namespace Unity.Netcode.RuntimeTests Assert.True(serverSideSubChild.TrySetParent(serverSideChild.transform, worldPositionStays), "[Server-Side SubChild] Failed to set sub-child's parent!"); // This waits for all child instances to be parented - yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild); + WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild); AssertOnTimeout("Timed out waiting for all instances to have parented a child!"); - var latencyWait = new WaitForSeconds(k_Latency * 0.003f); + var latencyWait = k_Latency * 0.003f; // Wait for at least 3x designated latency period - yield return latencyWait; + TimeTravel(latencyWait); // This validates each child instance has preserved their local space values - yield return AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients); + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients); // This validates each sub-child instance has preserved their local space values - yield return AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients); + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients); // Verify that a late joining client will synchronize to the parented NetworkObjects properly - yield return CreateAndStartNewClient(); + CreateAndStartNewClientWithTimeTravel(); // Assure all of the child object instances are spawned (basically for the newly connected client) - yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned); + WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesAreSpawned); AssertOnTimeout("Timed out waiting for all child instances to be spawned!"); // This waits for all child instances to be parented - yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild); + WaitForConditionOrTimeOutWithTimeTravel(AllChildObjectInstancesHaveChild); AssertOnTimeout("Timed out waiting for all instances to have parented a child!"); // Wait for at least 3x designated latency period - yield return latencyWait; + TimeTravel(latencyWait); // This validates each child instance has preserved their local space values - yield return AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client); + AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client); // This validates each sub-child instance has preserved their local space values - yield return AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client); + AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client); } /// @@ -192,10 +196,10 @@ namespace Unity.Netcode.RuntimeTests /// When testing 3 axis: Interpolation is enabled, sometimes an axis is intentionally excluded during a /// delta update, and it runs through 8 delta updates per unique test. /// - [UnityTest] - public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] Axis axis) + [Test] + public void NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] Axis axis) { - yield return s_DefaultWaitForTick; + TimeTravelAdvanceTick(); // Just test for OverrideState.Update (they are already being tested for functionality in normal NetworkTransformTests) var overideState = OverrideState.Update; var tickRelativeTime = new WaitForSeconds(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate); @@ -255,7 +259,7 @@ namespace Unity.Netcode.RuntimeTests // Wait for the deltas to be pushed - yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed); + WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed); // Just in case we drop the first few state updates if (s_GlobalTimeoutHelper.TimedOut) @@ -266,17 +270,17 @@ namespace Unity.Netcode.RuntimeTests state.InLocalSpace = !m_AuthoritativeTransform.InLocalSpace; m_AuthoritativeTransform.LocalAuthoritativeNetworkState = state; // Wait for the deltas to be pushed - yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed); + WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed); } AssertOnTimeout("State was never pushed!"); // Allow the precision settings to propagate first as changing precision // causes a teleport event to occur - yield return s_DefaultWaitForTick; - yield return s_DefaultWaitForTick; - yield return s_DefaultWaitForTick; - yield return s_DefaultWaitForTick; - yield return s_DefaultWaitForTick; + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); + TimeTravelAdvanceTick(); var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations; // Move and rotate within the same tick, validate the non-authoritative instance updates @@ -311,7 +315,7 @@ namespace Unity.Netcode.RuntimeTests MoveRotateAndScaleAuthority(position, rotation, scale, overideState); // Wait for the deltas to be pushed (unlike the original test, we don't wait for state to be updated as that could be dropped here) - yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed); + WaitForConditionOrTimeOutWithTimeTravel(() => m_AuthoritativeTransform.StatePushed); AssertOnTimeout($"[Non-Interpolate {i}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!"); // For 3 axis, we will skip validating that the non-authority interpolates to its target point at least once. @@ -321,7 +325,7 @@ namespace Unity.Netcode.RuntimeTests if (m_AxisExcluded || axisCount < 3) { // Wait for deltas to synchronize on non-authoritative side - yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches); + WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); // Provide additional debug info about what failed (if it fails) if (s_GlobalTimeoutHelper.TimedOut) { @@ -335,7 +339,7 @@ namespace Unity.Netcode.RuntimeTests // If we matched, then something was dropped and recovered when synchronized break; } - yield return s_DefaultWaitForTick; + TimeTravelAdvanceTick(); } // Only if we still didn't match @@ -354,7 +358,7 @@ namespace Unity.Netcode.RuntimeTests if (axisCount == 3) { // As a final test, wait for deltas to synchronize on non-authoritative side to assure it interpolates to the correct values - yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches); + WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches); // Provide additional debug info about what failed (if it fails) if (s_GlobalTimeoutHelper.TimedOut) { @@ -368,7 +372,7 @@ namespace Unity.Netcode.RuntimeTests // If we matched, then something was dropped and recovered when synchronized break; } - yield return s_DefaultWaitForTick; + TimeTravelAdvanceTick(); } // Only if we still didn't match @@ -392,8 +396,8 @@ namespace Unity.Netcode.RuntimeTests /// - While in local space and world space /// - While interpolation is enabled and disabled /// - [UnityTest] - public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation) + [Test] + public void TestAuthoritativeTransformChangeOneAtATime([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation) { // Just test for OverrideState.Update (they are already being tested for functionality in normal NetworkTransformTests) m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; @@ -411,7 +415,7 @@ namespace Unity.Netcode.RuntimeTests m_AuthoritativeTransform.transform.position = GetRandomVector3(2f, 30f); - yield return WaitForConditionOrTimeOut(() => PositionsMatch()); + WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatch()); AssertOnTimeout($"Timed out waiting for positions to match {m_AuthoritativeTransform.transform.position} | {m_NonAuthoritativeTransform.transform.position}"); // test rotation @@ -420,19 +424,19 @@ namespace Unity.Netcode.RuntimeTests m_AuthoritativeTransform.transform.rotation = Quaternion.Euler(GetRandomVector3(5, 60)); // using euler angles instead of quaternions directly to really see issues users might encounter // Make sure the values match - yield return WaitForConditionOrTimeOut(() => RotationsMatch()); + WaitForConditionOrTimeOutWithTimeTravel(() => RotationsMatch()); AssertOnTimeout($"Timed out waiting for rotations to match"); m_AuthoritativeTransform.StatePushed = false; m_AuthoritativeTransform.transform.localScale = GetRandomVector3(1, 6); // Make sure the scale values match - yield return WaitForConditionOrTimeOut(() => ScaleValuesMatch()); + WaitForConditionOrTimeOutWithTimeTravel(() => ScaleValuesMatch()); AssertOnTimeout($"Timed out waiting for scale values to match"); } - [UnityTest] - public IEnumerator TestSameFrameDeltaStateAndTeleport([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation) + [Test] + public void TestSameFrameDeltaStateAndTeleport([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation) { m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate; @@ -449,10 +453,10 @@ namespace Unity.Netcode.RuntimeTests m_RandomPosition = GetRandomVector3(2f, 30f); m_AuthoritativeTransform.transform.position = m_RandomPosition; m_Teleported = false; - yield return WaitForConditionOrTimeOut(() => m_Teleported); + WaitForConditionOrTimeOutWithTimeTravel(() => m_Teleported); AssertOnTimeout($"Timed out waiting for random position to be pushed!"); - yield return WaitForConditionOrTimeOut(() => PositionsMatch()); + WaitForConditionOrTimeOutWithTimeTravel(() => PositionsMatch()); AssertOnTimeout($"Timed out waiting for positions to match {m_AuthoritativeTransform.transform.position} | {m_NonAuthoritativeTransform.transform.position}"); var authPosition = m_AuthoritativeTransform.GetSpaceRelativePosition(); diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs index 0b7be8a..bf72bba 100644 --- a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs +++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -1,3 +1,4 @@ +using System.Collections; using NUnit.Framework; using UnityEngine; @@ -41,6 +42,24 @@ namespace Unity.Netcode.RuntimeTests return k_TickRate; } + private bool m_UseParentingThreshold; + private const float k_ParentingThreshold = 0.25f; + + protected override float GetDeltaVarianceThreshold() + { + if (m_UseParentingThreshold) + { + return k_ParentingThreshold; + } + return base.GetDeltaVarianceThreshold(); + } + + protected override IEnumerator OnSetup() + { + m_UseParentingThreshold = false; + return base.OnSetup(); + } + /// /// Handles validating the local space values match the original local space values. /// If not, it generates a message containing the axial values that did not match @@ -77,6 +96,7 @@ namespace Unity.Netcode.RuntimeTests [Test] public void ParentedNetworkTransformTest([Values] Interpolation interpolation, [Values] bool worldPositionStays, [Values(0.5f, 1.0f, 5.0f)] float scale) { + m_UseParentingThreshold = true; // Get the NetworkManager that will have authority in order to spawn with the correct authority var isServerAuthority = m_Authority == Authority.ServerAuthority; var authorityNetworkManager = m_ServerNetworkManager; diff --git a/Tests/Runtime/NetworkTransformAnticipationTests.cs b/Tests/Runtime/NetworkTransformAnticipationTests.cs new file mode 100644 index 0000000..d554cfc --- /dev/null +++ b/Tests/Runtime/NetworkTransformAnticipationTests.cs @@ -0,0 +1,521 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Netcode.Components; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkTransformAnticipationComponent : NetworkBehaviour + { + [Rpc(SendTo.Server)] + public void MoveRpc(Vector3 newPosition) + { + transform.position = newPosition; + } + + [Rpc(SendTo.Server)] + public void ScaleRpc(Vector3 newScale) + { + transform.localScale = newScale; + } + + [Rpc(SendTo.Server)] + public void RotateRpc(Quaternion newRotation) + { + transform.rotation = newRotation; + } + + public bool ShouldSmooth = false; + public bool ShouldMove = false; + + public override void OnReanticipate(double lastRoundTripTime) + { + var transform_ = GetComponent(); + if (transform_.ShouldReanticipate) + { + if (ShouldSmooth) + { + transform_.Smooth(transform_.PreviousAnticipatedState, transform_.AuthoritativeState, 1); + } + + if (ShouldMove) + { + transform_.AnticipateMove(transform_.AuthoritativeState.Position + new Vector3(0, 5, 0)); + + } + } + } + } + + public class NetworkTransformAnticipationTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + protected override bool m_EnableTimeTravel => true; + protected override bool m_SetupIsACoroutine => false; + protected override bool m_TearDownIsACoroutine => false; + + protected override void OnPlayerPrefabGameObjectCreated() + { + m_PlayerPrefab.AddComponent(); + m_PlayerPrefab.AddComponent(); + } + + protected override void OnTimeTravelServerAndClientsConnected() + { + var serverComponent = GetServerComponent(); + var testComponent = GetTestComponent(); + var otherClientComponent = GetOtherClientComponent(); + + serverComponent.transform.position = Vector3.zero; + serverComponent.transform.localScale = Vector3.one; + serverComponent.transform.rotation = Quaternion.LookRotation(Vector3.forward); + testComponent.transform.position = Vector3.zero; + testComponent.transform.localScale = Vector3.one; + testComponent.transform.rotation = Quaternion.LookRotation(Vector3.forward); + otherClientComponent.transform.position = Vector3.zero; + otherClientComponent.transform.localScale = Vector3.one; + otherClientComponent.transform.rotation = Quaternion.LookRotation(Vector3.forward); + } + + public AnticipatedNetworkTransform GetTestComponent() + { + return m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); + } + + public AnticipatedNetworkTransform GetServerComponent() + { + foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + if (obj.NetworkManager == m_ServerNetworkManager && obj.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId) + { + return obj; + } + } + + return null; + } + + public AnticipatedNetworkTransform GetOtherClientComponent() + { + foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + if (obj.NetworkManager == m_ClientNetworkManagers[1] && obj.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId) + { + return obj; + } + } + + return null; + } + + [Test] + public void WhenAnticipating_ValueChangesImmediately() + { + var testComponent = GetTestComponent(); + + testComponent.AnticipateMove(new Vector3(0, 1, 2)); + testComponent.AnticipateScale(new Vector3(1, 2, 3)); + testComponent.AnticipateRotate(Quaternion.LookRotation(new Vector3(2, 3, 4))); + + Assert.AreEqual(new Vector3(0, 1, 2), testComponent.transform.position); + Assert.AreEqual(new Vector3(1, 2, 3), testComponent.transform.localScale); + Assert.AreEqual(Quaternion.LookRotation(new Vector3(2, 3, 4)), testComponent.transform.rotation); + + Assert.AreEqual(new Vector3(0, 1, 2), testComponent.AnticipatedState.Position); + Assert.AreEqual(new Vector3(1, 2, 3), testComponent.AnticipatedState.Scale); + Assert.AreEqual(Quaternion.LookRotation(new Vector3(2, 3, 4)), testComponent.AnticipatedState.Rotation); + + } + + [Test] + public void WhenAnticipating_AuthoritativeValueDoesNotChange() + { + var testComponent = GetTestComponent(); + + var startPosition = testComponent.transform.position; + var startScale = testComponent.transform.localScale; + var startRotation = testComponent.transform.rotation; + + testComponent.AnticipateMove(new Vector3(0, 1, 2)); + testComponent.AnticipateScale(new Vector3(1, 2, 3)); + testComponent.AnticipateRotate(Quaternion.LookRotation(new Vector3(2, 3, 4))); + + Assert.AreEqual(startPosition, testComponent.AuthoritativeState.Position); + Assert.AreEqual(startScale, testComponent.AuthoritativeState.Scale); + Assert.AreEqual(startRotation, testComponent.AuthoritativeState.Rotation); + } + + [Test] + public void WhenAnticipating_ServerDoesNotChange() + { + var testComponent = GetTestComponent(); + + var startPosition = testComponent.transform.position; + var startScale = testComponent.transform.localScale; + var startRotation = testComponent.transform.rotation; + + testComponent.AnticipateMove(new Vector3(0, 1, 2)); + testComponent.AnticipateScale(new Vector3(1, 2, 3)); + testComponent.AnticipateRotate(Quaternion.LookRotation(new Vector3(2, 3, 4))); + + var serverComponent = GetServerComponent(); + + Assert.AreEqual(startPosition, serverComponent.AuthoritativeState.Position); + Assert.AreEqual(startScale, serverComponent.AuthoritativeState.Scale); + Assert.AreEqual(startRotation, serverComponent.AuthoritativeState.Rotation); + Assert.AreEqual(startPosition, serverComponent.AnticipatedState.Position); + Assert.AreEqual(startScale, serverComponent.AnticipatedState.Scale); + Assert.AreEqual(startRotation, serverComponent.AnticipatedState.Rotation); + + TimeTravel(2, 120); + + Assert.AreEqual(startPosition, serverComponent.AuthoritativeState.Position); + Assert.AreEqual(startScale, serverComponent.AuthoritativeState.Scale); + Assert.AreEqual(startRotation, serverComponent.AuthoritativeState.Rotation); + Assert.AreEqual(startPosition, serverComponent.AnticipatedState.Position); + Assert.AreEqual(startScale, serverComponent.AnticipatedState.Scale); + Assert.AreEqual(startRotation, serverComponent.AnticipatedState.Rotation); + } + + [Test] + public void WhenAnticipating_OtherClientDoesNotChange() + { + var testComponent = GetTestComponent(); + + var startPosition = testComponent.transform.position; + var startScale = testComponent.transform.localScale; + var startRotation = testComponent.transform.rotation; + + testComponent.AnticipateMove(new Vector3(0, 1, 2)); + testComponent.AnticipateScale(new Vector3(1, 2, 3)); + testComponent.AnticipateRotate(Quaternion.LookRotation(new Vector3(2, 3, 4))); + + var otherClientComponent = GetOtherClientComponent(); + + Assert.AreEqual(startPosition, otherClientComponent.AuthoritativeState.Position); + Assert.AreEqual(startScale, otherClientComponent.AuthoritativeState.Scale); + Assert.AreEqual(startRotation, otherClientComponent.AuthoritativeState.Rotation); + Assert.AreEqual(startPosition, otherClientComponent.AnticipatedState.Position); + Assert.AreEqual(startScale, otherClientComponent.AnticipatedState.Scale); + Assert.AreEqual(startRotation, otherClientComponent.AnticipatedState.Rotation); + + TimeTravel(2, 120); + + Assert.AreEqual(startPosition, otherClientComponent.AuthoritativeState.Position); + Assert.AreEqual(startScale, otherClientComponent.AuthoritativeState.Scale); + Assert.AreEqual(startRotation, otherClientComponent.AuthoritativeState.Rotation); + Assert.AreEqual(startPosition, otherClientComponent.AnticipatedState.Position); + Assert.AreEqual(startScale, otherClientComponent.AnticipatedState.Scale); + Assert.AreEqual(startRotation, otherClientComponent.AnticipatedState.Rotation); + } + + [Test] + public void WhenServerChangesSnapValue_ValuesAreUpdated() + { + var testComponent = GetTestComponent(); + var serverComponent = GetServerComponent(); + serverComponent.Interpolate = false; + + testComponent.AnticipateMove(new Vector3(0, 1, 2)); + testComponent.AnticipateScale(new Vector3(1, 2, 3)); + testComponent.AnticipateRotate(Quaternion.LookRotation(new Vector3(2, 3, 4))); + + var rpcComponent = testComponent.GetComponent(); + rpcComponent.MoveRpc(new Vector3(2, 3, 4)); + + WaitForMessageReceivedWithTimeTravel(new List { m_ServerNetworkManager }); + var otherClientComponent = GetOtherClientComponent(); + + WaitForConditionOrTimeOutWithTimeTravel(() => testComponent.AuthoritativeState.Position == serverComponent.transform.position && otherClientComponent.AuthoritativeState.Position == serverComponent.transform.position); + + Assert.AreEqual(serverComponent.transform.position, testComponent.transform.position); + Assert.AreEqual(serverComponent.transform.position, testComponent.AnticipatedState.Position); + Assert.AreEqual(serverComponent.transform.position, testComponent.AuthoritativeState.Position); + + Assert.AreEqual(serverComponent.transform.position, otherClientComponent.transform.position); + Assert.AreEqual(serverComponent.transform.position, otherClientComponent.AnticipatedState.Position); + Assert.AreEqual(serverComponent.transform.position, otherClientComponent.AuthoritativeState.Position); + } + + public void AssertQuaternionsAreEquivalent(Quaternion a, Quaternion b) + { + var aAngles = a.eulerAngles; + var bAngles = b.eulerAngles; + Assert.AreEqual(aAngles.x, bAngles.x, 0.001, $"Quaternions were not equal. Expected: {a}, but was {b}"); + Assert.AreEqual(aAngles.y, bAngles.y, 0.001, $"Quaternions were not equal. Expected: {a}, but was {b}"); + Assert.AreEqual(aAngles.z, bAngles.z, 0.001, $"Quaternions were not equal. Expected: {a}, but was {b}"); + } + public void AssertVectorsAreEquivalent(Vector3 a, Vector3 b) + { + Assert.AreEqual(a.x, b.x, 0.001, $"Vectors were not equal. Expected: {a}, but was {b}"); + Assert.AreEqual(a.y, b.y, 0.001, $"Vectors were not equal. Expected: {a}, but was {b}"); + Assert.AreEqual(a.z, b.z, 0.001, $"Vectors were not equal. Expected: {a}, but was {b}"); + } + + [Test] + public void WhenServerChangesSmoothValue_ValuesAreLerped() + { + var testComponent = GetTestComponent(); + var otherClientComponent = GetOtherClientComponent(); + + testComponent.StaleDataHandling = StaleDataHandling.Ignore; + otherClientComponent.StaleDataHandling = StaleDataHandling.Ignore; + + var serverComponent = GetServerComponent(); + serverComponent.Interpolate = false; + + testComponent.GetComponent().ShouldSmooth = true; + otherClientComponent.GetComponent().ShouldSmooth = true; + + var startPosition = testComponent.transform.position; + var startScale = testComponent.transform.localScale; + var startRotation = testComponent.transform.rotation; + var anticipePosition = new Vector3(0, 1, 2); + var anticipeScale = new Vector3(1, 2, 3); + var anticipeRotation = Quaternion.LookRotation(new Vector3(2, 3, 4)); + var serverSetPosition = new Vector3(3, 4, 5); + var serverSetScale = new Vector3(4, 5, 6); + var serverSetRotation = Quaternion.LookRotation(new Vector3(5, 6, 7)); + + testComponent.AnticipateMove(anticipePosition); + testComponent.AnticipateScale(anticipeScale); + testComponent.AnticipateRotate(anticipeRotation); + + var rpcComponent = testComponent.GetComponent(); + rpcComponent.MoveRpc(serverSetPosition); + rpcComponent.RotateRpc(serverSetRotation); + rpcComponent.ScaleRpc(serverSetScale); + + WaitForMessagesReceivedWithTimeTravel(new List + { + typeof(RpcMessage), + typeof(RpcMessage), + typeof(RpcMessage), + }, new List { m_ServerNetworkManager }); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + var percentChanged = 1f / 60f; + + AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.transform.position); + AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.transform.localScale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.transform.rotation); + + AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.AnticipatedState.Position); + AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.AnticipatedState.Scale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.AnticipatedState.Rotation); + + AssertVectorsAreEquivalent(serverSetPosition, testComponent.AuthoritativeState.Position); + AssertVectorsAreEquivalent(serverSetScale, testComponent.AuthoritativeState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, testComponent.AuthoritativeState.Rotation); + + AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.transform.position); + AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.transform.localScale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.transform.rotation); + + AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.AnticipatedState.Position); + AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.AnticipatedState.Scale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.AnticipatedState.Rotation); + + AssertVectorsAreEquivalent(serverSetPosition, otherClientComponent.AuthoritativeState.Position); + AssertVectorsAreEquivalent(serverSetScale, otherClientComponent.AuthoritativeState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, otherClientComponent.AuthoritativeState.Rotation); + + for (var i = 1; i < 60; ++i) + { + TimeTravel(1f / 60f, 1); + percentChanged = 1f / 60f * (i + 1); + + AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.transform.position); + AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.transform.localScale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.transform.rotation); + + AssertVectorsAreEquivalent(Vector3.Lerp(anticipePosition, serverSetPosition, percentChanged), testComponent.AnticipatedState.Position); + AssertVectorsAreEquivalent(Vector3.Lerp(anticipeScale, serverSetScale, percentChanged), testComponent.AnticipatedState.Scale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(anticipeRotation, serverSetRotation, percentChanged), testComponent.AnticipatedState.Rotation); + + AssertVectorsAreEquivalent(serverSetPosition, testComponent.AuthoritativeState.Position); + AssertVectorsAreEquivalent(serverSetScale, testComponent.AuthoritativeState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, testComponent.AuthoritativeState.Rotation); + + AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.transform.position); + AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.transform.localScale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.transform.rotation); + + AssertVectorsAreEquivalent(Vector3.Lerp(startPosition, serverSetPosition, percentChanged), otherClientComponent.AnticipatedState.Position); + AssertVectorsAreEquivalent(Vector3.Lerp(startScale, serverSetScale, percentChanged), otherClientComponent.AnticipatedState.Scale); + AssertQuaternionsAreEquivalent(Quaternion.Slerp(startRotation, serverSetRotation, percentChanged), otherClientComponent.AnticipatedState.Rotation); + + AssertVectorsAreEquivalent(serverSetPosition, otherClientComponent.AuthoritativeState.Position); + AssertVectorsAreEquivalent(serverSetScale, otherClientComponent.AuthoritativeState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, otherClientComponent.AuthoritativeState.Rotation); + } + TimeTravel(1f / 60f, 1); + + AssertVectorsAreEquivalent(serverSetPosition, testComponent.transform.position); + AssertVectorsAreEquivalent(serverSetScale, testComponent.transform.localScale); + AssertQuaternionsAreEquivalent(serverSetRotation, testComponent.transform.rotation); + + AssertVectorsAreEquivalent(serverSetPosition, testComponent.AnticipatedState.Position); + AssertVectorsAreEquivalent(serverSetScale, testComponent.AnticipatedState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, testComponent.AnticipatedState.Rotation); + + AssertVectorsAreEquivalent(serverSetPosition, testComponent.AuthoritativeState.Position); + AssertVectorsAreEquivalent(serverSetScale, testComponent.AuthoritativeState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, testComponent.AuthoritativeState.Rotation); + + AssertVectorsAreEquivalent(serverSetPosition, otherClientComponent.transform.position); + AssertVectorsAreEquivalent(serverSetScale, otherClientComponent.transform.localScale); + AssertQuaternionsAreEquivalent(serverSetRotation, otherClientComponent.transform.rotation); + + AssertVectorsAreEquivalent(serverSetPosition, otherClientComponent.AnticipatedState.Position); + AssertVectorsAreEquivalent(serverSetScale, otherClientComponent.AnticipatedState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, otherClientComponent.AnticipatedState.Rotation); + + AssertVectorsAreEquivalent(serverSetPosition, otherClientComponent.AuthoritativeState.Position); + AssertVectorsAreEquivalent(serverSetScale, otherClientComponent.AuthoritativeState.Scale); + AssertQuaternionsAreEquivalent(serverSetRotation, otherClientComponent.AuthoritativeState.Rotation); + } + + [Test] + public void WhenServerChangesReanticipeValue_ValuesAreReanticiped() + { + var testComponent = GetTestComponent(); + var otherClientComponent = GetOtherClientComponent(); + + testComponent.GetComponent().ShouldMove = true; + otherClientComponent.GetComponent().ShouldMove = true; + + var serverComponent = GetServerComponent(); + serverComponent.Interpolate = false; + serverComponent.transform.position = new Vector3(0, 1, 2); + var rpcComponent = testComponent.GetComponent(); + rpcComponent.MoveRpc(new Vector3(0, 1, 2)); + + WaitForMessageReceivedWithTimeTravel(new List { m_ServerNetworkManager }); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + Assert.AreEqual(new Vector3(0, 6, 2), testComponent.transform.position); + Assert.AreEqual(new Vector3(0, 6, 2), testComponent.AnticipatedState.Position); + Assert.AreEqual(new Vector3(0, 1, 2), testComponent.AuthoritativeState.Position); + + Assert.AreEqual(new Vector3(0, 6, 2), otherClientComponent.transform.position); + Assert.AreEqual(new Vector3(0, 6, 2), otherClientComponent.AnticipatedState.Position); + Assert.AreEqual(new Vector3(0, 1, 2), otherClientComponent.AuthoritativeState.Position); + } + + [Test] + public void WhenStaleDataArrivesToIgnoreVariable_ItIsIgnored([Values(10u, 30u, 60u)] uint tickRate, [Values(0u, 1u, 2u)] uint skipFrames) + { + m_ServerNetworkManager.NetworkConfig.TickRate = tickRate; + m_ServerNetworkManager.NetworkTickSystem.TickRate = tickRate; + + for (var i = 0; i < skipFrames; ++i) + { + TimeTravel(1 / 60f, 1); + } + + var serverComponent = GetServerComponent(); + serverComponent.Interpolate = false; + + var testComponent = GetTestComponent(); + testComponent.StaleDataHandling = StaleDataHandling.Ignore; + testComponent.Interpolate = false; + + var otherClientComponent = GetOtherClientComponent(); + otherClientComponent.StaleDataHandling = StaleDataHandling.Ignore; + otherClientComponent.Interpolate = false; + + var rpcComponent = testComponent.GetComponent(); + rpcComponent.MoveRpc(new Vector3(1, 2, 3)); + + WaitForMessageReceivedWithTimeTravel(new List { m_ServerNetworkManager }); + + testComponent.AnticipateMove(new Vector3(0, 5, 0)); + rpcComponent.MoveRpc(new Vector3(4, 5, 6)); + + // Depending on tick rate, one of these two things will happen. + // The assertions are different based on this... either the tick rate is slow enough that the second RPC is received + // before the next update and we move to 4, 5, 6, or the tick rate is fast enough that the next update is sent out + // before the RPC is received and we get the update for the move to 1, 2, 3. Both are valid, what we want to assert + // here is that the anticipated state never becomes 1, 2, 3. + WaitForConditionOrTimeOutWithTimeTravel(() => testComponent.AuthoritativeState.Position == new Vector3(1, 2, 3) || testComponent.AuthoritativeState.Position == new Vector3(4, 5, 6)); + + if (testComponent.AnticipatedState.Position == new Vector3(4, 5, 6)) + { + // Anticiped client received this data for a time earlier than its anticipation, and should have prioritized the anticiped value + Assert.AreEqual(new Vector3(4, 5, 6), testComponent.transform.position); + Assert.AreEqual(new Vector3(4, 5, 6), testComponent.AnticipatedState.Position); + // However, the authoritative value still gets updated + Assert.AreEqual(new Vector3(4, 5, 6), testComponent.AuthoritativeState.Position); + + // Other client got the server value and had made no anticipation, so it applies it to the anticiped value as well. + Assert.AreEqual(new Vector3(4, 5, 6), otherClientComponent.transform.position); + Assert.AreEqual(new Vector3(4, 5, 6), otherClientComponent.AnticipatedState.Position); + Assert.AreEqual(new Vector3(4, 5, 6), otherClientComponent.AuthoritativeState.Position); + } + else + { + // Anticiped client received this data for a time earlier than its anticipation, and should have prioritized the anticiped value + Assert.AreEqual(new Vector3(0, 5, 0), testComponent.transform.position); + Assert.AreEqual(new Vector3(0, 5, 0), testComponent.AnticipatedState.Position); + // However, the authoritative value still gets updated + Assert.AreEqual(new Vector3(1, 2, 3), testComponent.AuthoritativeState.Position); + + // Other client got the server value and had made no anticipation, so it applies it to the anticiped value as well. + Assert.AreEqual(new Vector3(1, 2, 3), otherClientComponent.transform.position); + Assert.AreEqual(new Vector3(1, 2, 3), otherClientComponent.AnticipatedState.Position); + Assert.AreEqual(new Vector3(1, 2, 3), otherClientComponent.AuthoritativeState.Position); + } + } + + + [Test] + public void WhenNonStaleDataArrivesToIgnoreVariable_ItIsNotIgnored([Values(10u, 30u, 60u)] uint tickRate, [Values(0u, 1u, 2u)] uint skipFrames) + { + m_ServerNetworkManager.NetworkConfig.TickRate = tickRate; + m_ServerNetworkManager.NetworkTickSystem.TickRate = tickRate; + + for (var i = 0; i < skipFrames; ++i) + { + TimeTravel(1 / 60f, 1); + } + + var serverComponent = GetServerComponent(); + serverComponent.Interpolate = false; + + var testComponent = GetTestComponent(); + testComponent.StaleDataHandling = StaleDataHandling.Ignore; + testComponent.Interpolate = false; + + var otherClientComponent = GetOtherClientComponent(); + otherClientComponent.StaleDataHandling = StaleDataHandling.Ignore; + otherClientComponent.Interpolate = false; + + testComponent.AnticipateMove(new Vector3(0, 5, 0)); + var rpcComponent = testComponent.GetComponent(); + rpcComponent.MoveRpc(new Vector3(1, 2, 3)); + + WaitForMessageReceivedWithTimeTravel(new List { m_ServerNetworkManager }); + + WaitForConditionOrTimeOutWithTimeTravel(() => testComponent.AuthoritativeState.Position == serverComponent.transform.position && otherClientComponent.AuthoritativeState.Position == serverComponent.transform.position); + + // Anticiped client received this data for a time earlier than its anticipation, and should have prioritized the anticiped value + Assert.AreEqual(new Vector3(1, 2, 3), testComponent.transform.position); + Assert.AreEqual(new Vector3(1, 2, 3), testComponent.AnticipatedState.Position); + // However, the authoritative value still gets updated + Assert.AreEqual(new Vector3(1, 2, 3), testComponent.AuthoritativeState.Position); + + // Other client got the server value and had made no anticipation, so it applies it to the anticiped value as well. + Assert.AreEqual(new Vector3(1, 2, 3), otherClientComponent.transform.position); + Assert.AreEqual(new Vector3(1, 2, 3), otherClientComponent.AnticipatedState.Position); + Assert.AreEqual(new Vector3(1, 2, 3), otherClientComponent.AuthoritativeState.Position); + } + } +} diff --git a/Tests/Runtime/NetworkTransformAnticipationTests.cs.meta b/Tests/Runtime/NetworkTransformAnticipationTests.cs.meta new file mode 100644 index 0000000..717612e --- /dev/null +++ b/Tests/Runtime/NetworkTransformAnticipationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4f149de86bec4f6eb1a4d62a1b52938a +timeCreated: 1706630210 \ No newline at end of file diff --git a/Tests/Runtime/NetworkVariableAnticipationTests.cs b/Tests/Runtime/NetworkVariableAnticipationTests.cs new file mode 100644 index 0000000..e852fba --- /dev/null +++ b/Tests/Runtime/NetworkVariableAnticipationTests.cs @@ -0,0 +1,420 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkVariableAnticipationComponent : NetworkBehaviour + { + public AnticipatedNetworkVariable SnapOnAnticipationFailVariable = new AnticipatedNetworkVariable(0, StaleDataHandling.Ignore); + public AnticipatedNetworkVariable SmoothOnAnticipationFailVariable = new AnticipatedNetworkVariable(0, StaleDataHandling.Reanticipate); + public AnticipatedNetworkVariable ReanticipateOnAnticipationFailVariable = new AnticipatedNetworkVariable(0, StaleDataHandling.Reanticipate); + + public override void OnReanticipate(double lastRoundTripTime) + { + if (SmoothOnAnticipationFailVariable.ShouldReanticipate) + { + if (Mathf.Abs(SmoothOnAnticipationFailVariable.AuthoritativeValue - SmoothOnAnticipationFailVariable.PreviousAnticipatedValue) > Mathf.Epsilon) + { + SmoothOnAnticipationFailVariable.Smooth(SmoothOnAnticipationFailVariable.PreviousAnticipatedValue, SmoothOnAnticipationFailVariable.AuthoritativeValue, 1, Mathf.Lerp); + } + } + + if (ReanticipateOnAnticipationFailVariable.ShouldReanticipate) + { + // Would love to test some stuff about anticipation based on time, but that is difficult to test accurately. + // This reanticipating variable will just always anticipate a value 5 higher than the server value. + ReanticipateOnAnticipationFailVariable.Anticipate(ReanticipateOnAnticipationFailVariable.AuthoritativeValue + 5); + } + } + + public bool SnapRpcResponseReceived = false; + + [Rpc(SendTo.Server)] + public void SetSnapValueRpc(int i, RpcParams rpcParams = default) + { + SnapOnAnticipationFailVariable.AuthoritativeValue = i; + SetSnapValueResponseRpc(RpcTarget.Single(rpcParams.Receive.SenderClientId, RpcTargetUse.Temp)); + } + + [Rpc(SendTo.SpecifiedInParams)] + public void SetSnapValueResponseRpc(RpcParams rpcParams) + { + SnapRpcResponseReceived = true; + } + + [Rpc(SendTo.Server)] + public void SetSmoothValueRpc(float f) + { + SmoothOnAnticipationFailVariable.AuthoritativeValue = f; + } + + [Rpc(SendTo.Server)] + public void SetReanticipateValueRpc(float f) + { + ReanticipateOnAnticipationFailVariable.AuthoritativeValue = f; + } + } + + public class NetworkVariableAnticipationTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + protected override bool m_EnableTimeTravel => true; + protected override bool m_SetupIsACoroutine => false; + protected override bool m_TearDownIsACoroutine => false; + + protected override void OnPlayerPrefabGameObjectCreated() + { + m_PlayerPrefab.AddComponent(); + } + + public NetworkVariableAnticipationComponent GetTestComponent() + { + return m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); + } + + public NetworkVariableAnticipationComponent GetServerComponent() + { + foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + if (obj.NetworkManager == m_ServerNetworkManager && obj.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId) + { + return obj; + } + } + + return null; + } + + public NetworkVariableAnticipationComponent GetOtherClientComponent() + { + foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + if (obj.NetworkManager == m_ClientNetworkManagers[1] && obj.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId) + { + return obj; + } + } + + return null; + } + + [Test] + public void WhenAnticipating_ValueChangesImmediately() + { + var testComponent = GetTestComponent(); + + testComponent.SnapOnAnticipationFailVariable.Anticipate(10); + testComponent.SmoothOnAnticipationFailVariable.Anticipate(15); + testComponent.ReanticipateOnAnticipationFailVariable.Anticipate(20); + + Assert.AreEqual(10, testComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(15, testComponent.SmoothOnAnticipationFailVariable.Value); + Assert.AreEqual(20, testComponent.ReanticipateOnAnticipationFailVariable.Value); + } + + [Test] + public void WhenAnticipating_AuthoritativeValueDoesNotChange() + { + var testComponent = GetTestComponent(); + + testComponent.SnapOnAnticipationFailVariable.Anticipate(10); + testComponent.SmoothOnAnticipationFailVariable.Anticipate(15); + testComponent.ReanticipateOnAnticipationFailVariable.Anticipate(20); + + Assert.AreEqual(0, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, testComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, testComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + } + + [Test] + public void WhenAnticipating_ServerDoesNotChange() + { + var testComponent = GetTestComponent(); + + testComponent.SnapOnAnticipationFailVariable.Anticipate(10); + testComponent.SmoothOnAnticipationFailVariable.Anticipate(15); + testComponent.ReanticipateOnAnticipationFailVariable.Anticipate(20); + + var serverComponent = GetServerComponent(); + + Assert.AreEqual(0, serverComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, serverComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, serverComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, serverComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, serverComponent.SmoothOnAnticipationFailVariable.Value); + Assert.AreEqual(0, serverComponent.ReanticipateOnAnticipationFailVariable.Value); + + TimeTravel(2, 120); + + Assert.AreEqual(0, serverComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, serverComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, serverComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, serverComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, serverComponent.SmoothOnAnticipationFailVariable.Value); + Assert.AreEqual(0, serverComponent.ReanticipateOnAnticipationFailVariable.Value); + } + + [Test] + public void WhenAnticipating_OtherClientDoesNotChange() + { + var testComponent = GetTestComponent(); + + testComponent.SnapOnAnticipationFailVariable.Anticipate(10); + testComponent.SmoothOnAnticipationFailVariable.Anticipate(15); + testComponent.ReanticipateOnAnticipationFailVariable.Anticipate(20); + + var otherClientComponent = GetOtherClientComponent(); + + Assert.AreEqual(0, otherClientComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, otherClientComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, otherClientComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, otherClientComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, otherClientComponent.SmoothOnAnticipationFailVariable.Value); + Assert.AreEqual(0, otherClientComponent.ReanticipateOnAnticipationFailVariable.Value); + + TimeTravel(2, 120); + + Assert.AreEqual(0, otherClientComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, otherClientComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, otherClientComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + Assert.AreEqual(0, otherClientComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, otherClientComponent.SmoothOnAnticipationFailVariable.Value); + Assert.AreEqual(0, otherClientComponent.ReanticipateOnAnticipationFailVariable.Value); + } + + [Test] + public void WhenServerChangesSnapValue_ValuesAreUpdated() + { + var testComponent = GetTestComponent(); + + testComponent.SnapOnAnticipationFailVariable.Anticipate(10); + + Assert.AreEqual(10, testComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + testComponent.SetSnapValueRpc(10); + + WaitForMessageReceivedWithTimeTravel( + new List { m_ServerNetworkManager } + ); + + var serverComponent = GetServerComponent(); + Assert.AreEqual(10, serverComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(10, serverComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + var otherClientComponent = GetOtherClientComponent(); + Assert.AreEqual(0, otherClientComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, otherClientComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + Assert.AreEqual(10, testComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(10, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + Assert.AreEqual(10, otherClientComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(10, otherClientComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + } + + [Test] + public void WhenServerChangesSmoothValue_ValuesAreLerped() + { + var testComponent = GetTestComponent(); + + testComponent.SmoothOnAnticipationFailVariable.Anticipate(15); + + Assert.AreEqual(15, testComponent.SmoothOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(0, testComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + // Set to a different value to simulate a anticipation failure - will lerp between the anticipated value + // and the actual one + testComponent.SetSmoothValueRpc(20); + + WaitForMessageReceivedWithTimeTravel( + new List { m_ServerNetworkManager } + ); + + var serverComponent = GetServerComponent(); + Assert.AreEqual(20, serverComponent.SmoothOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(20, serverComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + var otherClientComponent = GetOtherClientComponent(); + Assert.AreEqual(0, otherClientComponent.SmoothOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(0, otherClientComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + Assert.AreEqual(15 + 1f / 60f * 5, testComponent.SmoothOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(20, testComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + Assert.AreEqual(0 + 1f / 60f * 20, otherClientComponent.SmoothOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(20, otherClientComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + for (var i = 1; i < 60; ++i) + { + TimeTravel(1f / 60f, 1); + + Assert.AreEqual(15 + 1f / 60f * 5 * (i + 1), testComponent.SmoothOnAnticipationFailVariable.Value, 0.00001); + Assert.AreEqual(20, testComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + Assert.AreEqual(0 + 1f / 60f * 20 * (i + 1), otherClientComponent.SmoothOnAnticipationFailVariable.Value, 0.00001); + Assert.AreEqual(20, otherClientComponent.SmoothOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + } + TimeTravel(1f / 60f, 1); + Assert.AreEqual(20, testComponent.SmoothOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(20, otherClientComponent.SmoothOnAnticipationFailVariable.Value, Mathf.Epsilon); + } + + [Test] + public void WhenServerChangesReanticipateValue_ValuesAreReanticipated() + { + var testComponent = GetTestComponent(); + + testComponent.ReanticipateOnAnticipationFailVariable.Anticipate(15); + + Assert.AreEqual(15, testComponent.ReanticipateOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(0, testComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + // Set to a different value to simulate a anticipation failure - will lerp between the anticipated value + // and the actual one + testComponent.SetReanticipateValueRpc(20); + + WaitForMessageReceivedWithTimeTravel( + new List { m_ServerNetworkManager } + ); + + var serverComponent = GetServerComponent(); + Assert.AreEqual(20, serverComponent.ReanticipateOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(20, serverComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + var otherClientComponent = GetOtherClientComponent(); + Assert.AreEqual(0, otherClientComponent.ReanticipateOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(0, otherClientComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + Assert.AreEqual(25, testComponent.ReanticipateOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(20, testComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + + Assert.AreEqual(25, otherClientComponent.ReanticipateOnAnticipationFailVariable.Value, Mathf.Epsilon); + Assert.AreEqual(20, otherClientComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue, Mathf.Epsilon); + } + + [Test] + public void WhenNonStaleDataArrivesToIgnoreVariable_ItIsNotIgnored([Values(10u, 30u, 60u)] uint tickRate, [Values(0u, 1u, 2u)] uint skipFrames) + { + m_ServerNetworkManager.NetworkConfig.TickRate = tickRate; + m_ServerNetworkManager.NetworkTickSystem.TickRate = tickRate; + + for (var i = 0; i < skipFrames; ++i) + { + TimeTravel(1 / 60f, 1); + } + var testComponent = GetTestComponent(); + testComponent.SnapOnAnticipationFailVariable.Anticipate(10); + + Assert.AreEqual(10, testComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + testComponent.SetSnapValueRpc(20); + WaitForMessageReceivedWithTimeTravel(new List { m_ServerNetworkManager }); + + var serverComponent = GetServerComponent(); + + Assert.AreEqual(20, serverComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(20, serverComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + // Both values get updated + Assert.AreEqual(20, testComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(20, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + // Other client got the server value and had made no anticipation, so it applies it to the anticipated value as well. + var otherClientComponent = GetOtherClientComponent(); + Assert.AreEqual(20, otherClientComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(20, otherClientComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + } + + [Test] + public void WhenStaleDataArrivesToIgnoreVariable_ItIsIgnored([Values(10u, 30u, 60u)] uint tickRate, [Values(0u, 1u, 2u)] uint skipFrames) + { + m_ServerNetworkManager.NetworkConfig.TickRate = tickRate; + m_ServerNetworkManager.NetworkTickSystem.TickRate = tickRate; + + for (var i = 0; i < skipFrames; ++i) + { + TimeTravel(1 / 60f, 1); + } + var testComponent = GetTestComponent(); + testComponent.SnapOnAnticipationFailVariable.Anticipate(10); + + Assert.AreEqual(10, testComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(0, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + testComponent.SetSnapValueRpc(30); + + var serverComponent = GetServerComponent(); + serverComponent.SnapOnAnticipationFailVariable.AuthoritativeValue = 20; + + Assert.AreEqual(20, serverComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(20, serverComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + if (testComponent.SnapRpcResponseReceived) + { + // In this case the tick rate is slow enough that the RPC was received and processed, so we check that. + Assert.AreEqual(30, testComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(30, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + var otherClientComponent = GetOtherClientComponent(); + Assert.AreEqual(30, otherClientComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(30, otherClientComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + } + else + { + // In this case, we got an update before the RPC was processed, so we should have ignored it. + // Anticipated client received this data for a tick earlier than its anticipation, and should have prioritized the anticipated value + Assert.AreEqual(10, testComponent.SnapOnAnticipationFailVariable.Value); + // However, the authoritative value still gets updated + Assert.AreEqual(20, testComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + + // Other client got the server value and had made no anticipation, so it applies it to the anticipated value as well. + var otherClientComponent = GetOtherClientComponent(); + Assert.AreEqual(20, otherClientComponent.SnapOnAnticipationFailVariable.Value); + Assert.AreEqual(20, otherClientComponent.SnapOnAnticipationFailVariable.AuthoritativeValue); + } + } + + [Test] + public void WhenStaleDataArrivesToReanticipatedVariable_ItIsAppliedAndReanticipated() + { + var testComponent = GetTestComponent(); + testComponent.ReanticipateOnAnticipationFailVariable.Anticipate(10); + + Assert.AreEqual(10, testComponent.ReanticipateOnAnticipationFailVariable.Value); + Assert.AreEqual(0, testComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + + var serverComponent = GetServerComponent(); + serverComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue = 20; + + Assert.AreEqual(20, serverComponent.ReanticipateOnAnticipationFailVariable.Value); + Assert.AreEqual(20, serverComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); + + Assert.AreEqual(25, testComponent.ReanticipateOnAnticipationFailVariable.Value); + // However, the authoritative value still gets updated + Assert.AreEqual(20, testComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + + // Other client got the server value and had made no anticipation, so it applies it to the anticipated value as well. + var otherClientComponent = GetOtherClientComponent(); + Assert.AreEqual(25, otherClientComponent.ReanticipateOnAnticipationFailVariable.Value); + Assert.AreEqual(20, otherClientComponent.ReanticipateOnAnticipationFailVariable.AuthoritativeValue); + } + } +} diff --git a/Tests/Runtime/NetworkVariableAnticipationTests.cs.meta b/Tests/Runtime/NetworkVariableAnticipationTests.cs.meta new file mode 100644 index 0000000..ea2f140 --- /dev/null +++ b/Tests/Runtime/NetworkVariableAnticipationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 43cd37f850534b7db07e20281442f10d +timeCreated: 1706288570 \ No newline at end of file diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs index 8c17d1b..b3cef36 100644 --- a/Tests/Runtime/NetworkVariableTests.cs +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -11,441 +11,6 @@ using Random = UnityEngine.Random; namespace Unity.Netcode.RuntimeTests { - public class NetVarPermTestComp : NetworkBehaviour - { - public NetworkVariable OwnerWritable_Position = new NetworkVariable(Vector3.one, NetworkVariableBase.DefaultReadPerm, NetworkVariableWritePermission.Owner); - public NetworkVariable ServerWritable_Position = new NetworkVariable(Vector3.one, NetworkVariableBase.DefaultReadPerm, NetworkVariableWritePermission.Server); - public NetworkVariable OwnerReadWrite_Position = new NetworkVariable(Vector3.one, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner); - } - - public class NetworkVariableMiddleclass : NetworkVariable - { - - } - - public class NetworkVariableSubclass : NetworkVariableMiddleclass - { - - } - - public class NetworkBehaviourWithNetVarArray : NetworkBehaviour - { - public NetworkVariable Int0 = new NetworkVariable(); - public NetworkVariable Int1 = new NetworkVariable(); - public NetworkVariable Int2 = new NetworkVariable(); - public NetworkVariable Int3 = new NetworkVariable(); - public NetworkVariable Int4 = new NetworkVariable(); - public NetworkVariable[] AllInts = new NetworkVariable[5]; - - public int InitializedFieldCount => NetworkVariableFields.Count; - - - private void Awake() - { - AllInts[0] = Int0; - AllInts[1] = Int1; - AllInts[2] = Int2; - AllInts[3] = Int3; - AllInts[4] = Int4; - } - } - - internal struct TypeReferencedOnlyInCustomSerialization1 : INetworkSerializeByMemcpy - { - public int I; - } - - internal struct TypeReferencedOnlyInCustomSerialization2 : INetworkSerializeByMemcpy - { - public int I; - } - - internal struct TypeReferencedOnlyInCustomSerialization3 : INetworkSerializeByMemcpy - { - public int I; - } - - internal struct TypeReferencedOnlyInCustomSerialization4 : INetworkSerializeByMemcpy - { - public int I; - } - - internal struct TypeReferencedOnlyInCustomSerialization5 : INetworkSerializeByMemcpy - { - public int I; - } - - internal struct TypeReferencedOnlyInCustomSerialization6 : INetworkSerializeByMemcpy - { - public int I; - } - - // Both T and U are serializable - [GenerateSerializationForGenericParameter(0)] - [GenerateSerializationForGenericParameter(1)] - internal class CustomSerializableClass - { - - } - - // Only U is serializable - [GenerateSerializationForGenericParameter(1)] - internal class CustomSerializableBaseClass - { - - } - - // T is serializable, passes TypeReferencedOnlyInCustomSerialization3 as U to the subclass, making it serializable - [GenerateSerializationForGenericParameter(0)] - internal class CustomSerializableSubclass : CustomSerializableBaseClass - { - - } - - // T is serializable, passes TypeReferencedOnlyInCustomSerialization3 as U to the subclass, making it serializable - [GenerateSerializationForGenericParameter(0)] - internal class CustomSerializableSubclassWithNativeArray : CustomSerializableBaseClass> - { - - } - - internal class CustomGenericSerializationTestBehaviour : NetworkBehaviour - { - public CustomSerializableClass Value1; - public CustomSerializableClass, NativeArray> Value2; - public CustomSerializableSubclass Value3; - public CustomSerializableSubclassWithNativeArray> Value4; - } - - [GenerateSerializationForType(typeof(TypeReferencedOnlyInCustomSerialization5))] - [GenerateSerializationForType(typeof(NativeArray))] - internal struct SomeRandomStruct - { - [GenerateSerializationForType(typeof(TypeReferencedOnlyInCustomSerialization6))] - [GenerateSerializationForType(typeof(NativeArray))] - public void Foo() - { - - } - } - - public struct TemplatedValueOnlyReferencedByNetworkVariableSubclass : INetworkSerializeByMemcpy - where T : unmanaged - { - public T Value; - } - - public enum ByteEnum : byte - { - A, - B, - C = byte.MaxValue - } - public enum SByteEnum : sbyte - { - A, - B, - C = sbyte.MaxValue - } - public enum ShortEnum : short - { - A, - B, - C = short.MaxValue - } - public enum UShortEnum : ushort - { - A, - B, - C = ushort.MaxValue - } - public enum IntEnum : int - { - A, - B, - C = int.MaxValue - } - public enum UIntEnum : uint - { - A, - B, - C = uint.MaxValue - } - public enum LongEnum : long - { - A, - B, - C = long.MaxValue - } - public enum ULongEnum : ulong - { - A, - B, - C = ulong.MaxValue - } - - public struct NetworkVariableTestStruct : INetworkSerializeByMemcpy - { - public byte A; - public short B; - public ushort C; - public int D; - public uint E; - public long F; - public ulong G; - public bool H; - public char I; - public float J; - public double K; - - private static System.Random s_Random = new System.Random(); - - public static NetworkVariableTestStruct GetTestStruct() - { - var testStruct = new NetworkVariableTestStruct - { - A = (byte)s_Random.Next(), - B = (short)s_Random.Next(), - C = (ushort)s_Random.Next(), - D = s_Random.Next(), - E = (uint)s_Random.Next(), - F = ((long)s_Random.Next() << 32) + s_Random.Next(), - G = ((ulong)s_Random.Next() << 32) + (ulong)s_Random.Next(), - H = true, - I = '\u263a', - J = (float)s_Random.NextDouble(), - K = s_Random.NextDouble(), - }; - - return testStruct; - } - } - - // The ILPP code for NetworkVariables to determine how to serialize them relies on them existing as fields of a NetworkBehaviour to find them. - // Some of the tests below create NetworkVariables on the stack, so this class is here just to make sure the relevant types are all accounted for. - public class NetVarILPPClassForTests : NetworkBehaviour - { - public NetworkVariable ByteVar; - public NetworkVariable> ByteArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> ByteListVar; -#endif - public NetworkVariable SbyteVar; - public NetworkVariable> SbyteArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> SbyteListVar; -#endif - public NetworkVariable ShortVar; - public NetworkVariable> ShortArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> ShortListVar; -#endif - public NetworkVariable UshortVar; - public NetworkVariable> UshortArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> UshortListVar; -#endif - public NetworkVariable IntVar; - public NetworkVariable> IntArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> IntListVar; -#endif - public NetworkVariable UintVar; - public NetworkVariable> UintArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> UintListVar; -#endif - public NetworkVariable LongVar; - public NetworkVariable> LongArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> LongListVar; -#endif - public NetworkVariable UlongVar; - public NetworkVariable> UlongArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> UlongListVar; -#endif - public NetworkVariable BoolVar; - public NetworkVariable> BoolArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> BoolListVar; -#endif - public NetworkVariable CharVar; - public NetworkVariable> CharArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> CharListVar; -#endif - public NetworkVariable FloatVar; - public NetworkVariable> FloatArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> FloatListVar; -#endif - public NetworkVariable DoubleVar; - public NetworkVariable> DoubleArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> DoubleListVar; -#endif - public NetworkVariable ByteEnumVar; - public NetworkVariable> ByteEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> ByteEnumListVar; -#endif - public NetworkVariable SByteEnumVar; - public NetworkVariable> SByteEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> SByteEnumListVar; -#endif - public NetworkVariable ShortEnumVar; - public NetworkVariable> ShortEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> ShortEnumListVar; -#endif - public NetworkVariable UShortEnumVar; - public NetworkVariable> UShortEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> UShortEnumListVar; -#endif - public NetworkVariable IntEnumVar; - public NetworkVariable> IntEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> IntEnumListVar; -#endif - public NetworkVariable UIntEnumVar; - public NetworkVariable> UIntEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> UIntEnumListVar; -#endif - public NetworkVariable LongEnumVar; - public NetworkVariable> LongEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> LongEnumListVar; -#endif - public NetworkVariable ULongEnumVar; - public NetworkVariable> ULongEnumArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> ULongEnumListVar; -#endif - public NetworkVariable Vector2Var; - public NetworkVariable> Vector2ArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> Vector2ListVar; -#endif - public NetworkVariable Vector3Var; - public NetworkVariable> Vector3ArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> Vector3ListVar; -#endif - public NetworkVariable Vector2IntVar; - public NetworkVariable> Vector2IntArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> Vector2IntListVar; -#endif - public NetworkVariable Vector3IntVar; - public NetworkVariable> Vector3IntArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> Vector3IntListVar; -#endif - public NetworkVariable Vector4Var; - public NetworkVariable> Vector4ArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> Vector4ListVar; -#endif - public NetworkVariable QuaternionVar; - public NetworkVariable> QuaternionArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> QuaternionListVar; -#endif - public NetworkVariable ColorVar; - public NetworkVariable> ColorArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> ColorListVar; -#endif - public NetworkVariable Color32Var; - public NetworkVariable> Color32ArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> Color32ListVar; -#endif - public NetworkVariable RayVar; - public NetworkVariable> RayArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> RayListVar; -#endif - public NetworkVariable Ray2DVar; - public NetworkVariable> Ray2DArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> Ray2DListVar; -#endif - public NetworkVariable TestStructVar; - public NetworkVariable> TestStructArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> TestStructListVar; -#endif - - public NetworkVariable FixedStringVar; - public NetworkVariable> FixedStringArrayVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> FixedStringListVar; -#endif - - public NetworkVariable UnmanagedNetworkSerializableTypeVar; -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public NetworkVariable> UnmanagedNetworkSerializableListVar; -#endif - public NetworkVariable> UnmanagedNetworkSerializableArrayVar; - - public NetworkVariable ManagedNetworkSerializableTypeVar; - - public NetworkVariable StringVar; - public NetworkVariable GuidVar; - public NetworkVariableSubclass> SubclassVar; - } - - public class TemplateNetworkBehaviourType : NetworkBehaviour - { - public NetworkVariable TheVar; - } - - public class IntermediateNetworkBehavior : TemplateNetworkBehaviourType - { - public NetworkVariable TheVar2; - } - - public class ClassHavingNetworkBehaviour : IntermediateNetworkBehavior - { - - } - - // Please do not reference TestClass_ReferencedOnlyByTemplateNetworkBehavourType anywhere other than here! - public class ClassHavingNetworkBehaviour2 : TemplateNetworkBehaviourType - { - - } - - public class StructHavingNetworkBehaviour : TemplateNetworkBehaviourType - { - - } - - public struct StructUsedOnlyInNetworkList : IEquatable, INetworkSerializeByMemcpy - { - public int Value; - - public bool Equals(StructUsedOnlyInNetworkList other) - { - return Value == other.Value; - } - - public override bool Equals(object obj) - { - return obj is StructUsedOnlyInNetworkList other && Equals(other); - } - - public override int GetHashCode() - { - return Value; - } - } - [TestFixtureSource(nameof(TestDataSource))] public class NetworkVariablePermissionTests : NetcodeIntegrationTest { @@ -815,9 +380,12 @@ namespace Unity.Netcode.RuntimeTests // Used just to create a NetworkVariable in the templated NetworkBehaviour type that isn't referenced anywhere else // Please do not reference this class anywhere else! - public class TestClass_ReferencedOnlyByTemplateNetworkBehavourType : TestClass + public class TestClass_ReferencedOnlyByTemplateNetworkBehavourType : TestClass, IEquatable { - + public bool Equals(TestClass_ReferencedOnlyByTemplateNetworkBehavourType other) + { + return Equals((TestClass)other); + } } public class NetworkVariableTest : NetworkBehaviour @@ -1632,38 +1200,61 @@ namespace Unity.Netcode.RuntimeTests Assert.IsTrue(NetworkVariableSerialization.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); } + public void AssertArraysMatch(ref NativeArray a, ref NativeArray b) where T : unmanaged + { + Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Lists do not match: {ArrayStr(a)} != {ArrayStr(b)}"); + } + public void AssertArraysDoNotMatch(ref NativeArray a, ref NativeArray b) where T : unmanaged + { + Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Lists match when they should not: {ArrayStr(a)} == {ArrayStr(b)}"); + } + private void TestValueTypeNativeArray(NativeArray testValue, NativeArray changedValue) where T : unmanaged { + VerboseDebug($"Changing {ArrayStr(testValue)} to {ArrayStr(changedValue)}"); var serverVariable = new NetworkVariable>(testValue); var clientVariable = new NetworkVariable>(new NativeArray(1, Allocator.Persistent)); - using var writer = new FastBufferWriter(1024, Allocator.Temp); + using var writer = new FastBufferWriter(1024, Allocator.Temp, int.MaxValue); serverVariable.WriteField(writer); - Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertArraysDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); using var reader = new FastBufferReader(writer, Allocator.None); clientVariable.ReadField(reader); - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertArraysMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); - serverVariable.Dispose(); + serverVariable.ResetDirty(); + serverVariable.Value.Dispose(); serverVariable.Value = changedValue; - Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertArraysDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); writer.Seek(0); serverVariable.WriteDelta(writer); - Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertArraysDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); using var reader2 = new FastBufferReader(writer, Allocator.None); clientVariable.ReadDelta(reader2, false); - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertArraysMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); serverVariable.ResetDirty(); Assert.IsFalse(serverVariable.IsDirty()); var cachedValue = changedValue[0]; - changedValue[0] = testValue[0]; + var differentValue = changedValue[0]; + foreach (var checkValue in testValue) + { + var checkValueRef = checkValue; + if (!NetworkVariableSerialization.AreEqual(ref checkValueRef, ref differentValue)) + { + differentValue = checkValue; + break; + } + } + changedValue[0] = differentValue; Assert.IsTrue(serverVariable.IsDirty()); serverVariable.ResetDirty(); Assert.IsFalse(serverVariable.IsDirty()); @@ -1675,45 +1266,57 @@ namespace Unity.Netcode.RuntimeTests clientVariable.Dispose(); } -#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - private void TestValueTypeNativeList(NativeList testValue, NativeList changedValue) where T : unmanaged + public void AssertListsMatch(ref List a, ref List b) { - var serverVariable = new NetworkVariable>(testValue); - var inPlaceList = new NativeList(1, Allocator.Temp); - var clientVariable = new NetworkVariable>(inPlaceList); - using var writer = new FastBufferWriter(1024, Allocator.Temp); + Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Lists do not match: {ListStr(a)} != {ListStr(b)}"); + } + public void AssertListsDoNotMatch(ref List a, ref List b) + { + Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Lists match when they should not: {ListStr(a)} == {ListStr(b)}"); + } + + + private void TestList(List testValue, List changedValue) + { + VerboseDebug($"Changing {ListStr(testValue)} to {ListStr(changedValue)}"); + var serverVariable = new NetworkVariable>(testValue); + var inPlaceList = new List(); + var clientVariable = new NetworkVariable>(inPlaceList); + using var writer = new FastBufferWriter(1024, Allocator.Temp, int.MaxValue); serverVariable.WriteField(writer); - Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertListsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref clientVariable.RefValue(), ref inPlaceList)); + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); using var reader = new FastBufferReader(writer, Allocator.None); clientVariable.ReadField(reader); - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertListsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref clientVariable.RefValue(), ref inPlaceList)); + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); - serverVariable.Dispose(); + serverVariable.ResetDirty(); serverVariable.Value = changedValue; - Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertListsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref clientVariable.RefValue(), ref inPlaceList)); + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); writer.Seek(0); serverVariable.WriteDelta(writer); - Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertListsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref clientVariable.RefValue(), ref inPlaceList)); + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); using var reader2 = new FastBufferReader(writer, Allocator.None); clientVariable.ReadDelta(reader2, false); - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref serverVariable.RefValue(), ref clientVariable.RefValue())); + AssertListsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! - Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref clientVariable.RefValue(), ref inPlaceList)); + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); serverVariable.ResetDirty(); Assert.IsFalse(serverVariable.IsDirty()); @@ -1725,6 +1328,353 @@ namespace Unity.Netcode.RuntimeTests Assert.IsFalse(serverVariable.IsDirty()); serverVariable.Value.Add(default); Assert.IsTrue(serverVariable.IsDirty()); + } + + + public void AssertSetsMatch(ref HashSet a, ref HashSet b) where T : IEquatable + { + Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Sets do not match: {HashSetStr(a)} != {HashSetStr(b)}"); + } + public void AssertSetsDoNotMatch(ref HashSet a, ref HashSet b) where T : IEquatable + { + Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Sets match when they should not: {HashSetStr(a)} == {HashSetStr(b)}"); + } + + private void TestHashSet(HashSet testValue, HashSet changedValue) where T : IEquatable + { + VerboseDebug($"Changing {HashSetStr(testValue)} to {HashSetStr(changedValue)}"); + var serverVariable = new NetworkVariable>(testValue); + var inPlaceList = new HashSet(); + var clientVariable = new NetworkVariable>(inPlaceList); + using var writer = new FastBufferWriter(1024, Allocator.Temp, int.MaxValue); + serverVariable.WriteField(writer); + + AssertSetsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadField(reader); + + AssertSetsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + serverVariable.Value = changedValue; + AssertSetsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + writer.Seek(0); + + serverVariable.WriteDelta(writer); + + AssertSetsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader2 = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadDelta(reader2, false); + AssertSetsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Clear(); + Assert.IsTrue(serverVariable.IsDirty()); + + serverVariable.ResetDirty(); + + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Add(default); + Assert.IsTrue(serverVariable.IsDirty()); + } + + + public void AssertMapsMatch(ref Dictionary a, ref Dictionary b) + where TKey : IEquatable + { + Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Maps do not match: {DictionaryStr(a)} != {DictionaryStr(b)}"); + } + + public void AssertMapsDoNotMatch(ref Dictionary a, ref Dictionary b) + where TKey : IEquatable + { + Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Maps match when they should not: {DictionaryStr(a)} != {DictionaryStr(b)}"); + } + + private void TestDictionary(Dictionary testValue, Dictionary changedValue) + where TKey : IEquatable + { + VerboseDebug($"Changing {DictionaryStr(testValue)} to {DictionaryStr(changedValue)}"); + var serverVariable = new NetworkVariable>(testValue); + var inPlaceList = new Dictionary(); + var clientVariable = new NetworkVariable>(inPlaceList); + using var writer = new FastBufferWriter(1024, Allocator.Temp, int.MaxValue); + serverVariable.WriteField(writer); + + AssertMapsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadField(reader); + + AssertMapsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + serverVariable.Value = changedValue; + AssertMapsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + writer.Seek(0); + + serverVariable.WriteDelta(writer); + + AssertMapsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader2 = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadDelta(reader2, false); + AssertMapsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Clear(); + Assert.IsTrue(serverVariable.IsDirty()); + + serverVariable.ResetDirty(); + + Assert.IsFalse(serverVariable.IsDirty()); + foreach (var kvp in testValue) + { + if (!serverVariable.Value.ContainsKey(kvp.Key)) + { + serverVariable.Value.Add(kvp.Key, kvp.Value); + } + } + Assert.IsTrue(serverVariable.IsDirty()); + } + +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public void AssertListsMatch(ref NativeList a, ref NativeList b) where T : unmanaged + { + Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Lists do not match: {NativeListStr(a)} != {NativeListStr(b)}"); + } + public void AssertListsDoNotMatch(ref NativeList a, ref NativeList b) where T : unmanaged + { + Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Lists match when they should not: {NativeListStr(a)} == {NativeListStr(b)}"); + } + + + private void TestValueTypeNativeList(NativeList testValue, NativeList changedValue) where T : unmanaged + { + VerboseDebug($"Changing {NativeListStr(testValue)} to {NativeListStr(changedValue)}"); + var serverVariable = new NetworkVariable>(testValue); + var inPlaceList = new NativeList(1, Allocator.Temp); + var clientVariable = new NetworkVariable>(inPlaceList); + using var writer = new FastBufferWriter(1024, Allocator.Temp, int.MaxValue); + serverVariable.WriteField(writer); + + AssertListsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadField(reader); + + AssertListsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + serverVariable.Value.Dispose(); + serverVariable.Value = changedValue; + AssertListsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + writer.Seek(0); + + serverVariable.WriteDelta(writer); + + AssertListsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader2 = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadDelta(reader2, false); + AssertListsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertListsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Clear(); + Assert.IsTrue(serverVariable.IsDirty()); + + serverVariable.ResetDirty(); + + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Add(default); + Assert.IsTrue(serverVariable.IsDirty()); + + serverVariable.Dispose(); + clientVariable.Dispose(); + } + + + public void AssertSetsMatch(ref NativeHashSet a, ref NativeHashSet b) where T : unmanaged, IEquatable + { + Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Sets do not match: {NativeHashSetStr(a)} != {NativeHashSetStr(b)}"); + } + public void AssertSetsDoNotMatch(ref NativeHashSet a, ref NativeHashSet b) where T : unmanaged, IEquatable + { + Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Sets match when they should not: {NativeHashSetStr(a)} == {NativeHashSetStr(b)}"); + } + + private void TestValueTypeNativeHashSet(NativeHashSet testValue, NativeHashSet changedValue) where T : unmanaged, IEquatable + { + VerboseDebug($"Changing {NativeHashSetStr(testValue)} to {NativeHashSetStr(changedValue)}"); + var serverVariable = new NetworkVariable>(testValue); + var inPlaceList = new NativeHashSet(1, Allocator.Temp); + var clientVariable = new NetworkVariable>(inPlaceList); + using var writer = new FastBufferWriter(1024, Allocator.Temp, int.MaxValue); + serverVariable.WriteField(writer); + + AssertSetsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadField(reader); + + AssertSetsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + serverVariable.Value.Dispose(); + serverVariable.Value = changedValue; + AssertSetsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + writer.Seek(0); + + serverVariable.WriteDelta(writer); + + AssertSetsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader2 = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadDelta(reader2, false); + AssertSetsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertSetsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Clear(); + Assert.IsTrue(serverVariable.IsDirty()); + + serverVariable.ResetDirty(); + + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Add(default); + Assert.IsTrue(serverVariable.IsDirty()); + + serverVariable.Dispose(); + clientVariable.Dispose(); + } + + + public void AssertMapsMatch(ref NativeHashMap a, ref NativeHashMap b) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Maps do not match: {NativeHashMapStr(a)} != {NativeHashMapStr(b)}"); + } + + public void AssertMapsDoNotMatch(ref NativeHashMap a, ref NativeHashMap b) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + Assert.IsFalse(NetworkVariableSerialization>.AreEqual(ref a, ref b), + $"Maps match when they should not: {NativeHashMapStr(a)} != {NativeHashMapStr(b)}"); + } + + private void TestValueTypeNativeHashMap(NativeHashMap testValue, NativeHashMap changedValue) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + VerboseDebug($"Changing {NativeHashMapStr(testValue)} to {NativeHashMapStr(changedValue)}"); + var serverVariable = new NetworkVariable>(testValue); + var inPlaceList = new NativeHashMap(1, Allocator.Temp); + var clientVariable = new NetworkVariable>(inPlaceList); + using var writer = new FastBufferWriter(1024, Allocator.Temp, int.MaxValue); + serverVariable.WriteField(writer); + + AssertMapsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadField(reader); + + AssertMapsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + serverVariable.Value.Dispose(); + serverVariable.Value = changedValue; + AssertMapsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + writer.Seek(0); + + serverVariable.WriteDelta(writer); + + AssertMapsDoNotMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + using var reader2 = new FastBufferReader(writer, Allocator.None); + clientVariable.ReadDelta(reader2, false); + AssertMapsMatch(ref serverVariable.RefValue(), ref clientVariable.RefValue()); + // Lists are deserialized in place so this should ALWAYS be true. Checking it every time to make sure! + AssertMapsMatch(ref clientVariable.RefValue(), ref inPlaceList); + + serverVariable.ResetDirty(); + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Clear(); + Assert.IsTrue(serverVariable.IsDirty()); + + serverVariable.ResetDirty(); + + Assert.IsFalse(serverVariable.IsDirty()); + serverVariable.Value.Add(default, default); + Assert.IsTrue(serverVariable.IsDirty()); serverVariable.Dispose(); clientVariable.Dispose(); @@ -2132,6 +2082,1488 @@ namespace Unity.Netcode.RuntimeTests } } + public delegate T GetRandomElement(System.Random rand); + + public unsafe T RandGenBytes(System.Random rand) where T : unmanaged + { + var t = new T(); + T* tPtr = &t; + var s = new Span(tPtr, sizeof(T)); + rand.NextBytes(s); + return t; + } + + public FixedString32Bytes RandGenFixedString32(System.Random rand) + { + var s = new FixedString32Bytes(); + var len = rand.Next(s.Capacity); + s.Length = len; + for (var i = 0; i < len; ++i) + { + // Ascii visible character range + s[i] = (byte)rand.Next(32, 126); + } + + return s; + } + public string ArrayStr(NativeArray arr) where T : unmanaged + { + var str = "["; + var comma = false; + foreach (var item in arr) + { + if (comma) + { + str += ", "; + } + + comma = true; + str += $"{item}"; + } + + str += "]"; + return str; + } + + public (NativeArray original, NativeArray original2, NativeArray changed, NativeArray changed2) GetArarys(GetRandomElement generator) where T : unmanaged + { + + var rand = new System.Random(); + var originalSize = rand.Next(32, 64); + var changedSize = rand.Next(32, 64); + var changed2Changes = rand.Next(12, 16); + var changed2Adds = rand.Next(-16, 16); + + var original = new NativeArray(originalSize, Allocator.Temp); + var changed = new NativeArray(changedSize, Allocator.Temp); + var original2 = new NativeArray(originalSize, Allocator.Temp); + var changed2 = new NativeArray(originalSize + changed2Adds, Allocator.Temp); + + + for (var i = 0; i < originalSize; ++i) + { + var item = generator(rand); + original[i] = item; + original2[i] = item; + if (i < changed2.Length) + { + changed2[i] = item; + } + } + for (var i = 0; i < changedSize; ++i) + { + var item = generator(rand); + changed[i] = item; + } + + for (var i = 0; i < changed2Changes; ++i) + { + var idx = rand.Next(changed2.Length - 1); + var item = generator(rand); + changed2[idx] = item; + } + + for (var i = 0; i < changed2Adds; ++i) + { + var item = generator(rand); + changed2[originalSize + i] = item; + } + + VerboseDebug($"Original: {ArrayStr(original)}"); + VerboseDebug($"Changed: {ArrayStr(changed)}"); + VerboseDebug($"Original2: {ArrayStr(original2)}"); + VerboseDebug($"Changed2: {ArrayStr(changed2)}"); + return (original, original2, changed, changed2); + } + + [Test] + [Repeat(5)] + public void WhenSerializingAndDeserializingVeryLargeValueTypeNativeArrayNetworkVariables_ValuesAreSerializedCorrectly( + + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), + typeof(NetworkVariableTestStruct), typeof(FixedString32Bytes))] + Type testType) + { + if (testType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(sbyte)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(short)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(ushort)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(int)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(uint)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(long)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(bool)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(char)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(float)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(double)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Vector3)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Vector2Int)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Vector2Int(rand.Next(), rand.Next()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Vector3Int)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Vector4)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Quaternion)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Color)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Color32)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Color32((byte)rand.Next(), (byte)rand.Next(), (byte)rand.Next(), (byte)rand.Next()) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Ray)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Ray( + new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()), + new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(Ray2D)) + { + (var original, var original2, var changed, var changed2) = GetArarys( + (rand) => new Ray2D( + new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), + new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ) + ); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(NetworkVariableTestStruct)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenBytes); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + else if (testType == typeof(FixedString32Bytes)) + { + (var original, var original2, var changed, var changed2) = GetArarys(RandGenFixedString32); + TestValueTypeNativeArray(original, changed); + TestValueTypeNativeArray(original2, changed2); + } + } + + + public string ListStr(List list) + { + var str = "["; + var comma = false; + foreach (var item in list) + { + if (comma) + { + str += ", "; + } + + comma = true; + str += $"{item}"; + } + + str += "]"; + return str; + } + + public string HashSetStr(HashSet list) where T : IEquatable + { + var str = "{"; + var comma = false; + foreach (var item in list) + { + if (comma) + { + str += ", "; + } + + comma = true; + str += $"{item}"; + } + + str += "}"; + return str; + } + + public string DictionaryStr(Dictionary list) + where TKey : IEquatable + { + var str = "{"; + var comma = false; + foreach (var item in list) + { + if (comma) + { + str += ", "; + } + + comma = true; + str += $"{item.Key}: {item.Value}"; + } + + str += "}"; + return str; + } + + public (List original, List original2, List changed, List changed2) GetLists(GetRandomElement generator) + { + var original = new List(); + var changed = new List(); + var original2 = new List(); + var changed2 = new List(); + + var rand = new System.Random(); + var originalSize = rand.Next(32, 64); + var changedSize = rand.Next(32, 64); + var changed2Changes = rand.Next(12, 16); + var changed2Adds = rand.Next(-16, 16); + for (var i = 0; i < originalSize; ++i) + { + var item = generator(rand); + original.Add(item); + original2.Add(item); + changed2.Add(item); + } + for (var i = 0; i < changedSize; ++i) + { + var item = generator(rand); + changed.Add(item); + } + + for (var i = 0; i < changed2Changes; ++i) + { + var idx = rand.Next(changed2.Count - 1); + var item = generator(rand); + changed2[idx] = item; + } + + if (changed2Adds < 0) + { + changed2.RemoveRange(changed2.Count + changed2Adds, -changed2Adds); + } + else + { + for (var i = 0; i < changed2Adds; ++i) + { + var item = generator(rand); + changed2.Add(item); + } + + } + + VerboseDebug($"Original: {ListStr(original)}"); + VerboseDebug($"Changed: {ListStr(changed)}"); + VerboseDebug($"Original2: {ListStr(original2)}"); + VerboseDebug($"Changed2: {ListStr(changed2)}"); + return (original, original2, changed, changed2); + } + + public (HashSet original, HashSet original2, HashSet changed, HashSet changed2) GetHashSets(GetRandomElement generator) where T : IEquatable + { + var original = new HashSet(); + var changed = new HashSet(); + var original2 = new HashSet(); + var changed2 = new HashSet(); + + var rand = new System.Random(); + var originalSize = rand.Next(32, 64); + var changedSize = rand.Next(32, 64); + var changed2Removes = rand.Next(12, 16); + var changed2Adds = rand.Next(12, 16); + for (var i = 0; i < originalSize; ++i) + { + var item = generator(rand); + while (original.Contains(item)) + { + item = generator(rand); + } + original.Add(item); + original2.Add(item); + changed2.Add(item); + } + for (var i = 0; i < changedSize; ++i) + { + var item = generator(rand); + while (changed.Contains(item)) + { + item = generator(rand); + } + changed.Add(item); + } + + for (var i = 0; i < changed2Removes; ++i) + { + var which = rand.Next(changed2.Count()); + T toRemove = default; + foreach (var check in changed2) + { + if (which == 0) + { + toRemove = check; + break; + } + --which; + } + + changed2.Remove(toRemove); + } + + for (var i = 0; i < changed2Adds; ++i) + { + var item = generator(rand); + while (changed2.Contains(item)) + { + item = generator(rand); + } + changed2.Add(item); + } + + VerboseDebug($"Original: {HashSetStr(original)}"); + VerboseDebug($"Changed: {HashSetStr(changed)}"); + VerboseDebug($"Original2: {HashSetStr(original2)}"); + VerboseDebug($"Changed2: {HashSetStr(changed2)}"); + return (original, original2, changed, changed2); + } + + + public (Dictionary original, Dictionary original2, Dictionary changed, Dictionary changed2) GetDictionaries(GetRandomElement keyGenerator, GetRandomElement valGenerator) + where TKey : IEquatable + { + var original = new Dictionary(); + var changed = new Dictionary(); + var original2 = new Dictionary(); + var changed2 = new Dictionary(); + + var rand = new System.Random(); + var originalSize = rand.Next(32, 64); + var changedSize = rand.Next(32, 64); + var changed2Removes = rand.Next(12, 16); + var changed2Adds = rand.Next(12, 16); + var changed2Changes = rand.Next(12, 16); + for (var i = 0; i < originalSize; ++i) + { + var key = keyGenerator(rand); + while (original.ContainsKey(key)) + { + key = keyGenerator(rand); + } + var val = valGenerator(rand); + original.Add(key, val); + original2.Add(key, val); + changed2.Add(key, val); + } + for (var i = 0; i < changedSize; ++i) + { + var key = keyGenerator(rand); + while (changed.ContainsKey(key)) + { + key = keyGenerator(rand); + } + var val = valGenerator(rand); + changed.Add(key, val); + } + + for (var i = 0; i < changed2Removes; ++i) + { + var which = rand.Next(changed2.Count()); + TKey toRemove = default; + foreach (var check in changed2) + { + if (which == 0) + { + toRemove = check.Key; + break; + } + --which; + } + + changed2.Remove(toRemove); + } + + for (var i = 0; i < changed2Changes; ++i) + { + var which = rand.Next(changed2.Count()); + TKey key = default; + foreach (var check in changed2) + { + if (which == 0) + { + key = check.Key; + break; + } + --which; + } + + var val = valGenerator(rand); + changed2[key] = val; + } + + for (var i = 0; i < changed2Adds; ++i) + { + var key = keyGenerator(rand); + while (changed2.ContainsKey(key)) + { + key = keyGenerator(rand); + } + var val = valGenerator(rand); + changed2.Add(key, val); + } + + VerboseDebug($"Original: {DictionaryStr(original)}"); + VerboseDebug($"Changed: {DictionaryStr(changed)}"); + VerboseDebug($"Original2: {DictionaryStr(original2)}"); + VerboseDebug($"Changed2: {DictionaryStr(changed2)}"); + return (original, original2, changed, changed2); + } + + [Test] + [Repeat(5)] + public void WhenSerializingAndDeserializingVeryLargeListNetworkVariables_ValuesAreSerializedCorrectly( + + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), + typeof(NetworkVariableTestClass), typeof(FixedString32Bytes))] + Type testType) + { + if (testType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(sbyte)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(short)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(ushort)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(int)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(uint)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(long)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(bool)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(char)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(float)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(double)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenBytes); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Vector3)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Vector2Int)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Vector2Int(rand.Next(), rand.Next()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Vector3Int)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Vector4)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Quaternion)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Color)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Color32)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Color32((byte)rand.Next(), (byte)rand.Next(), (byte)rand.Next(), (byte)rand.Next()) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Ray)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Ray( + new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()), + new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(Ray2D)) + { + (var original, var original2, var changed, var changed2) = GetLists( + (rand) => new Ray2D( + new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), + new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ) + ); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(NetworkVariableTestClass)) + { + (var original, var original2, var changed, var changed2) = GetLists((rand) => + { + return new NetworkVariableTestClass { Data = RandGenBytes(rand) }; + }); + TestList(original, changed); + TestList(original2, changed2); + } + else if (testType == typeof(FixedString32Bytes)) + { + (var original, var original2, var changed, var changed2) = GetLists(RandGenFixedString32); + TestList(original, changed); + TestList(original2, changed2); + } + } + + [Test] + [Repeat(5)] + public void WhenSerializingAndDeserializingVeryLargeHashSetNetworkVariables_ValuesAreSerializedCorrectly( + + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), + typeof(Quaternion), typeof(HashableNetworkVariableTestClass), typeof(FixedString32Bytes))] + Type testType) + { + if (testType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(sbyte)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(short)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(ushort)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(int)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(uint)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(long)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(bool)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(char)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(float)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(double)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenBytes); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetHashSets( + (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(Vector3)) + { + (var original, var original2, var changed, var changed2) = GetHashSets( + (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(Vector2Int)) + { + (var original, var original2, var changed, var changed2) = GetHashSets( + (rand) => new Vector2Int(rand.Next(), rand.Next()) + ); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(Vector3Int)) + { + (var original, var original2, var changed, var changed2) = GetHashSets( + (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next()) + ); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(Vector4)) + { + (var original, var original2, var changed, var changed2) = GetHashSets( + (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(Quaternion)) + { + (var original, var original2, var changed, var changed2) = GetHashSets( + (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(Color)) + { + (var original, var original2, var changed, var changed2) = GetHashSets( + (rand) => new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(HashableNetworkVariableTestClass)) + { + (var original, var original2, var changed, var changed2) = GetHashSets((rand) => + { + return new HashableNetworkVariableTestClass { Data = RandGenBytes(rand) }; + }); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + else if (testType == typeof(FixedString32Bytes)) + { + (var original, var original2, var changed, var changed2) = GetHashSets(RandGenFixedString32); + TestHashSet(original, changed); + TestHashSet(original2, changed2); + } + } + + [Test] + [Repeat(5)] + public void WhenSerializingAndDeserializingVeryLargeDictionaryNetworkVariables_ValuesAreSerializedCorrectly( + + [Values(typeof(byte), typeof(ulong), typeof(Vector2), typeof(HashMapKeyClass))] Type keyType, + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), + typeof(Quaternion), typeof(HashMapValClass), typeof(FixedString32Bytes))] + Type valType) + { + if (valType == typeof(byte)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(sbyte)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(short)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(ushort)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(int)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(uint)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(long)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(ulong)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(bool)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(char)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(float)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(double)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenBytes); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(Vector2)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(Vector3)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(Vector2Int)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(Vector3Int)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(Vector4)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(Quaternion)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(HashMapValClass)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new HashMapValClass { Data = RandGenBytes(rand) }); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, (rand) => new HashMapValClass { Data = RandGenBytes(rand) }); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new HashMapValClass { Data = RandGenBytes(rand) }); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, (rand) => new HashMapValClass { Data = RandGenBytes(rand) }); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + else if (valType == typeof(FixedString32Bytes)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenFixedString32); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries(RandGenBytes, RandGenFixedString32); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenFixedString32); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyClass)) + { + (var original, var original2, var changed, var changed2) = GetDictionaries((rand) => new HashMapKeyClass { Data = RandGenBytes(rand) }, RandGenFixedString32); + TestDictionary(original, changed); + TestDictionary(original2, changed2); + } + } + } + + #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT [Test] public void WhenSerializingAndDeserializingValueTypeNativeListNetworkVariables_ValuesAreSerializedCorrectly( @@ -2373,6 +3805,1206 @@ namespace Unity.Netcode.RuntimeTests }); } } + + public string NativeListStr(NativeList list) where T : unmanaged + { + var str = "["; + var comma = false; + foreach (var item in list) + { + if (comma) + { + str += ", "; + } + + comma = true; + str += $"{item}"; + } + + str += "]"; + return str; + } + + public string NativeHashSetStr(NativeHashSet list) where T : unmanaged, IEquatable + { + var str = "{"; + var comma = false; + foreach (var item in list) + { + if (comma) + { + str += ", "; + } + + comma = true; + str += $"{item}"; + } + + str += "}"; + return str; + } + + public string NativeHashMapStr(NativeHashMap list) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + var str = "{"; + var comma = false; + foreach (var item in list) + { + if (comma) + { + str += ", "; + } + + comma = true; + str += $"{item.Key}: {item.Value}"; + } + + str += "}"; + return str; + } + + public (NativeList original, NativeList original2, NativeList changed, NativeList changed2) GetNativeLists(GetRandomElement generator) where T : unmanaged + { + var original = new NativeList(Allocator.Temp); + var changed = new NativeList(Allocator.Temp); + var original2 = new NativeList(Allocator.Temp); + var changed2 = new NativeList(Allocator.Temp); + + var rand = new System.Random(); + var originalSize = rand.Next(32, 64); + var changedSize = rand.Next(32, 64); + var changed2Changes = rand.Next(12, 16); + var changed2Adds = rand.Next(-16, 16); + for (var i = 0; i < originalSize; ++i) + { + var item = generator(rand); + original.Add(item); + original2.Add(item); + changed2.Add(item); + } + for (var i = 0; i < changedSize; ++i) + { + var item = generator(rand); + changed.Add(item); + } + + for (var i = 0; i < changed2Changes; ++i) + { + var idx = rand.Next(changed2.Length - 1); + var item = generator(rand); + changed2[idx] = item; + } + + if (changed2Adds < 0) + { + changed2.Resize(changed2.Length + changed2Adds, NativeArrayOptions.UninitializedMemory); + } + else + { + for (var i = 0; i < changed2Adds; ++i) + { + var item = generator(rand); + changed2.Add(item); + } + + } + + VerboseDebug($"Original: {NativeListStr(original)}"); + VerboseDebug($"Changed: {NativeListStr(changed)}"); + VerboseDebug($"Original2: {NativeListStr(original2)}"); + VerboseDebug($"Changed2: {NativeListStr(changed2)}"); + return (original, original2, changed, changed2); + } + + public (NativeHashSet original, NativeHashSet original2, NativeHashSet changed, NativeHashSet changed2) GetNativeHashSets(GetRandomElement generator) where T : unmanaged, IEquatable + { + var original = new NativeHashSet(16, Allocator.Temp); + var changed = new NativeHashSet(16, Allocator.Temp); + var original2 = new NativeHashSet(16, Allocator.Temp); + var changed2 = new NativeHashSet(16, Allocator.Temp); + + var rand = new System.Random(); + var originalSize = rand.Next(32, 64); + var changedSize = rand.Next(32, 64); + var changed2Removes = rand.Next(12, 16); + var changed2Adds = rand.Next(12, 16); + for (var i = 0; i < originalSize; ++i) + { + var item = generator(rand); + while (original.Contains(item)) + { + item = generator(rand); + } + original.Add(item); + original2.Add(item); + changed2.Add(item); + } + for (var i = 0; i < changedSize; ++i) + { + var item = generator(rand); + while (changed.Contains(item)) + { + item = generator(rand); + } + changed.Add(item); + } + + for (var i = 0; i < changed2Removes; ++i) + { + var which = rand.Next(changed2.Count()); + T toRemove = default; + foreach (var check in changed2) + { + if (which == 0) + { + toRemove = check; + break; + } + --which; + } + + changed2.Remove(toRemove); + } + + for (var i = 0; i < changed2Adds; ++i) + { + var item = generator(rand); + while (changed2.Contains(item)) + { + item = generator(rand); + } + changed2.Add(item); + } + + VerboseDebug($"Original: {NativeHashSetStr(original)}"); + VerboseDebug($"Changed: {NativeHashSetStr(changed)}"); + VerboseDebug($"Original2: {NativeHashSetStr(original2)}"); + VerboseDebug($"Changed2: {NativeHashSetStr(changed2)}"); + return (original, original2, changed, changed2); + } + + + public (NativeHashMap original, NativeHashMap original2, NativeHashMap changed, NativeHashMap changed2) GetMaps(GetRandomElement keyGenerator, GetRandomElement valGenerator) + where TKey : unmanaged, IEquatable + where TVal : unmanaged + { + var original = new NativeHashMap(16, Allocator.Temp); + var changed = new NativeHashMap(16, Allocator.Temp); + var original2 = new NativeHashMap(16, Allocator.Temp); + var changed2 = new NativeHashMap(16, Allocator.Temp); + + var rand = new System.Random(); + var originalSize = rand.Next(32, 64); + var changedSize = rand.Next(32, 64); + var changed2Removes = rand.Next(12, 16); + var changed2Adds = rand.Next(12, 16); + var changed2Changes = rand.Next(12, 16); + for (var i = 0; i < originalSize; ++i) + { + var key = keyGenerator(rand); + while (original.ContainsKey(key)) + { + key = keyGenerator(rand); + } + var val = valGenerator(rand); + original.Add(key, val); + original2.Add(key, val); + changed2.Add(key, val); + } + for (var i = 0; i < changedSize; ++i) + { + var key = keyGenerator(rand); + while (changed.ContainsKey(key)) + { + key = keyGenerator(rand); + } + var val = valGenerator(rand); + changed.Add(key, val); + } + + for (var i = 0; i < changed2Removes; ++i) + { + var which = rand.Next(changed2.Count()); + TKey toRemove = default; + foreach (var check in changed2) + { + if (which == 0) + { + toRemove = check.Key; + break; + } + --which; + } + + changed2.Remove(toRemove); + } + + for (var i = 0; i < changed2Changes; ++i) + { + var which = rand.Next(changed2.Count()); + TKey key = default; + foreach (var check in changed2) + { + if (which == 0) + { + key = check.Key; + break; + } + --which; + } + + var val = valGenerator(rand); + changed2[key] = val; + } + + for (var i = 0; i < changed2Adds; ++i) + { + var key = keyGenerator(rand); + while (changed2.ContainsKey(key)) + { + key = keyGenerator(rand); + } + var val = valGenerator(rand); + changed2.Add(key, val); + } + + VerboseDebug($"Original: {NativeHashMapStr(original)}"); + VerboseDebug($"Changed: {NativeHashMapStr(changed)}"); + VerboseDebug($"Original2: {NativeHashMapStr(original2)}"); + VerboseDebug($"Changed2: {NativeHashMapStr(changed2)}"); + return (original, original2, changed, changed2); + } + + [Test] + [Repeat(5)] + public void WhenSerializingAndDeserializingVeryLargeValueTypeNativeListNetworkVariables_ValuesAreSerializedCorrectly( + + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), + typeof(NetworkVariableTestStruct), typeof(FixedString32Bytes))] + Type testType) + { + if (testType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(sbyte)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(short)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(ushort)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(int)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(uint)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(long)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(bool)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(char)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(float)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(double)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Vector3)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Vector2Int)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Vector2Int(rand.Next(), rand.Next()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Vector3Int)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Vector4)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Quaternion)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Color)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Color32)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Color32((byte)rand.Next(), (byte)rand.Next(), (byte)rand.Next(), (byte)rand.Next()) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Ray)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Ray( + new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()), + new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(Ray2D)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists( + (rand) => new Ray2D( + new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), + new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ) + ); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(NetworkVariableTestStruct)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenBytes); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + else if (testType == typeof(FixedString32Bytes)) + { + (var original, var original2, var changed, var changed2) = GetNativeLists(RandGenFixedString32); + TestValueTypeNativeList(original, changed); + TestValueTypeNativeList(original2, changed2); + } + } + + [Test] + [Repeat(5)] + public void WhenSerializingAndDeserializingVeryLargeValueTypeNativeHashSetNetworkVariables_ValuesAreSerializedCorrectly( + + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), + typeof(Quaternion), typeof(HashableNetworkVariableTestStruct), typeof(FixedString32Bytes))] + Type testType) + { + if (testType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(sbyte)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(short)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(ushort)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(int)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(uint)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(long)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(bool)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(char)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(float)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(double)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets( + (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(Vector3)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets( + (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(Vector2Int)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets( + (rand) => new Vector2Int(rand.Next(), rand.Next()) + ); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(Vector3Int)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets( + (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next()) + ); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(Vector4)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets( + (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(Quaternion)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets( + (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(Color)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets( + (rand) => new Color((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble()) + ); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(HashableNetworkVariableTestStruct)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenBytes); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + else if (testType == typeof(FixedString32Bytes)) + { + (var original, var original2, var changed, var changed2) = GetNativeHashSets(RandGenFixedString32); + TestValueTypeNativeHashSet(original, changed); + TestValueTypeNativeHashSet(original2, changed2); + } + } + + [Test] + [Repeat(5)] + public void WhenSerializingAndDeserializingVeryLargeValueTypeNativeHashMapNetworkVariables_ValuesAreSerializedCorrectly( + + [Values(typeof(byte), typeof(ulong), typeof(Vector2), typeof(HashMapKeyStruct))] Type keyType, + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(Vector2), typeof(Vector3), typeof(Vector2Int), typeof(Vector3Int), typeof(Vector4), + typeof(Quaternion), typeof(HashMapValStruct), typeof(FixedString32Bytes))] + Type valType) + { + if (valType == typeof(byte)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(sbyte)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(short)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(ushort)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(int)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(uint)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(long)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(ulong)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(bool)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(char)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(float)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(double)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(Vector2)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(Vector3)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector3((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(Vector2Int)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector2Int(rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(Vector3Int)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector3Int(rand.Next(), rand.Next(), rand.Next())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(Vector4)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Vector4((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(Quaternion)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, (rand) => new Quaternion((float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble(), (float)rand.NextDouble())); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(HashMapValStruct)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenBytes); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + else if (valType == typeof(FixedString32Bytes)) + { + if (keyType == typeof(byte)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenFixedString32); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(ulong)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenFixedString32); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + else if (keyType == typeof(Vector2)) + { + (var original, var original2, var changed, var changed2) = GetMaps((rand) => new Vector2((float)rand.NextDouble(), (float)rand.NextDouble()), RandGenFixedString32); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + + } + else if (keyType == typeof(HashMapKeyStruct)) + { + (var original, var original2, var changed, var changed2) = GetMaps(RandGenBytes, RandGenFixedString32); + TestValueTypeNativeHashMap(original, changed); + TestValueTypeNativeHashMap(original2, changed2); + } + } + } + #endif [Test] diff --git a/Tests/Runtime/NetworkVariableTestsHelperTypes.cs b/Tests/Runtime/NetworkVariableTestsHelperTypes.cs new file mode 100644 index 0000000..67d54cd --- /dev/null +++ b/Tests/Runtime/NetworkVariableTestsHelperTypes.cs @@ -0,0 +1,936 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetVarPermTestComp : NetworkBehaviour + { + public NetworkVariable OwnerWritable_Position = new NetworkVariable(Vector3.one, NetworkVariableBase.DefaultReadPerm, NetworkVariableWritePermission.Owner); + public NetworkVariable ServerWritable_Position = new NetworkVariable(Vector3.one, NetworkVariableBase.DefaultReadPerm, NetworkVariableWritePermission.Server); + public NetworkVariable OwnerReadWrite_Position = new NetworkVariable(Vector3.one, NetworkVariableReadPermission.Owner, NetworkVariableWritePermission.Owner); + } + + public class NetworkVariableMiddleclass : NetworkVariable + { + + } + + public class NetworkVariableSubclass : NetworkVariableMiddleclass + { + + } + + public class NetworkBehaviourWithNetVarArray : NetworkBehaviour + { + public NetworkVariable Int0 = new NetworkVariable(); + public NetworkVariable Int1 = new NetworkVariable(); + public NetworkVariable Int2 = new NetworkVariable(); + public NetworkVariable Int3 = new NetworkVariable(); + public NetworkVariable Int4 = new NetworkVariable(); + public NetworkVariable[] AllInts = new NetworkVariable[5]; + + public int InitializedFieldCount => NetworkVariableFields.Count; + + + private void Awake() + { + AllInts[0] = Int0; + AllInts[1] = Int1; + AllInts[2] = Int2; + AllInts[3] = Int3; + AllInts[4] = Int4; + } + } + + internal struct TypeReferencedOnlyInCustomSerialization1 : INetworkSerializeByMemcpy + { + public int I; + } + + internal struct TypeReferencedOnlyInCustomSerialization2 : INetworkSerializeByMemcpy + { + public int I; + } + + internal struct TypeReferencedOnlyInCustomSerialization3 : INetworkSerializeByMemcpy + { + public int I; + } + + internal struct TypeReferencedOnlyInCustomSerialization4 : INetworkSerializeByMemcpy + { + public int I; + } + + internal struct TypeReferencedOnlyInCustomSerialization5 : INetworkSerializeByMemcpy + { + public int I; + } + + internal struct TypeReferencedOnlyInCustomSerialization6 : INetworkSerializeByMemcpy + { + public int I; + } + + // Both T and U are serializable + [GenerateSerializationForGenericParameter(0)] + [GenerateSerializationForGenericParameter(1)] + internal class CustomSerializableClass + { + + } + + // Only U is serializable + [GenerateSerializationForGenericParameter(1)] + internal class CustomSerializableBaseClass + { + + } + + // T is serializable, passes TypeReferencedOnlyInCustomSerialization3 as U to the subclass, making it serializable + [GenerateSerializationForGenericParameter(0)] + internal class CustomSerializableSubclass : CustomSerializableBaseClass + { + + } + + // T is serializable, passes TypeReferencedOnlyInCustomSerialization3 as U to the subclass, making it serializable + [GenerateSerializationForGenericParameter(0)] + internal class CustomSerializableSubclassWithNativeArray : CustomSerializableBaseClass> + { + + } + + internal class CustomGenericSerializationTestBehaviour : NetworkBehaviour + { + public CustomSerializableClass Value1; + public CustomSerializableClass, NativeArray> Value2; + public CustomSerializableSubclass Value3; + public CustomSerializableSubclassWithNativeArray> Value4; + } + + [GenerateSerializationForType(typeof(TypeReferencedOnlyInCustomSerialization5))] + [GenerateSerializationForType(typeof(NativeArray))] + internal struct SomeRandomStruct + { + [GenerateSerializationForType(typeof(TypeReferencedOnlyInCustomSerialization6))] + [GenerateSerializationForType(typeof(NativeArray))] + public void Foo() + { + + } + } + + public struct TemplatedValueOnlyReferencedByNetworkVariableSubclass : INetworkSerializeByMemcpy + where T : unmanaged + { + public T Value; + } + + public enum ByteEnum : byte + { + A, + B, + C = byte.MaxValue + } + public enum SByteEnum : sbyte + { + A, + B, + C = sbyte.MaxValue + } + public enum ShortEnum : short + { + A, + B, + C = short.MaxValue + } + public enum UShortEnum : ushort + { + A, + B, + C = ushort.MaxValue + } + public enum IntEnum : int + { + A, + B, + C = int.MaxValue + } + public enum UIntEnum : uint + { + A, + B, + C = uint.MaxValue + } + public enum LongEnum : long + { + A, + B, + C = long.MaxValue + } + public enum ULongEnum : ulong + { + A, + B, + C = ulong.MaxValue + } + + public struct HashableNetworkVariableTestStruct : INetworkSerializeByMemcpy, IEquatable + { + public byte A; + public short B; + public ushort C; + public int D; + public uint E; + public long F; + public ulong G; + public bool H; + public char I; + public float J; + public double K; + + public bool Equals(HashableNetworkVariableTestStruct other) + { + return A == other.A && B == other.B && C == other.C && D == other.D && E == other.E && F == other.F && G == other.G && H == other.H && I == other.I && J.Equals(other.J) && K.Equals(other.K); + } + + public override bool Equals(object obj) + { + return obj is HashableNetworkVariableTestStruct other && Equals(other); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(A); + hashCode.Add(B); + hashCode.Add(C); + hashCode.Add(D); + hashCode.Add(E); + hashCode.Add(F); + hashCode.Add(G); + hashCode.Add(H); + hashCode.Add(I); + hashCode.Add(J); + hashCode.Add(K); + return hashCode.ToHashCode(); + } + } + + public struct HashMapKeyStruct : INetworkSerializeByMemcpy, IEquatable + { + public byte A; + public short B; + public ushort C; + public int D; + public uint E; + public long F; + public ulong G; + public bool H; + public char I; + public float J; + public double K; + + public bool Equals(HashMapKeyStruct other) + { + return A == other.A && B == other.B && C == other.C && D == other.D && E == other.E && F == other.F && G == other.G && H == other.H && I == other.I && J.Equals(other.J) && K.Equals(other.K); + } + + public override bool Equals(object obj) + { + return obj is HashMapKeyStruct other && Equals(other); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(A); + hashCode.Add(B); + hashCode.Add(C); + hashCode.Add(D); + hashCode.Add(E); + hashCode.Add(F); + hashCode.Add(G); + hashCode.Add(H); + hashCode.Add(I); + hashCode.Add(J); + hashCode.Add(K); + return hashCode.ToHashCode(); + } + } + + public struct HashMapValStruct : INetworkSerializeByMemcpy, IEquatable + { + public byte A; + public short B; + public ushort C; + public int D; + public uint E; + public long F; + public ulong G; + public bool H; + public char I; + public float J; + public double K; + + public bool Equals(HashMapValStruct other) + { + return A == other.A && B == other.B && C == other.C && D == other.D && E == other.E && F == other.F && G == other.G && H == other.H && I == other.I && J.Equals(other.J) && K.Equals(other.K); + } + + public override bool Equals(object obj) + { + return obj is HashMapValStruct other && Equals(other); + } + + public override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(A); + hashCode.Add(B); + hashCode.Add(C); + hashCode.Add(D); + hashCode.Add(E); + hashCode.Add(F); + hashCode.Add(G); + hashCode.Add(H); + hashCode.Add(I); + hashCode.Add(J); + hashCode.Add(K); + return hashCode.ToHashCode(); + } + } + + public struct NetworkVariableTestStruct : INetworkSerializeByMemcpy + { + public byte A; + public short B; + public ushort C; + public int D; + public uint E; + public long F; + public ulong G; + public bool H; + public char I; + public float J; + public double K; + + private static System.Random s_Random = new System.Random(); + + public static NetworkVariableTestStruct GetTestStruct() + { + var testStruct = new NetworkVariableTestStruct + { + A = (byte)s_Random.Next(), + B = (short)s_Random.Next(), + C = (ushort)s_Random.Next(), + D = s_Random.Next(), + E = (uint)s_Random.Next(), + F = ((long)s_Random.Next() << 32) + s_Random.Next(), + G = ((ulong)s_Random.Next() << 32) + (ulong)s_Random.Next(), + H = true, + I = '\u263a', + J = (float)s_Random.NextDouble(), + K = s_Random.NextDouble(), + }; + + return testStruct; + } + } + + + public class HashableNetworkVariableTestClass : INetworkSerializable, IEquatable + { + public HashableNetworkVariableTestStruct Data; + + public bool Equals(HashableNetworkVariableTestClass other) + { + return Data.Equals(other.Data); + } + + public override bool Equals(object obj) + { + return obj is HashableNetworkVariableTestClass other && Equals(other); + } + + public override int GetHashCode() + { + return Data.GetHashCode(); + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Data); + } + } + + public class HashMapKeyClass : INetworkSerializable, IEquatable + { + public HashMapKeyStruct Data; + + public bool Equals(HashMapKeyClass other) + { + return Data.Equals(other.Data); + } + + public override bool Equals(object obj) + { + return obj is HashMapKeyClass other && Equals(other); + } + + public override int GetHashCode() + { + return Data.GetHashCode(); + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Data); + } + } + + public class HashMapValClass : INetworkSerializable, IEquatable + { + public HashMapValStruct Data; + + public bool Equals(HashMapValClass other) + { + return Data.Equals(other.Data); + } + + public override bool Equals(object obj) + { + return obj is HashMapValClass other && Equals(other); + } + + public override int GetHashCode() + { + return Data.GetHashCode(); + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Data); + } + } + + public class NetworkVariableTestClass : INetworkSerializable, IEquatable + { + public NetworkVariableTestStruct Data; + + public bool Equals(NetworkVariableTestClass other) + { + return NetworkVariableSerialization.AreEqual(ref Data, ref other.Data); + } + + public override bool Equals(object obj) + { + return obj is NetworkVariableTestClass other && Equals(other); + } + + // This type is not used for hashing, we just need to implement IEquatable to verify lists match. + public override int GetHashCode() + { + return 0; + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Data); + } + } + + + // The ILPP code for NetworkVariables to determine how to serialize them relies on them existing as fields of a NetworkBehaviour to find them. + // Some of the tests below create NetworkVariables on the stack, so this class is here just to make sure the relevant types are all accounted for. + public class NetVarILPPClassForTests : NetworkBehaviour + { + public NetworkVariable ByteVar; + public NetworkVariable> ByteArrayVar; + public NetworkVariable> ByteManagedListVar; + public NetworkVariable> ByteManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> ByteListVar; + public NetworkVariable> ByteHashSetVar; + public NetworkVariable> ByteByteHashMapVar; + public NetworkVariable> ULongByteHashMapVar; + public NetworkVariable> Vector2ByteHashMapVar; + public NetworkVariable> HashMapKeyStructByteHashMapVar; +#endif + public NetworkVariable> ByteByteDictionaryVar; + public NetworkVariable> ULongByteDictionaryVar; + public NetworkVariable> Vector2ByteDictionaryVar; + public NetworkVariable> HashMapKeyClassByteDictionaryVar; + + public NetworkVariable SbyteVar; + public NetworkVariable> SbyteArrayVar; + public NetworkVariable> SbyteManagedListVar; + public NetworkVariable> SbyteManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> SbyteListVar; + public NetworkVariable> SbyteHashSetVar; + public NetworkVariable> ByteSbyteHashMapVar; + public NetworkVariable> ULongSbyteHashMapVar; + public NetworkVariable> Vector2SbyteHashMapVar; + public NetworkVariable> HashMapKeyStructSbyteHashMapVar; +#endif + public NetworkVariable> ByteSbyteDictionaryVar; + public NetworkVariable> ULongSbyteDictionaryVar; + public NetworkVariable> Vector2SbyteDictionaryVar; + public NetworkVariable> HashMapKeyClassSbyteDictionaryVar; + + public NetworkVariable ShortVar; + public NetworkVariable> ShortArrayVar; + public NetworkVariable> ShortManagedListVar; + public NetworkVariable> ShortManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> ShortListVar; + public NetworkVariable> ShortHashSetVar; + public NetworkVariable> ByteShortHashMapVar; + public NetworkVariable> ULongShortHashMapVar; + public NetworkVariable> Vector2ShortHashMapVar; + public NetworkVariable> HashMapKeyStructShortHashMapVar; +#endif + public NetworkVariable> ByteShortDictionaryVar; + public NetworkVariable> ULongShortDictionaryVar; + public NetworkVariable> Vector2ShortDictionaryVar; + public NetworkVariable> HashMapKeyClassShortDictionaryVar; + + public NetworkVariable UshortVar; + public NetworkVariable> UshortArrayVar; + public NetworkVariable> UshortManagedListVar; + public NetworkVariable> UshortManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> UshortListVar; + public NetworkVariable> UshortHashSetVar; + public NetworkVariable> ByteUshortHashMapVar; + public NetworkVariable> ULongUshortHashMapVar; + public NetworkVariable> Vector2UshortHashMapVar; + public NetworkVariable> HashMapKeyStructUshortHashMapVar; +#endif + public NetworkVariable> ByteUshortDictionaryVar; + public NetworkVariable> ULongUshortDictionaryVar; + public NetworkVariable> Vector2UshortDictionaryVar; + public NetworkVariable> HashMapKeyClassUshortDictionaryVar; + + public NetworkVariable IntVar; + public NetworkVariable> IntArrayVar; + public NetworkVariable> IntManagedListVar; + public NetworkVariable> IntManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> IntListVar; + public NetworkVariable> IntHashSetVar; + public NetworkVariable> ByteIntHashMapVar; + public NetworkVariable> ULongIntHashMapVar; + public NetworkVariable> Vector2IntHashMapVar; + public NetworkVariable> HashMapKeyStructIntHashMapVar; +#endif + public NetworkVariable> ByteIntDictionaryVar; + public NetworkVariable> ULongIntDictionaryVar; + public NetworkVariable> Vector2IntDictionaryVar; + public NetworkVariable> HashMapKeyClassIntDictionaryVar; + + public NetworkVariable UintVar; + public NetworkVariable> UintArrayVar; + public NetworkVariable> UintManagedListVar; + public NetworkVariable> UintManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> UintListVar; + public NetworkVariable> UintHashSetVar; + public NetworkVariable> ByteUintHashMapVar; + public NetworkVariable> ULongUintHashMapVar; + public NetworkVariable> Vector2UintHashMapVar; + public NetworkVariable> HashMapKeyStructUintHashMapVar; +#endif + public NetworkVariable> ByteUintDictionaryVar; + public NetworkVariable> ULongUintDictionaryVar; + public NetworkVariable> Vector2UintDictionaryVar; + public NetworkVariable> HashMapKeyClassUintDictionaryVar; + + public NetworkVariable LongVar; + public NetworkVariable> LongArrayVar; + public NetworkVariable> LongManagedListVar; + public NetworkVariable> LongManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> LongListVar; + public NetworkVariable> LongHashSetVar; + public NetworkVariable> ByteLongHashMapVar; + public NetworkVariable> ULongLongHashMapVar; + public NetworkVariable> Vector2LongHashMapVar; + public NetworkVariable> HashMapKeyStructLongHashMapVar; +#endif + public NetworkVariable> ByteLongDictionaryVar; + public NetworkVariable> ULongLongDictionaryVar; + public NetworkVariable> Vector2LongDictionaryVar; + public NetworkVariable> HashMapKeyClassLongDictionaryVar; + + public NetworkVariable UlongVar; + public NetworkVariable> UlongArrayVar; + public NetworkVariable> UlongManagedListVar; + public NetworkVariable> UlongManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> UlongListVar; + public NetworkVariable> UlongHashSetVar; + public NetworkVariable> ByteUlongHashMapVar; + public NetworkVariable> ULongUlongHashMapVar; + public NetworkVariable> Vector2UlongHashMapVar; + public NetworkVariable> HashMapKeyStructUlongHashMapVar; +#endif + public NetworkVariable> ByteUlongDictionaryVar; + public NetworkVariable> ULongUlongDictionaryVar; + public NetworkVariable> Vector2UlongDictionaryVar; + public NetworkVariable> HashMapKeyClassUlongDictionaryVar; + + public NetworkVariable BoolVar; + public NetworkVariable> BoolArrayVar; + public NetworkVariable> BoolManagedListVar; + public NetworkVariable> BoolManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> BoolListVar; + public NetworkVariable> BoolHashSetVar; + public NetworkVariable> ByteBoolHashMapVar; + public NetworkVariable> ULongBoolHashMapVar; + public NetworkVariable> Vector2BoolHashMapVar; + public NetworkVariable> HashMapKeyStructBoolHashMapVar; +#endif + public NetworkVariable> ByteBoolDictionaryVar; + public NetworkVariable> ULongBoolDictionaryVar; + public NetworkVariable> Vector2BoolDictionaryVar; + public NetworkVariable> HashMapKeyClassBoolDictionaryVar; + + public NetworkVariable CharVar; + public NetworkVariable> CharArrayVar; + public NetworkVariable> CharManagedListVar; + public NetworkVariable> CharManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> CharListVar; + public NetworkVariable> CharHashSetVar; + public NetworkVariable> ByteCharHashMapVar; + public NetworkVariable> ULongCharHashMapVar; + public NetworkVariable> Vector2CharHashMapVar; + public NetworkVariable> HashMapKeyStructCharHashMapVar; +#endif + public NetworkVariable> ByteCharDictionaryVar; + public NetworkVariable> ULongCharDictionaryVar; + public NetworkVariable> Vector2CharDictionaryVar; + public NetworkVariable> HashMapKeyClassCharDictionaryVar; + + public NetworkVariable FloatVar; + public NetworkVariable> FloatArrayVar; + public NetworkVariable> FloatManagedListVar; + public NetworkVariable> FloatManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> FloatListVar; + public NetworkVariable> FloatHashSetVar; + public NetworkVariable> ByteFloatHashMapVar; + public NetworkVariable> ULongFloatHashMapVar; + public NetworkVariable> Vector2FloatHashMapVar; + public NetworkVariable> HashMapKeyStructFloatHashMapVar; +#endif + public NetworkVariable> ByteFloatDictionaryVar; + public NetworkVariable> ULongFloatDictionaryVar; + public NetworkVariable> Vector2FloatDictionaryVar; + public NetworkVariable> HashMapKeyClassFloatDictionaryVar; + + public NetworkVariable DoubleVar; + public NetworkVariable> DoubleArrayVar; + public NetworkVariable> DoubleManagedListVar; + public NetworkVariable> DoubleManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> DoubleListVar; + public NetworkVariable> DoubleHashSetVar; + public NetworkVariable> ByteDoubleHashMapVar; + public NetworkVariable> ULongDoubleHashMapVar; + public NetworkVariable> Vector2DoubleHashMapVar; + public NetworkVariable> HashMapKeyStructDoubleHashMapVar; +#endif + public NetworkVariable> ByteDoubleDictionaryVar; + public NetworkVariable> ULongDoubleDictionaryVar; + public NetworkVariable> Vector2DoubleDictionaryVar; + public NetworkVariable> HashMapKeyClassDoubleDictionaryVar; + + public NetworkVariable ByteEnumVar; + public NetworkVariable> ByteEnumArrayVar; + public NetworkVariable> ByteEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> ByteEnumListVar; +#endif + public NetworkVariable SByteEnumVar; + public NetworkVariable> SByteEnumArrayVar; + public NetworkVariable> SByteEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> SByteEnumListVar; +#endif + public NetworkVariable ShortEnumVar; + public NetworkVariable> ShortEnumArrayVar; + public NetworkVariable> ShortEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> ShortEnumListVar; +#endif + public NetworkVariable UShortEnumVar; + public NetworkVariable> UShortEnumArrayVar; + public NetworkVariable> UShortEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> UShortEnumListVar; +#endif + public NetworkVariable IntEnumVar; + public NetworkVariable> IntEnumArrayVar; + public NetworkVariable> IntEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> IntEnumListVar; +#endif + public NetworkVariable UIntEnumVar; + public NetworkVariable> UIntEnumArrayVar; + public NetworkVariable> UIntEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> UIntEnumListVar; +#endif + public NetworkVariable LongEnumVar; + public NetworkVariable> LongEnumArrayVar; + public NetworkVariable> LongEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> LongEnumListVar; +#endif + public NetworkVariable ULongEnumVar; + public NetworkVariable> ULongEnumArrayVar; + public NetworkVariable> ULongEnumManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> ULongEnumListVar; +#endif + public NetworkVariable Vector2Var; + public NetworkVariable> Vector2ArrayVar; + public NetworkVariable> Vector2ManagedListVar; + public NetworkVariable> Vector2ManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> Vector2ListVar; + public NetworkVariable> Vector2HashSetVar; + public NetworkVariable> ByteVector2HashMapVar; + public NetworkVariable> ULongVector2HashMapVar; + public NetworkVariable> Vector2Vector2HashMapVar; + public NetworkVariable> HashMapKeyStructVector2HashMapVar; +#endif + public NetworkVariable> ByteVector2DictionaryVar; + public NetworkVariable> ULongVector2DictionaryVar; + public NetworkVariable> Vector2Vector2DictionaryVar; + public NetworkVariable> HashMapKeyClassVector2DictionaryVar; + + public NetworkVariable Vector3Var; + public NetworkVariable> Vector3ArrayVar; + public NetworkVariable> Vector3ManagedListVar; + public NetworkVariable> Vector3ManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> Vector3ListVar; + public NetworkVariable> Vector3HashSetVar; + public NetworkVariable> ByteVector3HashMapVar; + public NetworkVariable> ULongVector3HashMapVar; + public NetworkVariable> Vector2Vector3HashMapVar; + public NetworkVariable> HashMapKeyStructVector3HashMapVar; +#endif + public NetworkVariable> ByteVector3DictionaryVar; + public NetworkVariable> ULongVector3DictionaryVar; + public NetworkVariable> Vector2Vector3DictionaryVar; + public NetworkVariable> HashMapKeyClassVector3DictionaryVar; + + public NetworkVariable Vector2IntVar; + public NetworkVariable> Vector2IntArrayVar; + public NetworkVariable> Vector2IntManagedListVar; + public NetworkVariable> Vector2IntManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> Vector2IntListVar; + public NetworkVariable> Vector2IntHashSetVar; + public NetworkVariable> ByteVector2IntHashMapVar; + public NetworkVariable> ULongVector2IntHashMapVar; + public NetworkVariable> Vector2Vector2IntHashMapVar; + public NetworkVariable> HashMapKeyStructVector2IntHashMapVar; +#endif + public NetworkVariable> ByteVector2IntDictionaryVar; + public NetworkVariable> ULongVector2IntDictionaryVar; + public NetworkVariable> Vector2Vector2IntDictionaryVar; + public NetworkVariable> HashMapKeyClassVector2IntDictionaryVar; + + public NetworkVariable Vector3IntVar; + public NetworkVariable> Vector3IntArrayVar; + public NetworkVariable> Vector3IntManagedListVar; + public NetworkVariable> Vector3IntManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> Vector3IntListVar; + public NetworkVariable> Vector3IntHashSetVar; + public NetworkVariable> ByteVector3IntHashMapVar; + public NetworkVariable> ULongVector3IntHashMapVar; + public NetworkVariable> Vector2Vector3IntHashMapVar; + public NetworkVariable> HashMapKeyStructVector3IntHashMapVar; +#endif + public NetworkVariable> ByteVector3IntDictionaryVar; + public NetworkVariable> ULongVector3IntDictionaryVar; + public NetworkVariable> Vector2Vector3IntDictionaryVar; + public NetworkVariable> HashMapKeyClassVector3IntDictionaryVar; + + public NetworkVariable Vector4Var; + public NetworkVariable> Vector4ArrayVar; + public NetworkVariable> Vector4ManagedListVar; + public NetworkVariable> Vector4ManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> Vector4ListVar; + public NetworkVariable> Vector4HashSetVar; + public NetworkVariable> ByteVector4HashMapVar; + public NetworkVariable> ULongVector4HashMapVar; + public NetworkVariable> Vector2Vector4HashMapVar; + public NetworkVariable> HashMapKeyStructVector4HashMapVar; +#endif + public NetworkVariable> ByteVector4DictionaryVar; + public NetworkVariable> ULongVector4DictionaryVar; + public NetworkVariable> Vector2Vector4DictionaryVar; + public NetworkVariable> HashMapKeyClassVector4DictionaryVar; + + public NetworkVariable QuaternionVar; + public NetworkVariable> QuaternionArrayVar; + public NetworkVariable> QuaternionManagedListVar; + public NetworkVariable> QuaternionManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> QuaternionListVar; + public NetworkVariable> QuaternionHashSetVar; + public NetworkVariable> ByteQuaternionHashMapVar; + public NetworkVariable> ULongQuaternionHashMapVar; + public NetworkVariable> Vector2QuaternionHashMapVar; + public NetworkVariable> HashMapKeyStructQuaternionHashMapVar; +#endif + public NetworkVariable> ByteQuaternionDictionaryVar; + public NetworkVariable> ULongQuaternionDictionaryVar; + public NetworkVariable> Vector2QuaternionDictionaryVar; + public NetworkVariable> HashMapKeyClassQuaternionDictionaryVar; + + public NetworkVariable ColorVar; + public NetworkVariable> ColorArrayVar; + public NetworkVariable> ColorManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> ColorListVar; +#endif + public NetworkVariable Color32Var; + public NetworkVariable> Color32ArrayVar; + public NetworkVariable> Color32ManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> Color32ListVar; +#endif + public NetworkVariable RayVar; + public NetworkVariable> RayArrayVar; + public NetworkVariable> RayManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> RayListVar; +#endif + public NetworkVariable Ray2DVar; + public NetworkVariable> Ray2DArrayVar; + public NetworkVariable> Ray2DManagedListVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> Ray2DListVar; +#endif + public NetworkVariable TestStructVar; + public NetworkVariable> TestStructArrayVar; + public NetworkVariable> TestStructManagedListVar; + public NetworkVariable> TestStructManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> TestStructListVar; + public NetworkVariable> TestStructHashSetVar; + public NetworkVariable> ByteTestStructHashMapVar; + public NetworkVariable> ULongTestStructHashMapVar; + public NetworkVariable> Vector2TestStructHashMapVar; + public NetworkVariable> HashMapKeyStructTestStructHashMapVar; +#endif + public NetworkVariable> ByteTestStructDictionaryVar; + public NetworkVariable> ULongTestStructDictionaryVar; + public NetworkVariable> Vector2TestStructDictionaryVar; + public NetworkVariable> HashMapKeyClassTestStructDictionaryVar; + + + public NetworkVariable FixedStringVar; + public NetworkVariable> FixedStringArrayVar; + public NetworkVariable> FixedStringManagedListVar; + public NetworkVariable> FixedStringManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> FixedStringListVar; + public NetworkVariable> FixedStringHashSetVar; + public NetworkVariable> ByteFixedStringHashMapVar; + public NetworkVariable> ULongFixedStringHashMapVar; + public NetworkVariable> Vector2FixedStringHashMapVar; + public NetworkVariable> HashMapKeyStructFixedStringHashMapVar; +#endif + public NetworkVariable> ByteFixedStringDictionaryVar; + public NetworkVariable> ULongFixedStringDictionaryVar; + public NetworkVariable> Vector2FixedStringDictionaryVar; + public NetworkVariable> HashMapKeyClassFixedStringDictionaryVar; + + + public NetworkVariable UnmanagedNetworkSerializableTypeVar; + public NetworkVariable> UnmanagedNetworkSerializableManagedHashSetVar; +#if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + public NetworkVariable> UnmanagedNetworkSerializableListVar; + public NetworkVariable> UnmanagedNetworkSerializableHashSetVar; + public NetworkVariable> ByteUnmanagedNetworkSerializableHashMapVar; + public NetworkVariable> ULongUnmanagedNetworkSerializableHashMapVar; + public NetworkVariable> Vector2UnmanagedNetworkSerializableHashMapVar; + public NetworkVariable> HashMapKeyStructUnmanagedNetworkSerializableHashMapVar; +#endif + public NetworkVariable> ByteUnmanagedNetworkSerializableDictionaryVar; + public NetworkVariable> ULongUnmanagedNetworkSerializableDictionaryVar; + public NetworkVariable> Vector2UnmanagedNetworkSerializableDictionaryVar; + public NetworkVariable> HashMapKeyClassUnmanagedNetworkSerializableDictionaryVar; + + public NetworkVariable> UnmanagedNetworkSerializableArrayVar; + public NetworkVariable> UnmanagedNetworkSerializableManagedListVar; + + public NetworkVariable ManagedNetworkSerializableTypeVar; + + public NetworkVariable StringVar; + public NetworkVariable GuidVar; + public NetworkVariableSubclass> SubclassVar; + } + + public class TemplateNetworkBehaviourType : NetworkBehaviour + { + public NetworkVariable TheVar; + } + + public class IntermediateNetworkBehavior : TemplateNetworkBehaviourType + { + public NetworkVariable TheVar2; + } + + public class ClassHavingNetworkBehaviour : IntermediateNetworkBehavior + { + + } + + // Please do not reference TestClass_ReferencedOnlyByTemplateNetworkBehavourType anywhere other than here! + public class ClassHavingNetworkBehaviour2 : TemplateNetworkBehaviourType + { + + } + + public class StructHavingNetworkBehaviour : TemplateNetworkBehaviourType + { + + } + + public struct StructUsedOnlyInNetworkList : IEquatable, INetworkSerializeByMemcpy + { + public int Value; + + public bool Equals(StructUsedOnlyInNetworkList other) + { + return Value == other.Value; + } + + public override bool Equals(object obj) + { + return obj is StructUsedOnlyInNetworkList other && Equals(other); + } + + public override int GetHashCode() + { + return Value; + } + } + +} diff --git a/Tests/Runtime/NetworkVariableTestsHelperTypes.cs.meta b/Tests/Runtime/NetworkVariableTestsHelperTypes.cs.meta new file mode 100644 index 0000000..c71cb92 --- /dev/null +++ b/Tests/Runtime/NetworkVariableTestsHelperTypes.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a580aaafc247486193f372174a99fae4 +timeCreated: 1705098783 \ No newline at end of file diff --git a/Tests/Runtime/NetworkVariableTraitsTests.cs b/Tests/Runtime/NetworkVariableTraitsTests.cs new file mode 100644 index 0000000..1c3872a --- /dev/null +++ b/Tests/Runtime/NetworkVariableTraitsTests.cs @@ -0,0 +1,138 @@ +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkVariableTraitsComponent : NetworkBehaviour + { + public NetworkVariable TheVariable = new NetworkVariable(); + } + + public class NetworkVariableTraitsTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + protected override bool m_EnableTimeTravel => true; + protected override bool m_SetupIsACoroutine => false; + protected override bool m_TearDownIsACoroutine => false; + + protected override void OnPlayerPrefabGameObjectCreated() + { + m_PlayerPrefab.AddComponent(); + } + + public NetworkVariableTraitsComponent GetTestComponent() + { + return m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); + } + + public NetworkVariableTraitsComponent GetServerComponent() + { + foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + if (obj.NetworkManager == m_ServerNetworkManager && obj.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId) + { + return obj; + } + } + + return null; + } + + [Test] + public void WhenNewValueIsLessThanThreshold_VariableIsNotSerialized() + { + var serverComponent = GetServerComponent(); + var testComponent = GetTestComponent(); + serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1; + + serverComponent.TheVariable.Value = 0.05f; + + TimeTravel(2, 120); + + Assert.AreEqual(0.05f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0, testComponent.TheVariable.Value); ; + } + [Test] + public void WhenNewValueIsGreaterThanThreshold_VariableIsSerialized() + { + var serverComponent = GetServerComponent(); + var testComponent = GetTestComponent(); + serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1; + + serverComponent.TheVariable.Value = 0.15f; + + TimeTravel(2, 120); + + Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0.15f, testComponent.TheVariable.Value); ; + } + + [Test] + public void WhenNewValueIsLessThanThresholdButMaxTimeHasPassed_VariableIsSerialized() + { + var serverComponent = GetServerComponent(); + var testComponent = GetTestComponent(); + serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1; + serverComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MaxSecondsBetweenUpdates = 2 }); + serverComponent.TheVariable.LastUpdateSent = m_ServerNetworkManager.NetworkTimeSystem.LocalTime; + + serverComponent.TheVariable.Value = 0.05f; + + TimeTravel(1 / 60f * 119, 119); + + Assert.AreEqual(0.05f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0, testComponent.TheVariable.Value); ; + + TimeTravel(1 / 60f * 4, 4); + + Assert.AreEqual(0.05f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0.05f, testComponent.TheVariable.Value); ; + } + + [Test] + public void WhenNewValueIsGreaterThanThresholdButMinTimeHasNotPassed_VariableIsNotSerialized() + { + var serverComponent = GetServerComponent(); + var testComponent = GetTestComponent(); + serverComponent.TheVariable.CheckExceedsDirtinessThreshold = (in float value, in float newValue) => Mathf.Abs(newValue - value) >= 0.1; + serverComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MinSecondsBetweenUpdates = 2 }); + serverComponent.TheVariable.LastUpdateSent = m_ServerNetworkManager.NetworkTimeSystem.LocalTime; + + serverComponent.TheVariable.Value = 0.15f; + + TimeTravel(1 / 60f * 119, 119); + + Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0, testComponent.TheVariable.Value); ; + + TimeTravel(1 / 60f * 4, 4); + + Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0.15f, testComponent.TheVariable.Value); ; + } + + [Test] + public void WhenNoThresholdIsSetButMinTimeHasNotPassed_VariableIsNotSerialized() + { + var serverComponent = GetServerComponent(); + var testComponent = GetTestComponent(); + serverComponent.TheVariable.SetUpdateTraits(new NetworkVariableUpdateTraits { MinSecondsBetweenUpdates = 2 }); + serverComponent.TheVariable.LastUpdateSent = m_ServerNetworkManager.NetworkTimeSystem.LocalTime; + + serverComponent.TheVariable.Value = 0.15f; + + TimeTravel(1 / 60f * 119, 119); + + Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0, testComponent.TheVariable.Value); ; + + TimeTravel(1 / 60f * 4, 4); + + Assert.AreEqual(0.15f, serverComponent.TheVariable.Value); ; + Assert.AreEqual(0.15f, testComponent.TheVariable.Value); ; + } + } +} diff --git a/Tests/Runtime/NetworkVariableTraitsTests.cs.meta b/Tests/Runtime/NetworkVariableTraitsTests.cs.meta new file mode 100644 index 0000000..4b0b040 --- /dev/null +++ b/Tests/Runtime/NetworkVariableTraitsTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c4ca75209dbd43f48930e4887392bafd +timeCreated: 1706826268 \ No newline at end of file diff --git a/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs b/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs index 4cc8811..984c95a 100644 --- a/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs +++ b/Tests/Runtime/NetworkVariableUserSerializableTypesTests.cs @@ -112,6 +112,16 @@ namespace Unity.Netcode.RuntimeTests protected override IEnumerator OnSetup() { WorkingUserNetworkVariableComponentBase.Reset(); + + UserNetworkVariableSerialization.WriteValue = null; + UserNetworkVariableSerialization.ReadValue = null; + UserNetworkVariableSerialization.DuplicateValue = null; + UserNetworkVariableSerialization.WriteValue = null; + UserNetworkVariableSerialization.ReadValue = null; + UserNetworkVariableSerialization.DuplicateValue = null; + UserNetworkVariableSerialization.WriteValue = null; + UserNetworkVariableSerialization.ReadValue = null; + UserNetworkVariableSerialization.DuplicateValue = null; return base.OnSetup(); } @@ -217,5 +227,36 @@ namespace Unity.Netcode.RuntimeTests } ); } + + protected override IEnumerator OnTearDown() + { + // These have to get set to SOMETHING, otherwise we will get an exception thrown because Object.Destroy() + // calls __initializeNetworkVariables, and the network variable initialization attempts to call FallbackSerializer, + // which throws an exception if any of these values are null. They don't have to DO anything, they just have to + // be non-null to keep the test from failing during teardown. + // None of this is related to what's being tested above, and in reality, these values being null is an invalid + // use case. But one of the tests is explicitly testing that invalid use case, and the values are being set + // to null in OnSetup to ensure test isolation. This wouldn't be a situation a user would have to think about + // in a real world use case. + UserNetworkVariableSerialization.WriteValue = (FastBufferWriter writer, in MyTypeOne value) => { }; + UserNetworkVariableSerialization.ReadValue = (FastBufferReader reader, out MyTypeOne value) => { value = new MyTypeOne(); }; + UserNetworkVariableSerialization.DuplicateValue = (in MyTypeOne value, ref MyTypeOne duplicatedValue) => + { + duplicatedValue = value; + }; + UserNetworkVariableSerialization.WriteValue = (FastBufferWriter writer, in MyTypeTwo value) => { }; + UserNetworkVariableSerialization.ReadValue = (FastBufferReader reader, out MyTypeTwo value) => { value = new MyTypeTwo(); }; + UserNetworkVariableSerialization.DuplicateValue = (in MyTypeTwo value, ref MyTypeTwo duplicatedValue) => + { + duplicatedValue = value; + }; + UserNetworkVariableSerialization.WriteValue = (FastBufferWriter writer, in MyTypeThree value) => { }; + UserNetworkVariableSerialization.ReadValue = (FastBufferReader reader, out MyTypeThree value) => { value = new MyTypeThree(); }; + UserNetworkVariableSerialization.DuplicateValue = (in MyTypeThree value, ref MyTypeThree duplicatedValue) => + { + duplicatedValue = value; + }; + return base.OnTearDown(); + } } } diff --git a/Tests/Runtime/RpcTypeSerializationTests.cs b/Tests/Runtime/RpcTypeSerializationTests.cs index 9d09d67..6016779 100644 --- a/Tests/Runtime/RpcTypeSerializationTests.cs +++ b/Tests/Runtime/RpcTypeSerializationTests.cs @@ -1,11 +1,9 @@ using System; -using System.Collections; using System.Linq; using NUnit.Framework; using Unity.Collections; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; -using UnityEngine.TestTools; using Quaternion = UnityEngine.Quaternion; using Vector2 = UnityEngine.Vector2; using Vector3 = UnityEngine.Vector3; @@ -21,6 +19,10 @@ namespace Unity.Netcode.RuntimeTests m_UseHost = false; } + protected override bool m_EnableTimeTravel => true; + protected override bool m_SetupIsACoroutine => true; + protected override bool m_TearDownIsACoroutine => true; + public class RpcTestNB : NetworkBehaviour { public delegate void OnReceivedDelegate(object obj); @@ -866,7 +868,7 @@ namespace Unity.Netcode.RuntimeTests m_PlayerPrefab.AddComponent(); } - public IEnumerator TestValueType(T firstTest, T secondTest) where T : unmanaged + public void TestValueType(T firstTest, T secondTest) where T : unmanaged { var methods = typeof(RpcTestNB).GetMethods(); foreach (var method in methods) @@ -894,7 +896,7 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending first RPC with {firstTest}"); method.Invoke(serverObject, new object[] { firstTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsNotNull(receivedValue); @@ -907,20 +909,20 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending second RPC with {secondTest}"); method.Invoke(serverObject, new object[] { secondTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsNotNull(receivedValue); Assert.AreEqual(receivedValue.GetType(), typeof(T)); value = (T)receivedValue; Assert.IsTrue(NetworkVariableSerialization.AreEqual(ref value, ref secondTest)); - yield break; + return; } } Assert.Fail($"Could not find RPC function for {typeof(T).Name}"); } - public IEnumerator TestValueTypeArray(T[] firstTest, T[] secondTest) where T : unmanaged + public void TestValueTypeArray(T[] firstTest, T[] secondTest) where T : unmanaged { var methods = typeof(RpcTestNB).GetMethods(); foreach (var method in methods) @@ -948,7 +950,7 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending first RPC with {firstTest}"); method.Invoke(serverObject, new object[] { firstTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsNotNull(receivedValue); @@ -961,7 +963,7 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending second RPC with {secondTest}"); method.Invoke(serverObject, new object[] { secondTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsNotNull(receivedValue); @@ -971,16 +973,16 @@ namespace Unity.Netcode.RuntimeTests method.Invoke(serverObject, new object[] { null }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsNull(receivedValue); - yield break; + return; } } Assert.Fail($"Could not find RPC function for {typeof(T).Name}"); } - public IEnumerator TestValueTypeNativeArray(NativeArray firstTest, NativeArray secondTest) where T : unmanaged + public void TestValueTypeNativeArray(NativeArray firstTest, NativeArray secondTest) where T : unmanaged { var methods = typeof(RpcTestNB).GetMethods(); foreach (var method in methods) @@ -1010,7 +1012,7 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending first RPC with {firstTest}"); method.Invoke(serverObject, new object[] { firstTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsTrue(receivedValue.IsCreated); @@ -1023,21 +1025,21 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending second RPC with {secondTest}"); method.Invoke(serverObject, new object[] { secondTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsTrue(receivedValue.IsCreated); Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref receivedValue, ref secondTest)); receivedValue.Dispose(); secondTest.Dispose(); - yield break; + return; } } Assert.Fail($"Could not find RPC function for {typeof(T).Name}"); } #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - public IEnumerator TestValueTypeNativeList(NativeList firstTest, NativeList secondTest) where T : unmanaged + public void TestValueTypeNativeList(NativeList firstTest, NativeList secondTest) where T : unmanaged { var methods = typeof(RpcTestNB).GetMethods(); foreach (var method in methods) @@ -1071,7 +1073,7 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending first RPC with {firstTest}"); method.Invoke(serverObject, new object[] { firstTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsTrue(receivedValue.IsCreated); @@ -1084,22 +1086,22 @@ namespace Unity.Netcode.RuntimeTests Debug.Log($"Sending second RPC with {secondTest}"); method.Invoke(serverObject, new object[] { secondTest }); - yield return WaitForMessageReceived(m_ClientNetworkManagers.ToList()); + WaitForMessageReceivedWithTimeTravel(m_ClientNetworkManagers.ToList()); Assert.IsTrue(receivedValue.IsCreated); Assert.IsTrue(NetworkVariableSerialization>.AreEqual(ref receivedValue, ref secondTest)); receivedValue.Dispose(); secondTest.Dispose(); - yield break; + return; } } Assert.Fail($"Could not find RPC function for {typeof(T).Name}"); } #endif - [UnityTest] - public IEnumerator WhenSendingAValueTypeOverAnRpc_ValuesAreSerializedCorrectly( + [Test] + public void WhenSendingAValueTypeOverAnRpc_ValuesAreSerializedCorrectly( [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), @@ -1111,156 +1113,156 @@ namespace Unity.Netcode.RuntimeTests { if (testType == typeof(byte)) { - yield return TestValueType(byte.MinValue + 5, byte.MaxValue); + TestValueType(byte.MinValue + 5, byte.MaxValue); } else if (testType == typeof(sbyte)) { - yield return TestValueType(sbyte.MinValue + 5, sbyte.MaxValue); + TestValueType(sbyte.MinValue + 5, sbyte.MaxValue); } else if (testType == typeof(short)) { - yield return TestValueType(short.MinValue + 5, short.MaxValue); + TestValueType(short.MinValue + 5, short.MaxValue); } else if (testType == typeof(ushort)) { - yield return TestValueType(ushort.MinValue + 5, ushort.MaxValue); + TestValueType(ushort.MinValue + 5, ushort.MaxValue); } else if (testType == typeof(int)) { - yield return TestValueType(int.MinValue + 5, int.MaxValue); + TestValueType(int.MinValue + 5, int.MaxValue); } else if (testType == typeof(uint)) { - yield return TestValueType(uint.MinValue + 5, uint.MaxValue); + TestValueType(uint.MinValue + 5, uint.MaxValue); } else if (testType == typeof(long)) { - yield return TestValueType(long.MinValue + 5, long.MaxValue); + TestValueType(long.MinValue + 5, long.MaxValue); } else if (testType == typeof(ulong)) { - yield return TestValueType(ulong.MinValue + 5, ulong.MaxValue); + TestValueType(ulong.MinValue + 5, ulong.MaxValue); } else if (testType == typeof(bool)) { - yield return TestValueType(true, false); + TestValueType(true, false); } else if (testType == typeof(char)) { - yield return TestValueType('z', ' '); + TestValueType('z', ' '); } else if (testType == typeof(float)) { - yield return TestValueType(float.MinValue + 5.12345678f, float.MaxValue); + TestValueType(float.MinValue + 5.12345678f, float.MaxValue); } else if (testType == typeof(double)) { - yield return TestValueType(double.MinValue + 5.12345678, double.MaxValue); + TestValueType(double.MinValue + 5.12345678, double.MaxValue); } else if (testType == typeof(ByteEnum)) { - yield return TestValueType(ByteEnum.B, ByteEnum.C); + TestValueType(ByteEnum.B, ByteEnum.C); } else if (testType == typeof(SByteEnum)) { - yield return TestValueType(SByteEnum.B, SByteEnum.C); + TestValueType(SByteEnum.B, SByteEnum.C); } else if (testType == typeof(ShortEnum)) { - yield return TestValueType(ShortEnum.B, ShortEnum.C); + TestValueType(ShortEnum.B, ShortEnum.C); } else if (testType == typeof(UShortEnum)) { - yield return TestValueType(UShortEnum.B, UShortEnum.C); + TestValueType(UShortEnum.B, UShortEnum.C); } else if (testType == typeof(IntEnum)) { - yield return TestValueType(IntEnum.B, IntEnum.C); + TestValueType(IntEnum.B, IntEnum.C); } else if (testType == typeof(UIntEnum)) { - yield return TestValueType(UIntEnum.B, UIntEnum.C); + TestValueType(UIntEnum.B, UIntEnum.C); } else if (testType == typeof(LongEnum)) { - yield return TestValueType(LongEnum.B, LongEnum.C); + TestValueType(LongEnum.B, LongEnum.C); } else if (testType == typeof(ULongEnum)) { - yield return TestValueType(ULongEnum.B, ULongEnum.C); + TestValueType(ULongEnum.B, ULongEnum.C); } else if (testType == typeof(Vector2)) { - yield return TestValueType( + TestValueType( new Vector2(5, 10), new Vector2(15, 20)); } else if (testType == typeof(Vector3)) { - yield return TestValueType( + TestValueType( new Vector3(5, 10, 15), new Vector3(20, 25, 30)); } else if (testType == typeof(Vector2Int)) { - yield return TestValueType( + TestValueType( new Vector2Int(5, 10), new Vector2Int(15, 20)); } else if (testType == typeof(Vector3Int)) { - yield return TestValueType( + TestValueType( new Vector3Int(5, 10, 15), new Vector3Int(20, 25, 30)); } else if (testType == typeof(Vector4)) { - yield return TestValueType( + TestValueType( new Vector4(5, 10, 15, 20), new Vector4(25, 30, 35, 40)); } else if (testType == typeof(Quaternion)) { - yield return TestValueType( + TestValueType( new Quaternion(5, 10, 15, 20), new Quaternion(25, 30, 35, 40)); } else if (testType == typeof(Color)) { - yield return TestValueType( + TestValueType( new Color(1, 0, 0), new Color(0, 1, 1)); } else if (testType == typeof(Color32)) { - yield return TestValueType( + TestValueType( new Color32(255, 0, 0, 128), new Color32(0, 255, 255, 255)); } else if (testType == typeof(Ray)) { - yield return TestValueType( + TestValueType( new Ray(new Vector3(0, 1, 2), new Vector3(3, 4, 5)), new Ray(new Vector3(6, 7, 8), new Vector3(9, 10, 11))); } else if (testType == typeof(Ray2D)) { - yield return TestValueType( + TestValueType( new Ray2D(new Vector2(0, 1), new Vector2(2, 3)), new Ray2D(new Vector2(4, 5), new Vector2(6, 7))); } else if (testType == typeof(NetworkVariableTestStruct)) { - yield return TestValueType(NetworkVariableTestStruct.GetTestStruct(), NetworkVariableTestStruct.GetTestStruct()); + TestValueType(NetworkVariableTestStruct.GetTestStruct(), NetworkVariableTestStruct.GetTestStruct()); } else if (testType == typeof(FixedString32Bytes)) { - yield return TestValueType(new FixedString32Bytes("foobar"), new FixedString32Bytes("12345678901234567890123456789")); + TestValueType(new FixedString32Bytes("foobar"), new FixedString32Bytes("12345678901234567890123456789")); } } - [UnityTest] - public IEnumerator WhenSendingAnArrayOfValueTypesOverAnRpc_ValuesAreSerializedCorrectly( + [Test] + public void WhenSendingAnArrayOfValueTypesOverAnRpc_ValuesAreSerializedCorrectly( [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), @@ -1272,175 +1274,175 @@ namespace Unity.Netcode.RuntimeTests { if (testType == typeof(byte)) { - yield return TestValueTypeArray( + TestValueTypeArray( new byte[] { byte.MinValue + 5, byte.MaxValue }, new byte[] { 0, byte.MinValue + 10, byte.MaxValue - 10 }); } else if (testType == typeof(sbyte)) { - yield return TestValueTypeArray( + TestValueTypeArray( new sbyte[] { sbyte.MinValue + 5, sbyte.MaxValue }, new sbyte[] { 0, sbyte.MinValue + 10, sbyte.MaxValue - 10 }); } else if (testType == typeof(short)) { - yield return TestValueTypeArray( + TestValueTypeArray( new short[] { short.MinValue + 5, short.MaxValue }, new short[] { 0, short.MinValue + 10, short.MaxValue - 10 }); } else if (testType == typeof(ushort)) { - yield return TestValueTypeArray( + TestValueTypeArray( new ushort[] { ushort.MinValue + 5, ushort.MaxValue }, new ushort[] { 0, ushort.MinValue + 10, ushort.MaxValue - 10 }); } else if (testType == typeof(int)) { - yield return TestValueTypeArray( + TestValueTypeArray( new int[] { int.MinValue + 5, int.MaxValue }, new int[] { 0, int.MinValue + 10, int.MaxValue - 10 }); } else if (testType == typeof(uint)) { - yield return TestValueTypeArray( + TestValueTypeArray( new uint[] { uint.MinValue + 5, uint.MaxValue }, new uint[] { 0, uint.MinValue + 10, uint.MaxValue - 10 }); } else if (testType == typeof(long)) { - yield return TestValueTypeArray( + TestValueTypeArray( new long[] { long.MinValue + 5, long.MaxValue }, new long[] { 0, long.MinValue + 10, long.MaxValue - 10 }); } else if (testType == typeof(ulong)) { - yield return TestValueTypeArray( + TestValueTypeArray( new ulong[] { ulong.MinValue + 5, ulong.MaxValue }, new ulong[] { 0, ulong.MinValue + 10, ulong.MaxValue - 10 }); } else if (testType == typeof(bool)) { - yield return TestValueTypeArray( + TestValueTypeArray( new bool[] { true, false, true }, new bool[] { false, true, false, true, false }); } else if (testType == typeof(char)) { - yield return TestValueTypeArray( + TestValueTypeArray( new char[] { 'z', ' ', '?' }, new char[] { 'n', 'e', 'w', ' ', 'v', 'a', 'l', 'u', 'e' }); } else if (testType == typeof(float)) { - yield return TestValueTypeArray( + TestValueTypeArray( new float[] { float.MinValue + 5.12345678f, float.MaxValue }, new float[] { 0, float.MinValue + 10.987654321f, float.MaxValue - 10.135792468f }); } else if (testType == typeof(double)) { - yield return TestValueTypeArray( + TestValueTypeArray( new double[] { double.MinValue + 5.12345678, double.MaxValue }, new double[] { 0, double.MinValue + 10.987654321, double.MaxValue - 10.135792468 }); } else if (testType == typeof(ByteEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new ByteEnum[] { ByteEnum.C, ByteEnum.B, ByteEnum.A }, new ByteEnum[] { ByteEnum.B, ByteEnum.C, ByteEnum.B, ByteEnum.A, ByteEnum.C }); } else if (testType == typeof(SByteEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new SByteEnum[] { SByteEnum.C, SByteEnum.B, SByteEnum.A }, new SByteEnum[] { SByteEnum.B, SByteEnum.C, SByteEnum.B, SByteEnum.A, SByteEnum.C }); } else if (testType == typeof(ShortEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new ShortEnum[] { ShortEnum.C, ShortEnum.B, ShortEnum.A }, new ShortEnum[] { ShortEnum.B, ShortEnum.C, ShortEnum.B, ShortEnum.A, ShortEnum.C }); } else if (testType == typeof(UShortEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new UShortEnum[] { UShortEnum.C, UShortEnum.B, UShortEnum.A }, new UShortEnum[] { UShortEnum.B, UShortEnum.C, UShortEnum.B, UShortEnum.A, UShortEnum.C }); } else if (testType == typeof(IntEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new IntEnum[] { IntEnum.C, IntEnum.B, IntEnum.A }, new IntEnum[] { IntEnum.B, IntEnum.C, IntEnum.B, IntEnum.A, IntEnum.C }); } else if (testType == typeof(UIntEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new UIntEnum[] { UIntEnum.C, UIntEnum.B, UIntEnum.A }, new UIntEnum[] { UIntEnum.B, UIntEnum.C, UIntEnum.B, UIntEnum.A, UIntEnum.C }); } else if (testType == typeof(LongEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new LongEnum[] { LongEnum.C, LongEnum.B, LongEnum.A }, new LongEnum[] { LongEnum.B, LongEnum.C, LongEnum.B, LongEnum.A, LongEnum.C }); } else if (testType == typeof(ULongEnum)) { - yield return TestValueTypeArray( + TestValueTypeArray( new ULongEnum[] { ULongEnum.C, ULongEnum.B, ULongEnum.A }, new ULongEnum[] { ULongEnum.B, ULongEnum.C, ULongEnum.B, ULongEnum.A, ULongEnum.C }); } else if (testType == typeof(Vector2)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Vector2[] { new Vector2(5, 10), new Vector2(15, 20) }, new Vector2[] { new Vector2(25, 30), new Vector2(35, 40), new Vector2(45, 50) }); } else if (testType == typeof(Vector3)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Vector3[] { new Vector3(5, 10, 15), new Vector3(20, 25, 30) }, new Vector3[] { new Vector3(35, 40, 45), new Vector3(50, 55, 60), new Vector3(65, 70, 75) }); } else if (testType == typeof(Vector2Int)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Vector2Int[] { new Vector2Int(5, 10), new Vector2Int(15, 20) }, new Vector2Int[] { new Vector2Int(25, 30), new Vector2Int(35, 40), new Vector2Int(45, 50) }); } else if (testType == typeof(Vector3Int)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Vector3Int[] { new Vector3Int(5, 10, 15), new Vector3Int(20, 25, 30) }, new Vector3Int[] { new Vector3Int(35, 40, 45), new Vector3Int(50, 55, 60), new Vector3Int(65, 70, 75) }); } else if (testType == typeof(Vector4)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Vector4[] { new Vector4(5, 10, 15, 20), new Vector4(25, 30, 35, 40) }, new Vector4[] { new Vector4(45, 50, 55, 60), new Vector4(65, 70, 75, 80), new Vector4(85, 90, 95, 100) }); } else if (testType == typeof(Quaternion)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Quaternion[] { new Quaternion(5, 10, 15, 20), new Quaternion(25, 30, 35, 40) }, new Quaternion[] { new Quaternion(45, 50, 55, 60), new Quaternion(65, 70, 75, 80), new Quaternion(85, 90, 95, 100) }); } else if (testType == typeof(Color)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Color[] { new Color(.5f, .10f, .15f), new Color(.20f, .25f, .30f) }, new Color[] { new Color(.35f, .40f, .45f), new Color(.50f, .55f, .60f), new Color(.65f, .70f, .75f) }); } else if (testType == typeof(Color32)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Color32[] { new Color32(5, 10, 15, 20), new Color32(25, 30, 35, 40) }, new Color32[] { new Color32(45, 50, 55, 60), new Color32(65, 70, 75, 80), new Color32(85, 90, 95, 100) }); } else if (testType == typeof(Ray)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Ray[] { new Ray(new Vector3(0, 1, 2), new Vector3(3, 4, 5)), @@ -1455,7 +1457,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(Ray2D)) { - yield return TestValueTypeArray( + TestValueTypeArray( new Ray2D[] { new Ray2D(new Vector2(0, 1), new Vector2(3, 4)), @@ -1470,7 +1472,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(NetworkVariableTestStruct)) { - yield return TestValueTypeArray( + TestValueTypeArray( new NetworkVariableTestStruct[] { NetworkVariableTestStruct.GetTestStruct(), @@ -1485,7 +1487,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(FixedString32Bytes)) { - yield return TestValueTypeArray( + TestValueTypeArray( new FixedString32Bytes[] { new FixedString32Bytes("foobar"), @@ -1500,8 +1502,8 @@ namespace Unity.Netcode.RuntimeTests } } - [UnityTest] - public IEnumerator WhenSendingANativeArrayOfValueTypesOverAnRpc_ValuesAreSerializedCorrectly( + [Test] + public void WhenSendingANativeArrayOfValueTypesOverAnRpc_ValuesAreSerializedCorrectly( [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), @@ -1513,175 +1515,175 @@ namespace Unity.Netcode.RuntimeTests { if (testType == typeof(byte)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new byte[] { byte.MinValue + 5, byte.MaxValue }, Allocator.Persistent), new NativeArray(new byte[] { 0, byte.MinValue + 10, byte.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(sbyte)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new sbyte[] { sbyte.MinValue + 5, sbyte.MaxValue }, Allocator.Persistent), new NativeArray(new sbyte[] { 0, sbyte.MinValue + 10, sbyte.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(short)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new short[] { short.MinValue + 5, short.MaxValue }, Allocator.Persistent), new NativeArray(new short[] { 0, short.MinValue + 10, short.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(ushort)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new ushort[] { ushort.MinValue + 5, ushort.MaxValue }, Allocator.Persistent), new NativeArray(new ushort[] { 0, ushort.MinValue + 10, ushort.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(int)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new int[] { int.MinValue + 5, int.MaxValue }, Allocator.Persistent), new NativeArray(new int[] { 0, int.MinValue + 10, int.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(uint)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new uint[] { uint.MinValue + 5, uint.MaxValue }, Allocator.Persistent), new NativeArray(new uint[] { 0, uint.MinValue + 10, uint.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(long)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new long[] { long.MinValue + 5, long.MaxValue }, Allocator.Persistent), new NativeArray(new long[] { 0, long.MinValue + 10, long.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(ulong)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new ulong[] { ulong.MinValue + 5, ulong.MaxValue }, Allocator.Persistent), new NativeArray(new ulong[] { 0, ulong.MinValue + 10, ulong.MaxValue - 10 }, Allocator.Persistent)); } else if (testType == typeof(bool)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new bool[] { true, false, true }, Allocator.Persistent), new NativeArray(new bool[] { false, true, false, true, false }, Allocator.Persistent)); } else if (testType == typeof(char)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new char[] { 'z', ' ', '?' }, Allocator.Persistent), new NativeArray(new char[] { 'n', 'e', 'w', ' ', 'v', 'a', 'l', 'u', 'e' }, Allocator.Persistent)); } else if (testType == typeof(float)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new float[] { float.MinValue + 5.12345678f, float.MaxValue }, Allocator.Persistent), new NativeArray(new float[] { 0, float.MinValue + 10.987654321f, float.MaxValue - 10.135792468f }, Allocator.Persistent)); } else if (testType == typeof(double)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new double[] { double.MinValue + 5.12345678, double.MaxValue }, Allocator.Persistent), new NativeArray(new double[] { 0, double.MinValue + 10.987654321, double.MaxValue - 10.135792468 }, Allocator.Persistent)); } else if (testType == typeof(ByteEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new ByteEnum[] { ByteEnum.C, ByteEnum.B, ByteEnum.A }, Allocator.Persistent), new NativeArray(new ByteEnum[] { ByteEnum.B, ByteEnum.C, ByteEnum.B, ByteEnum.A, ByteEnum.C }, Allocator.Persistent)); } else if (testType == typeof(SByteEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new SByteEnum[] { SByteEnum.C, SByteEnum.B, SByteEnum.A }, Allocator.Persistent), new NativeArray(new SByteEnum[] { SByteEnum.B, SByteEnum.C, SByteEnum.B, SByteEnum.A, SByteEnum.C }, Allocator.Persistent)); } else if (testType == typeof(ShortEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new ShortEnum[] { ShortEnum.C, ShortEnum.B, ShortEnum.A }, Allocator.Persistent), new NativeArray(new ShortEnum[] { ShortEnum.B, ShortEnum.C, ShortEnum.B, ShortEnum.A, ShortEnum.C }, Allocator.Persistent)); } else if (testType == typeof(UShortEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new UShortEnum[] { UShortEnum.C, UShortEnum.B, UShortEnum.A }, Allocator.Persistent), new NativeArray(new UShortEnum[] { UShortEnum.B, UShortEnum.C, UShortEnum.B, UShortEnum.A, UShortEnum.C }, Allocator.Persistent)); } else if (testType == typeof(IntEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new IntEnum[] { IntEnum.C, IntEnum.B, IntEnum.A }, Allocator.Persistent), new NativeArray(new IntEnum[] { IntEnum.B, IntEnum.C, IntEnum.B, IntEnum.A, IntEnum.C }, Allocator.Persistent)); } else if (testType == typeof(UIntEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new UIntEnum[] { UIntEnum.C, UIntEnum.B, UIntEnum.A }, Allocator.Persistent), new NativeArray(new UIntEnum[] { UIntEnum.B, UIntEnum.C, UIntEnum.B, UIntEnum.A, UIntEnum.C }, Allocator.Persistent)); } else if (testType == typeof(LongEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new LongEnum[] { LongEnum.C, LongEnum.B, LongEnum.A }, Allocator.Persistent), new NativeArray(new LongEnum[] { LongEnum.B, LongEnum.C, LongEnum.B, LongEnum.A, LongEnum.C }, Allocator.Persistent)); } else if (testType == typeof(ULongEnum)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new ULongEnum[] { ULongEnum.C, ULongEnum.B, ULongEnum.A }, Allocator.Persistent), new NativeArray(new ULongEnum[] { ULongEnum.B, ULongEnum.C, ULongEnum.B, ULongEnum.A, ULongEnum.C }, Allocator.Persistent)); } else if (testType == typeof(Vector2)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Vector2[] { new Vector2(5, 10), new Vector2(15, 20) }, Allocator.Persistent), new NativeArray(new Vector2[] { new Vector2(25, 30), new Vector2(35, 40), new Vector2(45, 50) }, Allocator.Persistent)); } else if (testType == typeof(Vector3)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Vector3[] { new Vector3(5, 10, 15), new Vector3(20, 25, 30) }, Allocator.Persistent), new NativeArray(new Vector3[] { new Vector3(35, 40, 45), new Vector3(50, 55, 60), new Vector3(65, 70, 75) }, Allocator.Persistent)); } else if (testType == typeof(Vector2Int)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Vector2Int[] { new Vector2Int(5, 10), new Vector2Int(15, 20) }, Allocator.Persistent), new NativeArray(new Vector2Int[] { new Vector2Int(25, 30), new Vector2Int(35, 40), new Vector2Int(45, 50) }, Allocator.Persistent)); } else if (testType == typeof(Vector3Int)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Vector3Int[] { new Vector3Int(5, 10, 15), new Vector3Int(20, 25, 30) }, Allocator.Persistent), new NativeArray(new Vector3Int[] { new Vector3Int(35, 40, 45), new Vector3Int(50, 55, 60), new Vector3Int(65, 70, 75) }, Allocator.Persistent)); } else if (testType == typeof(Vector4)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Vector4[] { new Vector4(5, 10, 15, 20), new Vector4(25, 30, 35, 40) }, Allocator.Persistent), new NativeArray(new Vector4[] { new Vector4(45, 50, 55, 60), new Vector4(65, 70, 75, 80), new Vector4(85, 90, 95, 100) }, Allocator.Persistent)); } else if (testType == typeof(Quaternion)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Quaternion[] { new Quaternion(5, 10, 15, 20), new Quaternion(25, 30, 35, 40) }, Allocator.Persistent), new NativeArray(new Quaternion[] { new Quaternion(45, 50, 55, 60), new Quaternion(65, 70, 75, 80), new Quaternion(85, 90, 95, 100) }, Allocator.Persistent)); } else if (testType == typeof(Color)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Color[] { new Color(.5f, .10f, .15f), new Color(.20f, .25f, .30f) }, Allocator.Persistent), new NativeArray(new Color[] { new Color(.35f, .40f, .45f), new Color(.50f, .55f, .60f), new Color(.65f, .70f, .75f) }, Allocator.Persistent)); } else if (testType == typeof(Color32)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Color32[] { new Color32(5, 10, 15, 20), new Color32(25, 30, 35, 40) }, Allocator.Persistent), new NativeArray(new Color32[] { new Color32(45, 50, 55, 60), new Color32(65, 70, 75, 80), new Color32(85, 90, 95, 100) }, Allocator.Persistent)); } else if (testType == typeof(Ray)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Ray[] { new Ray(new Vector3(0, 1, 2), new Vector3(3, 4, 5)), @@ -1696,7 +1698,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(Ray2D)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new Ray2D[] { new Ray2D(new Vector2(0, 1), new Vector2(3, 4)), @@ -1711,7 +1713,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(NetworkVariableTestStruct)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new NetworkVariableTestStruct[] { NetworkVariableTestStruct.GetTestStruct(), @@ -1726,7 +1728,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(FixedString32Bytes)) { - yield return TestValueTypeNativeArray( + TestValueTypeNativeArray( new NativeArray(new FixedString32Bytes[] { new FixedString32Bytes("foobar"), @@ -1742,8 +1744,8 @@ namespace Unity.Netcode.RuntimeTests } #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT - [UnityTest] - public IEnumerator WhenSendingANativeListOfValueTypesOverAnRpc_ValuesAreSerializedCorrectly( + [Test] + public void WhenSendingANativeListOfValueTypesOverAnRpc_ValuesAreSerializedCorrectly( [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), @@ -1755,175 +1757,175 @@ namespace Unity.Netcode.RuntimeTests { if (testType == typeof(byte)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { byte.MinValue + 5, byte.MaxValue }, new NativeList(Allocator.Persistent) { 0, byte.MinValue + 10, byte.MaxValue - 10 }); } else if (testType == typeof(sbyte)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { sbyte.MinValue + 5, sbyte.MaxValue }, new NativeList(Allocator.Persistent) { 0, sbyte.MinValue + 10, sbyte.MaxValue - 10 }); } else if (testType == typeof(short)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { short.MinValue + 5, short.MaxValue }, new NativeList(Allocator.Persistent) { 0, short.MinValue + 10, short.MaxValue - 10 }); } else if (testType == typeof(ushort)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { ushort.MinValue + 5, ushort.MaxValue }, new NativeList(Allocator.Persistent) { 0, ushort.MinValue + 10, ushort.MaxValue - 10 }); } else if (testType == typeof(int)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { int.MinValue + 5, int.MaxValue }, new NativeList(Allocator.Persistent) { 0, int.MinValue + 10, int.MaxValue - 10 }); } else if (testType == typeof(uint)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { uint.MinValue + 5, uint.MaxValue }, new NativeList(Allocator.Persistent) { 0, uint.MinValue + 10, uint.MaxValue - 10 }); } else if (testType == typeof(long)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { long.MinValue + 5, long.MaxValue }, new NativeList(Allocator.Persistent) { 0, long.MinValue + 10, long.MaxValue - 10 }); } else if (testType == typeof(ulong)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { ulong.MinValue + 5, ulong.MaxValue }, new NativeList(Allocator.Persistent) { 0, ulong.MinValue + 10, ulong.MaxValue - 10 }); } else if (testType == typeof(bool)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { true, false, true }, new NativeList(Allocator.Persistent) { false, true, false, true, false }); } else if (testType == typeof(char)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { 'z', ' ', '?' }, new NativeList(Allocator.Persistent) { 'n', 'e', 'w', ' ', 'v', 'a', 'l', 'u', 'e' }); } else if (testType == typeof(float)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { float.MinValue + 5.12345678f, float.MaxValue }, new NativeList(Allocator.Persistent) { 0, float.MinValue + 10.987654321f, float.MaxValue - 10.135792468f }); } else if (testType == typeof(double)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { double.MinValue + 5.12345678, double.MaxValue }, new NativeList(Allocator.Persistent) { 0, double.MinValue + 10.987654321, double.MaxValue - 10.135792468 }); } else if (testType == typeof(ByteEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { ByteEnum.C, ByteEnum.B, ByteEnum.A }, new NativeList(Allocator.Persistent) { ByteEnum.B, ByteEnum.C, ByteEnum.B, ByteEnum.A, ByteEnum.C }); } else if (testType == typeof(SByteEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { SByteEnum.C, SByteEnum.B, SByteEnum.A }, new NativeList(Allocator.Persistent) { SByteEnum.B, SByteEnum.C, SByteEnum.B, SByteEnum.A, SByteEnum.C }); } else if (testType == typeof(ShortEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { ShortEnum.C, ShortEnum.B, ShortEnum.A }, new NativeList(Allocator.Persistent) { ShortEnum.B, ShortEnum.C, ShortEnum.B, ShortEnum.A, ShortEnum.C }); } else if (testType == typeof(UShortEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { UShortEnum.C, UShortEnum.B, UShortEnum.A }, new NativeList(Allocator.Persistent) { UShortEnum.B, UShortEnum.C, UShortEnum.B, UShortEnum.A, UShortEnum.C }); } else if (testType == typeof(IntEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { IntEnum.C, IntEnum.B, IntEnum.A }, new NativeList(Allocator.Persistent) { IntEnum.B, IntEnum.C, IntEnum.B, IntEnum.A, IntEnum.C }); } else if (testType == typeof(UIntEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { UIntEnum.C, UIntEnum.B, UIntEnum.A }, new NativeList(Allocator.Persistent) { UIntEnum.B, UIntEnum.C, UIntEnum.B, UIntEnum.A, UIntEnum.C }); } else if (testType == typeof(LongEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { LongEnum.C, LongEnum.B, LongEnum.A }, new NativeList(Allocator.Persistent) { LongEnum.B, LongEnum.C, LongEnum.B, LongEnum.A, LongEnum.C }); } else if (testType == typeof(ULongEnum)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { ULongEnum.C, ULongEnum.B, ULongEnum.A }, new NativeList(Allocator.Persistent) { ULongEnum.B, ULongEnum.C, ULongEnum.B, ULongEnum.A, ULongEnum.C }); } else if (testType == typeof(Vector2)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Vector2(5, 10), new Vector2(15, 20) }, new NativeList(Allocator.Persistent) { new Vector2(25, 30), new Vector2(35, 40), new Vector2(45, 50) }); } else if (testType == typeof(Vector3)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Vector3(5, 10, 15), new Vector3(20, 25, 30) }, new NativeList(Allocator.Persistent) { new Vector3(35, 40, 45), new Vector3(50, 55, 60), new Vector3(65, 70, 75) }); } else if (testType == typeof(Vector2Int)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Vector2Int(5, 10), new Vector2Int(15, 20) }, new NativeList(Allocator.Persistent) { new Vector2Int(25, 30), new Vector2Int(35, 40), new Vector2Int(45, 50) }); } else if (testType == typeof(Vector3Int)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Vector3Int(5, 10, 15), new Vector3Int(20, 25, 30) }, new NativeList(Allocator.Persistent) { new Vector3Int(35, 40, 45), new Vector3Int(50, 55, 60), new Vector3Int(65, 70, 75) }); } else if (testType == typeof(Vector4)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Vector4(5, 10, 15, 20), new Vector4(25, 30, 35, 40) }, new NativeList(Allocator.Persistent) { new Vector4(45, 50, 55, 60), new Vector4(65, 70, 75, 80), new Vector4(85, 90, 95, 100) }); } else if (testType == typeof(Quaternion)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Quaternion(5, 10, 15, 20), new Quaternion(25, 30, 35, 40) }, new NativeList(Allocator.Persistent) { new Quaternion(45, 50, 55, 60), new Quaternion(65, 70, 75, 80), new Quaternion(85, 90, 95, 100) }); } else if (testType == typeof(Color)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Color(.5f, .10f, .15f), new Color(.20f, .25f, .30f) }, new NativeList(Allocator.Persistent) { new Color(.35f, .40f, .45f), new Color(.50f, .55f, .60f), new Color(.65f, .70f, .75f) }); } else if (testType == typeof(Color32)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Color32(5, 10, 15, 20), new Color32(25, 30, 35, 40) }, new NativeList(Allocator.Persistent) { new Color32(45, 50, 55, 60), new Color32(65, 70, 75, 80), new Color32(85, 90, 95, 100) }); } else if (testType == typeof(Ray)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Ray(new Vector3(0, 1, 2), new Vector3(3, 4, 5)), @@ -1938,7 +1940,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(Ray2D)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new Ray2D(new Vector2(0, 1), new Vector2(3, 4)), @@ -1953,7 +1955,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(NetworkVariableTestStruct)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { NetworkVariableTestStruct.GetTestStruct(), @@ -1968,7 +1970,7 @@ namespace Unity.Netcode.RuntimeTests } else if (testType == typeof(FixedString32Bytes)) { - yield return TestValueTypeNativeList( + TestValueTypeNativeList( new NativeList(Allocator.Persistent) { new FixedString32Bytes("foobar"), diff --git a/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs index 472d617..8cf3edc 100644 --- a/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs +++ b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs @@ -17,6 +17,8 @@ namespace Unity.Netcode.RuntimeTests { private class TestNetworkBehaviour : NetworkBehaviour { + public static bool ReceivedRPC; + public NetworkVariable TestVariable = new NetworkVariable(); public TestNetworkBehaviour RpcReceivedBehaviour; @@ -25,6 +27,7 @@ namespace Unity.Netcode.RuntimeTests public void SendReferenceServerRpc(NetworkBehaviourReference value) { RpcReceivedBehaviour = (TestNetworkBehaviour)value; + ReceivedRPC = true; } } @@ -57,8 +60,43 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour); } + [UnityTest] + public IEnumerator TestSerializeNull([Values] bool initializeWithNull) + { + TestNetworkBehaviour.ReceivedRPC = false; + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + // If not initializing with null, then use the default constructor with no assigned NetworkBehaviour + if (!initializeWithNull) + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkBehaviourReference()); + } + else // Otherwise, initialize and pass in null as the reference + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkBehaviourReference(null)); + } + + // wait for rpc completion + float t = 0; + while (!TestNetworkBehaviour.ReceivedRPC) + { + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(null, testNetworkBehaviour.RpcReceivedBehaviour); + } [UnityTest] public IEnumerator TestRpcImplicitNetworkBehaviour() @@ -89,6 +127,7 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour); } + [Test] public void TestNetworkVariable() { @@ -131,15 +170,6 @@ namespace Unity.Netcode.RuntimeTests }); } - [Test] - public void FailSerializeNullBehaviour() - { - Assert.Throws(() => - { - NetworkBehaviourReference outReference = null; - }); - } - public void Dispose() { //Stop, shutdown, and destroy diff --git a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs index 8b29d17..ef459de 100644 --- a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs +++ b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs @@ -19,6 +19,7 @@ namespace Unity.Netcode.RuntimeTests { private class TestNetworkBehaviour : NetworkBehaviour { + public static bool ReceivedRPC; public NetworkVariable TestVariable = new NetworkVariable(); public NetworkObject RpcReceivedNetworkObject; @@ -28,6 +29,7 @@ namespace Unity.Netcode.RuntimeTests [ServerRpc] public void SendReferenceServerRpc(NetworkObjectReference value) { + ReceivedRPC = true; RpcReceivedGameObject = value; RpcReceivedNetworkObject = value; } @@ -150,6 +152,60 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(networkObject, result); } + public enum NetworkObjectConstructorTypes + { + None, + NullNetworkObject, + NullGameObject + } + + [UnityTest] + public IEnumerator TestSerializeNull([Values] NetworkObjectConstructorTypes networkObjectConstructorTypes) + { + TestNetworkBehaviour.ReceivedRPC = false; + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + switch (networkObjectConstructorTypes) + { + case NetworkObjectConstructorTypes.None: + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference()); + break; + } + case NetworkObjectConstructorTypes.NullNetworkObject: + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference((NetworkObject)null)); + break; + } + case NetworkObjectConstructorTypes.NullGameObject: + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference((GameObject)null)); + break; + } + } + + + // wait for rpc completion + float t = 0; + while (!TestNetworkBehaviour.ReceivedRPC) + { + + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(null, testNetworkBehaviour.RpcReceivedNetworkObject); + Assert.AreEqual(null, testNetworkBehaviour.RpcReceivedGameObject); + } + [UnityTest] public IEnumerator TestRpc() { @@ -305,24 +361,6 @@ namespace Unity.Netcode.RuntimeTests }); } - [Test] - public void FailSerializeNullNetworkObject() - { - Assert.Throws(() => - { - NetworkObjectReference outReference = (NetworkObject)null; - }); - } - - [Test] - public void FailSerializeNullGameObject() - { - Assert.Throws(() => - { - NetworkObjectReference outReference = (GameObject)null; - }); - } - public void Dispose() { //Stop, shutdown, and destroy diff --git a/Tests/Runtime/UniversalRpcTests.cs b/Tests/Runtime/UniversalRpcTests.cs index 49a1e47..e157a3a 100644 --- a/Tests/Runtime/UniversalRpcTests.cs +++ b/Tests/Runtime/UniversalRpcTests.cs @@ -1,4 +1,6 @@ +#if !MULTIPLAYER_TOOLS && !NGO_MINIMALPROJECT using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -7,6 +9,7 @@ using NUnit.Framework; using Unity.Collections; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; +using UnityEngine.TestTools; using Object = UnityEngine.Object; using Random = System.Random; @@ -24,6 +27,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests public string Received = string.Empty; public Tuple ReceivedParams = null; public ulong ReceivedFrom = ulong.MaxValue; + public int ReceivedCount; public void OnRpcReceived() { @@ -32,6 +36,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests var currentMethod = sf.GetMethod(); Received = currentMethod.Name; + ReceivedCount++; } public void OnRpcReceivedWithParams(int a, bool b, float f, string s) { @@ -40,6 +45,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests var currentMethod = sf.GetMethod(); Received = currentMethod.Name; + ReceivedCount++; ReceivedParams = new Tuple(a, b, f, s); } @@ -448,6 +454,9 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests public class UniversalRpcTestsBase : NetcodeIntegrationTest { + public static int YieldCheck = 0; + public const int YieldCycleCount = 10; + protected override int NumberOfClients => 2; public UniversalRpcTestsBase(HostOrServer hostOrServer) : base(hostOrServer) @@ -488,6 +497,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests protected override void OnInlineTearDown() { + MockTransport.ClearQueues(); Clear(); } @@ -496,6 +506,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests foreach (var obj in Object.FindObjectsByType(FindObjectsSortMode.None)) { obj.Received = string.Empty; + obj.ReceivedCount = 0; obj.ReceivedParams = null; obj.ReceivedFrom = ulong.MaxValue; } @@ -528,10 +539,11 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests return m_PlayerNetworkObjects[onClient][ownerClientId].GetComponent(); } - protected void VerifyLocalReceived(ulong objectOwner, ulong sender, string name, bool verifyReceivedFrom) + protected void VerifyLocalReceived(ulong objectOwner, ulong sender, string name, bool verifyReceivedFrom, int expectedReceived = 1) { var obj = GetPlayerObject(objectOwner, sender); Assert.AreEqual(name, obj.Received); + Assert.That(obj.ReceivedCount, Is.EqualTo(expectedReceived)); Assert.IsNull(obj.ReceivedParams); if (verifyReceivedFrom) { @@ -543,6 +555,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests { var obj = GetPlayerObject(objectOwner, sender); Assert.AreEqual(name, obj.Received); + Assert.That(obj.ReceivedCount, Is.EqualTo(1)); Assert.IsNotNull(obj.ReceivedParams); Assert.AreEqual(i, obj.ReceivedParams.Item1); Assert.AreEqual(b, obj.ReceivedParams.Item2); @@ -556,17 +569,18 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests { UniversalRpcNetworkBehaviour playerObject = GetPlayerObject(objectOwner, client); Assert.AreEqual(string.Empty, playerObject.Received); + Assert.That(playerObject.ReceivedCount, Is.EqualTo(0)); Assert.IsNull(playerObject.ReceivedParams); } } - protected void VerifyRemoteReceived(ulong objectOwner, ulong sender, string message, ulong[] receivedBy, bool verifyReceivedFrom, bool waitForMessages = true) + protected void VerifyRemoteReceived(ulong objectOwner, ulong sender, string message, ulong[] receivedBy, bool verifyReceivedFrom, bool waitForMessages = true, int expectedReceived = 1) { foreach (var client in receivedBy) { if (client == sender) { - VerifyLocalReceived(objectOwner, sender, message, verifyReceivedFrom); + VerifyLocalReceived(objectOwner, sender, message, verifyReceivedFrom, expectedReceived); break; } @@ -628,6 +642,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests { UniversalRpcNetworkBehaviour playerObject = GetPlayerObject(objectOwner, client); Assert.AreEqual(message, playerObject.Received); + Assert.That(playerObject.ReceivedCount, Is.EqualTo(expectedReceived)); Assert.IsNull(playerObject.ReceivedParams); if (verifyReceivedFrom) { @@ -701,6 +716,7 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests { UniversalRpcNetworkBehaviour playerObject = GetPlayerObject(objectOwner, client); Assert.AreEqual(message, playerObject.Received); + Assert.That(playerObject.ReceivedCount, Is.EqualTo(1)); Assert.IsNotNull(playerObject.ReceivedParams); Assert.AreEqual(i, playerObject.ReceivedParams.Item1); @@ -1159,27 +1175,40 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests } - [Test] - public void TestSendingWithSingleOverride( - [Values] SendTo defaultSendTo, - [Values(0u, 1u, 2u)] ulong recipient, - [Values(0u, 1u, 2u)] ulong objectOwner, - [Values(0u, 1u, 2u)] ulong sender - ) + [UnityTest] + public IEnumerator TestSendingWithSingleOverride() { - var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + foreach (var defaultSendTo in Enum.GetValues(typeof(SendTo))) + { + for (ulong recipient = 0u; recipient <= 2u; ++recipient) + { + for (ulong objectOwner = 0u; objectOwner <= 2u; ++objectOwner) + { + for (ulong sender = 0u; sender <= 2u; ++sender) + { + if (++YieldCheck % YieldCycleCount == 0) + { + yield return null; + } + OnInlineSetup(); + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; - var senderObject = GetPlayerObject(objectOwner, sender); - var target = senderObject.RpcTarget.Single(recipient, RpcTargetUse.Temp); - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + var senderObject = GetPlayerObject(objectOwner, sender); + var target = senderObject.RpcTarget.Single(recipient, RpcTargetUse.Temp); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); - VerifyRemoteReceived(objectOwner, sender, sendMethodName, new[] { recipient }, false); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient != c).ToArray()); + VerifyRemoteReceived(objectOwner, sender, sendMethodName, new[] { recipient }, false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient != c).ToArray()); - // Pass some time to make sure that no other client ever receives this - TimeTravel(1f, 30); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient != c).ToArray()); + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient != c).ToArray()); + OnInlineTearDown(); + } + } + } + } } } @@ -1193,27 +1222,40 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests } - [Test] - public void TestSendingWithSingleNotOverride( - [Values] SendTo defaultSendTo, - [Values(0u, 1u, 2u)] ulong recipient, - [Values(0u, 1u, 2u)] ulong objectOwner, - [Values(0u, 1u, 2u)] ulong sender - ) + [UnityTest] + public IEnumerator TestSendingWithSingleNotOverride() { - var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + foreach (var defaultSendTo in Enum.GetValues(typeof(SendTo))) + { + for (ulong recipient = 0u; recipient <= 2u; ++recipient) + { + for (ulong objectOwner = 0u; objectOwner <= 2u; ++objectOwner) + { + for (ulong sender = 0u; sender <= 2u; ++sender) + { + if (++YieldCheck % YieldCycleCount == 0) + { + yield return null; + } + OnInlineSetup(); + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; - var senderObject = GetPlayerObject(objectOwner, sender); - var target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + var senderObject = GetPlayerObject(objectOwner, sender); + var target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); - VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient != c).ToArray(), false); - VerifyNotReceived(objectOwner, new[] { recipient }); + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient != c).ToArray(), false); + VerifyNotReceived(objectOwner, new[] { recipient }); - // Pass some time to make sure that no other client ever receives this - TimeTravel(1f, 30); - VerifyNotReceived(objectOwner, new[] { recipient }); + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, new[] { recipient }); + OnInlineTearDown(); + } + } + } + } } } @@ -1243,56 +1285,80 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests List } - [Test] - public void TestSendingWithGroupOverride( - [Values] SendTo defaultSendTo, - [ValueSource(nameof(RecipientGroups))] ulong[] recipient, - [Values(0u, 1u, 2u)] ulong objectOwner, - [Values(0u, 1u, 2u)] ulong sender, - [Values] AllocationType allocationType - ) + // Extending timeout since the added yield return causes this test to commonly timeout + [Timeout(600000)] + [UnityTest] + public IEnumerator TestSendingWithGroupOverride() { - var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; - - var senderObject = GetPlayerObject(objectOwner, sender); - BaseRpcTarget target = null; - switch (allocationType) + var waitFor = new WaitForFixedUpdate(); + foreach (var defaultSendTo in Enum.GetValues(typeof(SendTo))) { - case AllocationType.Array: - target = senderObject.RpcTarget.Group(recipient, RpcTargetUse.Temp); - break; - case AllocationType.List: - target = senderObject.RpcTarget.Group(recipient.ToList(), RpcTargetUse.Temp); - break; - case AllocationType.NativeArray: - var arr = new NativeArray(recipient, Allocator.Temp); - target = senderObject.RpcTarget.Group(arr, RpcTargetUse.Temp); - arr.Dispose(); - break; - case AllocationType.NativeList: - // For some reason on 2020.3, calling list.AsArray() and passing that to the next function - // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later - // versions of Unity. - var list = new NativeList(recipient.Length, Allocator.TempJob); - foreach (var id in recipient) + m_EnableVerboseDebug = true; + VerboseDebug($"Processing: {defaultSendTo}"); + m_EnableVerboseDebug = false; + + foreach (var recipient in RecipientGroups) + { + for (ulong objectOwner = 0u; objectOwner <= 2u; ++objectOwner) { - list.Add(id); + for (ulong sender = 0u; sender <= 2u; ++sender) + { + yield return waitFor; + foreach (var allocationType in Enum.GetValues(typeof(AllocationType))) + { + //if (++YieldCheck % YieldCycleCount == 0) + //{ + // yield return null; + //} + OnInlineSetup(); + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + BaseRpcTarget target = null; + switch (allocationType) + { + case AllocationType.Array: + target = senderObject.RpcTarget.Group(recipient, RpcTargetUse.Temp); + break; + case AllocationType.List: + target = senderObject.RpcTarget.Group(recipient.ToList(), RpcTargetUse.Temp); + break; + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + target = senderObject.RpcTarget.Group(arr, RpcTargetUse.Temp); + arr.Dispose(); + break; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) + { + list.Add(id); + } + + target = senderObject.RpcTarget.Group(list, RpcTargetUse.Temp); + list.Dispose(); + break; + } + + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient.Contains(c)).ToArray(), false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); + OnInlineTearDown(); + } + } } - target = senderObject.RpcTarget.Group(list, RpcTargetUse.Temp); - list.Dispose(); - break; + } } - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); - - VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => recipient.Contains(c)).ToArray(), false); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); - - // Pass some time to make sure that no other client ever receives this - TimeTravel(1f, 30); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray()); } - } [TestFixture(HostOrServer.Host)] @@ -1320,54 +1386,78 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests List } - [Test] - public void TestSendingWithGroupNotOverride( - [Values] SendTo defaultSendTo, - [ValueSource(nameof(RecipientGroups))] ulong[] recipient, - [Values(0u, 1u, 2u)] ulong objectOwner, - [Values(0u, 1u, 2u)] ulong sender, - [Values] AllocationType allocationType - ) + // Extending timeout since the added yield return causes this test to commonly timeout + [Timeout(600000)] + [UnityTest] + public IEnumerator TestSendingWithGroupNotOverride() { - var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; - - var senderObject = GetPlayerObject(objectOwner, sender); - BaseRpcTarget target = null; - switch (allocationType) + var waitFor = new WaitForFixedUpdate(); + foreach (var defaultSendTo in Enum.GetValues(typeof(SendTo))) { - case AllocationType.Array: - target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); - break; - case AllocationType.List: - target = senderObject.RpcTarget.Not(recipient.ToList(), RpcTargetUse.Temp); - break; - case AllocationType.NativeArray: - var arr = new NativeArray(recipient, Allocator.Temp); - target = senderObject.RpcTarget.Not(arr, RpcTargetUse.Temp); - arr.Dispose(); - break; - case AllocationType.NativeList: - // For some reason on 2020.3, calling list.AsArray() and passing that to the next function - // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later - // versions of Unity. - var list = new NativeList(recipient.Length, Allocator.TempJob); - foreach (var id in recipient) + m_EnableVerboseDebug = true; + VerboseDebug($"Processing: {defaultSendTo}"); + m_EnableVerboseDebug = false; + foreach (var recipient in RecipientGroups) + { + for (ulong objectOwner = 0u; objectOwner <= 2u; ++objectOwner) { - list.Add(id); + for (ulong sender = 0u; sender <= 2u; ++sender) + { + yield return waitFor; + + foreach (var allocationType in Enum.GetValues(typeof(AllocationType))) + { + //if (++YieldCheck % YieldCycleCount == 0) + //{ + // yield return waitFor; + //} + + OnInlineSetup(); + var sendMethodName = $"DefaultTo{defaultSendTo}AllowOverrideRpc"; + + var senderObject = GetPlayerObject(objectOwner, sender); + BaseRpcTarget target = null; + switch (allocationType) + { + case AllocationType.Array: + target = senderObject.RpcTarget.Not(recipient, RpcTargetUse.Temp); + break; + case AllocationType.List: + target = senderObject.RpcTarget.Not(recipient.ToList(), RpcTargetUse.Temp); + break; + case AllocationType.NativeArray: + var arr = new NativeArray(recipient, Allocator.Temp); + target = senderObject.RpcTarget.Not(arr, RpcTargetUse.Temp); + arr.Dispose(); + break; + case AllocationType.NativeList: + // For some reason on 2020.3, calling list.AsArray() and passing that to the next function + // causes Allocator.Temp allocations to become invalid somehow. This is not an issue on later + // versions of Unity. + var list = new NativeList(recipient.Length, Allocator.TempJob); + foreach (var id in recipient) + { + list.Add(id); + } + target = senderObject.RpcTarget.Not(list, RpcTargetUse.Temp); + list.Dispose(); + break; + } + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); + + VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray(), false); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); + + // Pass some time to make sure that no other client ever receives this + TimeTravel(1f, 30); + VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); + OnInlineTearDown(); + } + } } - target = senderObject.RpcTarget.Not(list, RpcTargetUse.Temp); - list.Dispose(); - break; + } } - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)target }); - - VerifyRemoteReceived(objectOwner, sender, sendMethodName, s_ClientIds.Where(c => !recipient.Contains(c)).ToArray(), false); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); - - // Pass some time to make sure that no other client ever receives this - TimeTravel(1f, 30); - VerifyNotReceived(objectOwner, s_ClientIds.Where(c => recipient.Contains(c)).ToArray()); } } @@ -1391,223 +1481,180 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests } - [Test] - // All the test cases that involve sends that will be delivered locally - [TestCase(SendTo.Everyone, 0u, 0u)] - [TestCase(SendTo.Everyone, 0u, 1u)] - [TestCase(SendTo.Everyone, 0u, 2u)] - [TestCase(SendTo.Everyone, 1u, 0u)] - [TestCase(SendTo.Everyone, 1u, 1u)] - [TestCase(SendTo.Everyone, 1u, 2u)] - [TestCase(SendTo.Everyone, 2u, 0u)] - [TestCase(SendTo.Everyone, 2u, 1u)] - [TestCase(SendTo.Everyone, 2u, 2u)] - [TestCase(SendTo.Me, 0u, 0u)] - [TestCase(SendTo.Me, 0u, 1u)] - [TestCase(SendTo.Me, 0u, 2u)] - [TestCase(SendTo.Me, 1u, 0u)] - [TestCase(SendTo.Me, 1u, 1u)] - [TestCase(SendTo.Me, 1u, 2u)] - [TestCase(SendTo.Me, 2u, 0u)] - [TestCase(SendTo.Me, 2u, 1u)] - [TestCase(SendTo.Me, 2u, 2u)] - [TestCase(SendTo.Owner, 0u, 0u)] - [TestCase(SendTo.Owner, 1u, 1u)] - [TestCase(SendTo.Owner, 2u, 2u)] - [TestCase(SendTo.Server, 0u, 0u)] - [TestCase(SendTo.Server, 1u, 0u)] - [TestCase(SendTo.Server, 2u, 0u)] - [TestCase(SendTo.NotOwner, 0u, 1u)] - [TestCase(SendTo.NotOwner, 0u, 2u)] - [TestCase(SendTo.NotOwner, 1u, 0u)] - [TestCase(SendTo.NotOwner, 1u, 2u)] - [TestCase(SendTo.NotOwner, 2u, 0u)] - [TestCase(SendTo.NotOwner, 2u, 1u)] - [TestCase(SendTo.NotServer, 0u, 1u)] - [TestCase(SendTo.NotServer, 0u, 2u)] - [TestCase(SendTo.NotServer, 1u, 1u)] - [TestCase(SendTo.NotServer, 1u, 2u)] - [TestCase(SendTo.NotServer, 2u, 1u)] - [TestCase(SendTo.NotServer, 2u, 2u)] - [TestCase(SendTo.ClientsAndHost, 0u, 0u)] - [TestCase(SendTo.ClientsAndHost, 0u, 1u)] - [TestCase(SendTo.ClientsAndHost, 0u, 2u)] - [TestCase(SendTo.ClientsAndHost, 1u, 0u)] - [TestCase(SendTo.ClientsAndHost, 1u, 1u)] - [TestCase(SendTo.ClientsAndHost, 1u, 2u)] - [TestCase(SendTo.ClientsAndHost, 2u, 0u)] - [TestCase(SendTo.ClientsAndHost, 2u, 1u)] - [TestCase(SendTo.ClientsAndHost, 2u, 2u)] - public void TestDeferLocal( - SendTo defaultSendTo, - ulong objectOwner, - ulong sender - ) + private struct TestData { - if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + public SendTo SendTo; + public ulong ObjectOwner; + public ulong Sender; + + public TestData(SendTo sendTo, ulong objectOwner, ulong sender) { - // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored - // Just consider this case a success... - return; + SendTo = sendTo; + ObjectOwner = objectOwner; + Sender = sender; } - var sendMethodName = $"DefaultTo{defaultSendTo}DeferLocalRpc"; - var verifyMethodName = $"VerifySentTo{defaultSendTo}"; - var senderObject = GetPlayerObject(objectOwner, sender); - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { new RpcParams() }); - - VerifyNotReceived(objectOwner, new[] { sender }); - // Should be received on the next frame - SimulateOneFrame(); - VerifyLocalReceived(objectOwner, sender, sendMethodName, false); - - var verifyMethod = GetType().GetMethod(verifyMethodName); - verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); } - [Test] // All the test cases that involve sends that will be delivered locally - [TestCase(SendTo.Everyone, 0u, 0u)] - [TestCase(SendTo.Everyone, 0u, 1u)] - [TestCase(SendTo.Everyone, 0u, 2u)] - [TestCase(SendTo.Everyone, 1u, 0u)] - [TestCase(SendTo.Everyone, 1u, 1u)] - [TestCase(SendTo.Everyone, 1u, 2u)] - [TestCase(SendTo.Everyone, 2u, 0u)] - [TestCase(SendTo.Everyone, 2u, 1u)] - [TestCase(SendTo.Everyone, 2u, 2u)] - [TestCase(SendTo.Me, 0u, 0u)] - [TestCase(SendTo.Me, 0u, 1u)] - [TestCase(SendTo.Me, 0u, 2u)] - [TestCase(SendTo.Me, 1u, 0u)] - [TestCase(SendTo.Me, 1u, 1u)] - [TestCase(SendTo.Me, 1u, 2u)] - [TestCase(SendTo.Me, 2u, 0u)] - [TestCase(SendTo.Me, 2u, 1u)] - [TestCase(SendTo.Me, 2u, 2u)] - [TestCase(SendTo.Owner, 0u, 0u)] - [TestCase(SendTo.Owner, 1u, 1u)] - [TestCase(SendTo.Owner, 2u, 2u)] - [TestCase(SendTo.Server, 0u, 0u)] - [TestCase(SendTo.Server, 1u, 0u)] - [TestCase(SendTo.Server, 2u, 0u)] - [TestCase(SendTo.NotOwner, 0u, 1u)] - [TestCase(SendTo.NotOwner, 0u, 2u)] - [TestCase(SendTo.NotOwner, 1u, 0u)] - [TestCase(SendTo.NotOwner, 1u, 2u)] - [TestCase(SendTo.NotOwner, 2u, 0u)] - [TestCase(SendTo.NotOwner, 2u, 1u)] - [TestCase(SendTo.NotServer, 0u, 1u)] - [TestCase(SendTo.NotServer, 0u, 2u)] - [TestCase(SendTo.NotServer, 1u, 1u)] - [TestCase(SendTo.NotServer, 1u, 2u)] - [TestCase(SendTo.NotServer, 2u, 1u)] - [TestCase(SendTo.NotServer, 2u, 2u)] - [TestCase(SendTo.ClientsAndHost, 0u, 0u)] - [TestCase(SendTo.ClientsAndHost, 0u, 1u)] - [TestCase(SendTo.ClientsAndHost, 0u, 2u)] - [TestCase(SendTo.ClientsAndHost, 1u, 0u)] - [TestCase(SendTo.ClientsAndHost, 1u, 1u)] - [TestCase(SendTo.ClientsAndHost, 1u, 2u)] - [TestCase(SendTo.ClientsAndHost, 2u, 0u)] - [TestCase(SendTo.ClientsAndHost, 2u, 1u)] - [TestCase(SendTo.ClientsAndHost, 2u, 2u)] - public void TestDeferLocalOverrideToTrue( - SendTo defaultSendTo, - ulong objectOwner, - ulong sender - ) + private static TestData[] s_LocalDeliveryTestCases = { - if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + new TestData(SendTo.Everyone, 0u, 0u), + new TestData(SendTo.Everyone, 0u, 1u), + new TestData(SendTo.Everyone, 0u, 2u), + new TestData(SendTo.Everyone, 1u, 0u), + new TestData(SendTo.Everyone, 1u, 1u), + new TestData(SendTo.Everyone, 1u, 2u), + new TestData(SendTo.Everyone, 2u, 0u), + new TestData(SendTo.Everyone, 2u, 1u), + new TestData(SendTo.Everyone, 2u, 2u), + new TestData(SendTo.Me, 0u, 0u), + new TestData(SendTo.Me, 0u, 1u), + new TestData(SendTo.Me, 0u, 2u), + new TestData(SendTo.Me, 1u, 0u), + new TestData(SendTo.Me, 1u, 1u), + new TestData(SendTo.Me, 1u, 2u), + new TestData(SendTo.Me, 2u, 0u), + new TestData(SendTo.Me, 2u, 1u), + new TestData(SendTo.Me, 2u, 2u), + new TestData(SendTo.Owner, 0u, 0u), + new TestData(SendTo.Owner, 1u, 1u), + new TestData(SendTo.Owner, 2u, 2u), + new TestData(SendTo.Server, 0u, 0u), + new TestData(SendTo.Server, 1u, 0u), + new TestData(SendTo.Server, 2u, 0u), + new TestData(SendTo.NotOwner, 0u, 1u), + new TestData(SendTo.NotOwner, 0u, 2u), + new TestData(SendTo.NotOwner, 1u, 0u), + new TestData(SendTo.NotOwner, 1u, 2u), + new TestData(SendTo.NotOwner, 2u, 0u), + new TestData(SendTo.NotOwner, 2u, 1u), + new TestData(SendTo.NotServer, 0u, 1u), + new TestData(SendTo.NotServer, 0u, 2u), + new TestData(SendTo.NotServer, 1u, 1u), + new TestData(SendTo.NotServer, 1u, 2u), + new TestData(SendTo.NotServer, 2u, 1u), + new TestData(SendTo.NotServer, 2u, 2u), + new TestData(SendTo.ClientsAndHost, 0u, 0u), + new TestData(SendTo.ClientsAndHost, 0u, 1u), + new TestData(SendTo.ClientsAndHost, 0u, 2u), + new TestData(SendTo.ClientsAndHost, 1u, 0u), + new TestData(SendTo.ClientsAndHost, 1u, 1u), + new TestData(SendTo.ClientsAndHost, 1u, 2u), + new TestData(SendTo.ClientsAndHost, 2u, 0u), + new TestData(SendTo.ClientsAndHost, 2u, 1u), + new TestData(SendTo.ClientsAndHost, 2u, 2u), + }; + + + [UnityTest] + public IEnumerator TestDeferLocal() + { + foreach (var testCase in s_LocalDeliveryTestCases) { - // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored - // Just consider this case a success... - return; + if (++YieldCheck % YieldCycleCount == 0) + { + yield return null; + } + OnInlineSetup(); + var defaultSendTo = testCase.SendTo; + var sender = testCase.Sender; + var objectOwner = testCase.ObjectOwner; + + if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + { + // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored + // Just consider this case a success... + yield break; + } + + var sendMethodName = $"DefaultTo{defaultSendTo}DeferLocalRpc"; + var verifyMethodName = $"VerifySentTo{defaultSendTo}"; + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { new RpcParams() }); + + VerifyNotReceived(objectOwner, new[] { sender }); + // Should be received on the next frame + SimulateOneFrame(); + VerifyLocalReceived(objectOwner, sender, sendMethodName, false); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + OnInlineTearDown(); } - var sendMethodName = $"DefaultTo{defaultSendTo}WithRpcParamsRpc"; - var verifyMethodName = $"VerifySentTo{defaultSendTo}"; - var senderObject = GetPlayerObject(objectOwner, sender); - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)LocalDeferMode.Defer }); - - VerifyNotReceived(objectOwner, new[] { sender }); - // Should be received on the next frame - SimulateOneFrame(); - VerifyLocalReceived(objectOwner, sender, sendMethodName, false); - - var verifyMethod = GetType().GetMethod(verifyMethodName); - verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); } - [Test] - // All the test cases that involve sends that will be delivered locally - [TestCase(SendTo.Everyone, 0u, 0u)] - [TestCase(SendTo.Everyone, 0u, 1u)] - [TestCase(SendTo.Everyone, 0u, 2u)] - [TestCase(SendTo.Everyone, 1u, 0u)] - [TestCase(SendTo.Everyone, 1u, 1u)] - [TestCase(SendTo.Everyone, 1u, 2u)] - [TestCase(SendTo.Everyone, 2u, 0u)] - [TestCase(SendTo.Everyone, 2u, 1u)] - [TestCase(SendTo.Everyone, 2u, 2u)] - [TestCase(SendTo.Me, 0u, 0u)] - [TestCase(SendTo.Me, 0u, 1u)] - [TestCase(SendTo.Me, 0u, 2u)] - [TestCase(SendTo.Me, 1u, 0u)] - [TestCase(SendTo.Me, 1u, 1u)] - [TestCase(SendTo.Me, 1u, 2u)] - [TestCase(SendTo.Me, 2u, 0u)] - [TestCase(SendTo.Me, 2u, 1u)] - [TestCase(SendTo.Me, 2u, 2u)] - [TestCase(SendTo.Owner, 0u, 0u)] - [TestCase(SendTo.Owner, 1u, 1u)] - [TestCase(SendTo.Owner, 2u, 2u)] - [TestCase(SendTo.Server, 0u, 0u)] - [TestCase(SendTo.Server, 1u, 0u)] - [TestCase(SendTo.Server, 2u, 0u)] - [TestCase(SendTo.NotOwner, 0u, 1u)] - [TestCase(SendTo.NotOwner, 0u, 2u)] - [TestCase(SendTo.NotOwner, 1u, 0u)] - [TestCase(SendTo.NotOwner, 1u, 2u)] - [TestCase(SendTo.NotOwner, 2u, 0u)] - [TestCase(SendTo.NotOwner, 2u, 1u)] - [TestCase(SendTo.NotServer, 0u, 1u)] - [TestCase(SendTo.NotServer, 0u, 2u)] - [TestCase(SendTo.NotServer, 1u, 1u)] - [TestCase(SendTo.NotServer, 1u, 2u)] - [TestCase(SendTo.NotServer, 2u, 1u)] - [TestCase(SendTo.NotServer, 2u, 2u)] - [TestCase(SendTo.ClientsAndHost, 0u, 0u)] - [TestCase(SendTo.ClientsAndHost, 0u, 1u)] - [TestCase(SendTo.ClientsAndHost, 0u, 2u)] - [TestCase(SendTo.ClientsAndHost, 1u, 0u)] - [TestCase(SendTo.ClientsAndHost, 1u, 1u)] - [TestCase(SendTo.ClientsAndHost, 1u, 2u)] - [TestCase(SendTo.ClientsAndHost, 2u, 0u)] - [TestCase(SendTo.ClientsAndHost, 2u, 1u)] - [TestCase(SendTo.ClientsAndHost, 2u, 2u)] - public void TestDeferLocalOverrideToFalse( - SendTo defaultSendTo, - ulong objectOwner, - ulong sender - ) + [UnityTest] + public IEnumerator TestDeferLocalOverrideToTrue() { - if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + foreach (var testCase in s_LocalDeliveryTestCases) { - // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored - // Just consider this case a success... - return; + if (++YieldCheck % YieldCycleCount == 0) + { + yield return null; + } + OnInlineSetup(); + var defaultSendTo = testCase.SendTo; + var sender = testCase.Sender; + var objectOwner = testCase.ObjectOwner; + + if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + { + // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored + // Just consider this case a success... + yield break; + } + + var sendMethodName = $"DefaultTo{defaultSendTo}WithRpcParamsRpc"; + var verifyMethodName = $"VerifySentTo{defaultSendTo}"; + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)LocalDeferMode.Defer }); + + VerifyNotReceived(objectOwner, new[] { sender }); + // Should be received on the next frame + SimulateOneFrame(); + VerifyLocalReceived(objectOwner, sender, sendMethodName, false); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + OnInlineTearDown(); } - var sendMethodName = $"DefaultTo{defaultSendTo}DeferLocalRpc"; - var verifyMethodName = $"VerifySentTo{defaultSendTo}"; - var senderObject = GetPlayerObject(objectOwner, sender); - var sendMethod = senderObject.GetType().GetMethod(sendMethodName); - sendMethod.Invoke(senderObject, new object[] { (RpcParams)LocalDeferMode.SendImmediate }); + } - VerifyLocalReceived(objectOwner, sender, sendMethodName, false); + [UnityTest] + public IEnumerator TestDeferLocalOverrideToFalse() + { + foreach (var testCase in s_LocalDeliveryTestCases) + { + if (++YieldCheck % YieldCycleCount == 0) + { + yield return null; + } + OnInlineSetup(); + var defaultSendTo = testCase.SendTo; + var sender = testCase.Sender; + var objectOwner = testCase.ObjectOwner; - var verifyMethod = GetType().GetMethod(verifyMethodName); - verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + if (defaultSendTo == SendTo.ClientsAndHost && sender == 0u && !m_ServerNetworkManager.IsHost) + { + // Not calling Assert.Ignore() because Unity will mark the whole block of tests as ignored + // Just consider this case a success... + yield break; + } + + var sendMethodName = $"DefaultTo{defaultSendTo}DeferLocalRpc"; + var verifyMethodName = $"VerifySentTo{defaultSendTo}"; + var senderObject = GetPlayerObject(objectOwner, sender); + var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + sendMethod.Invoke(senderObject, new object[] { (RpcParams)LocalDeferMode.SendImmediate }); + + VerifyLocalReceived(objectOwner, sender, sendMethodName, false); + + var verifyMethod = GetType().GetMethod(verifyMethodName); + verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName }); + OnInlineTearDown(); + } } } @@ -1636,17 +1683,20 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests VerifyNotReceived(NetworkManager.ServerClientId, s_ClientIds); - for (var i = 0; i < 10; ++i) + var clientListExpected = 1; + var serverListExpected = 2; + for (var i = 1; i <= 10; ++i) { WaitForMessageReceivedWithTimeTravel(clientList); - VerifyRemoteReceived(NetworkManager.ServerClientId, NetworkManager.ServerClientId, nameof(UniversalRpcNetworkBehaviour.MutualRecursionClientRpc), clientIdArray, false, false); + VerifyRemoteReceived(NetworkManager.ServerClientId, NetworkManager.ServerClientId, nameof(UniversalRpcNetworkBehaviour.MutualRecursionClientRpc), clientIdArray, false, false, clientListExpected); VerifyNotReceived(NetworkManager.ServerClientId, serverIdArray); + clientListExpected *= 2; Clear(); - WaitForMessageReceivedWithTimeTravel(serverList); - VerifyRemoteReceived(NetworkManager.ServerClientId, NetworkManager.ServerClientId, nameof(UniversalRpcNetworkBehaviour.MutualRecursionServerRpc), serverIdArray, false, false); + VerifyRemoteReceived(NetworkManager.ServerClientId, NetworkManager.ServerClientId, nameof(UniversalRpcNetworkBehaviour.MutualRecursionServerRpc), serverIdArray, false, false, serverListExpected); VerifyNotReceived(NetworkManager.ServerClientId, clientIdArray); + serverListExpected *= 2; Clear(); } @@ -1915,3 +1965,4 @@ namespace Unity.Netcode.RuntimeTests.UniversalRpcTests } } +#endif diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef index 719b7be..72d85ca 100644 --- a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef +++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef @@ -12,12 +12,20 @@ "Unity.Networking.Transport", "ClientNetworkTransform", "Unity.Netcode.TestHelpers.Runtime", - "Unity.Mathematics" + "Unity.Mathematics", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" ], - "optionalUnityReferences": [ - "TestAssemblies" + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" ], + "autoReferenced": false, "defineConstraints": [ + "UNITY_INCLUDE_TESTS", "UNITY_INCLUDE_TESTS" ], "versionDefines": [ @@ -46,5 +54,6 @@ "expression": "2.0.0-exp", "define": "UTP_TRANSPORT_2_0_ABOVE" } - ] -} + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/package.json b/package.json index 9774cf1..71c8ae1 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,23 @@ "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.8.1", + "version": "1.9.1", "unity": "2021.3", "dependencies": { "com.unity.nuget.mono-cecil": "1.10.1", "com.unity.transport": "1.4.0" }, "_upm": { - "changelog": "### Fixed\n\n- Fixed a compile error when compiling for IL2CPP targets when using the new `[Rpc]` attribute. (#2824)" + "changelog": "### Added\n- Added AnticipatedNetworkVariable, which adds support for client anticipation of NetworkVariable values, allowing for more responsive gameplay (#2820)\n- Added AnticipatedNetworkTransform, which adds support for client anticipation of NetworkTransforms (#2820)\n- Added NetworkVariableBase.ExceedsDirtinessThreshold to allow network variables to throttle updates by only sending updates when the difference between the current and previous values exceeds a threshold. (This is exposed in NetworkVariable with the callback NetworkVariable.CheckExceedsDirtinessThreshold) (#2820)\n- Added NetworkVariableUpdateTraits, which add additional throttling support: MinSecondsBetweenUpdates will prevent the NetworkVariable from sending updates more often than the specified time period (even if it exceeds the dirtiness threshold), while MaxSecondsBetweenUpdates will force a dirty NetworkVariable to send an update after the specified time period even if it has not yet exceeded the dirtiness threshold. (#2820)\n- Added virtual method NetworkVariableBase.OnInitialize() which can be used by NetworkVariable subclasses to add initialization code (#2820)\n- Added virtual method NetworkVariableBase.Update(), which is called once per frame to support behaviors such as interpolation between an anticipated value and an authoritative one. (#2820)\n- Added NetworkTime.TickWithPartial, which represents the current tick as a double that includes the fractional/partial tick value. (#2820)\n- Added NetworkTickSystem.AnticipationTick, which can be helpful with implementation of client anticipation. This value represents the tick the current local client was at at the beginning of the most recent network round trip, which enables it to correlate server update ticks with the client tick that may have triggered them. (#2820)\n- `NetworkVariable` now includes built-in support for `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, and `Dictionary` (#2813)\n- `NetworkVariable` now includes delta compression for collection values (`NativeList`, `NativeArray`, `NativeHashSet`, `NativeHashMap`, `List`, `HashSet`, `Dictionary`, and `FixedString` types) to save bandwidth by only sending the values that changed. (Note: For `NativeList`, `NativeArray`, and `List`, this algorithm works differently than that used in `NetworkList`. This algorithm will use less bandwidth for \"set\" and \"add\" operations, but `NetworkList` is more bandwidth-efficient if you are performing frequent \"insert\" operations.) (#2813)\n- `UserNetworkVariableSerialization` now has optional callbacks for `WriteDelta` and `ReadDelta`. If both are provided, they will be used for all serialization operations on NetworkVariables of that type except for the first one for each client. If either is missing, the existing `Write` and `Read` will always be used. (#2813)\n- Network variables wrapping `INetworkSerializable` types can perform delta serialization by setting `UserNetworkVariableSerialization.WriteDelta` and `UserNetworkVariableSerialization.ReadDelta` for those types. The built-in `INetworkSerializable` serializer will continue to be used for all other serialization operations, but if those callbacks are set, it will call into them on all but the initial serialization to perform delta serialization. (This could be useful if you have a large struct where most values do not change regularly and you want to send only the fields that did change.) (#2813)\n\n### Fixed\n\n- Fixed issue where NetworkTransformEditor would throw and exception if you excluded the physics package. (#2871)\n- Fixed issue where `NetworkTransform` could not properly synchronize its base position when using half float precision. (#2845)\n- Fixed issue where the host was not invoking `OnClientDisconnectCallback` for its own local client when internally shutting down. (#2822)\n- Fixed issue where NetworkTransform could potentially attempt to \"unregister\" a named message prior to it being registered. (#2807)\n- Fixed issue where in-scene placed `NetworkObject`s with complex nested children `NetworkObject`s (more than one child in depth) would not synchronize properly if WorldPositionStays was set to true. (#2796)\n\n### Changed\n\n- Changed `NetworkObjectReference` and `NetworkBehaviourReference` to allow null references when constructing and serializing. (#2874)\n- Changed `NetworkAnimator` no longer requires the `Animator` component to exist on the same `GameObject`. (#2872)\n- Changed `NetworkTransform` to now use `NetworkTransformMessage` as opposed to named messages for NetworkTransformState updates. (#2810)\n- Changed `CustomMessageManager` so it no longer attempts to register or \"unregister\" a null or empty string and will log an error if this condition occurs. (#2807)" }, "upmCi": { - "footprint": "5e57664d4f43bf176189c5ec8fd10f595b668e7d" + "footprint": "0c6c6a0e036a153ff11d0b99241c93ccebfec896" }, - "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.8/manual/index.html", + "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@1.9/manual/index.html", "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "eb213838233cbee2030891203bcb72d2354a5a7d" + "revision": "7d27c5141123e3b9f31419ccca7768a640a45d56" }, "samples": [ {