From 60e2dabef4862151710f58a80d12e553034f40a3 Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Fri, 1 Apr 2022 00:00:00 +0000 Subject: [PATCH] com.unity.netcode.gameobjects@1.0.0-pre.7 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.0.0-pre.7] - 2022-04-01 ### Added - Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828) - Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823) - Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762) - `UnityTransport` settings can now be set programmatically. (#1845) - `FastBufferWriter` and Reader IsInitialized property. (#1859) ### Changed - Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849) ### Removed - Removed `SnapshotSystem` (#1852) - Removed `com.unity.modules.animation`, `com.unity.modules.physics` and `com.unity.modules.physics2d` dependencies from the package (#1812) - Removed `com.unity.collections` dependency from the package (#1849) ### Fixed - Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850) - Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847) - Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841) - Fixed issue where entries were not being removed from the NetworkSpawnManager.OwnershipToObjectsTable. (#1838) - Fixed ClientRpcs would always send to all connected clients by default as opposed to only sending to the NetworkObject's Observers list by default. (#1836) - Fixed clarity for NetworkSceneManager client side notification when it receives a scene hash value that does not exist in its local hash table. (#1828) - Fixed client throws a key not found exception when it times out using UNet or UTP. (#1821) - Fixed network variable updates are no longer limited to 32,768 bytes when NetworkConfig.EnsureNetworkVariableLengthSafety is enabled. The limits are now determined by what the transport can send in a message. (#1811) - Fixed in-scene NetworkObjects get destroyed if a client fails to connect and shuts down the NetworkManager. (#1809) - Fixed user never being notified in the editor that a NetworkBehaviour requires a NetworkObject to function properly. (#1808) - Fixed PlayerObjects and dynamically spawned NetworkObjects not being added to the NetworkClient's OwnedObjects (#1801) - Fixed issue where NetworkManager would continue starting even if the NetworkTransport selected failed. (#1780) - Fixed issue when spawning new player if an already existing player exists it does not remove IsPlayer from the previous player (#1779) - Fixed lack of notification that NetworkManager and NetworkObject cannot be added to the same GameObject with in-editor notifications (#1777) - Fixed parenting warning printing for false positives (#1855) --- CHANGELOG.md | 38 + .../BufferedLinearInterpolator.cs | 11 +- Components/NetworkAnimator.cs | 16 +- Components/NetworkRigidbody.cs | 2 + Components/NetworkRigidbody2D.cs | 2 + Components/NetworkTransform.cs | 5 +- .../com.unity.netcode.components.asmdef | 19 +- Editor/NetworkAnimatorEditor.cs | 23 - Editor/NetworkBehaviourEditor.cs | 86 ++ Editor/NetworkManagerEditor.cs | 10 +- Editor/NetworkManagerHelper.cs | 91 +- Editor/NetworkObjectEditor.cs | 27 + Editor/NetworkTransformEditor.cs | 6 +- Editor/PackageChecker.meta | 8 + Editor/PackageChecker/UTPAdapterChecker.cs | 53 + .../PackageChecker/UTPAdapterChecker.cs.meta | 2 +- ...unity.netcode.editor.packagechecker.asmdef | 14 + ....netcode.editor.packagechecker.asmdef.meta | 7 + Runtime/AssemblyInfo.cs | 1 - Runtime/Configuration/NetworkConfig.cs | 13 - Runtime/Connection/NetworkClient.cs | 13 +- Runtime/Core/IndexAllocator.cs | 388 ------ Runtime/Core/NetworkBehaviour.cs | 209 ++- Runtime/Core/NetworkBehaviourUpdater.cs | 2 +- Runtime/Core/NetworkManager.cs | 253 ++-- Runtime/Core/NetworkObject.cs | 204 +-- Runtime/Core/SnapshotRtt.cs | 93 -- Runtime/Core/SnapshotSystem.cs | 1151 ---------------- Runtime/Logging/NetworkLog.cs | 10 +- .../Messages/ChangeOwnershipMessage.cs | 21 +- .../Messages/NetworkVariableDeltaMessage.cs | 77 +- .../Messaging/Messages/SnapshotDataMessage.cs | 160 --- .../Messages/SnapshotDataMessage.cs.meta | 11 - Runtime/Messaging/MessagingSystem.cs | 2 +- Runtime/Metrics/INetworkMetrics.cs | 8 +- Runtime/Metrics/NetworkMetrics.cs | 62 +- Runtime/Metrics/NullNetworkMetrics.cs | 14 +- .../Collections/NetworkList.cs | 26 +- Runtime/NetworkVariable/NetworkVariable.cs | 77 +- .../NetworkVariable/NetworkVariableBase.cs | 41 +- .../NetworkVariablePermission.cs | 16 +- .../SceneManagement/NetworkSceneManager.cs | 89 +- Runtime/SceneManagement/SceneEventData.cs | 4 - Runtime/Serialization/BytePacker.cs | 21 +- Runtime/Serialization/FastBufferReader.cs | 8 +- Runtime/Serialization/FastBufferWriter.cs | 12 +- Runtime/Spawning/NetworkSpawnManager.cs | 328 +++-- Runtime/Transports/UNET/UNetChannel.cs | 54 - Runtime/Transports/UNET/UNetChannel.cs.meta | 11 - Runtime/Transports/UTP.meta | 8 + Runtime/Transports/UTP/BatchedReceiveQueue.cs | 96 ++ .../UTP/BatchedReceiveQueue.cs.meta | 2 +- Runtime/Transports/UTP/BatchedSendQueue.cs | 233 ++++ .../UTP/BatchedSendQueue.cs.meta} | 2 +- .../Transports/UTP/NetworkMetricsContext.cs | 8 + .../UTP/NetworkMetricsContext.cs.meta} | 2 +- .../UTP/NetworkMetricsPipelineStage.cs | 70 + .../UTP/NetworkMetricsPipelineStage.cs.meta | 11 + Runtime/Transports/UTP/UnityTransport.cs | 1222 +++++++++++++++++ Runtime/Transports/UTP/UnityTransport.cs.meta | 11 + Runtime/com.unity.netcode.runtime.asmdef | 10 +- TestHelpers/Runtime/NetcodeIntegrationTest.cs | 61 +- .../Runtime/NetcodeIntegrationTestHelpers.cs | 7 +- ...m.unity.netcode.testhelpers.runtime.asmdef | 10 +- Tests/Editor/IndexAllocatorTests.cs | 116 -- Tests/Editor/IndexAllocatorTests.cs.meta | 11 - .../NetworkManagerConfigurationTests.cs | 44 + Tests/Editor/NetworkVar.meta | 3 + Tests/Editor/NetworkVar/NetworkVarTests.cs | 41 + .../Editor/NetworkVar/NetworkVarTests.cs.meta | 3 + .../Serialization/FastBufferReaderTests.cs | 28 + .../Serialization/FastBufferWriterTests.cs | 23 + Tests/Editor/SnapshotRttTests.cs | 73 - Tests/Editor/SnapshotRttTests.cs.meta | 11 - Tests/Editor/SnapshotTests.cs | 363 ----- Tests/Editor/SnapshotTests.cs.meta | 11 - Tests/Editor/Transports.meta | 8 + .../Transports/BatchedReceiveQueueTests.cs | 193 +++ .../BatchedReceiveQueueTests.cs.meta | 11 + .../Transports/BatchedSendQueueTests.cs | 266 ++++ .../Transports/BatchedSendQueueTests.cs.meta | 11 + .../Editor/Transports/UnityTransportTests.cs | 84 ++ .../Transports/UnityTransportTests.cs.meta | 11 + .../com.unity.netcode.editortests.asmdef | 3 +- Tests/Runtime/ClientOnlyConnectionTests.cs | 78 ++ .../Runtime/ClientOnlyConnectionTests.cs.meta | 11 + .../NetworkVariableTestComponent.cs | 42 +- .../Runtime/Metrics/ConnectionMetricsTests.cs | 69 + .../Metrics/ConnectionMetricsTests.cs.meta | 3 + .../Runtime/Metrics/MessagingMetricsTests.cs | 4 +- .../Metrics/NetworkObjectMetricsTests.cs | 70 + .../Runtime/Metrics/PacketLossMetricsTests.cs | 89 ++ .../Metrics/PacketLossMetricsTests.cs.meta | 3 + Tests/Runtime/Metrics/PacketMetricsTests.cs | 6 +- Tests/Runtime/Metrics/RttMetricsTests.cs | 8 +- .../NetworkAnimator/NetworkAnimatorTests.cs | 2 + Tests/Runtime/NetworkManagerTransportTests.cs | 133 ++ .../NetworkManagerTransportTests.cs.meta | 11 + .../NetworkObjectDontDestroyWithOwnerTests.cs | 81 +- ...orkObjectNetworkClientOwnedObjectsTests.cs | 6 +- .../NetworkObjectOwnershipTests.cs | 276 ++-- Tests/Runtime/NetworkSpawnManagerTests.cs | 2 +- Tests/Runtime/NetworkVariableTests.cs | 224 ++- .../Runtime/Physics/NetworkRigidbody2DTest.cs | 2 + Tests/Runtime/Physics/NetworkRigidbodyTest.cs | 2 + Tests/Runtime/PlayerObjectTests.cs | 51 + Tests/Runtime/PlayerObjectTests.cs.meta | 11 + Tests/Runtime/TransformInterpolationTests.cs | 7 +- .../{Transport.meta => Transports.meta} | 0 .../DummyTransport.cs | 0 .../DummyTransport.cs.meta | 0 .../SIPTransportTests.cs | 0 .../SIPTransportTests.cs.meta | 0 .../UnityTransportConnectionTests.cs | 383 ++++++ .../UnityTransportConnectionTests.cs.meta | 11 + .../Transports/UnityTransportDriverClient.cs | 94 ++ .../UnityTransportDriverClient.cs.meta | 11 + .../Transports/UnityTransportTestHelpers.cs | 85 ++ .../UnityTransportTestHelpers.cs.meta | 11 + .../Runtime/Transports/UnityTransportTests.cs | 461 +++++++ .../Transports/UnityTransportTests.cs.meta | 11 + .../com.unity.netcode.runtimetests.asmdef | 13 +- package.json | 13 +- 123 files changed, 5741 insertions(+), 3409 deletions(-) delete mode 100644 Editor/NetworkAnimatorEditor.cs create mode 100644 Editor/PackageChecker.meta create mode 100644 Editor/PackageChecker/UTPAdapterChecker.cs rename Runtime/Core/SnapshotRtt.cs.meta => Editor/PackageChecker/UTPAdapterChecker.cs.meta (83%) create mode 100644 Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef create mode 100644 Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef.meta delete mode 100644 Runtime/Core/IndexAllocator.cs delete mode 100644 Runtime/Core/SnapshotRtt.cs delete mode 100644 Runtime/Core/SnapshotSystem.cs delete mode 100644 Runtime/Messaging/Messages/SnapshotDataMessage.cs delete mode 100644 Runtime/Messaging/Messages/SnapshotDataMessage.cs.meta delete mode 100644 Runtime/Transports/UNET/UNetChannel.cs delete mode 100644 Runtime/Transports/UNET/UNetChannel.cs.meta create mode 100644 Runtime/Transports/UTP.meta create mode 100644 Runtime/Transports/UTP/BatchedReceiveQueue.cs rename Editor/NetworkAnimatorEditor.cs.meta => Runtime/Transports/UTP/BatchedReceiveQueue.cs.meta (83%) create mode 100644 Runtime/Transports/UTP/BatchedSendQueue.cs rename Runtime/{Core/SnapshotSystem.cs.meta => Transports/UTP/BatchedSendQueue.cs.meta} (83%) create mode 100644 Runtime/Transports/UTP/NetworkMetricsContext.cs rename Runtime/{Core/IndexAllocator.cs.meta => Transports/UTP/NetworkMetricsContext.cs.meta} (83%) create mode 100644 Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs create mode 100644 Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs.meta create mode 100644 Runtime/Transports/UTP/UnityTransport.cs create mode 100644 Runtime/Transports/UTP/UnityTransport.cs.meta delete mode 100644 Tests/Editor/IndexAllocatorTests.cs delete mode 100644 Tests/Editor/IndexAllocatorTests.cs.meta create mode 100644 Tests/Editor/NetworkVar.meta create mode 100644 Tests/Editor/NetworkVar/NetworkVarTests.cs create mode 100644 Tests/Editor/NetworkVar/NetworkVarTests.cs.meta delete mode 100644 Tests/Editor/SnapshotRttTests.cs delete mode 100644 Tests/Editor/SnapshotRttTests.cs.meta delete mode 100644 Tests/Editor/SnapshotTests.cs delete mode 100644 Tests/Editor/SnapshotTests.cs.meta create mode 100644 Tests/Editor/Transports.meta create mode 100644 Tests/Editor/Transports/BatchedReceiveQueueTests.cs create mode 100644 Tests/Editor/Transports/BatchedReceiveQueueTests.cs.meta create mode 100644 Tests/Editor/Transports/BatchedSendQueueTests.cs create mode 100644 Tests/Editor/Transports/BatchedSendQueueTests.cs.meta create mode 100644 Tests/Editor/Transports/UnityTransportTests.cs create mode 100644 Tests/Editor/Transports/UnityTransportTests.cs.meta create mode 100644 Tests/Runtime/ClientOnlyConnectionTests.cs create mode 100644 Tests/Runtime/ClientOnlyConnectionTests.cs.meta create mode 100644 Tests/Runtime/Metrics/ConnectionMetricsTests.cs create mode 100644 Tests/Runtime/Metrics/ConnectionMetricsTests.cs.meta create mode 100644 Tests/Runtime/Metrics/PacketLossMetricsTests.cs create mode 100644 Tests/Runtime/Metrics/PacketLossMetricsTests.cs.meta create mode 100644 Tests/Runtime/NetworkManagerTransportTests.cs create mode 100644 Tests/Runtime/NetworkManagerTransportTests.cs.meta create mode 100644 Tests/Runtime/PlayerObjectTests.cs create mode 100644 Tests/Runtime/PlayerObjectTests.cs.meta rename Tests/Runtime/{Transport.meta => Transports.meta} (100%) rename Tests/Runtime/{Transport => Transports}/DummyTransport.cs (100%) rename Tests/Runtime/{Transport => Transports}/DummyTransport.cs.meta (100%) rename Tests/Runtime/{Transport => Transports}/SIPTransportTests.cs (100%) rename Tests/Runtime/{Transport => Transports}/SIPTransportTests.cs.meta (100%) create mode 100644 Tests/Runtime/Transports/UnityTransportConnectionTests.cs create mode 100644 Tests/Runtime/Transports/UnityTransportConnectionTests.cs.meta create mode 100644 Tests/Runtime/Transports/UnityTransportDriverClient.cs create mode 100644 Tests/Runtime/Transports/UnityTransportDriverClient.cs.meta create mode 100644 Tests/Runtime/Transports/UnityTransportTestHelpers.cs create mode 100644 Tests/Runtime/Transports/UnityTransportTestHelpers.cs.meta create mode 100644 Tests/Runtime/Transports/UnityTransportTests.cs create mode 100644 Tests/Runtime/Transports/UnityTransportTests.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3f781..a7e3c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). +## [1.0.0-pre.7] - 2022-04-01 + +### Added + +- Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828) +- Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823) +- Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762) +- `UnityTransport` settings can now be set programmatically. (#1845) +- `FastBufferWriter` and Reader IsInitialized property. (#1859) + +### Changed + +- Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849) + +### Removed + +- Removed `SnapshotSystem` (#1852) +- Removed `com.unity.modules.animation`, `com.unity.modules.physics` and `com.unity.modules.physics2d` dependencies from the package (#1812) +- Removed `com.unity.collections` dependency from the package (#1849) + +### Fixed +- Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850) +- Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847) +- Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841) +- Fixed issue where entries were not being removed from the NetworkSpawnManager.OwnershipToObjectsTable. (#1838) +- Fixed ClientRpcs would always send to all connected clients by default as opposed to only sending to the NetworkObject's Observers list by default. (#1836) +- Fixed clarity for NetworkSceneManager client side notification when it receives a scene hash value that does not exist in its local hash table. (#1828) +- Fixed client throws a key not found exception when it times out using UNet or UTP. (#1821) +- Fixed network variable updates are no longer limited to 32,768 bytes when NetworkConfig.EnsureNetworkVariableLengthSafety is enabled. The limits are now determined by what the transport can send in a message. (#1811) +- Fixed in-scene NetworkObjects get destroyed if a client fails to connect and shuts down the NetworkManager. (#1809) +- Fixed user never being notified in the editor that a NetworkBehaviour requires a NetworkObject to function properly. (#1808) +- Fixed PlayerObjects and dynamically spawned NetworkObjects not being added to the NetworkClient's OwnedObjects (#1801) +- Fixed issue where NetworkManager would continue starting even if the NetworkTransport selected failed. (#1780) +- Fixed issue when spawning new player if an already existing player exists it does not remove IsPlayer from the previous player (#1779) +- Fixed lack of notification that NetworkManager and NetworkObject cannot be added to the same GameObject with in-editor notifications (#1777) +- Fixed parenting warning printing for false positives (#1855) + ## [1.0.0-pre.6] - 2022-03-02 ### Added @@ -33,6 +70,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Fixed OwnedObjects not being properly modified when using ChangeOwnership (#1731) - Improved performance in NetworkAnimator (#1735) - Removed the "always sync" network animator (aka "autosend") parameters (#1746) +- Fixed in-scene placed NetworkObjects not respawning after shutting down the NetworkManager and then starting it back up again (#1769) ## [1.0.0-pre.5] - 2022-01-26 diff --git a/Components/Interpolator/BufferedLinearInterpolator.cs b/Components/Interpolator/BufferedLinearInterpolator.cs index 9b8c9ea..efa5de4 100644 --- a/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/Components/Interpolator/BufferedLinearInterpolator.cs @@ -186,7 +186,14 @@ public T Update(float deltaTime, double renderTime, double serverTime) if (t < 0.0f) { - throw new OverflowException($"t = {t} but must be >= 0. range {range}, RenderTime {renderTime}, Start time {m_StartTimeConsumed}, end time {m_EndTimeConsumed}"); + // There is no mechanism to guarantee renderTime to not be before m_StartTimeConsumed + // This clamps t to a minimum of 0 and fixes issues with longer frames and pauses + + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogError($"renderTime was before m_StartTimeConsumed. This should never happen. {nameof(renderTime)} is {renderTime}, {nameof(m_StartTimeConsumed)} is {m_StartTimeConsumed}"); + } + t = 0.0f; } if (t > 3.0f) // max extrapolation @@ -218,6 +225,8 @@ public void AddMeasurement(T newMeasurement, double sentTime) { m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime); ResetTo(newMeasurement, sentTime); + // Next line keeps renderTime above m_StartTimeConsumed. Fixes pause/unpause issues + m_Buffer.Add(m_LastBufferedItemReceived); } return; diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs index 8ab739a..9635cf9 100644 --- a/Components/NetworkAnimator.cs +++ b/Components/NetworkAnimator.cs @@ -1,3 +1,4 @@ +#if COM_UNITY_MODULES_ANIMATION using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; @@ -5,7 +6,7 @@ namespace Unity.Netcode.Components { /// - /// A prototype component for syncing animations + /// NetworkAnimator enables remote synchronization of state for on network objects. /// [AddComponentMenu("Netcode/" + nameof(NetworkAnimator))] [RequireComponent(typeof(Animator))] @@ -380,9 +381,7 @@ public void SetTrigger(string triggerName) SetTrigger(Animator.StringToHash(triggerName)); } - /// - /// Sets the trigger for the associated animation. See note for SetTrigger(string) - /// + /// /// The hash for the trigger to activate /// If true, resets the trigger public void SetTrigger(int hash, bool reset = false) @@ -413,7 +412,7 @@ public void SetTrigger(int hash, bool reset = false) } /// - /// Resets the trigger for the associated animation. See note for SetTrigger(string) + /// Resets the trigger for the associated animation. See SetTrigger for more on how triggers are special /// /// The string name of the trigger to reset public void ResetTrigger(string triggerName) @@ -421,13 +420,12 @@ public void ResetTrigger(string triggerName) ResetTrigger(Animator.StringToHash(triggerName)); } - /// - /// Resets the trigger for the associated animation. See note for SetTrigger(string) - /// - /// The hash for the trigger to reset + /// + /// The hash for the trigger to activate public void ResetTrigger(int hash) { SetTrigger(hash, true); } } } +#endif // COM_UNITY_MODULES_ANIMATION diff --git a/Components/NetworkRigidbody.cs b/Components/NetworkRigidbody.cs index 08c225f..5093fb0 100644 --- a/Components/NetworkRigidbody.cs +++ b/Components/NetworkRigidbody.cs @@ -1,3 +1,4 @@ +#if COM_UNITY_MODULES_PHYSICS using UnityEngine; namespace Unity.Netcode.Components @@ -78,3 +79,4 @@ public override void OnNetworkDespawn() } } } +#endif // COM_UNITY_MODULES_PHYSICS diff --git a/Components/NetworkRigidbody2D.cs b/Components/NetworkRigidbody2D.cs index b9c809d..1ac82bb 100644 --- a/Components/NetworkRigidbody2D.cs +++ b/Components/NetworkRigidbody2D.cs @@ -1,3 +1,4 @@ +#if COM_UNITY_MODULES_PHYSICS2D using UnityEngine; namespace Unity.Netcode.Components @@ -78,3 +79,4 @@ public override void OnNetworkDespawn() } } } +#endif // COM_UNITY_MODULES_PHYSICS2D diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs index d065835..dac7690 100644 --- a/Components/NetworkTransform.cs +++ b/Components/NetworkTransform.cs @@ -272,7 +272,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade /// If using different values, please use RPCs to write to the server. Netcode doesn't support client side network variable writing /// // This is public to make sure that users don't depend on this IsClient && IsOwner check in their code. If this logic changes in the future, we can make it invisible here - public bool CanCommitToTransform; + public bool CanCommitToTransform { get; protected set; } protected bool m_CachedIsServer; protected NetworkManager m_CachedNetworkManager; @@ -691,7 +691,6 @@ private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransf { if (!NetworkObject.IsSpawned) { - // todo MTT-849 should never happen but yet it does! maybe revisit/dig after NetVar updates and snapshot system lands? return; } @@ -785,7 +784,7 @@ private void Initialize() { m_ReplicatedNetworkState.SetDirty(true); } - else + else if (m_Transform != null) { ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); } diff --git a/Components/com.unity.netcode.components.asmdef b/Components/com.unity.netcode.components.asmdef index 3f4c4b4..6a9c4c4 100644 --- a/Components/com.unity.netcode.components.asmdef +++ b/Components/com.unity.netcode.components.asmdef @@ -5,5 +5,22 @@ "Unity.Netcode.Runtime", "Unity.Collections" ], - "allowUnsafeCode": true + "allowUnsafeCode": true, + "versionDefines": [ + { + "name": "com.unity.modules.animation", + "expression": "", + "define": "COM_UNITY_MODULES_ANIMATION" + }, + { + "name": "com.unity.modules.physics", + "expression": "", + "define": "COM_UNITY_MODULES_PHYSICS" + }, + { + "name": "com.unity.modules.physics2d", + "expression": "", + "define": "COM_UNITY_MODULES_PHYSICS2D" + } + ] } \ No newline at end of file diff --git a/Editor/NetworkAnimatorEditor.cs b/Editor/NetworkAnimatorEditor.cs deleted file mode 100644 index e9d3b62..0000000 --- a/Editor/NetworkAnimatorEditor.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Unity.Netcode.Components; -using UnityEditor; -using UnityEngine; - -namespace Unity.Netcode.Editor -{ - [CustomEditor(typeof(NetworkAnimator), true)] - [CanEditMultipleObjects] - public class NetworkAnimatorEditor : UnityEditor.Editor - { - public override void OnInspectorGUI() - { - serializedObject.Update(); - - EditorGUI.BeginChangeCheck(); - var label = new GUIContent("Animator", "The Animator component to synchronize"); - EditorGUILayout.PropertyField(serializedObject.FindProperty("m_Animator"), label); - EditorGUI.EndChangeCheck(); - - serializedObject.ApplyModifiedProperties(); - } - } -} diff --git a/Editor/NetworkBehaviourEditor.cs b/Editor/NetworkBehaviourEditor.cs index d565c69..325244b 100644 --- a/Editor/NetworkBehaviourEditor.cs +++ b/Editor/NetworkBehaviourEditor.cs @@ -211,5 +211,91 @@ public override void OnInspectorGUI() serializedObject.ApplyModifiedProperties(); EditorGUI.EndChangeCheck(); } + + /// + /// Invoked once when a NetworkBehaviour component is + /// displayed in the inspector view. + /// + private void OnEnable() + { + // When we first add a NetworkBehaviour this editor will be enabled + // so we go ahead and check for an already existing NetworkObject here + CheckForNetworkObject((target as NetworkBehaviour).gameObject); + } + + internal const string AutoAddNetworkObjectIfNoneExists = "AutoAdd-NetworkObject-When-None-Exist"; + + public static Transform GetRootParentTransform(Transform transform) + { + if (transform.parent == null || transform.parent == transform) + { + return transform; + } + return GetRootParentTransform(transform.parent); + } + + /// + /// Used to determine if a GameObject has one or more NetworkBehaviours but + /// does not already have a NetworkObject component. If not it will notify + /// the user that NetworkBehaviours require a NetworkObject. + /// + public static void CheckForNetworkObject(GameObject gameObject, bool networkObjectRemoved = false) + { + // If there are no NetworkBehaviours or no gameObject, then exit early + if (gameObject == null || (gameObject.GetComponent() == null && gameObject.GetComponentInChildren() == null)) + { + return; + } + + // Now get the root parent transform to the current GameObject (or itself) + var rootTransform = GetRootParentTransform(gameObject.transform); + + // Otherwise, check to see if there is any NetworkObject from the root GameObject down to all children. + // If not, notify the user that NetworkBehaviours require that the relative GameObject has a NetworkObject component. + var networkObject = rootTransform.GetComponent(); + if (networkObject == null) + { + networkObject = rootTransform.GetComponentInChildren(); + + if (networkObject == null) + { + // If we are removing a NetworkObject but there is still one or more NetworkBehaviour components + // and the user has already turned "Auto-Add NetworkObject" on when first notified about the requirement + // then just send a reminder to the user why the NetworkObject they just deleted seemingly "re-appeared" + // again. + if (networkObjectRemoved && EditorPrefs.HasKey(AutoAddNetworkObjectIfNoneExists) && EditorPrefs.GetBool(AutoAddNetworkObjectIfNoneExists)) + { + Debug.LogWarning($"{gameObject.name} still has {nameof(NetworkBehaviour)}s and Auto-Add NetworkObjects is enabled. A NetworkObject is being added back to {gameObject.name}."); + Debug.Log($"To reset Auto-Add NetworkObjects: Select the Netcode->General->Reset Auto-Add NetworkObject menu item."); + } + + // Notify and provide the option to add it one time, always add a NetworkObject, or do nothing and let the user manually add it + if (EditorUtility.DisplayDialog($"{nameof(NetworkBehaviour)}s require a {nameof(NetworkObject)}", + $"{gameObject.name} does not have a {nameof(NetworkObject)} component. Would you like to add one now?", "Yes", "No (manually add it)", + DialogOptOutDecisionType.ForThisMachine, AutoAddNetworkObjectIfNoneExists)) + { + gameObject.AddComponent(); + var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); + UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(activeScene); + UnityEditor.SceneManagement.EditorSceneManager.SaveScene(activeScene); + } + } + } + } + + /// + /// This allows users to reset the Auto-Add NetworkObject preference + /// so the next time they add a NetworkBehaviour to a GameObject without + /// a NetworkObject it will display the dialog box again and not + /// automatically add a NetworkObject. + /// + [MenuItem("Netcode/General/Reset Auto-Add NetworkObject", false, 1)] + private static void ResetMultiplayerToolsTipStatus() + { + if (EditorPrefs.HasKey(AutoAddNetworkObjectIfNoneExists)) + { + EditorPrefs.SetBool(AutoAddNetworkObjectIfNoneExists, false); + } + } } } diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index f8d54f7..6496afe 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/Editor/NetworkManagerEditor.cs @@ -135,9 +135,13 @@ private void OnEnable() m_NetworkPrefabsList = new ReorderableList(serializedObject, serializedObject.FindProperty(nameof(NetworkManager.NetworkConfig)).FindPropertyRelative(nameof(NetworkConfig.NetworkPrefabs)), true, true, true, true); m_NetworkPrefabsList.elementHeightCallback = index => { - var networkPrefab = m_NetworkPrefabsList.serializedProperty.GetArrayElementAtIndex(index); - var networkOverrideProp = networkPrefab.FindPropertyRelative(nameof(NetworkPrefab.Override)); - var networkOverrideInt = networkOverrideProp.enumValueIndex; + var networkOverrideInt = 0; + if (m_NetworkPrefabsList.count > 0) + { + var networkPrefab = m_NetworkPrefabsList.serializedProperty.GetArrayElementAtIndex(index); + var networkOverrideProp = networkPrefab.FindPropertyRelative(nameof(NetworkPrefab.Override)); + networkOverrideInt = networkOverrideProp.enumValueIndex; + } return 8 + (networkOverrideInt == 0 ? EditorGUIUtility.singleLineHeight : (EditorGUIUtility.singleLineHeight * 2) + 5); }; diff --git a/Editor/NetworkManagerHelper.cs b/Editor/NetworkManagerHelper.cs index cc34a6e..5430404 100644 --- a/Editor/NetworkManagerHelper.cs +++ b/Editor/NetworkManagerHelper.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Linq; using UnityEngine; +using UnityEngine.SceneManagement; using UnityEditor; namespace Unity.Netcode.Editor @@ -25,7 +27,6 @@ private static void InitializeOnload() { Singleton = new NetworkManagerHelper(); NetworkManager.NetworkManagerHelper = Singleton; - EditorApplication.playModeStateChanged -= EditorApplication_playModeStateChanged; EditorApplication.hierarchyChanged -= EditorApplication_hierarchyChanged; @@ -40,20 +41,106 @@ private static void EditorApplication_playModeStateChanged(PlayModeStateChange p case PlayModeStateChange.ExitingEditMode: { s_LastKnownNetworkManagerParents.Clear(); + ScenesInBuildActiveSceneCheck(); break; } } } + /// + /// Detects if a user is trying to enter into play mode when both conditions are true: + /// - the currently active and open scene is not added to the scenes in build list + /// - an instance of a NetworkManager with scene management enabled can be found + /// If both conditions are met then the user is presented with a dialog box that + /// provides the user with the option to add the scene to the scenes in build list + /// before entering into play mode or the user can continue under those conditions. + /// + /// ** When scene management is enabled the user should treat all scenes that need to + /// be synchronized using network scene management as if they were preparing for a build. + /// Any scene that needs to be loaded at run time has to be included in the scenes in + /// build list. ** + /// + private static void ScenesInBuildActiveSceneCheck() + { + var scenesList = EditorBuildSettings.scenes.ToList(); + var activeScene = SceneManager.GetActiveScene(); + var isSceneInBuildSettings = scenesList.Where((c) => c.path == activeScene.path).Count() == 1; + var networkManager = Object.FindObjectOfType(); + if (!isSceneInBuildSettings && networkManager != null) + { + if (networkManager.NetworkConfig != null && networkManager.NetworkConfig.EnableSceneManagement) + { + if (EditorUtility.DisplayDialog("Add Scene to Scenes in Build", $"The current scene was not found in the scenes" + + $" in build and a {nameof(NetworkManager)} instance was found with scene management enabled! Clients will not be able " + + $"to synchronize to this scene unless it is added to the scenes in build list.\n\nWould you like to add it now?", + "Yes", "No - Continue")) + { + scenesList.Add(new EditorBuildSettingsScene(activeScene.path, true)); + EditorBuildSettings.scenes = scenesList.ToArray(); + } + } + } + } + + /// + /// Invoked only when the hierarchy changes + /// private static void EditorApplication_hierarchyChanged() { var allNetworkManagers = Resources.FindObjectsOfTypeAll(); foreach (var networkManager in allNetworkManagers) { - networkManager.NetworkManagerCheckForParent(); + if (!networkManager.NetworkManagerCheckForParent()) + { + Singleton.CheckAndNotifyUserNetworkObjectRemoved(networkManager); + } } } + /// + /// Handles notifying users that they cannot add a NetworkObject component + /// to a GameObject that also has a NetworkManager component. The NetworkObject + /// will always be removed. + /// GameObject + NetworkObject then NetworkManager = NetworkObject removed + /// GameObject + NetworkManager then NetworkObject = NetworkObject removed + /// Note: Since this is always invoked after + /// we do not need to check for parent when searching for a NetworkObject component + /// + public void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false) + { + // Check for any NetworkObject at the same gameObject relative layer + var networkObject = networkManager.gameObject.GetComponent(); + + if (networkObject == null) + { + // if none is found, check to see if any children have a NetworkObject + networkObject = networkManager.gameObject.GetComponentInChildren(); + if (networkObject == null) + { + return; + } + } + + if (!EditorApplication.isUpdating) + { + Object.DestroyImmediate(networkObject); + + if (!EditorApplication.isPlaying && !editorTest) + { + EditorUtility.DisplayDialog($"Removing {nameof(NetworkObject)}", NetworkManagerAndNetworkObjectNotAllowedMessage(), "OK"); + } + else + { + Debug.LogError(NetworkManagerAndNetworkObjectNotAllowedMessage()); + } + } + } + + public string NetworkManagerAndNetworkObjectNotAllowedMessage() + { + return $"A {nameof(GameObject)} cannot have both a {nameof(NetworkManager)} and {nameof(NetworkObject)} assigned to it or any children under it."; + } + /// /// Handles notifying the user, via display dialog window, that they have nested a NetworkManager. /// When in edit mode it provides the option to automatically fix the issue diff --git a/Editor/NetworkObjectEditor.cs b/Editor/NetworkObjectEditor.cs index d9e9d70..6b00164 100644 --- a/Editor/NetworkObjectEditor.cs +++ b/Editor/NetworkObjectEditor.cs @@ -100,5 +100,32 @@ public override void OnInspectorGUI() GUI.enabled = guiEnabled; } } + + // Saved for use in OnDestroy + private GameObject m_GameObject; + + /// + /// Invoked once when a NetworkObject component is + /// displayed in the inspector view. + /// + private void OnEnable() + { + // We set the GameObject upon being enabled because when the + // NetworkObject component is removed (i.e. when OnDestroy is invoked) + // it is no longer valid/available. + m_GameObject = (target as NetworkObject).gameObject; + } + + /// + /// Invoked once when a NetworkObject component is + /// no longer displayed in the inspector view. + /// + private void OnDestroy() + { + // Since this is also invoked when a NetworkObject component is removed + // from a GameObject, we go ahead and check for a NetworkObject when + // this custom editor is destroyed. + NetworkBehaviourEditor.CheckForNetworkObject(m_GameObject, true); + } } } diff --git a/Editor/NetworkTransformEditor.cs b/Editor/NetworkTransformEditor.cs index bc8eca3..6510c6c 100644 --- a/Editor/NetworkTransformEditor.cs +++ b/Editor/NetworkTransformEditor.cs @@ -4,7 +4,7 @@ namespace Unity.Netcode.Editor { - [CustomEditor(typeof(NetworkTransform))] + [CustomEditor(typeof(NetworkTransform), true)] public class NetworkTransformEditor : UnityEditor.Editor { private SerializedProperty m_SyncPositionXProperty; @@ -112,6 +112,7 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(m_InLocalSpaceProperty); EditorGUILayout.PropertyField(m_InterpolateProperty); +#if COM_UNITY_MODULES_PHYSICS // if rigidbody is present but network rigidbody is not present var go = ((NetworkTransform)target).gameObject; if (go.TryGetComponent(out _) && go.TryGetComponent(out _) == false) @@ -119,12 +120,15 @@ public override void OnInspectorGUI() EditorGUILayout.HelpBox("This GameObject contains a Rigidbody but no NetworkRigidbody.\n" + "Add a NetworkRigidbody component to improve Rigidbody synchronization.", MessageType.Warning); } +#endif // COM_UNITY_MODULES_PHYSICS +#if COM_UNITY_MODULES_PHYSICS2D if (go.TryGetComponent(out _) && go.TryGetComponent(out _) == false) { EditorGUILayout.HelpBox("This GameObject contains a Rigidbody2D but no NetworkRigidbody2D.\n" + "Add a NetworkRigidbody2D component to improve Rigidbody2D synchronization.", MessageType.Warning); } +#endif // COM_UNITY_MODULES_PHYSICS2D serializedObject.ApplyModifiedProperties(); } diff --git a/Editor/PackageChecker.meta b/Editor/PackageChecker.meta new file mode 100644 index 0000000..f7aee05 --- /dev/null +++ b/Editor/PackageChecker.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a325130169714440ba1b4878082e8956 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/PackageChecker/UTPAdapterChecker.cs b/Editor/PackageChecker/UTPAdapterChecker.cs new file mode 100644 index 0000000..0d109a0 --- /dev/null +++ b/Editor/PackageChecker/UTPAdapterChecker.cs @@ -0,0 +1,53 @@ +#if COM_UNITY_NETCODE_ADAPTER_UTP +using System.Linq; +using UnityEngine; +using UnityEditor; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; + +namespace Unity.Netcode.Editor.PackageChecker +{ + [InitializeOnLoad] + internal class UTPAdapterChecker + { + private const string k_UTPAdapterPackageName = "com.unity.netcode.adapter.utp"; + + private static ListRequest s_ListRequest = null; + + static UTPAdapterChecker() + { + if (s_ListRequest == null) + { + s_ListRequest = Client.List(); + EditorApplication.update += EditorUpdate; + } + } + + private static void EditorUpdate() + { + if (!s_ListRequest.IsCompleted) + { + return; + } + + EditorApplication.update -= EditorUpdate; + + if (s_ListRequest.Status == StatusCode.Success) + { + if (s_ListRequest.Result.Any(p => p.name == k_UTPAdapterPackageName)) + { + Debug.Log($"({nameof(UTPAdapterChecker)}) Found UTP Adapter package, it is no longer needed, `UnityTransport` is now directly integrated into the SDK therefore removing it from the project."); + Client.Remove(k_UTPAdapterPackageName); + } + } + else + { + var error = s_ListRequest.Error; + Debug.LogError($"({nameof(UTPAdapterChecker)}) Cannot check the list of packages -> error #{error.errorCode}: {error.message}"); + } + + s_ListRequest = null; + } + } +} +#endif // COM_UNITY_NETCODE_ADAPTER_UTP diff --git a/Runtime/Core/SnapshotRtt.cs.meta b/Editor/PackageChecker/UTPAdapterChecker.cs.meta similarity index 83% rename from Runtime/Core/SnapshotRtt.cs.meta rename to Editor/PackageChecker/UTPAdapterChecker.cs.meta index 92cc411..03c935f 100644 --- a/Runtime/Core/SnapshotRtt.cs.meta +++ b/Editor/PackageChecker/UTPAdapterChecker.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 69c3c1c5a885d4aed99ee2e1fa40f763 +guid: df5ed97df956b4aad91a221ba59fa304 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef b/Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef new file mode 100644 index 0000000..8f86057 --- /dev/null +++ b/Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Unity.Netcode.Editor.PackageChecker", + "rootNamespace": "Unity.Netcode.Editor.PackageChecker", + "includePlatforms": [ + "Editor" + ], + "versionDefines": [ + { + "name": "com.unity.netcode.adapter.utp", + "expression": "", + "define": "COM_UNITY_NETCODE_ADAPTER_UTP" + } + ] +} \ No newline at end of file diff --git a/Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef.meta b/Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef.meta new file mode 100644 index 0000000..9c32829 --- /dev/null +++ b/Editor/PackageChecker/com.unity.netcode.editor.packagechecker.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: de64d7f9ca85d4bf59c8c24738bc1057 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs index 74b906e..2ff2f06 100644 --- a/Runtime/AssemblyInfo.cs +++ b/Runtime/AssemblyInfo.cs @@ -11,4 +11,3 @@ [assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")] [assembly: InternalsVisibleTo("Unity.Netcode.TestHelpers.Runtime")] [assembly: InternalsVisibleTo("Unity.Netcode.Adapter.UTP")] - diff --git a/Runtime/Configuration/NetworkConfig.cs b/Runtime/Configuration/NetworkConfig.cs index 83cc18c..5d9b92e 100644 --- a/Runtime/Configuration/NetworkConfig.cs +++ b/Runtime/Configuration/NetworkConfig.cs @@ -138,19 +138,6 @@ public class NetworkConfig /// public bool EnableNetworkLogs = true; - /// - /// Whether or not to enable Snapshot System for variable updates. Not supported in this version. - /// - public bool UseSnapshotDelta { get; internal set; } = false; - /// - /// Whether or not to enable Snapshot System for spawn and despawn commands. Not supported in this version. - /// - public bool UseSnapshotSpawn { get; internal set; } = false; - /// - /// When Snapshot System spawn is enabled: max size of Snapshot Messages. Meant to fit MTU. - /// - public int SnapshotMaxSpawnUsage { get; } = 1000; - public const int RttAverageSamples = 5; // number of RTT to keep an average of (plus one) public const int RttWindowSize = 64; // number of slots to use for RTT computations (max number of in-flight packets) /// diff --git a/Runtime/Connection/NetworkClient.cs b/Runtime/Connection/NetworkClient.cs index 38ae544..bfc10d1 100644 --- a/Runtime/Connection/NetworkClient.cs +++ b/Runtime/Connection/NetworkClient.cs @@ -20,6 +20,17 @@ public class NetworkClient /// /// The NetworkObject's owned by this Client /// - public readonly List OwnedObjects = new List(); + public List OwnedObjects + { + get + { + if (PlayerObject != null && PlayerObject.NetworkManager != null && PlayerObject.NetworkManager.IsListening) + { + return PlayerObject.NetworkManager.SpawnManager.GetClientOwnedObjects(ClientId); + } + + return new List(); + } + } } } diff --git a/Runtime/Core/IndexAllocator.cs b/Runtime/Core/IndexAllocator.cs deleted file mode 100644 index 892fd6d..0000000 --- a/Runtime/Core/IndexAllocator.cs +++ /dev/null @@ -1,388 +0,0 @@ -using UnityEngine; - -namespace Unity.Netcode -{ - internal struct IndexAllocatorEntry - { - internal int Pos; // Position where the memory of this slot is - internal int Length; // Length of the memory allocated to this slot - internal int Next; // Next and Prev define the order of the slots in the buffer - internal int Prev; - internal bool Free; // Whether this is a free slot - } - - internal class IndexAllocator - { - private const int k_NotSet = -1; - private readonly int m_MaxSlot; // Maximum number of sections (free or not) in the buffer - private readonly int m_BufferSize; // Size of the buffer we allocated into - private int m_LastSlot = 0; // Last allocated slot - private IndexAllocatorEntry[] m_Slots; // Array of slots - private int[] m_IndexToSlot; // Mapping from the client's index to the slot index - - internal IndexAllocator(int bufferSize, int maxSlot) - { - m_MaxSlot = maxSlot; - m_BufferSize = bufferSize; - m_Slots = new IndexAllocatorEntry[m_MaxSlot]; - m_IndexToSlot = new int[m_MaxSlot]; - Reset(); - } - - /// - /// Reset this IndexAllocator to an empty one, with the same sized buffer and slots - /// - internal void Reset() - { - // todo: could be made faster, for example by having a last index - // and not needing valid stuff past it - for (int i = 0; i < m_MaxSlot; i++) - { - m_Slots[i].Free = true; - m_Slots[i].Next = i + 1; - m_Slots[i].Prev = i - 1; - m_Slots[i].Pos = m_BufferSize; - m_Slots[i].Length = 0; - - m_IndexToSlot[i] = k_NotSet; - } - - m_Slots[0].Pos = 0; - m_Slots[0].Length = m_BufferSize; - m_Slots[0].Prev = k_NotSet; - m_Slots[m_MaxSlot - 1].Next = k_NotSet; - } - - /// - /// Returns the amount of memory used - /// - /// - /// Returns the amount of memory used, starting at 0, ending after the last used slot - /// - internal int Range - { - get - { - // when the whole buffer is free, m_LastSlot points to an empty slot - if (m_Slots[m_LastSlot].Free) - { - return 0; - } - // otherwise return the end of the last slot used - return m_Slots[m_LastSlot].Pos + m_Slots[m_LastSlot].Length; - } - } - - /// - /// Allocate a slot with "size" position, for index "index" - /// - /// The client index to identify this. Used in Deallocate to identify which slot - /// The size required. - /// Returns the position to use in the buffer - /// - /// true if successful, false is there isn't enough memory available or no slots are large enough - /// - internal bool Allocate(int index, int size, out int pos) - { - pos = 0; - // size must be positive, index must be within range - if (size < 0 || index < 0 || index >= m_MaxSlot) - { - return false; - } - - // refuse allocation if the index is already in use - if (m_IndexToSlot[index] != k_NotSet) - { - return false; - } - - // todo: this is the slowest part - // improvement 1: list of free blocks (minor) - // improvement 2: heap of free blocks - for (int i = 0; i < m_MaxSlot; i++) - { - if (m_Slots[i].Free && m_Slots[i].Length >= size) - { - m_IndexToSlot[index] = i; - - int leftOver = m_Slots[i].Length - size; - int next = m_Slots[i].Next; - if (m_Slots[next].Free) - { - m_Slots[next].Pos -= leftOver; - m_Slots[next].Length += leftOver; - } - else - { - int add = MoveSlotAfter(i); - - m_Slots[add].Pos = m_Slots[i].Pos + size; - m_Slots[add].Length = m_Slots[i].Length - size; - } - - m_Slots[i].Free = false; - m_Slots[i].Length = size; - - pos = m_Slots[i].Pos; - - // if we allocate past the current range, we are the last slot - if (m_Slots[i].Pos + m_Slots[i].Length > Range) - { - m_LastSlot = i; - } - - return true; - } - } - - return false; - } - - /// - /// Deallocate a slot - /// - /// The client index to identify this. Same index used in Allocate - /// - /// true if successful, false is there isn't an allocated slot at this index - /// - internal bool Deallocate(int index) - { - // size must be positive, index must be within range - if (index < 0 || index >= m_MaxSlot) - { - return false; - } - - int slot = m_IndexToSlot[index]; - - if (slot == k_NotSet) - { - return false; - } - - if (m_Slots[slot].Free) - { - return false; - } - - m_Slots[slot].Free = true; - - int prev = m_Slots[slot].Prev; - int next = m_Slots[slot].Next; - - // if previous slot was free, merge and grow - if (prev != k_NotSet && m_Slots[prev].Free) - { - m_Slots[prev].Length += m_Slots[slot].Length; - m_Slots[slot].Length = 0; - - // if the slot we're merging was the last one, the last one is now the one we merged with - if (slot == m_LastSlot) - { - m_LastSlot = prev; - } - - // todo: verify what this does on full or nearly full cases - MoveSlotToEnd(slot); - slot = prev; - } - - next = m_Slots[slot].Next; - - // merge with next slot if it is free - if (next != k_NotSet && m_Slots[next].Free) - { - m_Slots[slot].Length += m_Slots[next].Length; - m_Slots[next].Length = 0; - MoveSlotToEnd(next); - } - - // if we just deallocate the last one, we need to move last back - if (slot == m_LastSlot) - { - m_LastSlot = m_Slots[m_LastSlot].Prev; - // if there's nothing allocated anymore, use 0 - if (m_LastSlot == k_NotSet) - { - m_LastSlot = 0; - } - } - - // mark the index as available - m_IndexToSlot[index] = k_NotSet; - - return true; - } - - // Take a slot at the end and link it to go just after "slot". - // Used when allocating part of a slot and we need an entry for the rest - // Returns the slot that was picked - private int MoveSlotAfter(int slot) - { - int ret = m_Slots[m_MaxSlot - 1].Prev; - int p0 = m_Slots[ret].Prev; - - m_Slots[p0].Next = m_MaxSlot - 1; - m_Slots[m_MaxSlot - 1].Prev = p0; - - int p1 = m_Slots[slot].Next; - m_Slots[slot].Next = ret; - m_Slots[p1].Prev = ret; - - m_Slots[ret].Prev = slot; - m_Slots[ret].Next = p1; - - return ret; - } - - // Move the slot "slot" to the end of the list. - // Used when merging two slots, that gives us an extra entry at the end - private void MoveSlotToEnd(int slot) - { - // if we're already there - if (m_Slots[slot].Next == k_NotSet) - { - return; - } - - int prev = m_Slots[slot].Prev; - int next = m_Slots[slot].Next; - - m_Slots[prev].Next = next; - if (next != k_NotSet) - { - m_Slots[next].Prev = prev; - } - - int p0 = m_Slots[m_MaxSlot - 1].Prev; - - m_Slots[p0].Next = slot; - m_Slots[slot].Next = m_MaxSlot - 1; - - m_Slots[m_MaxSlot - 1].Prev = slot; - m_Slots[slot].Prev = p0; - - m_Slots[slot].Pos = m_BufferSize; - } - - // runs a bunch of consistency check on the Allocator - internal bool Verify() - { - int pos = k_NotSet; - int count = 0; - int total = 0; - int endPos = 0; - - do - { - int prev = pos; - if (pos != k_NotSet) - { - pos = m_Slots[pos].Next; - if (pos == k_NotSet) - { - break; - } - } - else - { - pos = 0; - } - - if (m_Slots[pos].Prev != prev) - { - // the previous is not correct - return false; - } - - if (m_Slots[pos].Length < 0) - { - // Length should be positive - return false; - } - - if (prev != k_NotSet && m_Slots[prev].Free && m_Slots[pos].Free && m_Slots[pos].Length > 0) - { - // should not have two consecutive free slots - return false; - } - - if (m_Slots[pos].Pos != total) - { - // slots should all line up nicely - return false; - } - - if (!m_Slots[pos].Free) - { - endPos = m_Slots[pos].Pos + m_Slots[pos].Length; - } - - total += m_Slots[pos].Length; - count++; - - } while (pos != k_NotSet); - - if (count != m_MaxSlot) - { - // some slots were lost - return false; - } - - if (total != m_BufferSize) - { - // total buffer should be accounted for - return false; - } - - if (endPos != Range) - { - // end position should match reported end position - return false; - } - - return true; - } - - // Debug display the allocator structure - internal void DebugDisplay() - { - string logMessage = "IndexAllocator structure\n"; - - bool[] seen = new bool[m_MaxSlot]; - - int pos = 0; - int count = 0; - bool prevEmpty = false; - do - { - seen[pos] = true; - count++; - if (m_Slots[pos].Length == 0 && prevEmpty) - { - // don't display repetitive empty slots - } - else - { - logMessage += string.Format("{0}:{1}, {2} ({3}) \n", m_Slots[pos].Pos, m_Slots[pos].Length, - m_Slots[pos].Free ? "Free" : "Used", pos); - if (m_Slots[pos].Length == 0) - { - prevEmpty = true; - } - else - { - prevEmpty = false; - } - } - - pos = m_Slots[pos].Next; - } while (pos != k_NotSet && !seen[pos]); - - logMessage += string.Format("{0} Total entries\n", count); - - Debug.Log(logMessage); - } - } -} diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index 20de22a..64babb1 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -158,37 +158,57 @@ internal void __endSendClientRpc(ref FastBufferWriter bufferWriter, uint rpcMeth // We check to see if we need to shortcut for the case where we are the host/server and we can send a clientRPC // to ourself. Sadly we have to figure that out from the list of clientIds :( bool shouldSendToHost = false; - if (clientRpcParams.Send.TargetClientIds != null) { - foreach (var clientId in clientRpcParams.Send.TargetClientIds) + foreach (var targetClientId in clientRpcParams.Send.TargetClientIds) { - if (clientId == NetworkManager.ServerClientId) + if (targetClientId == NetworkManager.ServerClientId) { shouldSendToHost = true; break; } + + // Check to make sure we are sending to only observers, if not log an error. + if (NetworkManager.LogLevel >= LogLevel.Error && !NetworkObject.Observers.Contains(targetClientId)) + { + NetworkLog.LogError(GenerateObserverErrorMessage(clientRpcParams, targetClientId)); + } } rpcWriteSize = NetworkManager.SendMessage(ref clientRpcMessage, networkDelivery, in clientRpcParams.Send.TargetClientIds); } else if (clientRpcParams.Send.TargetClientIdsNativeArray != null) { - foreach (var clientId in clientRpcParams.Send.TargetClientIdsNativeArray) + foreach (var targetClientId in clientRpcParams.Send.TargetClientIdsNativeArray) { - if (clientId == NetworkManager.ServerClientId) + if (targetClientId == NetworkManager.ServerClientId) { shouldSendToHost = true; break; } + + // Check to make sure we are sending to only observers, if not log an error. + if (NetworkManager.LogLevel >= LogLevel.Error && !NetworkObject.Observers.Contains(targetClientId)) + { + NetworkLog.LogError(GenerateObserverErrorMessage(clientRpcParams, targetClientId)); + } } rpcWriteSize = NetworkManager.SendMessage(ref clientRpcMessage, networkDelivery, clientRpcParams.Send.TargetClientIdsNativeArray.Value); } else { - shouldSendToHost = IsHost; - rpcWriteSize = NetworkManager.SendMessage(ref clientRpcMessage, networkDelivery, NetworkManager.ConnectedClientsIds); + var observerEnumerator = NetworkObject.Observers.GetEnumerator(); + while (observerEnumerator.MoveNext()) + { + // Skip over the host + if (IsHost && observerEnumerator.Current == NetworkManager.LocalClientId) + { + shouldSendToHost = true; + continue; + } + rpcWriteSize = NetworkManager.MessagingSystem.SendMessage(ref clientRpcMessage, networkDelivery, observerEnumerator.Current); + } } // If we are a server/host then we just no op and send to ourself @@ -228,6 +248,12 @@ internal void __endSendClientRpc(ref FastBufferWriter bufferWriter, uint rpcMeth #endif } + internal string GenerateObserverErrorMessage(ClientRpcParams clientRpcParams, ulong targetClientId) + { + var containerNameHoldingId = clientRpcParams.Send.TargetClientIds != null ? nameof(ClientRpcParams.Send.TargetClientIds) : nameof(ClientRpcParams.Send.TargetClientIdsNativeArray); + return $"Sending ClientRpc to non-observer! {containerNameHoldingId} contains clientId {targetClientId} that is not an observer!"; + } + /// /// Gets the NetworkManager that owns this NetworkBehaviour instance /// See note around `NetworkObject` for how there is a chicken / egg problem when we are not initialized @@ -235,42 +261,42 @@ internal void __endSendClientRpc(ref FastBufferWriter bufferWriter, uint rpcMeth public NetworkManager NetworkManager => NetworkObject.NetworkManager; /// - /// Gets if the object is the the personal clients player object + /// If a NetworkObject is assigned, it will return whether or not this NetworkObject + /// is the local player object. If no NetworkObject is assigned it will always return false. /// - public bool IsLocalPlayer => NetworkObject.IsLocalPlayer; + public bool IsLocalPlayer { get; private set; } /// /// Gets if the object is owned by the local player or if the object is the local player object /// - public bool IsOwner => NetworkObject.IsOwner; + public bool IsOwner { get; internal set; } /// /// Gets if we are executing as server /// - protected bool IsServer => IsRunning && NetworkManager.IsServer; + protected bool IsServer { get; private set; } /// /// Gets if we are executing as client /// - protected bool IsClient => IsRunning && NetworkManager.IsClient; + protected bool IsClient { get; private set; } + /// /// Gets if we are executing as Host, I.E Server and Client /// - protected bool IsHost => IsRunning && NetworkManager.IsHost; - - private bool IsRunning => NetworkManager && NetworkManager.IsListening; + protected bool IsHost { get; private set; } /// /// Gets Whether or not the object has a owner /// - public bool IsOwnedByServer => NetworkObject.IsOwnedByServer; + public bool IsOwnedByServer { get; internal set; } /// /// Used to determine if it is safe to access NetworkObject and NetworkManager from within a NetworkBehaviour component /// Primarily useful when checking NetworkObject/NetworkManager properties within FixedUpate /// - public bool IsSpawned => HasNetworkObject ? NetworkObject.IsSpawned : false; + public bool IsSpawned { get; internal set; } internal bool IsBehaviourEditable() { @@ -327,12 +353,12 @@ public NetworkObject NetworkObject /// /// Gets the NetworkId of the NetworkObject that owns this NetworkBehaviour /// - public ulong NetworkObjectId => NetworkObject.NetworkObjectId; + public ulong NetworkObjectId { get; internal set; } /// /// Gets NetworkId for this NetworkBehaviour from the owner NetworkObject /// - public ushort NetworkBehaviourId => NetworkObject.GetNetworkBehaviourOrderIndex(this); + public ushort NetworkBehaviourId { get; internal set; } /// /// Internally caches the Id of this behaviour in a NetworkObject. Makes look-up faster @@ -352,7 +378,47 @@ protected NetworkBehaviour GetNetworkBehaviour(ushort behaviourId) /// /// Gets the ClientId that owns the NetworkObject /// - public ulong OwnerClientId => NetworkObject.OwnerClientId; + public ulong OwnerClientId { get; internal set; } + + /// + /// Updates properties with network session related + /// dependencies such as a NetworkObject's spawned + /// state or NetworkManager's session state. + /// + internal void UpdateNetworkProperties() + { + // Set NetworkObject dependent properties + if (NetworkObject != null) + { + // Set identification related properties + NetworkObjectId = NetworkObject.NetworkObjectId; + IsLocalPlayer = NetworkObject.IsLocalPlayer; + + // This is "OK" because GetNetworkBehaviourOrderIndex uses the order of + // NetworkObject.ChildNetworkBehaviours which is set once when first + // accessed. + NetworkBehaviourId = NetworkObject.GetNetworkBehaviourOrderIndex(this); + + // Set ownership related properties + IsOwnedByServer = NetworkObject.IsOwnedByServer; + IsOwner = NetworkObject.IsOwner; + OwnerClientId = NetworkObject.OwnerClientId; + + // Set NetworkManager dependent properties + if (NetworkManager != null) + { + IsHost = NetworkManager.IsListening && NetworkManager.IsHost; + IsClient = NetworkManager.IsListening && NetworkManager.IsClient; + IsServer = NetworkManager.IsListening && NetworkManager.IsServer; + } + } + else // Shouldn't happen, but if so then set the properties to their default value; + { + OwnerClientId = NetworkObjectId = default; + IsOwnedByServer = IsOwner = IsHost = IsClient = IsServer = default; + NetworkBehaviourId = default; + } + } /// /// Gets called when the gets spawned, message handlers are ready to be registered and the network is setup. @@ -366,21 +432,41 @@ public virtual void OnNetworkDespawn() { } internal void InternalOnNetworkSpawn() { + IsSpawned = true; InitializeVariables(); + UpdateNetworkProperties(); + OnNetworkSpawn(); } - internal void InternalOnNetworkDespawn() { } + internal void InternalOnNetworkDespawn() + { + IsSpawned = false; + UpdateNetworkProperties(); + OnNetworkDespawn(); + } /// /// Gets called when the local client gains ownership of this object /// public virtual void OnGainedOwnership() { } + internal void InternalOnGainedOwnership() + { + UpdateNetworkProperties(); + OnGainedOwnership(); + } + /// /// Gets called when we loose ownership of this object /// public virtual void OnLostOwnership() { } + internal void InternalOnLostOwnership() + { + UpdateNetworkProperties(); + OnLostOwnership(); + } + /// /// Gets called when the parent NetworkObject of this NetworkBehaviour's NetworkObject has changed /// @@ -433,12 +519,10 @@ internal void InitializeVariables() m_VarInit = true; - FieldInfo[] sortedFields = GetFieldInfoForType(GetType()); - + var sortedFields = GetFieldInfoForType(GetType()); for (int i = 0; i < sortedFields.Length; i++) { - Type fieldType = sortedFields[i].FieldType; - + var fieldType = sortedFields[i].FieldType; if (fieldType.IsSubclassOf(typeof(NetworkVariableBase))) { var instance = (NetworkVariableBase)sortedFields[i].GetValue(this); @@ -499,7 +583,7 @@ internal void PostNetworkVariableWrite() } } - internal void VariableUpdate(ulong clientId) + internal void VariableUpdate(ulong targetClientId) { if (!m_VarInit) { @@ -507,67 +591,58 @@ internal void VariableUpdate(ulong clientId) } PreNetworkVariableWrite(); - NetworkVariableUpdate(clientId, NetworkBehaviourId); + NetworkVariableUpdate(targetClientId, NetworkBehaviourId); } internal readonly List NetworkVariableIndexesToReset = new List(); internal readonly HashSet NetworkVariableIndexesToResetSet = new HashSet(); - private void NetworkVariableUpdate(ulong clientId, int behaviourIndex) + private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex) { if (!CouldHaveDirtyNetworkVariables()) { return; } - if (NetworkManager.NetworkConfig.UseSnapshotDelta) + for (int j = 0; j < m_DeliveryMappedNetworkVariableIndices.Count; j++) { + var shouldSend = false; for (int k = 0; k < NetworkVariableFields.Count; k++) { - NetworkManager.SnapshotSystem.Store(NetworkObjectId, behaviourIndex, k, NetworkVariableFields[k]); + var networkVariable = NetworkVariableFields[k]; + if (networkVariable.IsDirty() && networkVariable.CanClientRead(targetClientId)) + { + shouldSend = true; + break; + } } - } - if (!NetworkManager.NetworkConfig.UseSnapshotDelta) - { - for (int j = 0; j < m_DeliveryMappedNetworkVariableIndices.Count; j++) + if (shouldSend) { - var shouldSend = false; - for (int k = 0; k < NetworkVariableFields.Count; k++) + var message = new NetworkVariableDeltaMessage + { + NetworkObjectId = NetworkObjectId, + NetworkBehaviourIndex = NetworkObject.GetNetworkBehaviourOrderIndex(this), + NetworkBehaviour = this, + TargetClientId = targetClientId, + DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j] + }; + // TODO: Serialization is where the IsDirty flag gets changed. + // Messages don't get sent from the server to itself, so if we're host and sending to ourselves, + // we still have to actually serialize the message even though we're not sending it, otherwise + // the dirty flag doesn't change properly. These two pieces should be decoupled at some point + // so we don't have to do this serialization work if we're not going to use the result. + if (IsServer && targetClientId == NetworkManager.ServerClientId) { - if (NetworkVariableFields[k].ShouldWrite(clientId, IsServer)) + var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, MessagingSystem.FRAGMENTED_MESSAGE_MAX_SIZE); + using (tmpWriter) { - shouldSend = true; + message.Serialize(tmpWriter); } } - - if (shouldSend) + else { - var message = new NetworkVariableDeltaMessage - { - NetworkObjectId = NetworkObjectId, - NetworkBehaviourIndex = NetworkObject.GetNetworkBehaviourOrderIndex(this), - NetworkBehaviour = this, - ClientId = clientId, - DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j] - }; - // TODO: Serialization is where the IsDirty flag gets changed. - // Messages don't get sent from the server to itself, so if we're host and sending to ourselves, - // we still have to actually serialize the message even though we're not sending it, otherwise - // the dirty flag doesn't change properly. These two pieces should be decoupled at some point - // so we don't have to do this serialization work if we're not going to use the result. - if (IsServer && clientId == NetworkManager.ServerClientId) - { - var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, MessagingSystem.FRAGMENTED_MESSAGE_MAX_SIZE); - using (tmpWriter) - { - message.Serialize(tmpWriter); - } - } - else - { - NetworkManager.SendMessage(ref message, m_DeliveryTypesForNetworkVariableGroups[j], clientId); - } + NetworkManager.SendMessage(ref message, m_DeliveryTypesForNetworkVariableGroups[j], targetClientId); } } } @@ -595,7 +670,7 @@ internal void MarkVariablesDirty() } } - internal void WriteNetworkVariableData(FastBufferWriter writer, ulong clientId) + internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId) { if (NetworkVariableFields.Count == 0) { @@ -604,7 +679,7 @@ internal void WriteNetworkVariableData(FastBufferWriter writer, ulong clientId) for (int j = 0; j < NetworkVariableFields.Count; j++) { - bool canClientRead = NetworkVariableFields[j].CanClientRead(clientId); + bool canClientRead = NetworkVariableFields[j].CanClientRead(targetClientId); if (canClientRead) { diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs index aace5b8..6433f95 100644 --- a/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/Runtime/Core/NetworkBehaviourUpdater.cs @@ -57,7 +57,7 @@ internal void NetworkBehaviourUpdate(NetworkManager networkManager) { for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) { - sobj.ChildNetworkBehaviours[k].VariableUpdate(networkManager.ServerClientId); + sobj.ChildNetworkBehaviours[k].VariableUpdate(NetworkManager.ServerClientId); } } } diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index 4ecb254..f2f585e 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -12,6 +12,7 @@ #endif using Unity.Profiling; using UnityEngine.SceneManagement; +using System.Runtime.CompilerServices; using Debug = UnityEngine.Debug; namespace Unity.Netcode @@ -45,7 +46,7 @@ public class NetworkManager : MonoBehaviour, INetworkUpdateSystem private static ProfilerMarker s_TransportDisconnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportDisconnect"); #endif - private const double k_TimeSyncFrequency = 1.0d; // sync every second, TODO will be removed once timesync is done via snapshots + private const double k_TimeSyncFrequency = 1.0d; // sync every second private const float k_DefaultBufferSizeSec = 0.05f; // todo talk with UX/Product, find good default value for this internal static string PrefabDebugHelper(NetworkPrefab networkPrefab) @@ -53,7 +54,6 @@ internal static string PrefabDebugHelper(NetworkPrefab networkPrefab) return $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.gameObject.name}\""; } - internal SnapshotSystem SnapshotSystem { get; private set; } internal NetworkBehaviourUpdater BehaviourUpdater { get; private set; } internal MessagingSystem MessagingSystem { get; private set; } @@ -125,13 +125,11 @@ public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelive public bool OnVerifyCanReceive(ulong senderId, Type messageType) { if (m_NetworkManager.PendingClients.TryGetValue(senderId, out PendingClient client) && - (client.ConnectionState == PendingClient.State.PendingApproval || - (client.ConnectionState == PendingClient.State.PendingConnection && - messageType != typeof(ConnectionRequestMessage)))) + (client.ConnectionState == PendingClient.State.PendingApproval || (client.ConnectionState == PendingClient.State.PendingConnection && messageType != typeof(ConnectionRequestMessage)))) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId.ToString()} before it has been accepted"); + NetworkLog.LogWarning($"Message received from {nameof(senderId)}={senderId} before it has been accepted"); } return false; @@ -229,14 +227,12 @@ public GameObject GetNetworkPrefabOverride(GameObject gameObject) public NetworkSceneManager SceneManager { get; private set; } - public readonly ulong ServerClientId = 0; + public const ulong ServerClientId = 0; /// /// Gets the networkId of the server /// - private ulong m_ServerTransportId => NetworkConfig.NetworkTransport?.ServerClientId ?? - throw new NullReferenceException( - $"The transport in the active {nameof(NetworkConfig)} is null"); + private ulong m_ServerTransportId => NetworkConfig.NetworkTransport?.ServerClientId ?? throw new NullReferenceException($"The transport in the active {nameof(NetworkConfig)} is null"); /// /// Returns ServerClientId if IsServer or LocalClientId if not @@ -367,16 +363,14 @@ public IReadOnlyList ConnectedClientsIds /// Whether or not the client was approved /// The position to spawn the client at. If null, the prefab position is used. /// The rotation to spawn the client with. If null, the prefab position is used. - public delegate void ConnectionApprovedDelegate(bool createPlayerObject, uint? playerPrefabHash, bool approved, - Vector3? position, Quaternion? rotation); + public delegate void ConnectionApprovedDelegate(bool createPlayerObject, uint? playerPrefabHash, bool approved, Vector3? position, Quaternion? rotation); /// /// The callback to invoke during connection approval /// public event Action ConnectionApprovalCallback = null; - internal void InvokeConnectionApproval(byte[] payload, ulong clientId, ConnectionApprovedDelegate action) => - ConnectionApprovalCallback?.Invoke(payload, clientId, action); + internal void InvokeConnectionApproval(byte[] payload, ulong clientId, ConnectionApprovedDelegate action) => ConnectionApprovalCallback?.Invoke(payload, clientId, action); /// /// The current NetworkConfig @@ -565,13 +559,6 @@ private void Initialize(bool server) NetworkConfig.NetworkTransport.NetworkMetrics = NetworkMetrics; - //This 'if' should never enter - if (SnapshotSystem != null) - { - SnapshotSystem.Dispose(); - SnapshotSystem = null; - } - if (server) { NetworkTimeSystem = NetworkTimeSystem.ServerTimeSystem(); @@ -584,8 +571,6 @@ private void Initialize(bool server) NetworkTickSystem = new NetworkTickSystem(NetworkConfig.TickRate, 0, 0); NetworkTickSystem.Tick += OnNetworkManagerTick; - SnapshotSystem = new SnapshotSystem(this, NetworkConfig, NetworkTickSystem); - this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); // This is used to remove entries not needed or invalid @@ -800,44 +785,35 @@ public bool StartServer() { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { - NetworkLog.LogInfo("StartServer()"); + NetworkLog.LogInfo(nameof(StartServer)); } - if (IsServer || IsClient) + if (!CanStart(StartType.Server)) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning("Cannot start server while an instance is already running"); - } - return false; } - if (NetworkConfig.ConnectionApproval) - { - if (ConnectionApprovalCallback == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning( - "No ConnectionApproval callback defined. Connection approval will timeout"); - } - } - } - Initialize(true); - var result = NetworkConfig.NetworkTransport.StartServer(); - - IsServer = true; - IsClient = false; - IsListening = true; + // If we failed to start then shutdown and notify user that the transport failed to start + if (NetworkConfig.NetworkTransport.StartServer()) + { + IsServer = true; + IsClient = false; + IsListening = true; - SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); + SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); - OnServerStarted?.Invoke(); + OnServerStarted?.Invoke(); + return true; + } + else + { + Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); + Shutdown(); + } - return result; + return false; } /// @@ -850,26 +826,26 @@ public bool StartClient() NetworkLog.LogInfo(nameof(StartClient)); } - if (IsServer || IsClient) + if (!CanStart(StartType.Client)) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning("Cannot start client while an instance is already running"); - } - return false; } Initialize(false); MessagingSystem.ClientConnected(ServerClientId); - var result = NetworkConfig.NetworkTransport.StartClient(); + if (!NetworkConfig.NetworkTransport.StartClient()) + { + Debug.LogError($"Client is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); + Shutdown(); + return false; + } IsServer = false; IsClient = true; IsListening = true; - return result; + return true; } /// @@ -882,31 +858,21 @@ public bool StartHost() NetworkLog.LogInfo(nameof(StartHost)); } - if (IsServer || IsClient) + if (!CanStart(StartType.Host)) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning("Cannot start host while an instance is already running"); - } - return false; } - if (NetworkConfig.ConnectionApproval) + Initialize(true); + + // If we failed to start then shutdown and notify user that the transport failed to start + if (!NetworkConfig.NetworkTransport.StartServer()) { - if (ConnectionApprovalCallback == null) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning( - "No ConnectionApproval callback defined. Connection approval will timeout"); - } - } + Debug.LogError($"Server is shutting down due to network transport start failure of {NetworkConfig.NetworkTransport.GetType().Name}!"); + Shutdown(); + return false; } - Initialize(true); - - var result = NetworkConfig.NetworkTransport.StartServer(); MessagingSystem.ClientConnected(ServerClientId); LocalClientId = ServerClientId; NetworkMetrics.SetConnectionId(LocalClientId); @@ -942,7 +908,53 @@ public bool StartHost() OnServerStarted?.Invoke(); - return result; + return true; + } + + private enum StartType + { + Server, + Host, + Client + } + + private bool CanStart(StartType type) + { + if (IsListening) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning("Cannot start " + type + " while an instance is already running"); + } + + return false; + } + + if (NetworkConfig.ConnectionApproval) + { + if (ConnectionApprovalCallback == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning( + "No ConnectionApproval callback defined. Connection approval will timeout"); + } + } + } + + if (ConnectionApprovalCallback != null) + { + if (!NetworkConfig.ConnectionApproval) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning( + "A ConnectionApproval callback is defined but ConnectionApproval is disabled. In order to use ConnectionApproval it has to be explicitly enabled "); + } + } + } + + return true; } public void SetSingleton() @@ -1014,6 +1026,7 @@ static internal string GenerateNestedNetworkManagerMessage(Transform transform) internal interface INetworkManagerHelper { bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool ignoreNetworkManagerCache = false, bool editorTest = false); + void CheckAndNotifyUserNetworkObjectRemoved(NetworkManager networkManager, bool editorTest = false); } #endif @@ -1133,12 +1146,6 @@ internal void ShutdownInternal() this.UnregisterAllNetworkUpdates(); - if (SnapshotSystem != null) - { - SnapshotSystem.Dispose(); - SnapshotSystem = null; - } - if (NetworkTickSystem != null) { NetworkTickSystem.Tick -= OnNetworkManagerTick; @@ -1274,7 +1281,11 @@ private void OnNetworkPostLateUpdate() if (!m_ShuttingDown || !m_StopProcessingMessages) { MessagingSystem.ProcessSendQueues(); + NetworkMetrics.UpdateNetworkObjectsCount(SpawnManager.SpawnedObjects.Count); + NetworkMetrics.UpdateConnectionsCount((IsServer) ? ConnectedClients.Count : 1); NetworkMetrics.DispatchFrame(); + + NetworkObject.VerifyParentingStatus(); } SpawnManager.CleanupStaleTriggers(); @@ -1288,7 +1299,6 @@ private void OnNetworkPostLateUpdate() /// This function runs once whenever the local tick is incremented and is responsible for the following (in order): /// - collect commands/inputs and send them to the server (TBD) /// - call NetworkFixedUpdate on all NetworkBehaviours in prediction/client authority mode - /// - create a snapshot from resulting state /// private void OnNetworkManagerTick() { @@ -1419,18 +1429,15 @@ private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, A #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportDisconnect.Begin(); #endif - clientId = TransportIdToClientId(clientId); - - OnClientDisconnectCallback?.Invoke(clientId); - - m_TransportIdToClientIdMap.Remove(transportId); - m_ClientIdToTransportIdMap.Remove(clientId); + clientId = TransportIdCleanUp(clientId, transportId); if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo($"Disconnect Event From {clientId}"); } + OnClientDisconnectCallback?.Invoke(clientId); + if (IsServer) { OnClientDisconnectFromServer(clientId); @@ -1446,6 +1453,31 @@ private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, A } } + /// + /// Handles cleaning up the transport id/client id tables after + /// receiving a disconnect event from transport + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ulong TransportIdCleanUp(ulong clientId, ulong transportId) + { + // This check is for clients that attempted to connect but failed. + // When this happens, the client will not have an entry within the + // m_TransportIdToClientIdMap or m_ClientIdToTransportIdMap lookup + // tables so we exit early and just return 0 to be used for the + // disconnect event. + if (!IsServer && !m_TransportIdToClientIdMap.ContainsKey(clientId)) + { + return 0; + } + + clientId = TransportIdToClientId(clientId); + + m_TransportIdToClientIdMap.Remove(transportId); + m_ClientIdToTransportIdMap.Remove(clientId); + + return clientId; + } + internal unsafe int SendMessage(ref TMessageType message, NetworkDelivery delivery, in TClientIdListType clientIds) where TMessageType : INetworkMessage where TClientIdListType : IReadOnlyList @@ -1591,32 +1623,45 @@ private void OnClientDisconnectFromServer(ulong clientId) } } - for (int i = networkClient.OwnedObjects.Count - 1; i >= 0; i--) + // Get the NetworkObjects owned by the disconnected client + var clientOwnedObjects = SpawnManager.GetClientOwnedObjects(clientId); + if (clientOwnedObjects == null) + { + // This could happen if a client is never assigned a player object and is disconnected + // Only log this in verbose/developer mode + if (LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"ClientID {clientId} disconnected with (0) zero owned objects! Was a player prefab not assigned?"); + } + } + else { - var ownedObject = networkClient.OwnedObjects[i]; - if (ownedObject != null) + // Handle changing ownership and prefab handlers + for (int i = clientOwnedObjects.Count - 1; i >= 0; i--) { - if (!ownedObject.DontDestroyWithOwner) + var ownedObject = clientOwnedObjects[i]; + if (ownedObject != null) { - if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].OwnedObjects[i] - .GlobalObjectIdHash)) + if (!ownedObject.DontDestroyWithOwner) { - PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].OwnedObjects[i]); + if (PrefabHandler.ContainsHandler(clientOwnedObjects[i].GlobalObjectIdHash)) + { + PrefabHandler.HandleNetworkPrefabDestroy(clientOwnedObjects[i]); + } + else + { + Destroy(ownedObject.gameObject); + } } else { - Destroy(ownedObject.gameObject); + ownedObject.RemoveOwnership(); } } - else - { - ownedObject.RemoveOwnership(); - } } } // TODO: Could(should?) be replaced with more memory per client, by storing the visibility - foreach (var sobj in SpawnManager.SpawnedObjectsList) { sobj.Observers.Remove(clientId); @@ -1764,7 +1809,7 @@ internal void ApprovedPlayerSpawn(ulong clientId, uint playerPrefabHash) var message = new CreateObjectMessage { - ObjectInfo = ConnectedClients[clientId].PlayerObject.GetMessageSceneObject(clientPair.Key, false) + ObjectInfo = ConnectedClients[clientId].PlayerObject.GetMessageSceneObject(clientPair.Key) }; message.ObjectInfo.Header.Hash = playerPrefabHash; message.ObjectInfo.Header.IsSceneObject = false; diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs index a5d08ca..d9b8ee1 100644 --- a/Runtime/Core/NetworkObject.cs +++ b/Runtime/Core/NetworkObject.cs @@ -54,8 +54,6 @@ internal void GenerateGlobalObjectIdHash() /// internal NetworkManager NetworkManagerOwner; - private ulong m_NetworkObjectId; - /// /// Gets the unique Id of this object that is synced across the network /// @@ -64,33 +62,7 @@ internal void GenerateGlobalObjectIdHash() /// /// Gets the ClientId of the owner of this NetworkObject /// - public ulong OwnerClientId - { - get - { - if (OwnerClientIdInternal == null) - { - return NetworkManager != null ? NetworkManager.ServerClientId : 0; - } - else - { - return OwnerClientIdInternal.Value; - } - } - internal set - { - if (NetworkManager != null && value == NetworkManager.ServerClientId) - { - OwnerClientIdInternal = null; - } - else - { - OwnerClientIdInternal = value; - } - } - } - - internal ulong? OwnerClientIdInternal = null; + public ulong OwnerClientId { get; internal set; } /// /// If true, the object will always be replicated as root on clients and the parent will be ignored. @@ -234,11 +206,6 @@ public void NetworkShow(ulong clientId) throw new VisibilityChangeException("The object is already visible"); } - if (NetworkManager.NetworkConfig.UseSnapshotSpawn) - { - SnapshotSpawn(clientId); - } - Observers.Add(clientId); NetworkManager.SpawnManager.SendSpawnCallForObject(clientId, this); @@ -314,23 +281,15 @@ public void NetworkHide(ulong clientId) throw new VisibilityChangeException("Cannot hide an object from the server"); } - Observers.Remove(clientId); - if (NetworkManager.NetworkConfig.UseSnapshotSpawn) + var message = new DestroyObjectMessage { - SnapshotDespawn(clientId); - } - else - { - var message = new DestroyObjectMessage - { - NetworkObjectId = NetworkObjectId - }; - // Send destroy call - var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); - NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size); - } + NetworkObjectId = NetworkObjectId + }; + // Send destroy call + var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size); } /// @@ -345,14 +304,14 @@ public static void NetworkHide(List networkObjects, ulong clientI throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided"); } - NetworkManager networkManager = networkObjects[0].NetworkManager; + var networkManager = networkObjects[0].NetworkManager; if (!networkManager.IsServer) { throw new NotServerException("Only server can change visibility"); } - if (clientId == networkManager.ServerClientId) + if (clientId == NetworkManager.ServerClientId) { throw new VisibilityChangeException("Cannot hide an object from the server"); } @@ -384,84 +343,21 @@ public static void NetworkHide(List networkObjects, ulong clientI private void OnDestroy() { - if (NetworkManager != null && NetworkManager.IsListening && NetworkManager.IsServer == false && IsSpawned - && (IsSceneObject == null || (IsSceneObject != null && IsSceneObject.Value != true))) + if (NetworkManager != null && NetworkManager.IsListening && NetworkManager.IsServer == false && IsSpawned && + (IsSceneObject == null || (IsSceneObject != null && IsSceneObject.Value != true))) { throw new NotServerException($"Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); } - if (NetworkManager != null && NetworkManager.SpawnManager != null && NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject)) + if (NetworkManager != null && NetworkManager.SpawnManager != null && + NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject)) { NetworkManager.SpawnManager.OnDespawnObject(networkObject, false); } } - private SnapshotDespawnCommand GetDespawnCommand() - { - var command = new SnapshotDespawnCommand(); - command.NetworkObjectId = NetworkObjectId; - - return command; - } - - private SnapshotSpawnCommand GetSpawnCommand() - { - var command = new SnapshotSpawnCommand(); - command.NetworkObjectId = NetworkObjectId; - command.OwnerClientId = OwnerClientId; - command.IsPlayerObject = IsPlayerObject; - command.IsSceneObject = (IsSceneObject == null) || IsSceneObject.Value; - - ulong? parent = NetworkManager.SpawnManager.GetSpawnParentId(this); - if (parent != null) - { - command.ParentNetworkId = parent.Value; - } - else - { - // write own network id, when no parents. todo: optimize this. - command.ParentNetworkId = command.NetworkObjectId; - } - - command.GlobalObjectIdHash = HostCheckForGlobalObjectIdHashOverride(); - // todo: check if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(clientId)) for any clientId - command.ObjectPosition = transform.position; - command.ObjectRotation = transform.rotation; - command.ObjectScale = transform.localScale; - - return command; - } - - private void SnapshotSpawn() - { - var command = GetSpawnCommand(); - NetworkManager.SnapshotSystem.Spawn(command); - } - - private void SnapshotSpawn(ulong clientId) - { - var command = GetSpawnCommand(); - command.TargetClientIds = new List(); - command.TargetClientIds.Add(clientId); - NetworkManager.SnapshotSystem.Spawn(command); - } - - internal void SnapshotDespawn() - { - var command = GetDespawnCommand(); - NetworkManager.SnapshotSystem.Despawn(command); - } - - internal void SnapshotDespawn(ulong clientId) - { - var command = GetDespawnCommand(); - command.TargetClientIds = new List(); - command.TargetClientIds.Add(clientId); - NetworkManager.SnapshotSystem.Despawn(command); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SpawnInternal(bool destroyWithScene, ulong? ownerClientId, bool playerObject) + private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool playerObject) { if (!NetworkManager.IsListening) { @@ -475,12 +371,6 @@ private void SpawnInternal(bool destroyWithScene, ulong? ownerClientId, bool pla NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), false, playerObject, ownerClientId, destroyWithScene); - if (NetworkManager.NetworkConfig.UseSnapshotSpawn) - { - SnapshotSpawn(); - } - - ulong ownerId = ownerClientId != null ? ownerClientId.Value : NetworkManager.ServerClientId; for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++) { if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) @@ -496,7 +386,7 @@ private void SpawnInternal(bool destroyWithScene, ulong? ownerClientId, bool pla /// Should the object be destroyed when the scene is changed public void Spawn(bool destroyWithScene = false) { - SpawnInternal(destroyWithScene, null, false); + SpawnInternal(destroyWithScene, NetworkManager.ServerClientId, false); } /// @@ -547,17 +437,29 @@ public void ChangeOwnership(ulong newOwnerClientId) internal void InvokeBehaviourOnLostOwnership() { + // Server already handles this earlier, hosts should ignore, all clients should update + if (!NetworkManager.IsServer) + { + NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true); + } + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { - ChildNetworkBehaviours[i].OnLostOwnership(); + ChildNetworkBehaviours[i].InternalOnLostOwnership(); } } internal void InvokeBehaviourOnGainedOwnership() { + // Server already handles this earlier, hosts should ignore and only client owners should update + if (!NetworkManager.IsServer && NetworkManager.LocalClientId == OwnerClientId) + { + NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); + } + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { - ChildNetworkBehaviours[i].OnGainedOwnership(); + ChildNetworkBehaviours[i].InternalOnGainedOwnership(); } } @@ -756,13 +658,7 @@ internal bool ApplyNetworkParenting() if (!NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(m_LatestParent.Value)) { - if (OrphanChildren.Add(this)) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning($"{nameof(NetworkObject)} ({name}) cannot find its parent, added to {nameof(OrphanChildren)} set"); - } - } + OrphanChildren.Add(this); return false; } @@ -793,19 +689,21 @@ internal static void CheckOrphanChildren() internal void InvokeBehaviourNetworkSpawn() { + NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { ChildNetworkBehaviours[i].InternalOnNetworkSpawn(); - ChildNetworkBehaviours[i].OnNetworkSpawn(); } } internal void InvokeBehaviourNetworkDespawn() { + NetworkManager.SpawnManager.UpdateOwnershipTable(this, OwnerClientId, true); + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { ChildNetworkBehaviours[i].InternalOnNetworkDespawn(); - ChildNetworkBehaviours[i].OnNetworkDespawn(); } } @@ -834,13 +732,13 @@ internal List ChildNetworkBehaviours } } - internal void WriteNetworkVariableData(FastBufferWriter writer, ulong clientId) + internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) { var behavior = ChildNetworkBehaviours[i]; behavior.InitializeVariables(); - behavior.WriteNetworkVariableData(writer, clientId); + behavior.WriteNetworkVariableData(writer, targetClientId); } } @@ -853,6 +751,27 @@ internal void MarkVariablesDirty() } } + // NGO currently guarantees that the client will receive spawn data for all objects in one network tick. + // Children may arrive before their parents; when they do they are stored in OrphanedChildren and then + // resolved when their parents arrived. Because we don't send a partial list of spawns (yet), something + // has gone wrong if by the end of an update we still have unresolved orphans + // + + // if and when we have different systems for where it is expected that orphans survive across ticks, + // then this warning will remind us that we need to revamp the system because then we can no longer simply + // spawn the orphan without its parent (at least, not when its transform is set to local coords mode) + // - because then you’ll have children popping at the wrong location not having their parent’s global position to root them + // - and then they’ll pop to the correct location after they get the parent, and that would be not good + internal static void VerifyParentingStatus() + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + if (OrphanChildren.Count > 0) + { + NetworkLog.LogWarning($"{nameof(NetworkObject)} ({OrphanChildren.Count}) children not resolved to parents by the end of frame"); + } + } + } internal void SetNetworkVariableData(FastBufferReader reader) { for (int i = 0; i < ChildNetworkBehaviours.Count; i++) @@ -918,7 +837,6 @@ public struct HeaderData public bool IsSceneObject; public bool HasTransform; public bool IsReparented; - public bool HasNetworkVariables; } public HeaderData Header; @@ -979,10 +897,7 @@ public unsafe void Serialize(FastBufferWriter writer) } } - if (Header.HasNetworkVariables) - { - OwnerObject.WriteNetworkVariableData(writer, TargetClientId); - } + OwnerObject.WriteNetworkVariableData(writer, TargetClientId); } public unsafe void Deserialize(FastBufferReader reader) @@ -1022,7 +937,7 @@ public unsafe void Deserialize(FastBufferReader reader) } } - internal SceneObject GetMessageSceneObject(ulong targetClientId, bool includeNetworkVariableData = true) + internal SceneObject GetMessageSceneObject(ulong targetClientId) { var obj = new SceneObject { @@ -1033,7 +948,6 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId, bool includeNet OwnerClientId = OwnerClientId, IsSceneObject = IsSceneObject ?? true, Hash = HostCheckForGlobalObjectIdHashOverride(), - HasNetworkVariables = includeNetworkVariableData }, OwnerObject = this, TargetClientId = targetClientId diff --git a/Runtime/Core/SnapshotRtt.cs b/Runtime/Core/SnapshotRtt.cs deleted file mode 100644 index 207160f..0000000 --- a/Runtime/Core/SnapshotRtt.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; - -namespace Unity.Netcode -{ - internal class ConnectionRtt - { - private double[] m_RttSendTimes; // times at which packet were sent for RTT computations - private int[] m_SendSequence; // tick, or other key, at which packets were sent (to allow matching) - private double[] m_MeasuredLatencies; // measured latencies (ring buffer) - private int m_LatenciesBegin = 0; // ring buffer begin - private int m_LatenciesEnd = 0; // ring buffer end - - /// - /// Round-trip-time data - /// - public struct Rtt - { - public double BestSec; // best RTT - public double AverageSec; // average RTT - public double WorstSec; // worst RTT - public double LastSec; // latest ack'ed RTT - public int SampleCount; // number of contributing samples - } - - public ConnectionRtt() - { - m_RttSendTimes = new double[NetworkConfig.RttWindowSize]; - m_SendSequence = new int[NetworkConfig.RttWindowSize]; - m_MeasuredLatencies = new double[NetworkConfig.RttWindowSize]; - } - - /// - /// Returns the Round-trip-time computation for this client - /// - public Rtt GetRtt() - { - var ret = new Rtt(); - var index = m_LatenciesBegin; - double total = 0.0; - ret.BestSec = m_MeasuredLatencies[m_LatenciesBegin]; - ret.WorstSec = m_MeasuredLatencies[m_LatenciesBegin]; - - while (index != m_LatenciesEnd) - { - total += m_MeasuredLatencies[index]; - ret.SampleCount++; - ret.BestSec = Math.Min(ret.BestSec, m_MeasuredLatencies[index]); - ret.WorstSec = Math.Max(ret.WorstSec, m_MeasuredLatencies[index]); - index = (index + 1) % NetworkConfig.RttAverageSamples; - } - - if (ret.SampleCount != 0) - { - ret.AverageSec = total / ret.SampleCount; - // the latest RTT is one before m_LatenciesEnd - ret.LastSec = m_MeasuredLatencies[(m_LatenciesEnd + (NetworkConfig.RttWindowSize - 1)) % NetworkConfig.RttWindowSize]; - } - else - { - ret.AverageSec = 0; - ret.BestSec = 0; - ret.WorstSec = 0; - ret.SampleCount = 0; - ret.LastSec = 0; - } - - return ret; - } - - internal void NotifySend(int sequence, double timeSec) - { - m_RttSendTimes[sequence % NetworkConfig.RttWindowSize] = timeSec; - m_SendSequence[sequence % NetworkConfig.RttWindowSize] = sequence; - } - - internal void NotifyAck(int sequence, double timeSec) - { - // if the same slot was not used by a later send - if (m_SendSequence[sequence % NetworkConfig.RttWindowSize] == sequence) - { - double latency = timeSec - m_RttSendTimes[sequence % NetworkConfig.RttWindowSize]; - - m_MeasuredLatencies[m_LatenciesEnd] = latency; - m_LatenciesEnd = (m_LatenciesEnd + 1) % NetworkConfig.RttAverageSamples; - - if (m_LatenciesEnd == m_LatenciesBegin) - { - m_LatenciesBegin = (m_LatenciesBegin + 1) % NetworkConfig.RttAverageSamples; - } - } - } - } -} diff --git a/Runtime/Core/SnapshotSystem.cs b/Runtime/Core/SnapshotSystem.cs deleted file mode 100644 index f6c29f4..0000000 --- a/Runtime/Core/SnapshotSystem.cs +++ /dev/null @@ -1,1151 +0,0 @@ -using System; -using System.Collections.Generic; -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using UnityEngine; - -namespace Unity.Netcode -{ - // Structure that acts as a key for a NetworkVariable - // Allows telling which variable we're talking about. - // Might include tick in a future milestone, to address past variable value - internal struct VariableKey - { - internal ulong NetworkObjectId; // the NetworkObjectId of the owning GameObject - internal ushort BehaviourIndex; // the index of the behaviour in this GameObject - internal ushort VariableIndex; // the index of the variable in this NetworkBehaviour - internal int TickWritten; // the network tick at which this variable was set - } - - // Index for a NetworkVariable in our table of variables - // Store when a variable was written and where the variable is serialized - internal struct Entry - { - internal VariableKey Key; - internal ushort Position; // the offset in our Buffer - internal ushort Length; // the Length of the data in Buffer - - internal const int NotFound = -1; - } - - internal struct SnapshotDespawnCommand - { - // identity - internal ulong NetworkObjectId; - - // snapshot internal - internal int TickWritten; - internal List TargetClientIds; - internal int TimesWritten; - } - - internal struct SnapshotSpawnCommand - { - // identity - internal ulong NetworkObjectId; - - // archetype - internal uint GlobalObjectIdHash; - internal bool IsSceneObject; - - // parameters - internal bool IsPlayerObject; - internal ulong OwnerClientId; - internal ulong ParentNetworkId; - internal Vector3 ObjectPosition; - internal Quaternion ObjectRotation; - internal Vector3 ObjectScale; - - // snapshot internal - internal int TickWritten; - internal List TargetClientIds; - internal int TimesWritten; - } - - internal class ClientData - { - internal struct SentSpawn // this struct also stores Despawns, not just Spawns - { - internal ulong SequenceNumber; - internal ulong ObjectId; - internal int Tick; - } - - internal ushort SequenceNumber = 0; // the next sequence number to use for this client - internal ushort LastReceivedSequence = 0; // the last sequence number received by this client - internal ushort ReceivedSequenceMask = 0; // bitmask of the messages before the last one that we received. - - internal int NextSpawnIndex = 0; // index of the last spawn sent. Used to cycle through spawns (LRU scheme) - internal int NextDespawnIndex = 0; // same as above, but for despawns. - - // by objectId - // which spawns and despawns did this connection ack'ed ? - internal Dictionary SpawnAck = new Dictionary(); - - // list of spawn and despawns commands we sent, with sequence number - // need to manage acknowledgements - internal List SentSpawns = new List(); - } - - internal delegate int MockSendMessage(ref SnapshotDataMessage message, NetworkDelivery delivery, ulong clientId); - internal delegate int MockSpawnObject(SnapshotSpawnCommand spawnCommand); - internal delegate int MockDespawnObject(SnapshotDespawnCommand despawnCommand); - - - // A table of NetworkVariables that constitutes a Snapshot. - // Stores serialized NetworkVariables - // todo --M1-- - // The Snapshot will change for M1b with memory management, instead of just FreeMemoryPosition, there will be data structure - // around available buffer, etc. - internal class SnapshotSystem : INetworkUpdateSystem, IDisposable - { - // todo --M1-- functionality to grow these will be needed in a later milestone - private const int k_MaxVariables = 2000; - internal int SpawnsBufferCount { get; private set; } = 100; - internal int DespawnsBufferCount { get; private set; } = 100; - - private const int k_BufferSize = 30000; - - private NetworkManager m_NetworkManager = default; - - // by clientId - private Dictionary m_ClientData = new Dictionary(); - private Dictionary m_ConnectionRtts = new Dictionary(); - - private bool m_UseSnapshotDelta; - private bool m_UseSnapshotSpawn; - private int m_SnapshotMaxSpawnUsage; - private NetworkTickSystem m_NetworkTickSystem; - - private int m_CurrentTick = NetworkTickSystem.NoTick; - - internal byte[] MainBuffer = new byte[k_BufferSize]; // buffer holding a snapshot in memory - internal byte[] RecvBuffer = new byte[k_BufferSize]; // buffer holding the received snapshot message - - internal IndexAllocator Allocator; - - internal Entry[] Entries = new Entry[k_MaxVariables]; - internal int LastEntry = 0; - - internal SnapshotSpawnCommand[] Spawns; - internal int NumSpawns = 0; - - internal SnapshotDespawnCommand[] Despawns; - internal int NumDespawns = 0; - - // indexed by ObjectId - internal Dictionary TickAppliedSpawn = new Dictionary(); - internal Dictionary TickAppliedDespawn = new Dictionary(); - - internal bool IsServer { get; set; } - internal bool IsConnectedClient { get; set; } - internal ulong ServerClientId { get; set; } - internal List ConnectedClientsId { get; } = new List(); - internal MockSendMessage MockSendMessage { get; set; } - internal MockSpawnObject MockSpawnObject { get; set; } - internal MockDespawnObject MockDespawnObject { get; set; } - - internal void Clear() - { - LastEntry = 0; - Allocator.Reset(); - } - - /// - /// Finds the position of a given NetworkVariable, given its key - /// - /// The key we're looking for - internal int Find(VariableKey key) - { - // todo: Add a IEquatable interface for VariableKey. Rely on that instead. - for (int i = 0; i < LastEntry; i++) - { - // todo: revisit how we store past ticks - if (Entries[i].Key.NetworkObjectId == key.NetworkObjectId && - Entries[i].Key.BehaviourIndex == key.BehaviourIndex && - Entries[i].Key.VariableIndex == key.VariableIndex) - { - return i; - } - } - - return Entry.NotFound; - } - - /// - /// Adds an entry in the table for a new key - /// - internal int AddEntry(in VariableKey k) - { - var pos = LastEntry++; - var entry = Entries[pos]; - - entry.Key = k; - entry.Position = 0; - entry.Length = 0; - Entries[pos] = entry; - - return pos; - } - - internal List GetClientList() - { - List clientList; - clientList = new List(); - - if (!IsServer) - { - clientList.Add(m_NetworkManager.ServerClientId); - } - else - { - foreach (var clientId in ConnectedClientsId) - { - if (clientId != m_NetworkManager.ServerClientId) - { - clientList.Add(clientId); - } - } - } - - return clientList; - } - - internal void AddSpawn(SnapshotSpawnCommand command) - { - if (NumSpawns >= SpawnsBufferCount) - { - Array.Resize(ref Spawns, 2 * SpawnsBufferCount); - SpawnsBufferCount = SpawnsBufferCount * 2; - // Debug.Log($"[JEFF] spawn size is now {m_MaxSpawns}"); - } - - if (NumSpawns < SpawnsBufferCount) - { - if (command.TargetClientIds == default) - { - command.TargetClientIds = GetClientList(); - } - - // todo: store, for each client, the spawn not ack'ed yet, - // to prevent sending despawns to them. - // for clientData in client list - // clientData.SpawnSet.Add(command.NetworkObjectId); - - // todo: - // this 'if' might be temporary, but is needed to help in debugging - // or maybe it stays - if (command.TargetClientIds.Count > 0) - { - Spawns[NumSpawns] = command; - NumSpawns++; - } - } - } - - internal void AddDespawn(SnapshotDespawnCommand command) - { - if (NumDespawns >= DespawnsBufferCount) - { - Array.Resize(ref Despawns, 2 * DespawnsBufferCount); - DespawnsBufferCount = DespawnsBufferCount * 2; - // Debug.Log($"[JEFF] despawn size is now {m_MaxDespawns}"); - } - - if (NumDespawns < DespawnsBufferCount) - { - if (command.TargetClientIds == default) - { - command.TargetClientIds = GetClientList(); - } - if (command.TargetClientIds.Count > 0) - { - Despawns[NumDespawns] = command; - NumDespawns++; - } - } - } - - internal void ReduceBufferUsage() - { - var count = Math.Max(1, NumDespawns); - Array.Resize(ref Despawns, count); - DespawnsBufferCount = count; - - count = Math.Max(1, NumSpawns); - Array.Resize(ref Spawns, count); - SpawnsBufferCount = count; - } - - internal ClientData.SentSpawn GetSpawnData(in ClientData clientData, in SnapshotSpawnCommand spawn, out SnapshotDataMessage.SpawnData data) - { - // remember which spawn we sent this connection with which sequence number - // that way, upon ack, we can track what is being ack'ed - ClientData.SentSpawn sentSpawn; - sentSpawn.ObjectId = spawn.NetworkObjectId; - sentSpawn.Tick = spawn.TickWritten; - sentSpawn.SequenceNumber = clientData.SequenceNumber; - - data = new SnapshotDataMessage.SpawnData - { - NetworkObjectId = spawn.NetworkObjectId, - Hash = spawn.GlobalObjectIdHash, - IsSceneObject = spawn.IsSceneObject, - - IsPlayerObject = spawn.IsPlayerObject, - OwnerClientId = spawn.OwnerClientId, - ParentNetworkId = spawn.ParentNetworkId, - Position = spawn.ObjectPosition, - Rotation = spawn.ObjectRotation, - Scale = spawn.ObjectScale, - - TickWritten = spawn.TickWritten - }; - return sentSpawn; - } - - internal ClientData.SentSpawn GetDespawnData(in ClientData clientData, in SnapshotDespawnCommand despawn, out SnapshotDataMessage.DespawnData data) - { - // remember which spawn we sent this connection with which sequence number - // that way, upon ack, we can track what is being ack'ed - ClientData.SentSpawn sentSpawn; - sentSpawn.ObjectId = despawn.NetworkObjectId; - sentSpawn.Tick = despawn.TickWritten; - sentSpawn.SequenceNumber = clientData.SequenceNumber; - - data = new SnapshotDataMessage.DespawnData - { - NetworkObjectId = despawn.NetworkObjectId, - TickWritten = despawn.TickWritten - }; - - return sentSpawn; - } - /// - /// Read a received Entry - /// Must match WriteEntry - /// - /// Deserialized snapshot entry data - internal Entry ReadEntry(SnapshotDataMessage.EntryData data) - { - Entry entry; - entry.Key.NetworkObjectId = data.NetworkObjectId; - entry.Key.BehaviourIndex = data.BehaviourIndex; - entry.Key.VariableIndex = data.VariableIndex; - entry.Key.TickWritten = data.TickWritten; - entry.Position = data.Position; - entry.Length = data.Length; - - return entry; - } - - internal SnapshotSpawnCommand ReadSpawn(SnapshotDataMessage.SpawnData data) - { - var command = new SnapshotSpawnCommand(); - - command.NetworkObjectId = data.NetworkObjectId; - command.GlobalObjectIdHash = data.Hash; - command.IsSceneObject = data.IsSceneObject; - command.IsPlayerObject = data.IsPlayerObject; - command.OwnerClientId = data.OwnerClientId; - command.ParentNetworkId = data.ParentNetworkId; - command.ObjectPosition = data.Position; - command.ObjectRotation = data.Rotation; - command.ObjectScale = data.Scale; - - command.TickWritten = data.TickWritten; - - return command; - } - - internal SnapshotDespawnCommand ReadDespawn(SnapshotDataMessage.DespawnData data) - { - var command = new SnapshotDespawnCommand(); - - command.NetworkObjectId = data.NetworkObjectId; - command.TickWritten = data.TickWritten; - - return command; - } - - /// - /// Allocate memory from the buffer for the Entry and update it to point to the right location - /// - /// The entry to allocate for - /// The need size in bytes - internal void AllocateEntry(ref Entry entry, int index, int size) - { - // todo: deal with full buffer - - if (entry.Length > 0) - { - Allocator.Deallocate(index); - } - - int pos; - bool ret = Allocator.Allocate(index, size, out pos); - - if (!ret) - { - //todo: error handling - } - - entry.Position = (ushort)pos; - entry.Length = (ushort)size; - } - - /// - /// Read the buffer part of a snapshot - /// Must match WriteBuffer - /// The stream is actually a memory stream and we seek to each variable position as we deserialize them - /// - /// The message to pull the buffer from - internal void ReadBuffer(in SnapshotDataMessage message) - { - RecvBuffer = message.ReceiveMainBuffer.ToArray(); // Note: Allocates - } - - /// - /// Read the snapshot index from a buffer - /// Stores the entry. Allocates memory if needed. The actual buffer will be read later - /// - /// The message to read the index from - internal void ReadIndex(in SnapshotDataMessage message) - { - Entry entry; - - for (var i = 0; i < message.Entries.Length; i++) - { - bool added = false; - - entry = ReadEntry(message.Entries[i]); - - int pos = Find(entry.Key);// should return if there's anything more recent - if (pos == Entry.NotFound) - { - pos = AddEntry(entry.Key); - added = true; - } - - // if we need to allocate more memory (the variable grew in size) - if (Entries[pos].Length < entry.Length) - { - AllocateEntry(ref entry, pos, entry.Length); - added = true; - } - - if (added || entry.Key.TickWritten > Entries[pos].Key.TickWritten) - { - Buffer.BlockCopy(RecvBuffer, entry.Position, MainBuffer, Entries[pos].Position, entry.Length); - - Entries[pos] = entry; - - // copy from readbuffer into buffer - var networkVariable = FindNetworkVar(Entries[pos].Key); - if (networkVariable != null) - { - unsafe - { - // This avoids copies - using Allocator.None creates a direct memory view into the buffer. - fixed (byte* buffer = RecvBuffer) - { - var reader = new FastBufferReader(buffer, Collections.Allocator.None, RecvBuffer.Length); - using (reader) - { - reader.Seek(Entries[pos].Position); - // todo: consider refactoring out in its own function to accomodate - // other ways to (de)serialize - // Not using keepDirtyDelta anymore which is great. todo: remove and check for the overall effect on > 2 player - networkVariable.ReadDelta(reader, false); - } - } - } - } - } - } - } - - internal void SpawnObject(SnapshotSpawnCommand spawnCommand, ulong srcClientId) - { - if (m_NetworkManager) - { - NetworkObject networkObject; - if (spawnCommand.ParentNetworkId == spawnCommand.NetworkObjectId) - { - networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false, - spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, null, spawnCommand.ObjectPosition, - spawnCommand.ObjectRotation); - } - else - { - networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false, - spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, spawnCommand.ParentNetworkId, spawnCommand.ObjectPosition, - spawnCommand.ObjectRotation); - } - - m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, - true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, false); - //todo: discuss with tools how to report shared bytes - m_NetworkManager.NetworkMetrics.TrackObjectSpawnReceived(srcClientId, networkObject, 8); - } - else - { - MockSpawnObject(spawnCommand); - } - } - - internal void DespawnObject(SnapshotDespawnCommand despawnCommand, ulong srcClientId) - { - if (m_NetworkManager) - { - m_NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(despawnCommand.NetworkObjectId, - out NetworkObject networkObject); - - m_NetworkManager.SpawnManager.OnDespawnObject(networkObject, true); - //todo: discuss with tools how to report shared bytes - m_NetworkManager.NetworkMetrics.TrackObjectDestroyReceived(srcClientId, networkObject, 8); - } - else - { - MockDespawnObject(despawnCommand); - } - } - - - internal void ReadSpawns(in SnapshotDataMessage message, ulong srcClientId) - { - SnapshotSpawnCommand spawnCommand; - SnapshotDespawnCommand despawnCommand; - - for (var i = 0; i < message.Spawns.Length; i++) - { - spawnCommand = ReadSpawn(message.Spawns[i]); - - if (TickAppliedSpawn.ContainsKey(spawnCommand.NetworkObjectId) && - spawnCommand.TickWritten <= TickAppliedSpawn[spawnCommand.NetworkObjectId]) - { - continue; - } - - TickAppliedSpawn[spawnCommand.NetworkObjectId] = spawnCommand.TickWritten; - - // Debug.Log($"[Spawn] {spawnCommand.NetworkObjectId} {spawnCommand.TickWritten}"); - - SpawnObject(spawnCommand, srcClientId); - } - for (var i = 0; i < message.Despawns.Length; i++) - { - despawnCommand = ReadDespawn(message.Despawns[i]); - - if (TickAppliedDespawn.ContainsKey(despawnCommand.NetworkObjectId) && - despawnCommand.TickWritten <= TickAppliedDespawn[despawnCommand.NetworkObjectId]) - { - continue; - } - - TickAppliedDespawn[despawnCommand.NetworkObjectId] = despawnCommand.TickWritten; - - // Debug.Log($"[DeSpawn] {despawnCommand.NetworkObjectId} {despawnCommand.TickWritten}"); - - DespawnObject(despawnCommand, srcClientId); - } - } - - internal void ReadAcks(ulong clientId, ClientData clientData, in SnapshotDataMessage message, ConnectionRtt connection) - { - ushort ackSequence = message.Ack.LastReceivedSequence; - ushort seqMask = message.Ack.ReceivedSequenceMask; - - // process the latest acknowledgment - ProcessSingleAck(ackSequence, clientId, clientData, connection); - - // for each bit in the mask, acknowledge one message before - while (seqMask != 0) - { - ackSequence--; - // extract least bit - if (seqMask % 2 == 1) - { - ProcessSingleAck(ackSequence, clientId, clientData, connection); - } - // move to next bit - seqMask >>= 1; - } - } - - internal void ProcessSingleAck(ushort ackSequence, ulong clientId, ClientData clientData, ConnectionRtt connection) - { - // look through the spawns sent - for (int index = 0; index < clientData.SentSpawns.Count; /*no increment*/) - { - // needless copy, but I didn't find a way around - ClientData.SentSpawn sent = clientData.SentSpawns[index]; - - // for those with the sequence number being ack'ed - if (sent.SequenceNumber == ackSequence) - { - // remember the tick - if (!clientData.SpawnAck.ContainsKey(sent.ObjectId)) - { - clientData.SpawnAck.Add(sent.ObjectId, sent.Tick); - } - else - { - clientData.SpawnAck[sent.ObjectId] = sent.Tick; - } - - // check the spawn and despawn commands, find them, and if this is the last connection - // to ack, let's remove them - for (var i = 0; i < NumSpawns; i++) - { - if (Spawns[i].TickWritten == sent.Tick && - Spawns[i].NetworkObjectId == sent.ObjectId) - { - Spawns[i].TargetClientIds.Remove(clientId); - - if (Spawns[i].TargetClientIds.Count == 0) - { - // remove by moving the last spawn over - Spawns[i] = Spawns[NumSpawns - 1]; - NumSpawns--; - break; - } - } - } - for (var i = 0; i < NumDespawns; i++) - { - if (Despawns[i].TickWritten == sent.Tick && - Despawns[i].NetworkObjectId == sent.ObjectId) - { - Despawns[i].TargetClientIds.Remove(clientId); - - if (Despawns[i].TargetClientIds.Count == 0) - { - // remove by moving the last spawn over - Despawns[i] = Despawns[NumDespawns - 1]; - NumDespawns--; - break; - } - } - } - - // remove current `sent`, by moving last over, - // as it was acknowledged. - // skip incrementing index - clientData.SentSpawns[index] = clientData.SentSpawns[clientData.SentSpawns.Count - 1]; - clientData.SentSpawns.RemoveAt(clientData.SentSpawns.Count - 1); - } - else - { - index++; - } - } - - // keep track of RTTs, using the sequence number acknowledgement as a marker - connection.NotifyAck(ackSequence, Time.unscaledTime); - } - - /// - /// Helper function to find the NetworkVariable object from a key - /// This will look into all spawned objects - /// - /// The key to search for - private NetworkVariableBase FindNetworkVar(VariableKey key) - { - var spawnedObjects = m_NetworkManager.SpawnManager.SpawnedObjects; - - if (spawnedObjects.ContainsKey(key.NetworkObjectId)) - { - var behaviour = spawnedObjects[key.NetworkObjectId] - .GetNetworkBehaviourAtOrderIndex(key.BehaviourIndex); - return behaviour.NetworkVariableFields[key.VariableIndex]; - } - - return null; - } - - /// - /// Constructor - /// - /// Registers the snapshot system for early updates, keeps reference to the NetworkManager - internal SnapshotSystem(NetworkManager networkManager, NetworkConfig config, NetworkTickSystem networkTickSystem) - { - m_NetworkManager = networkManager; - m_NetworkTickSystem = networkTickSystem; - - m_UseSnapshotDelta = config.UseSnapshotDelta; - m_UseSnapshotSpawn = config.UseSnapshotSpawn; - m_SnapshotMaxSpawnUsage = config.SnapshotMaxSpawnUsage; - - UpdateClientServerData(); - - this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); - - // we ask for twice as many slots because there could end up being one free spot between each pair of slot used - Allocator = new IndexAllocator(k_BufferSize, k_MaxVariables * 2); - Spawns = new SnapshotSpawnCommand[SpawnsBufferCount]; - Despawns = new SnapshotDespawnCommand[DespawnsBufferCount]; - } - - // since we don't want to access the NetworkManager directly, we refresh those values on Update - internal void UpdateClientServerData() - { - if (m_NetworkManager) - { - IsServer = m_NetworkManager.IsServer; - IsConnectedClient = m_NetworkManager.IsConnectedClient; - ServerClientId = m_NetworkManager.ServerClientId; - - // todo: This is extremely inefficient. What is the efficient and idiomatic way ? - ConnectedClientsId.Clear(); - if (IsServer) - { - foreach (var id in m_NetworkManager.ConnectedClientsIds) - { - ConnectedClientsId.Add(id); - } - } - } - } - - internal ConnectionRtt GetConnectionRtt(ulong clientId) - { - if (!m_ConnectionRtts.ContainsKey(clientId)) - { - m_ConnectionRtts.Add(clientId, new ConnectionRtt()); - } - - return m_ConnectionRtts[clientId]; - } - - /// - /// Dispose - /// - /// Unregisters the snapshot system from early updates - public void Dispose() - { - this.UnregisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); - } - - public void NetworkUpdate(NetworkUpdateStage updateStage) - { - if (!m_UseSnapshotDelta && !m_UseSnapshotSpawn) - { - return; - } - - if (updateStage == NetworkUpdateStage.EarlyUpdate) - { - UpdateClientServerData(); - - var tick = m_NetworkTickSystem.LocalTime.Tick; - - if (tick != m_CurrentTick) - { - m_CurrentTick = tick; - if (IsServer) - { - for (int i = 0; i < ConnectedClientsId.Count; i++) - { - var clientId = ConnectedClientsId[i]; - - // don't send to ourselves - if (clientId != ServerClientId) - { - SendSnapshot(clientId); - } - } - } - else if (IsConnectedClient) - { - SendSnapshot(ServerClientId); - } - } - - // useful for debugging, but generates LOTS of spam - // DebugDisplayStore(); - } - } - - // todo --M1-- - // for now, the full snapshot is always sent - // this will change significantly - /// - /// Send the snapshot to a specific client - /// - /// The client index to send to - private void SendSnapshot(ulong clientId) - { - // make sure we have a ClientData and ConnectionRtt entry for each client - if (!m_ClientData.ContainsKey(clientId)) - { - m_ClientData.Add(clientId, new ClientData()); - } - - if (!m_ConnectionRtts.ContainsKey(clientId)) - { - m_ConnectionRtts.Add(clientId, new ConnectionRtt()); - } - - m_ConnectionRtts[clientId].NotifySend(m_ClientData[clientId].SequenceNumber, Time.unscaledTime); - - var sequence = m_ClientData[clientId].SequenceNumber; - var message = new SnapshotDataMessage - { - CurrentTick = m_CurrentTick, - Sequence = sequence, - Range = (ushort)Allocator.Range, - - // todo --M1-- - // this sends the whole buffer - // we'll need to build a per-client list - SendMainBuffer = MainBuffer, - - Ack = new SnapshotDataMessage.AckData - { - LastReceivedSequence = m_ClientData[clientId].LastReceivedSequence, - ReceivedSequenceMask = m_ClientData[clientId].ReceivedSequenceMask - } - }; - - - // write the snapshot: buffer, index, spawns, despawns - WriteIndex(ref message); - WriteSpawns(ref message, clientId); - - try - { - if (m_NetworkManager) - { - m_NetworkManager.SendMessage(ref message, NetworkDelivery.Unreliable, clientId); - } - else - { - MockSendMessage(ref message, NetworkDelivery.Unreliable, clientId); - } - } - finally - { - message.Entries.Dispose(); - message.Spawns.Dispose(); - message.Despawns.Dispose(); - } - - m_ClientData[clientId].LastReceivedSequence = 0; - - // todo: this is incorrect (well, sub-optimal) - // we should still continue ack'ing past messages, in case this one is dropped - m_ClientData[clientId].ReceivedSequenceMask = 0; - m_ClientData[clientId].SequenceNumber++; - } - - // Checks if a given SpawnCommand should be written to a Snapshot Message - // Performs exponential back off. To write a spawn a second time - // two ticks must have gone by. To write it a third time, four ticks, etc... - // This prioritize commands that have been re-sent less than others - private bool ShouldWriteSpawn(in SnapshotSpawnCommand spawnCommand) - { - if (m_CurrentTick < spawnCommand.TickWritten) - { - return false; - } - - // 63 as we can't shift more than that. - var diff = Math.Min(63, m_CurrentTick - spawnCommand.TickWritten); - - // -1 to make the first resend immediate - return (1 << diff) > (spawnCommand.TimesWritten - 1); - } - - private bool ShouldWriteDespawn(in SnapshotDespawnCommand despawnCommand) - { - if (m_CurrentTick < despawnCommand.TickWritten) - { - return false; - } - - // 63 as we can't shift more than that. - var diff = Math.Min(63, m_CurrentTick - despawnCommand.TickWritten); - - // -1 to make the first resend immediate - return (1 << diff) > (despawnCommand.TimesWritten - 1); - } - - private void WriteSpawns(ref SnapshotDataMessage message, ulong clientId) - { - var spawnWritten = 0; - var despawnWritten = 0; - var overSize = false; - - ClientData clientData = m_ClientData[clientId]; - - // this is needed because spawns being removed may have reduce the size below LRU position - if (NumSpawns > 0) - { - clientData.NextSpawnIndex %= NumSpawns; - } - else - { - clientData.NextSpawnIndex = 0; - } - - if (NumDespawns > 0) - { - clientData.NextDespawnIndex %= NumDespawns; - } - else - { - clientData.NextDespawnIndex = 0; - } - - message.Spawns = new NativeList(NumSpawns, Collections.Allocator.TempJob); - message.Despawns = new NativeList(NumDespawns, Collections.Allocator.TempJob); - var spawnUsage = 0; - - for (var j = 0; j < NumSpawns && !overSize; j++) - { - var index = clientData.NextSpawnIndex; - - // todo: re-enable ShouldWriteSpawn, once we have a mechanism to not let despawn pass in front of spawns - if (Spawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteSpawn(Spawns[index])*/) - { - spawnUsage += FastBufferWriter.GetWriteSize(); - - // limit spawn sizes, compare current pos to very first position we wrote to - if (spawnUsage > m_SnapshotMaxSpawnUsage) - { - overSize = true; - break; - } - var sentSpawn = GetSpawnData(clientData, in Spawns[index], out var spawn); - message.Spawns.Add(spawn); - - Spawns[index].TimesWritten++; - clientData.SentSpawns.Add(sentSpawn); - spawnWritten++; - } - clientData.NextSpawnIndex = (clientData.NextSpawnIndex + 1) % NumSpawns; - } - - // even though we might have a spawn we could not fit, it's possible despawns will fit (they're smaller) - - // todo: this next line is commented for now because there's no check for a spawn command to have been - // ack'ed before sending a despawn for the same object. - // Uncommenting this line would allow some despawn to be sent while spawns are pending. - // As-is it is overly restrictive but allows us to go forward without the spawn/despawn dependency check - // overSize = false; - - for (var j = 0; j < NumDespawns && !overSize; j++) - { - var index = clientData.NextDespawnIndex; - - // todo: re-enable ShouldWriteSpawn, once we have a mechanism to not let despawn pass in front of spawns - if (Despawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteDespawn(Despawns[index])*/) - { - spawnUsage += FastBufferWriter.GetWriteSize(); - - // limit spawn sizes, compare current pos to very first position we wrote to - if (spawnUsage > m_SnapshotMaxSpawnUsage) - { - overSize = true; - break; - } - var sentDespawn = GetDespawnData(clientData, in Despawns[index], out var despawn); - message.Despawns.Add(despawn); - Despawns[index].TimesWritten++; - clientData.SentSpawns.Add(sentDespawn); - despawnWritten++; - } - clientData.NextDespawnIndex = (clientData.NextDespawnIndex + 1) % NumDespawns; - } - } - - /// - /// Write the snapshot index to a buffer - /// - /// The message to write the index to - private void WriteIndex(ref SnapshotDataMessage message) - { - message.Entries = new NativeList(LastEntry, Collections.Allocator.TempJob); - for (var i = 0; i < LastEntry; i++) - { - var entryMeta = Entries[i]; - var entry = entryMeta.Key; - message.Entries.Add(new SnapshotDataMessage.EntryData - { - NetworkObjectId = entry.NetworkObjectId, - BehaviourIndex = entry.BehaviourIndex, - VariableIndex = entry.VariableIndex, - TickWritten = entry.TickWritten, - Position = entryMeta.Position, - Length = entryMeta.Length - }); - } - } - - internal void Spawn(SnapshotSpawnCommand command) - { - command.TickWritten = m_CurrentTick; - AddSpawn(command); - - // Debug.Log($"[Spawn] {command.NetworkObjectId} {command.TickWritten}"); - } - - internal void Despawn(SnapshotDespawnCommand command) - { - command.TickWritten = m_CurrentTick; - AddDespawn(command); - - // Debug.Log($"[DeSpawn] {command.NetworkObjectId} {command.TickWritten}"); - } - - // todo: consider using a Key, instead of 3 ints, if it can be exposed - /// - /// Called by the rest of the netcode when a NetworkVariable changed and need to go in our snapshot - /// Might not happen for all variable on every frame. Might even happen more than once. - /// - /// The NetworkVariable to write, or rather, its INetworkVariable - internal void Store(ulong networkObjectId, int behaviourIndex, int variableIndex, NetworkVariableBase networkVariable) - { - VariableKey k; - k.NetworkObjectId = networkObjectId; - k.BehaviourIndex = (ushort)behaviourIndex; - k.VariableIndex = (ushort)variableIndex; - k.TickWritten = m_NetworkTickSystem.LocalTime.Tick; - - int pos = Find(k); - if (pos == Entry.NotFound) - { - pos = AddEntry(k); - } - - Entries[pos].Key.TickWritten = k.TickWritten; - - WriteVariable(networkVariable, pos); - } - - private unsafe void WriteVariable(NetworkVariableBase networkVariable, int index) - { - // write var into buffer, possibly adjusting entry's position and Length - var varBuffer = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Collections.Allocator.Temp); - using (varBuffer) - { - networkVariable.WriteDelta(varBuffer); - if (varBuffer.Length > Entries[index].Length) - { - // allocate this Entry's buffer - AllocateEntry(ref Entries[index], index, (int)varBuffer.Length); - } - - fixed (byte* buffer = MainBuffer) - { - UnsafeUtility.MemCpy(buffer + Entries[index].Position, varBuffer.GetUnsafePtr(), varBuffer.Length); - } - } - } - - - /// - /// Entry point when a Snapshot is received - /// This is where we read and store the received snapshot - /// - /// - /// The message to read from - internal void HandleSnapshot(ulong clientId, in SnapshotDataMessage message) - { - // make sure we have a ClientData entry for each client - if (!m_ClientData.ContainsKey(clientId)) - { - m_ClientData.Add(clientId, new ClientData()); - } - - if (message.Sequence >= m_ClientData[clientId].LastReceivedSequence) - { - if (m_ClientData[clientId].ReceivedSequenceMask != 0) - { - // since each bit in ReceivedSequenceMask is relative to the last received sequence - // we need to shift all the bits by the difference in sequence - var shift = message.Sequence - m_ClientData[clientId].LastReceivedSequence; - if (shift < sizeof(ushort) * 8) - { - m_ClientData[clientId].ReceivedSequenceMask <<= shift; - } - else - { - m_ClientData[clientId].ReceivedSequenceMask = 0; - } - } - - if (m_ClientData[clientId].LastReceivedSequence != 0) - { - // because the bit we're adding for the previous ReceivedSequenceMask - // was implicit, it needs to be shift by one less - var shift = message.Sequence - 1 - m_ClientData[clientId].LastReceivedSequence; - if (shift < sizeof(ushort) * 8) - { - m_ClientData[clientId].ReceivedSequenceMask |= (ushort)(1 << shift); - } - } - - m_ClientData[clientId].LastReceivedSequence = message.Sequence; - } - else - { - // todo: Missing: dealing with out-of-order message acknowledgments - // we should set m_ClientData[clientId].ReceivedSequenceMask accordingly - // testing this will require a way to reorder SnapshotMessages, which we lack at the moment - // - // without this, we incur extra retransmit, not a catastrophic failure - } - - ReadBuffer(message); - ReadIndex(message); - ReadAcks(clientId, m_ClientData[clientId], message, GetConnectionRtt(clientId)); - ReadSpawns(message, clientId); - } - - // todo --M1-- - // This is temporary debugging code. Once the feature is complete, we can consider removing it - // But we could also leave it in in debug to help developers - private void DebugDisplayStore() - { - string table = "=== Snapshot table ===\n"; - table += $"We're clientId {m_NetworkManager.LocalClientId}\n"; - - table += "=== Variables ===\n"; - for (int i = 0; i < LastEntry; i++) - { - table += string.Format("NetworkVariable {0}:{1}:{2} written {5}, range [{3}, {4}] ", Entries[i].Key.NetworkObjectId, Entries[i].Key.BehaviourIndex, - Entries[i].Key.VariableIndex, Entries[i].Position, Entries[i].Position + Entries[i].Length, Entries[i].Key.TickWritten); - - for (int j = 0; j < Entries[i].Length && j < 4; j++) - { - table += MainBuffer[Entries[i].Position + j].ToString("X2") + " "; - } - - table += "\n"; - } - - table += "=== Spawns ===\n"; - - for (int i = 0; i < NumSpawns; i++) - { - string targets = ""; - foreach (var target in Spawns[i].TargetClientIds) - { - targets += target.ToString() + ", "; - } - table += $"Spawn Object Id {Spawns[i].NetworkObjectId}, Tick {Spawns[i].TickWritten}, Target {targets}\n"; - } - - table += $"=== RTTs ===\n"; - foreach (var iterator in m_ConnectionRtts) - { - table += $"client {iterator.Key} RTT {iterator.Value.GetRtt().AverageSec}\n"; - } - - table += "======\n"; - Debug.Log(table); - } - } -} diff --git a/Runtime/Logging/NetworkLog.cs b/Runtime/Logging/NetworkLog.cs index bf98928..ef4008d 100644 --- a/Runtime/Logging/NetworkLog.cs +++ b/Runtime/Logging/NetworkLog.cs @@ -14,9 +14,9 @@ public static class NetworkLog public static LogLevel CurrentLogLevel => NetworkManager.Singleton == null ? LogLevel.Normal : NetworkManager.Singleton.LogLevel; // internal logging - internal static void LogInfo(string message) => Debug.Log($"[Netcode] {message}"); - internal static void LogWarning(string message) => Debug.LogWarning($"[Netcode] {message}"); - internal static void LogError(string message) => Debug.LogError($"[Netcode] {message}"); + public static void LogInfo(string message) => Debug.Log($"[Netcode] {message}"); + public static void LogWarning(string message) => Debug.LogWarning($"[Netcode] {message}"); + public static void LogError(string message) => Debug.LogError($"[Netcode] {message}"); /// /// Logs an info log locally and on the server if possible. @@ -62,9 +62,9 @@ private static void LogServer(string message, LogType logType) LogType = logType, Message = message }; - var size = NetworkManager.Singleton.SendMessage(ref networkMessage, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.Singleton.ServerClientId); + var size = NetworkManager.Singleton.SendMessage(ref networkMessage, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); - NetworkManager.Singleton.NetworkMetrics.TrackServerLogSent(NetworkManager.Singleton.ServerClientId, (uint)logType, size); + NetworkManager.Singleton.NetworkMetrics.TrackServerLogSent(NetworkManager.ServerClientId, (uint)logType, size); } } diff --git a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs index e4dde7b..f1ed9f4 100644 --- a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs +++ b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs @@ -29,24 +29,33 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context) public void Handle(ref NetworkContext context) { - var networkManager = (NetworkManager)context.SystemOwner; var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId]; + var originalOwner = networkObject.OwnerClientId; + + networkObject.OwnerClientId = OwnerClientId; - if (networkObject.OwnerClientId == networkManager.LocalClientId) + // We are current owner. + if (originalOwner == networkManager.LocalClientId) { - //We are current owner. networkObject.InvokeBehaviourOnLostOwnership(); } - networkObject.OwnerClientId = OwnerClientId; - + // We are new owner. if (OwnerClientId == networkManager.LocalClientId) { - //We are new owner. networkObject.InvokeBehaviourOnGainedOwnership(); } + // For all other clients that are neither the former or current owner, update the behaviours' properties + if (OwnerClientId != networkManager.LocalClientId && originalOwner != networkManager.LocalClientId) + { + for (int i = 0; i < networkObject.ChildNetworkBehaviours.Count; i++) + { + networkObject.ChildNetworkBehaviours[i].UpdateNetworkProperties(); + } + } + networkManager.NetworkMetrics.TrackOwnershipChangeReceived(context.SenderId, networkObject, context.MessageSize); } } diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs index 27fcb51..a7d1e0d 100644 --- a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs +++ b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs @@ -16,7 +16,7 @@ internal struct NetworkVariableDeltaMessage : INetworkMessage public ushort NetworkBehaviourIndex; public HashSet DeliveryMappedNetworkVariableIndex; - public ulong ClientId; + public ulong TargetClientId; public NetworkBehaviour NetworkBehaviour; private FastBufferReader m_ReceivedNetworkVariableData; @@ -31,9 +31,9 @@ public void Serialize(FastBufferWriter writer) writer.WriteValue(NetworkObjectId); writer.WriteValue(NetworkBehaviourIndex); - for (int k = 0; k < NetworkBehaviour.NetworkVariableFields.Count; k++) + for (int i = 0; i < NetworkBehaviour.NetworkVariableFields.Count; i++) { - if (!DeliveryMappedNetworkVariableIndex.Contains(k)) + if (!DeliveryMappedNetworkVariableIndex.Contains(i)) { // This var does not belong to the currently iterating delivery group. if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) @@ -48,15 +48,17 @@ public void Serialize(FastBufferWriter writer) continue; } - // if I'm dirty AND a client, write (server always has all permissions) - // if I'm dirty AND the server AND the client can read me, send. - bool shouldWrite = NetworkBehaviour.NetworkVariableFields[k].ShouldWrite(ClientId, NetworkBehaviour.NetworkManager.IsServer); + var startingSize = writer.Length; + var networkVariable = NetworkBehaviour.NetworkVariableFields[i]; + var shouldWrite = networkVariable.IsDirty() && + networkVariable.CanClientRead(TargetClientId) && + (NetworkBehaviour.NetworkManager.IsServer || networkVariable.CanClientWrite(NetworkBehaviour.NetworkManager.LocalClientId)); if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { if (!shouldWrite) { - writer.WriteValueSafe((ushort)0); + BytePacker.WriteValueBitPacked(writer, 0); } } else @@ -68,34 +70,34 @@ public void Serialize(FastBufferWriter writer) { if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { - var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, short.MaxValue); - NetworkBehaviour.NetworkVariableFields[k].WriteDelta(tmpWriter); + var tempWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, MessagingSystem.FRAGMENTED_MESSAGE_MAX_SIZE); + NetworkBehaviour.NetworkVariableFields[i].WriteDelta(tempWriter); + BytePacker.WriteValueBitPacked(writer, tempWriter.Length); - if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize() + tmpWriter.Length)) + if (!writer.TryBeginWrite(tempWriter.Length)) { throw new OverflowException($"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}"); } - writer.WriteValue((ushort)tmpWriter.Length); - tmpWriter.CopyTo(writer); + tempWriter.CopyTo(writer); } else { - NetworkBehaviour.NetworkVariableFields[k].WriteDelta(writer); + networkVariable.WriteDelta(writer); } - if (!NetworkBehaviour.NetworkVariableIndexesToResetSet.Contains(k)) + if (!NetworkBehaviour.NetworkVariableIndexesToResetSet.Contains(i)) { - NetworkBehaviour.NetworkVariableIndexesToResetSet.Add(k); - NetworkBehaviour.NetworkVariableIndexesToReset.Add(k); + NetworkBehaviour.NetworkVariableIndexesToResetSet.Add(i); + NetworkBehaviour.NetworkVariableIndexesToReset.Add(i); } NetworkBehaviour.NetworkManager.NetworkMetrics.TrackNetworkVariableDeltaSent( - ClientId, + TargetClientId, NetworkBehaviour.NetworkObject, - NetworkBehaviour.NetworkVariableFields[k].Name, + networkVariable.Name, NetworkBehaviour.__getTypeName(), - writer.Length); + writer.Length - startingSize); } } } @@ -121,9 +123,9 @@ public void Handle(ref NetworkContext context) if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out NetworkObject networkObject)) { - NetworkBehaviour behaviour = networkObject.GetNetworkBehaviourAtOrderIndex(NetworkBehaviourIndex); + var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(NetworkBehaviourIndex); - if (behaviour == null) + if (networkBehaviour == null) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { @@ -132,13 +134,12 @@ public void Handle(ref NetworkContext context) } else { - for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++) + for (int i = 0; i < networkBehaviour.NetworkVariableFields.Count; i++) { - ushort varSize = 0; - + int varSize = 0; if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { - m_ReceivedNetworkVariableData.ReadValueSafe(out varSize); + ByteUnpacker.ReadValueBitPacked(m_ReceivedNetworkVariableData, out varSize); if (varSize == 0) { @@ -154,15 +155,17 @@ public void Handle(ref NetworkContext context) } } - if (networkManager.IsServer) + var networkVariable = networkBehaviour.NetworkVariableFields[i]; + + if (networkManager.IsServer && !networkVariable.CanClientWrite(context.SenderId)) { // we are choosing not to fire an exception here, because otherwise a malicious client could use this to crash the server if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { - NetworkLog.LogWarning($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}"); - NetworkLog.LogError($"[{behaviour.NetworkVariableFields[i].GetType().Name}]"); + NetworkLog.LogWarning($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}"); + NetworkLog.LogError($"[{networkVariable.GetType().Name}]"); } m_ReceivedNetworkVariableData.Seek(m_ReceivedNetworkVariableData.Position + varSize); @@ -176,23 +179,23 @@ public void Handle(ref NetworkContext context) //A dummy read COULD be added to the interface for this situation, but it's just being too nice. //This is after all a developer fault. A critical error should be fine. // - TwoTen - if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { - NetworkLog.LogError($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. No more variables can be read. This is critical. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}"); - NetworkLog.LogError($"[{behaviour.NetworkVariableFields[i].GetType().Name}]"); + NetworkLog.LogError($"Client wrote to {typeof(NetworkVariable<>).Name} without permission. No more variables can be read. This is critical. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}"); + NetworkLog.LogError($"[{networkVariable.GetType().Name}]"); } return; } int readStartPos = m_ReceivedNetworkVariableData.Position; - behaviour.NetworkVariableFields[i].ReadDelta(m_ReceivedNetworkVariableData, networkManager.IsServer); + networkVariable.ReadDelta(m_ReceivedNetworkVariableData, networkManager.IsServer); networkManager.NetworkMetrics.TrackNetworkVariableDeltaReceived( context.SenderId, networkObject, - behaviour.NetworkVariableFields[i].Name, - behaviour.__getTypeName(), + networkVariable.Name, + networkBehaviour.__getTypeName(), context.MessageSize); @@ -202,7 +205,7 @@ public void Handle(ref NetworkContext context) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogWarning($"Var delta read too far. {m_ReceivedNetworkVariableData.Position - (readStartPos + varSize)} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}"); + NetworkLog.LogWarning($"Var delta read too far. {m_ReceivedNetworkVariableData.Position - (readStartPos + varSize)} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}"); } m_ReceivedNetworkVariableData.Seek(readStartPos + varSize); @@ -211,7 +214,7 @@ public void Handle(ref NetworkContext context) { if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) { - NetworkLog.LogWarning($"Var delta read too little. {(readStartPos + varSize) - m_ReceivedNetworkVariableData.Position} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}"); + NetworkLog.LogWarning($"Var delta read too little. {readStartPos + varSize - m_ReceivedNetworkVariableData.Position} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(networkBehaviour)} - VariableIndex: {i}"); } m_ReceivedNetworkVariableData.Seek(readStartPos + varSize); diff --git a/Runtime/Messaging/Messages/SnapshotDataMessage.cs b/Runtime/Messaging/Messages/SnapshotDataMessage.cs deleted file mode 100644 index ae887ea..0000000 --- a/Runtime/Messaging/Messages/SnapshotDataMessage.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using UnityEngine; - -namespace Unity.Netcode -{ - internal struct SnapshotDataMessage : INetworkMessage - { - public int CurrentTick; - public ushort Sequence; - - public ushort Range; - - public byte[] SendMainBuffer; - public NativeArray ReceiveMainBuffer; - - public struct AckData - { - public ushort LastReceivedSequence; - public ushort ReceivedSequenceMask; - } - - public AckData Ack; - - public struct EntryData - { - public ulong NetworkObjectId; - public ushort BehaviourIndex; - public ushort VariableIndex; - public int TickWritten; - public ushort Position; - public ushort Length; - } - - public NativeList Entries; - - public struct SpawnData - { - public ulong NetworkObjectId; - public uint Hash; - public bool IsSceneObject; - - public bool IsPlayerObject; - public ulong OwnerClientId; - public ulong ParentNetworkId; - public Vector3 Position; - public Quaternion Rotation; - public Vector3 Scale; - - public int TickWritten; - } - - public NativeList Spawns; - - public struct DespawnData - { - public ulong NetworkObjectId; - public int TickWritten; - } - - public NativeList Despawns; - - public unsafe void Serialize(FastBufferWriter writer) - { - if (!writer.TryBeginWrite( - FastBufferWriter.GetWriteSize(CurrentTick) + - FastBufferWriter.GetWriteSize(Sequence) + - FastBufferWriter.GetWriteSize(Range) + Range + - FastBufferWriter.GetWriteSize(Ack) + - FastBufferWriter.GetWriteSize() + - Entries.Length * sizeof(EntryData) + - FastBufferWriter.GetWriteSize() + - Spawns.Length * sizeof(SpawnData) + - FastBufferWriter.GetWriteSize() + - Despawns.Length * sizeof(DespawnData) - )) - { - throw new OverflowException($"Not enough space to serialize {nameof(SnapshotDataMessage)}"); - } - writer.WriteValue(CurrentTick); - writer.WriteValue(Sequence); - - writer.WriteValue(Range); - writer.WriteBytes(SendMainBuffer, Range); - writer.WriteValue(Ack); - - writer.WriteValue((ushort)Entries.Length); - writer.WriteBytes((byte*)Entries.GetUnsafePtr(), Entries.Length * sizeof(EntryData)); - - writer.WriteValue((ushort)Spawns.Length); - writer.WriteBytes((byte*)Spawns.GetUnsafePtr(), Spawns.Length * sizeof(SpawnData)); - - writer.WriteValue((ushort)Despawns.Length); - writer.WriteBytes((byte*)Despawns.GetUnsafePtr(), Despawns.Length * sizeof(DespawnData)); - } - - public unsafe bool Deserialize(FastBufferReader reader, ref NetworkContext context) - { - if (!reader.TryBeginRead( - FastBufferWriter.GetWriteSize(CurrentTick) + - FastBufferWriter.GetWriteSize(Sequence) + - FastBufferWriter.GetWriteSize(Range) - )) - { - throw new OverflowException($"Not enough space to deserialize {nameof(SnapshotDataMessage)}"); - } - reader.ReadValue(out CurrentTick); - reader.ReadValue(out Sequence); - - reader.ReadValue(out Range); - ReceiveMainBuffer = new NativeArray(Range, Allocator.Temp); - reader.ReadBytesSafe((byte*)ReceiveMainBuffer.GetUnsafePtr(), Range); - reader.ReadValueSafe(out Ack); - - reader.ReadValueSafe(out ushort length); - Entries = new NativeList(length, Allocator.Temp) { Length = length }; - reader.ReadBytesSafe((byte*)Entries.GetUnsafePtr(), Entries.Length * sizeof(EntryData)); - - reader.ReadValueSafe(out length); - Spawns = new NativeList(length, Allocator.Temp) { Length = length }; - reader.ReadBytesSafe((byte*)Spawns.GetUnsafePtr(), Spawns.Length * sizeof(SpawnData)); - - reader.ReadValueSafe(out length); - Despawns = new NativeList(length, Allocator.Temp) { Length = length }; - reader.ReadBytesSafe((byte*)Despawns.GetUnsafePtr(), Despawns.Length * sizeof(DespawnData)); - - return true; - } - - public void Handle(ref NetworkContext context) - { - using (ReceiveMainBuffer) - using (Entries) - using (Spawns) - using (Despawns) - { - var systemOwner = context.SystemOwner; - var senderId = context.SenderId; - if (systemOwner is NetworkManager networkManager) - { - // todo: temporary hack around bug - if (!networkManager.IsServer) - { - senderId = networkManager.ServerClientId; - } - - var snapshotSystem = networkManager.SnapshotSystem; - snapshotSystem.HandleSnapshot(senderId, this); - } - else - { - var ownerData = (Tuple)systemOwner; - var snapshotSystem = ownerData.Item1; - snapshotSystem.HandleSnapshot(ownerData.Item2, this); - } - } - } - } -} diff --git a/Runtime/Messaging/Messages/SnapshotDataMessage.cs.meta b/Runtime/Messaging/Messages/SnapshotDataMessage.cs.meta deleted file mode 100644 index a549a7c..0000000 --- a/Runtime/Messaging/Messages/SnapshotDataMessage.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 5cf75026c2ab86646aac16b39d7259ad -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Messaging/MessagingSystem.cs b/Runtime/Messaging/MessagingSystem.cs index 07f1524..422401c 100644 --- a/Runtime/Messaging/MessagingSystem.cs +++ b/Runtime/Messaging/MessagingSystem.cs @@ -67,7 +67,7 @@ internal uint GetMessageType(Type t) } public const int NON_FRAGMENTED_MESSAGE_MAX_SIZE = 1300; - public const int FRAGMENTED_MESSAGE_MAX_SIZE = int.MaxValue; + public const int FRAGMENTED_MESSAGE_MAX_SIZE = BytePacker.BitPackedIntMax; internal struct MessageWithHandler { diff --git a/Runtime/Metrics/INetworkMetrics.cs b/Runtime/Metrics/INetworkMetrics.cs index a49693a..96a99b5 100644 --- a/Runtime/Metrics/INetworkMetrics.cs +++ b/Runtime/Metrics/INetworkMetrics.cs @@ -87,7 +87,13 @@ void TrackRpcReceived( void TrackPacketReceived(uint packetCount); - void TrackRttToServer(int rtt); + void UpdateRttToServer(int rtt); + + void UpdateNetworkObjectsCount(int count); + + void UpdateConnectionsCount(int count); + + void UpdatePacketLoss(float packetLoss); void DispatchFrame(); } diff --git a/Runtime/Metrics/NetworkMetrics.cs b/Runtime/Metrics/NetworkMetrics.cs index 7df6d53..fb92716 100644 --- a/Runtime/Metrics/NetworkMetrics.cs +++ b/Runtime/Metrics/NetworkMetrics.cs @@ -66,7 +66,7 @@ private static string GetSceneEventTypeName(uint typeCode) private readonly EventMetric m_SceneEventSentEvent = new EventMetric(NetworkMetricTypes.SceneEventSent.Id); private readonly EventMetric m_SceneEventReceivedEvent = new EventMetric(NetworkMetricTypes.SceneEventReceived.Id); -#if MULTIPLAYER_TOOLS_1_0_0_PRE_4 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 private readonly Counter m_PacketSentCounter = new Counter(NetworkMetricTypes.PacketsSent.Id) { ShouldResetOnDispatch = true, @@ -79,6 +79,15 @@ private static string GetSceneEventTypeName(uint typeCode) { ShouldResetOnDispatch = true, }; + private readonly Gauge m_NetworkObjectsGauge = new Gauge(NetworkMetricTypes.NetworkObjects.Id) + { + ShouldResetOnDispatch = true, + }; + private readonly Gauge m_ConnectionsGauge = new Gauge(NetworkMetricTypes.ConnectedClients.Id) + { + ShouldResetOnDispatch = true, + }; + private readonly Gauge m_PacketLossGauge = new Gauge(NetworkMetricTypes.PacketLoss.Id); #endif private ulong m_NumberOfMetricsThisFrame; @@ -97,9 +106,12 @@ public NetworkMetrics() .WithMetricEvents(m_RpcSentEvent, m_RpcReceivedEvent) .WithMetricEvents(m_ServerLogSentEvent, m_ServerLogReceivedEvent) .WithMetricEvents(m_SceneEventSentEvent, m_SceneEventReceivedEvent) -#if MULTIPLAYER_TOOLS_1_0_0_PRE_4 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 .WithCounters(m_PacketSentCounter, m_PacketReceivedCounter) .WithGauges(m_RttToServerGauge) + .WithGauges(m_NetworkObjectsGauge) + .WithGauges(m_ConnectionsGauge) + .WithGauges(m_PacketLossGauge) #endif .Build(); @@ -428,7 +440,7 @@ public void TrackSceneEventReceived(ulong senderClientId, uint sceneEventType, s public void TrackPacketSent(uint packetCount) { -#if MULTIPLAYER_TOOLS_1_0_0_PRE_4 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 if (!CanSendMetrics) { return; @@ -441,7 +453,7 @@ public void TrackPacketSent(uint packetCount) public void TrackPacketReceived(uint packetCount) { -#if MULTIPLAYER_TOOLS_1_0_0_PRE_4 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 if (!CanSendMetrics) { return; @@ -452,15 +464,51 @@ public void TrackPacketReceived(uint packetCount) #endif } - public void TrackRttToServer(int rtt) + public void UpdateRttToServer(int rttMilliseconds) + { +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + if (!CanSendMetrics) + { + return; + } + var rttSeconds = rttMilliseconds * 1e-3; + m_RttToServerGauge.Set(rttSeconds); +#endif + } + + public void UpdateNetworkObjectsCount(int count) + { +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + if (!CanSendMetrics) + { + return; + } + + m_NetworkObjectsGauge.Set(count); +#endif + } + + public void UpdateConnectionsCount(int count) + { +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + if (!CanSendMetrics) + { + return; + } + + m_ConnectionsGauge.Set(count); +#endif + } + + public void UpdatePacketLoss(float packetLoss) { -#if MULTIPLAYER_TOOLS_1_0_0_PRE_4 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 if (!CanSendMetrics) { return; } - m_RttToServerGauge.Set(rtt); + m_PacketLossGauge.Set(packetLoss); #endif } diff --git a/Runtime/Metrics/NullNetworkMetrics.cs b/Runtime/Metrics/NullNetworkMetrics.cs index b925110..5d276b1 100644 --- a/Runtime/Metrics/NullNetworkMetrics.cs +++ b/Runtime/Metrics/NullNetworkMetrics.cs @@ -145,7 +145,19 @@ public void TrackPacketReceived(uint packetCount) { } - public void TrackRttToServer(int rtt) + public void UpdateRttToServer(int rtt) + { + } + + public void UpdateNetworkObjectsCount(int count) + { + } + + public void UpdateConnectionsCount(int count) + { + } + + public void UpdatePacketLoss(float packetLoss) { } diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs index 53a325f..a6f221e 100644 --- a/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -24,34 +24,16 @@ public class NetworkList : NetworkVariableBase where T : unmanaged, IEquatabl /// public event OnListChangedDelegate OnListChanged; - /// - /// Creates a NetworkList with the default value and settings - /// public NetworkList() { } - /// - /// Creates a NetworkList with the default value and custom settings - /// - /// The read permission to use for the NetworkList - /// The initial value to use for the NetworkList - public NetworkList(NetworkVariableReadPermission readPerm, IEnumerable values) : base(readPerm) - { - foreach (var value in values) - { - m_List.Add(value); - } - } - - /// - /// Creates a NetworkList with a custom value and the default settings - /// - /// The initial value to use for the NetworkList - public NetworkList(IEnumerable values) + public NetworkList(IEnumerable values = default, + NetworkVariableReadPermission readPerm = DefaultReadPerm, + NetworkVariableWritePermission writePerm = DefaultWritePerm) + : base(readPerm, writePerm) { foreach (var value in values) { m_List.Add(value); - } } diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index ec8cb50..e7c0ca6 100644 --- a/Runtime/NetworkVariable/NetworkVariable.cs +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -1,5 +1,7 @@ using UnityEngine; using System; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; namespace Unity.Netcode { @@ -22,7 +24,8 @@ internal static void ReadNetworkSerializable(FastBufferReader reader } // Functions that serialize other types - private static void WriteValue(FastBufferWriter writer, in TForMethod value) where TForMethod : unmanaged + private static void WriteValue(FastBufferWriter writer, in TForMethod value) + where TForMethod : unmanaged { writer.WriteValueSafe(value); } @@ -37,16 +40,13 @@ private static void ReadValue(FastBufferReader reader, out TForMetho internal delegate void ReadDelegate(FastBufferReader reader, out TForMethod value); - // These static delegates provide the right implementation for writing and reading a particular network variable - // type. - // + // These static delegates provide the right implementation for writing and reading a particular network variable type. // For most types, these default to WriteValue() and ReadValue(), which perform simple memcpy operations. // // INetworkSerializableILPP will generate startup code that will set it to WriteNetworkSerializable() // and ReadNetworkSerializable() for INetworkSerializable types, which will call NetworkSerialize(). // - // In the future we may be able to use this to provide packing implementations for floats and integers to - // optimize bandwidth usage. + // In the future we may be able to use this to provide packing implementations for floats and integers to optimize bandwidth usage. // // The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this, // NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable, @@ -69,38 +69,11 @@ private static void ReadValue(FastBufferReader reader, out TForMetho /// public OnValueChangedDelegate OnValueChanged; - /// - /// Creates a NetworkVariable with the default value and custom read permission - /// - /// The read permission for the NetworkVariable - public NetworkVariable() - { - } - - /// - /// Creates a NetworkVariable with the default value and custom read permission - /// - /// The read permission for the NetworkVariable - public NetworkVariable(NetworkVariableReadPermission readPerm) : base(readPerm) - { - } - - /// - /// Creates a NetworkVariable with a custom value and custom settings - /// - /// The read permission for the NetworkVariable - /// The initial value to use for the NetworkVariable - public NetworkVariable(NetworkVariableReadPermission readPerm, T value) : base(readPerm) - { - m_InternalValue = value; - } - - /// - /// Creates a NetworkVariable with a custom value and the default read permission - /// - /// The initial value to use for the NetworkVariable - public NetworkVariable(T value) + public NetworkVariable(T value = default, + NetworkVariableReadPermission readPerm = DefaultReadPerm, + NetworkVariableWritePermission writePerm = DefaultWritePerm) + : base(readPerm, writePerm) { m_InternalValue = value; } @@ -116,19 +89,36 @@ public virtual T Value get => m_InternalValue; set { - // this could be improved. The Networking Manager is not always initialized here - // Good place to decouple network manager from the network variable + // Compare bitwise + if (ValueEquals(ref m_InternalValue, ref value)) + { + return; + } - // Also, note this is not really very water-tight, if you are running as a host - // we cannot tell if a NetworkVariable write is happening inside client-ish code - if (m_NetworkBehaviour && (m_NetworkBehaviour.NetworkManager.IsClient && !m_NetworkBehaviour.NetworkManager.IsHost)) + if (m_NetworkBehaviour && !CanClientWrite(m_NetworkBehaviour.NetworkManager.LocalClientId)) { - throw new InvalidOperationException("Client can't write to NetworkVariables"); + throw new InvalidOperationException("Client is not allowed to write to this NetworkVariable"); } + Set(value); } } + // Compares two values of the same unmanaged type by underlying memory + // Ignoring any overriden value checks + // Size is fixed + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe bool ValueEquals(ref T a, ref T b) + { + // get unmanaged pointers + var aptr = UnsafeUtility.AddressOf(ref a); + var bptr = UnsafeUtility.AddressOf(ref b); + + // compare addresses + return UnsafeUtility.MemCmp(aptr, bptr, sizeof(T)) == 0; + } + + private protected void Set(T value) { m_IsDirty = true; @@ -146,7 +136,6 @@ public override void WriteDelta(FastBufferWriter writer) WriteField(writer); } - /// /// Reads value from the reader and applies it /// diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs index f3db645..e383c31 100644 --- a/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -19,9 +19,15 @@ public void Initialize(NetworkBehaviour networkBehaviour) m_NetworkBehaviour = networkBehaviour; } - protected NetworkVariableBase(NetworkVariableReadPermission readPermIn = NetworkVariableReadPermission.Everyone) + public const NetworkVariableReadPermission DefaultReadPerm = NetworkVariableReadPermission.Everyone; + public const NetworkVariableWritePermission DefaultWritePerm = NetworkVariableWritePermission.Server; + + protected NetworkVariableBase( + NetworkVariableReadPermission readPerm = DefaultReadPerm, + NetworkVariableWritePermission writePerm = DefaultWritePerm) { - ReadPerm = readPermIn; + ReadPerm = readPerm; + WritePerm = writePerm; } private protected bool m_IsDirty; @@ -37,6 +43,8 @@ protected NetworkVariableBase(NetworkVariableReadPermission readPermIn = Network /// public readonly NetworkVariableReadPermission ReadPerm; + public readonly NetworkVariableWritePermission WritePerm; + /// /// Sets whether or not the variable needs to be delta synced /// @@ -62,26 +70,28 @@ public virtual bool IsDirty() return m_IsDirty; } - public virtual bool ShouldWrite(ulong clientId, bool isServer) - { - return IsDirty() && isServer && CanClientRead(clientId); - } - - /// - /// Gets Whether or not a specific client can read to the varaible - /// - /// The clientId of the remote client - /// Whether or not the client can read to the variable public bool CanClientRead(ulong clientId) { switch (ReadPerm) { + default: case NetworkVariableReadPermission.Everyone: return true; - case NetworkVariableReadPermission.OwnerOnly: - return m_NetworkBehaviour.OwnerClientId == clientId; + case NetworkVariableReadPermission.Owner: + return clientId == m_NetworkBehaviour.NetworkObject.OwnerClientId; + } + } + + public bool CanClientWrite(ulong clientId) + { + switch (WritePerm) + { + default: + case NetworkVariableWritePermission.Server: + return clientId == NetworkManager.ServerClientId; + case NetworkVariableWritePermission.Owner: + return clientId == m_NetworkBehaviour.NetworkObject.OwnerClientId; } - return true; } /// @@ -107,7 +117,6 @@ public bool CanClientRead(ulong clientId) /// /// The stream to read the delta from /// Whether or not the delta should be kept as dirty or consumed - public abstract void ReadDelta(FastBufferReader reader, bool keepDirtyDelta); public virtual void Dispose() diff --git a/Runtime/NetworkVariable/NetworkVariablePermission.cs b/Runtime/NetworkVariable/NetworkVariablePermission.cs index df2456f..bd9daa1 100644 --- a/Runtime/NetworkVariable/NetworkVariablePermission.cs +++ b/Runtime/NetworkVariable/NetworkVariablePermission.cs @@ -1,18 +1,14 @@ namespace Unity.Netcode { - /// - /// Permission type - /// public enum NetworkVariableReadPermission { - /// - /// Everyone - /// Everyone, + Owner, + } - /// - /// Owner-ownly - /// - OwnerOnly, + public enum NetworkVariableWritePermission + { + Server, + Owner } } diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs index 9d692a8..17c8e53 100644 --- a/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/Runtime/SceneManagement/NetworkSceneManager.cs @@ -132,7 +132,7 @@ public class NetworkSceneManager : IDisposable private const NetworkDelivery k_DeliveryType = NetworkDelivery.ReliableFragmentedSequenced; internal const int InvalidSceneNameOrPath = -1; - // Used to be able to turn re-synchronization off for future snapshot development purposes. + // Used to be able to turn re-synchronization off internal static bool DisableReSynchronization; /// @@ -488,8 +488,18 @@ internal void GenerateScenesInBuild() var scenePath = SceneUtility.GetScenePathByBuildIndex(i); var hash = XXHash.Hash32(scenePath); var buildIndex = SceneUtility.GetBuildIndexByScenePath(scenePath); - HashToBuildIndex.Add(hash, buildIndex); - BuildIndexToHash.Add(buildIndex, hash); + + // In the rare-case scenario where a programmatically generated build has duplicate + // scene entries, we will log an error and skip the entry + if (!HashToBuildIndex.ContainsKey(hash)) + { + HashToBuildIndex.Add(hash, buildIndex); + BuildIndexToHash.Add(buildIndex, hash); + } + else + { + Debug.LogError($"{nameof(NetworkSceneManager)} is skipping duplicate scene path entry {scenePath}. Make sure your scenes in build list does not contain duplicates!"); + } } } @@ -520,7 +530,8 @@ internal string ScenePathFromHash(uint sceneHash) } else { - throw new Exception($"Scene Hash {sceneHash} does not exist in the {nameof(HashToBuildIndex)} table!"); + throw new Exception($"Scene Hash {sceneHash} does not exist in the {nameof(HashToBuildIndex)} table! Verify that all scenes requiring" + + $" server to client synchronization are in the scenes in build list."); } } @@ -876,7 +887,7 @@ private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress { SceneEventType = sceneEventProgress.SceneEventType, SceneName = SceneNameFromHash(sceneEventProgress.SceneHash), - ClientId = m_NetworkManager.ServerClientId, + ClientId = NetworkManager.ServerClientId, LoadSceneMode = sceneEventProgress.LoadSceneMode, ClientsThatCompleted = sceneEventProgress.DoneClients, ClientsThatTimedOut = m_NetworkManager.ConnectedClients.Keys.Except(sceneEventProgress.DoneClients).ToList(), @@ -947,10 +958,10 @@ public SceneEventProgressStatus UnloadScene(Scene scene) SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = sceneName, - ClientId = m_NetworkManager.ServerClientId // Server can only invoke this + ClientId = NetworkManager.ServerClientId // Server can only invoke this }); - OnUnload?.Invoke(m_NetworkManager.ServerClientId, sceneName, sceneUnload); + OnUnload?.Invoke(NetworkManager.ServerClientId, sceneName, sceneUnload); //Return the status return sceneEventProgress.Status; @@ -1017,12 +1028,12 @@ private void OnSceneUnloaded(uint sceneEventId) // Server sends the unload scene notification after unloading because it will despawn all scene relative in-scene NetworkObjects // If we send this event to all clients before the server is finished unloading they will get warning about an object being // despawned that no longer exists - SendSceneEventData(sceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != m_NetworkManager.ServerClientId).ToArray()); + SendSceneEventData(sceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != NetworkManager.ServerClientId).ToArray()); //Only if we are a host do we want register having loaded for the associated SceneEventProgress if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { - SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId); + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(NetworkManager.ServerClientId); } } @@ -1035,7 +1046,7 @@ private void OnSceneUnloaded(uint sceneEventId) SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = m_NetworkManager.IsServer ? m_NetworkManager.ServerClientId : m_NetworkManager.LocalClientId + ClientId = m_NetworkManager.IsServer ? NetworkManager.ServerClientId : m_NetworkManager.LocalClientId }); OnUnloadComplete?.Invoke(m_NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash)); @@ -1043,7 +1054,7 @@ private void OnSceneUnloaded(uint sceneEventId) // Clients send a notification back to the server they have completed the unload scene event if (!m_NetworkManager.IsServer) { - SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId }); + SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); } EndSceneEvent(sceneEventId); @@ -1079,7 +1090,7 @@ internal void UnloadAdditivelyLoadedScenes(uint sceneEventId) SceneEventType = SceneEventType.Unload, SceneName = keyHandleEntry.Value.name, LoadSceneMode = LoadSceneMode.Additive, // The only scenes unloaded are scenes that were additively loaded - ClientId = m_NetworkManager.ServerClientId + ClientId = NetworkManager.ServerClientId }); } } @@ -1147,10 +1158,10 @@ public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSc SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = sceneName, - ClientId = m_NetworkManager.ServerClientId + ClientId = NetworkManager.ServerClientId }); - OnLoad?.Invoke(m_NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); + OnLoad?.Invoke(NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); //Return our scene progress instance return sceneEventProgress.Status; @@ -1187,7 +1198,6 @@ private void OnClientSceneLoadingEvent(uint sceneEventId) // When it is set: Just before starting the asynchronous loading call // When it is unset: After the scene has loaded, the PopulateScenePlacedObjects is called, and all NetworkObjects in the do // not destroy temporary scene are moved into the active scene - // TODO: When Snapshot scene spawning is enabled this needs to be removed. if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) { IsSpawnedObjectsPendingInDontDestroyOnLoad = true; @@ -1278,7 +1288,9 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) { if (!keyValuePairBySceneHandle.Value.IsPlayerObject) { - m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value, m_NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, null, true); + // All in-scene placed NetworkObjects default to being owned by the server + m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value, + m_NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, NetworkManager.ServerClientId, true); } } } @@ -1290,7 +1302,7 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) for (int j = 0; j < m_NetworkManager.ConnectedClientsList.Count; j++) { var clientId = m_NetworkManager.ConnectedClientsList[j].ClientId; - if (clientId != m_NetworkManager.ServerClientId) + if (clientId != NetworkManager.ServerClientId) { sceneEventData.TargetClientId = clientId; var message = new SceneEventMessage @@ -1309,16 +1321,16 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) SceneEventType = SceneEventType.LoadComplete, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = m_NetworkManager.ServerClientId, + ClientId = NetworkManager.ServerClientId, Scene = scene, }); - OnLoadComplete?.Invoke(m_NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); + OnLoadComplete?.Invoke(NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); //Second, only if we are a host do we want register having loaded for the associated SceneEventProgress if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { - SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId); + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(NetworkManager.ServerClientId); } EndSceneEvent(sceneEventId); } @@ -1333,7 +1345,7 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene) sceneEventData.DeserializeScenePlacedObjects(); sceneEventData.SceneEventType = SceneEventType.LoadComplete; - SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId }); + SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); m_IsSceneEventActive = false; // Notify local client that the scene was loaded @@ -1544,9 +1556,9 @@ private void ClientLoadedSynchronization(uint sceneEventId) { EventData = responseSceneEventData }; - var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, m_NetworkManager.ServerClientId); + var size = m_NetworkManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId); - m_NetworkManager.NetworkMetrics.TrackSceneEventSent(m_NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size); + m_NetworkManager.NetworkMetrics.TrackSceneEventSent(NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size); EndSceneEvent(responseSceneEventData.SceneEventId); @@ -1600,7 +1612,7 @@ private void HandleClientSceneEvent(uint sceneEventId) sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager); sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete; - SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId }); + SendSceneEventData(sceneEventId, new ulong[] { NetworkManager.ServerClientId }); // All scenes are synchronized, let the server know we are done synchronizing m_NetworkManager.IsConnectedClient = true; @@ -1627,7 +1639,7 @@ private void HandleClientSceneEvent(uint sceneEventId) OnSceneEvent?.Invoke(new SceneEvent() { SceneEventType = sceneEventData.SceneEventType, - ClientId = m_NetworkManager.ServerClientId, // Server sent this to client + ClientId = NetworkManager.ServerClientId, // Server sent this to client }); EndSceneEvent(sceneEventId); @@ -1642,7 +1654,7 @@ private void HandleClientSceneEvent(uint sceneEventId) SceneEventType = sceneEventData.SceneEventType, LoadSceneMode = sceneEventData.LoadSceneMode, SceneName = SceneNameFromHash(sceneEventData.SceneHash), - ClientId = m_NetworkManager.ServerClientId, + ClientId = NetworkManager.ServerClientId, ClientsThatCompleted = sceneEventData.ClientsCompleted, ClientsThatTimedOut = sceneEventData.ClientsTimedOut, }); @@ -1734,8 +1746,6 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId) // NetworkObjects m_NetworkManager.InvokeOnClientConnectedCallback(clientId); - // TODO: This check and associated code can be removed once we determine all - // snapshot destroy messages are being updated until the server receives ACKs if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization) { sceneEventData.SceneEventType = SceneEventType.ReSynchronize; @@ -1830,7 +1840,7 @@ internal void MoveObjectsToDontDestroyOnLoad() /// Using the local scene relative Scene.handle as a sub-key to the root dictionary allows us to /// distinguish between duplicate in-scene placed NetworkObjects /// - private void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePlacedObjects = true) + internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePlacedObjects = true) { if (clearScenePlacedObjects) { @@ -1845,25 +1855,26 @@ private void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePl // at the end of scene loading we use this list to soft synchronize all in-scene placed NetworkObjects foreach (var networkObjectInstance in networkObjects) { - // We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (additive scenes) - if (networkObjectInstance.IsSceneObject == null && networkObjectInstance.NetworkManager == m_NetworkManager && networkObjectInstance.gameObject.scene == sceneToFilterBy && - networkObjectInstance.gameObject.scene.handle == sceneToFilterBy.handle) + var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash; + var sceneHandle = networkObjectInstance.gameObject.scene.handle; + // We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes) + if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && networkObjectInstance.gameObject.scene == sceneToFilterBy && + sceneHandle == sceneToFilterBy.handle) { - if (!ScenePlacedObjects.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash)) { - ScenePlacedObjects.Add(networkObjectInstance.GlobalObjectIdHash, new Dictionary()); + ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); } - if (!ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash].ContainsKey(networkObjectInstance.gameObject.scene.handle)) + if (!ScenePlacedObjects[globalObjectIdHash].ContainsKey(sceneHandle)) { - ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash].Add(networkObjectInstance.gameObject.scene.handle, networkObjectInstance); + ScenePlacedObjects[globalObjectIdHash].Add(sceneHandle, networkObjectInstance); } else { - var exitingEntryName = ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash][networkObjectInstance.gameObject.scene.handle] != null ? - ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash][networkObjectInstance.gameObject.scene.handle].name : "Null Entry"; + var exitingEntryName = ScenePlacedObjects[globalObjectIdHash][sceneHandle] != null ? ScenePlacedObjects[globalObjectIdHash][sceneHandle].name : "Null Entry"; throw new Exception($"{networkObjectInstance.name} tried to registered with {nameof(ScenePlacedObjects)} which already contains " + - $"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {networkObjectInstance.GlobalObjectIdHash} for {exitingEntryName}!"); + $"the same {nameof(NetworkObject.GlobalObjectIdHash)} value {globalObjectIdHash} for {exitingEntryName}!"); } } } diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs index 0fe9142..65f2f03 100644 --- a/Runtime/SceneManagement/SceneEventData.cs +++ b/Runtime/SceneManagement/SceneEventData.cs @@ -41,7 +41,6 @@ public enum SceneEventType : byte /// Invocation: Server Side
/// Message Flow: Server to client
/// Event Notification: Both server and client receive a local notification
- /// Note: This will be removed once snapshot and buffered messages are finalized as it will no longer be needed at that point. ///
ReSynchronize, /// @@ -592,9 +591,6 @@ internal void ReadClientReSynchronizationData(FastBufferReader reader) networkObject.IsSpawned = false; if (m_NetworkManager.PrefabHandler.ContainsHandler(networkObject)) { - // Since this is the client side and we have missed the delete message, until the Snapshot system is in place for spawn and despawn handling - // we have to remove this from the list of spawned objects manually or when a NetworkObjectId is recycled the client will throw an error - // about the id already being assigned. if (m_NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId)) { m_NetworkManager.SpawnManager.SpawnedObjects.Remove(networkObjectId); diff --git a/Runtime/Serialization/BytePacker.cs b/Runtime/Serialization/BytePacker.cs index 3a50f7a..c017592 100644 --- a/Runtime/Serialization/BytePacker.cs +++ b/Runtime/Serialization/BytePacker.cs @@ -10,7 +10,7 @@ namespace Unity.Netcode public static class BytePacker { #if UNITY_NETCODE_DEBUG_NO_PACKING - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteValuePacked(FastBufferWriter writer, T value) where T: unmanaged => writer.WriteValueSafe(value); #else @@ -277,10 +277,21 @@ public static void WriteValuePacked(FastBufferWriter writer, string s) #if UNITY_NETCODE_DEBUG_NO_PACKING - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteValueBitPacked(FastBufferWriter writer, T value) where T: unmanaged => writer.WriteValueSafe(value); #else + + public const ushort BitPackedUshortMax = (1 << 15) - 1; + public const short BitPackedShortMax = (1 << 14) - 1; + public const short BitPackedShortMin = -(1 << 14); + public const uint BitPackedUintMax = (1 << 30) - 1; + public const int BitPackedIntMax = (1 << 29) - 1; + public const int BitPackedIntMin = -(1 << 29); + public const ulong BitPackedULongMax = (1L << 61) - 1; + public const long BitPackedLongMax = (1L << 60) - 1; + public const long BitPackedLongMin = -(1L << 60); + /// /// Writes a 14-bit signed short to the buffer in a bit-encoded packed format. /// The first bit indicates whether the value is 1 byte or 2. @@ -307,7 +318,7 @@ public static void WriteValuePacked(FastBufferWriter writer, string s) public static void WriteValueBitPacked(FastBufferWriter writer, ushort value) { #if DEVELOPMENT_BUILD || UNITY_EDITOR - if (value >= 0b1000_0000_0000_0000) + if (value >= BitPackedUshortMax) { throw new ArgumentException("BitPacked ushorts must be <= 15 bits"); } @@ -356,7 +367,7 @@ public static void WriteValueBitPacked(FastBufferWriter writer, ushort value) public static void WriteValueBitPacked(FastBufferWriter writer, uint value) { #if DEVELOPMENT_BUILD || UNITY_EDITOR - if (value >= 0b0100_0000_0000_0000_0000_0000_0000_0000) + if (value > BitPackedUintMax) { throw new ArgumentException("BitPacked uints must be <= 30 bits"); } @@ -396,7 +407,7 @@ public static void WriteValueBitPacked(FastBufferWriter writer, uint value) public static void WriteValueBitPacked(FastBufferWriter writer, ulong value) { #if DEVELOPMENT_BUILD || UNITY_EDITOR - if (value >= 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000) + if (value > BitPackedULongMax) { throw new ArgumentException("BitPacked ulongs must be <= 61 bits"); } diff --git a/Runtime/Serialization/FastBufferReader.cs b/Runtime/Serialization/FastBufferReader.cs index 443941d..e56d867 100644 --- a/Runtime/Serialization/FastBufferReader.cs +++ b/Runtime/Serialization/FastBufferReader.cs @@ -19,7 +19,7 @@ internal struct ReaderHandle #endif } - internal readonly unsafe ReaderHandle* Handle; + internal unsafe ReaderHandle* Handle; /// /// Get the current read position @@ -39,6 +39,11 @@ public unsafe int Length get => Handle->Length; } + /// + /// Gets a value indicating whether the reader has been initialized and a handle allocated. + /// + public unsafe bool IsInitialized => Handle != null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe void CommitBitwiseReads(int amount) { @@ -196,6 +201,7 @@ public unsafe FastBufferReader(FastBufferWriter writer, Allocator allocator, int public unsafe void Dispose() { UnsafeUtility.Free(Handle, Handle->Allocator); + Handle = null; } /// diff --git a/Runtime/Serialization/FastBufferWriter.cs b/Runtime/Serialization/FastBufferWriter.cs index 5875e92..5f8dd25 100644 --- a/Runtime/Serialization/FastBufferWriter.cs +++ b/Runtime/Serialization/FastBufferWriter.cs @@ -22,7 +22,7 @@ internal struct WriterHandle #endif } - internal readonly unsafe WriterHandle* Handle; + internal unsafe WriterHandle* Handle; private static byte[] s_ByteArrayCache = new byte[65535]; @@ -62,6 +62,11 @@ public unsafe int Length get => Handle->Position > Handle->Length ? Handle->Position : Handle->Length; } + /// + /// Gets a value indicating whether the writer has been initialized and a handle allocated. + /// + public unsafe bool IsInitialized => Handle != null; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe void CommitBitwiseWrites(int amount) @@ -111,6 +116,7 @@ public unsafe void Dispose() UnsafeUtility.Free(Handle->BufferPointer, Handle->Allocator); } UnsafeUtility.Free(Handle, Handle->Allocator); + Handle = null; } /// @@ -207,7 +213,7 @@ internal unsafe void Grow(int additionalSizeRequired) /// When you know you will be writing multiple fields back-to-back and you know the total size, /// you can call TryBeginWrite() once on the total size, and then follow it with calls to /// WriteValue() instead of WriteValueSafe() for faster serialization. - /// + /// /// Unsafe write operations will throw OverflowException in editor and development builds if you /// go past the point you've marked using TryBeginWrite(). In release builds, OverflowException will not be thrown /// for performance reasons, since the point of using TryBeginWrite is to avoid bounds checking in the following @@ -253,7 +259,7 @@ public unsafe bool TryBeginWrite(int bytes) /// When you know you will be writing multiple fields back-to-back and you know the total size, /// you can call TryBeginWrite() once on the total size, and then follow it with calls to /// WriteValue() instead of WriteValueSafe() for faster serialization. - /// + /// /// Unsafe write operations will throw OverflowException in editor and development builds if you /// go past the point you've marked using TryBeginWrite(). In release builds, OverflowException will not be thrown /// for performance reasons, since the point of using TryBeginWrite is to avoid bounds checking in the following diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index 2f0028f..fb6ae5b 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -21,6 +21,122 @@ public class NetworkSpawnManager /// public readonly HashSet SpawnedObjectsList = new HashSet(); + /// + /// Use to get all NetworkObjects owned by a client + /// Ownership to Objects Table Format: + /// [ClientId][NetworkObjectId][NetworkObject] + /// Server: Keeps track of all clients' ownership + /// Client: Keeps track of only its ownership + /// + public readonly Dictionary> OwnershipToObjectsTable = new Dictionary>(); + + /// + /// Object to Ownership Table: + /// [NetworkObjectId][ClientId] + /// Used internally to find the client Id that currently owns + /// the NetworkObject + /// + private Dictionary m_ObjectToOwnershipTable = new Dictionary(); + + /// + /// Used to update a NetworkObject's ownership + /// + internal void UpdateOwnershipTable(NetworkObject networkObject, ulong newOwner, bool isRemoving = false) + { + var previousOwner = newOwner; + + // Use internal lookup table to see if the NetworkObject has a previous owner + if (m_ObjectToOwnershipTable.ContainsKey(networkObject.NetworkObjectId)) + { + // Keep track of the previous owner's ClientId + previousOwner = m_ObjectToOwnershipTable[networkObject.NetworkObjectId]; + + // We are either despawning (remove) or changing ownership (assign) + if (isRemoving) + { + m_ObjectToOwnershipTable.Remove(networkObject.NetworkObjectId); + } + else + { + m_ObjectToOwnershipTable[networkObject.NetworkObjectId] = newOwner; + } + } + else + { + // Otherwise, just add a new lookup entry + m_ObjectToOwnershipTable.Add(networkObject.NetworkObjectId, newOwner); + } + + // Check to see if we had a previous owner + if (previousOwner != newOwner && OwnershipToObjectsTable.ContainsKey(previousOwner)) + { + // Before updating the previous owner, assure this entry exists + if (OwnershipToObjectsTable[previousOwner].ContainsKey(networkObject.NetworkObjectId)) + { + // Remove the previous owner's entry + OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); + + // Server or Host alway invokes the lost ownership notification locally + if (NetworkManager.IsServer) + { + networkObject.InvokeBehaviourOnLostOwnership(); + } + + // If we are removing the entry (i.e. despawning or client lost ownership) + if (isRemoving) + { + return; + } + } + else + { + // Really, as long as UpdateOwnershipTable is invoked when ownership is gained or lost this should never happen + throw new Exception($"Client-ID {previousOwner} had a partial {nameof(m_ObjectToOwnershipTable)} entry! Potentially corrupted {nameof(OwnershipToObjectsTable)}?"); + } + } + + // If the owner doesn't have an entry then create one + if (!OwnershipToObjectsTable.ContainsKey(newOwner)) + { + OwnershipToObjectsTable.Add(newOwner, new Dictionary()); + } + + // Sanity check to make sure we don't already have this entry (we shouldn't) + if (!OwnershipToObjectsTable[newOwner].ContainsKey(networkObject.NetworkObjectId)) + { + // Add the new ownership entry + OwnershipToObjectsTable[newOwner].Add(networkObject.NetworkObjectId, networkObject); + + // Server or Host always invokes the gained ownership notification locally + if (NetworkManager.IsServer) + { + networkObject.InvokeBehaviourOnGainedOwnership(); + } + } + else if (isRemoving) + { + OwnershipToObjectsTable[previousOwner].Remove(networkObject.NetworkObjectId); + } + else if (NetworkManager.LogLevel == LogLevel.Developer) + { + NetworkLog.LogWarning($"Setting ownership twice? Client-ID {previousOwner} already owns NetworkObject ID {networkObject.NetworkObjectId}!"); + } + } + + /// + /// Returns a list of all NetworkObjects that belong to a client. + /// + /// the client's id + public List GetClientOwnedObjects(ulong clientId) + { + if (!OwnershipToObjectsTable.ContainsKey(clientId)) + { + OwnershipToObjectsTable.Add(clientId, new Dictionary()); + } + return OwnershipToObjectsTable[clientId].Values.ToList(); + } + + private struct TriggerData { public FastBufferReader Reader; @@ -96,8 +212,7 @@ public NetworkObject GetPlayerNetworkObject(ulong clientId) /// /// Defers processing of a message until the moment a specific networkObjectId is spawned. /// This is to handle situations where an RPC or other object-specific message arrives before the spawn does, - /// either due to it being requested in OnNetworkSpawn before the spawn call has been executed, or with - /// snapshot spawns enabled where the spawn is sent unreliably and not until the end of the frame. + /// either due to it being requested in OnNetworkSpawn before the spawn call has been executed /// /// There is a one second maximum lifetime of triggers to avoid memory leaks. After one second has passed /// without the requested object ID being spawned, the triggers for it are automatically deleted. @@ -194,37 +309,21 @@ internal void RemoveOwnership(NetworkObject networkObject) return; } - // Make sure the connected client entry exists before trying to remove ownership. - if (TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient)) - { - for (int i = networkClient.OwnedObjects.Count - 1; i > -1; i--) - { - if (networkClient.OwnedObjects[i] == networkObject) - { - networkClient.OwnedObjects.RemoveAt(i); - } - } + // Server removes the entry and takes over ownership before notifying + UpdateOwnershipTable(networkObject, NetworkManager.ServerClientId, true); - networkObject.OwnerClientIdInternal = null; + networkObject.OwnerClientId = NetworkManager.ServerClientId; - var message = new ChangeOwnershipMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - OwnerClientId = networkObject.OwnerClientId - }; - var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds); + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId + }; + var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds); - foreach (var client in NetworkManager.ConnectedClients) - { - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); - } - } - else + foreach (var client in NetworkManager.ConnectedClients) { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) - { - NetworkLog.LogWarning($"No connected clients prior to removing ownership for {networkObject.name}. Make sure you are not initializing or shutting down when removing ownership."); - } + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); } } @@ -265,25 +364,10 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId) throw new SpawnStateException("Object is not spawned"); } - if (TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient)) - { - for (int i = networkClient.OwnedObjects.Count - 1; i >= 0; i--) - { - if (networkClient.OwnedObjects[i] == networkObject) - { - networkClient.OwnedObjects.RemoveAt(i); - } - } - - networkClient.OwnedObjects.Add(networkObject); - } - networkObject.OwnerClientId = clientId; - if (TryGetNetworkClient(clientId, out NetworkClient newNetworkClient)) - { - newNetworkClient.OwnedObjects.Add(networkObject); - } + // Server adds entries for all client ownership + UpdateOwnershipTable(networkObject, networkObject.OwnerClientId); var message = new ChangeOwnershipMessage { @@ -414,7 +498,7 @@ internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalO } // Ran on both server and client - internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong? ownerClientId, bool destroyWithScene) + internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) { if (networkObject == null) { @@ -452,15 +536,12 @@ internal void SpawnNetworkObjectLocally(NetworkObject networkObject, in NetworkO throw new SpawnStateException("Object is already spawned"); } - if (sceneObject.Header.HasNetworkVariables) - { - networkObject.SetNetworkVariableData(variableData); - } + networkObject.SetNetworkVariableData(variableData); SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.Header.NetworkObjectId, sceneObject.Header.IsSceneObject, sceneObject.Header.IsPlayerObject, sceneObject.Header.OwnerClientId, destroyWithScene); } - private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong? ownerClientId, bool destroyWithScene) + private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) { if (SpawnedObjects.ContainsKey(networkId)) { @@ -471,38 +552,45 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong // this initialization really should be at the bottom of the function networkObject.IsSpawned = true; - // this initialization really should be at the top of this function. If and when we break the + // this initialization really should be at the top of this function. If and when we break the // NetworkVariable dependency on NetworkBehaviour, this otherwise creates problems because // SetNetworkVariableData above calls InitializeVariables, and the 'baked out' data isn't ready there; - // the current design banks on getting the network behaviour set and then only reading from it - // after the below initialization code. However cowardice compels me to hold off on moving this until - // that commit + // the current design banks on getting the network behaviour set and then only reading from it after the + // below initialization code. However cowardice compels me to hold off on moving this until that commit networkObject.IsSceneObject = sceneObject; networkObject.NetworkObjectId = networkId; networkObject.DestroyWithScene = sceneObject || destroyWithScene; - networkObject.OwnerClientIdInternal = ownerClientId; + networkObject.OwnerClientId = ownerClientId; + networkObject.IsPlayerObject = playerObject; SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); SpawnedObjectsList.Add(networkObject); - if (ownerClientId != null) + if (NetworkManager.IsServer) { - if (NetworkManager.IsServer) + if (playerObject) { - if (playerObject) + // If there was an already existing player object for this player, then mark it as no longer + // a player object. + if (NetworkManager.ConnectedClients[ownerClientId].PlayerObject != null) { - NetworkManager.ConnectedClients[ownerClientId.Value].PlayerObject = networkObject; - } - else - { - NetworkManager.ConnectedClients[ownerClientId.Value].OwnedObjects.Add(networkObject); + NetworkManager.ConnectedClients[ownerClientId].PlayerObject.IsPlayerObject = false; } + NetworkManager.ConnectedClients[ownerClientId].PlayerObject = networkObject; } - else if (playerObject && ownerClientId.Value == NetworkManager.LocalClientId) + } + else if (ownerClientId == NetworkManager.LocalClientId) + { + if (playerObject) { + // If there was an already existing player object for this player, then mark it as no longer a player object. + if (NetworkManager.LocalClient.PlayerObject != null) + { + NetworkManager.LocalClient.PlayerObject.IsPlayerObject = false; + } NetworkManager.LocalClient.PlayerObject = networkObject; } } @@ -549,25 +637,21 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject) { - if (!NetworkManager.NetworkConfig.UseSnapshotSpawn) + //Currently, if this is called and the clientId (destination) is the server's client Id, this case will be checked + // within the below Send function. To avoid unwarranted allocation of a PooledNetworkBuffer placing this check here. [NSS] + if (NetworkManager.IsServer && clientId == NetworkManager.ServerClientId) { - //Currently, if this is called and the clientId (destination) is the server's client Id, this case - //will be checked within the below Send function. To avoid unwarranted allocation of a PooledNetworkBuffer - //placing this check here. [NSS] - if (NetworkManager.IsServer && clientId == NetworkManager.ServerClientId) - { - return; - } + return; + } - var message = new CreateObjectMessage - { - ObjectInfo = networkObject.GetMessageSceneObject(clientId) - }; - var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); - NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); + var message = new CreateObjectMessage + { + ObjectInfo = networkObject.GetMessageSceneObject(clientId) + }; + var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); + NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); - networkObject.MarkVariablesDirty(); - } + networkObject.MarkVariablesDirty(); } internal ulong? GetSpawnParentId(NetworkObject networkObject) @@ -605,14 +689,12 @@ internal void DespawnObject(NetworkObject networkObject, bool destroyObject = fa // Makes scene objects ready to be reused internal void ServerResetShudownStateForSceneObjects() { - foreach (var sobj in SpawnedObjectsList) + var networkObjects = UnityEngine.Object.FindObjectsOfType().Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); + foreach (var sobj in networkObjects) { - if ((sobj.IsSceneObject != null && sobj.IsSceneObject == true) || sobj.DestroyWithScene) - { - sobj.IsSpawned = false; - sobj.DestroyWithScene = false; - sobj.IsSceneObject = null; - } + sobj.IsSpawned = false; + sobj.DestroyWithScene = false; + sobj.IsSceneObject = null; } } @@ -653,14 +735,12 @@ internal void DespawnAndDestroyNetworkObjects() else if (networkObjects[i].IsSpawned) { // If it is an in-scene placed NetworkObject then just despawn - // and let it be destroyed when the scene is unloaded. Otherwise, - // despawn and destroy it. - var shouldDestroy = !(networkObjects[i].IsSceneObject != null - && networkObjects[i].IsSceneObject.Value); + // and let it be destroyed when the scene is unloaded. Otherwise, despawn and destroy it. + var shouldDestroy = !(networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value); OnDespawnObject(networkObjects[i], shouldDestroy); } - else + else if (networkObjects[i].IsSceneObject != null && !networkObjects[i].IsSceneObject.Value) { UnityEngine.Object.Destroy(networkObjects[i].gameObject); } @@ -711,9 +791,10 @@ internal void ServerSpawnSceneObjectsOnStartSweep() } } + foreach (var networkObject in networkObjectsToSpawn) { - SpawnNetworkObjectLocally(networkObject, GetNetworkObjectId(), true, false, null, true); + SpawnNetworkObjectLocally(networkObject, GetNetworkObjectId(), true, false, networkObject.OwnerClientId, true); } } @@ -757,18 +838,6 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec } } - if (!networkObject.IsOwnedByServer && !networkObject.IsPlayerObject && TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient)) - { - //Someone owns it. - for (int i = networkClient.OwnedObjects.Count - 1; i > -1; i--) - { - if (networkClient.OwnedObjects[i].NetworkObjectId == networkObject.NetworkObjectId) - { - networkClient.OwnedObjects.RemoveAt(i); - } - } - } - networkObject.InvokeBehaviourNetworkDespawn(); if (NetworkManager != null && NetworkManager.IsServer) @@ -782,38 +851,31 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec }); } - if (NetworkManager.NetworkConfig.UseSnapshotSpawn) - { - networkObject.SnapshotDespawn(); - } - else + if (networkObject != null) { - if (networkObject != null) + // As long as we have any remaining clients, then notify of the object being destroy. + if (NetworkManager.ConnectedClientsList.Count > 0) { - // As long as we have any remaining clients, then notify of the object being destroy. - if (NetworkManager.ConnectedClientsList.Count > 0) - { - m_TargetClientIds.Clear(); + m_TargetClientIds.Clear(); - // We keep only the client for which the object is visible - // as the other clients have them already despawned - foreach (var clientId in NetworkManager.ConnectedClientsIds) + // We keep only the client for which the object is visible + // as the other clients have them already despawned + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (networkObject.IsNetworkVisibleTo(clientId)) { - if (networkObject.IsNetworkVisibleTo(clientId)) - { - m_TargetClientIds.Add(clientId); - } + m_TargetClientIds.Add(clientId); } + } - var message = new DestroyObjectMessage - { - NetworkObjectId = networkObject.NetworkObjectId - }; - var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, m_TargetClientIds); - foreach (var targetClientId in m_TargetClientIds) - { - NetworkManager.NetworkMetrics.TrackObjectDestroySent(targetClientId, networkObject, size); - } + var message = new DestroyObjectMessage + { + NetworkObjectId = networkObject.NetworkObjectId + }; + var size = NetworkManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, m_TargetClientIds); + foreach (var targetClientId in m_TargetClientIds) + { + NetworkManager.NetworkMetrics.TrackObjectDestroySent(targetClientId, networkObject, size); } } } diff --git a/Runtime/Transports/UNET/UNetChannel.cs b/Runtime/Transports/UNET/UNetChannel.cs deleted file mode 100644 index 358ac51..0000000 --- a/Runtime/Transports/UNET/UNetChannel.cs +++ /dev/null @@ -1,54 +0,0 @@ -#if UNITY_UNET_PRESENT -using System; -#if UNITY_EDITOR -using UnityEditor; -#endif -using UnityEngine; -using UnityEngine.Networking; - -namespace Unity.Netcode.Transports.UNET -{ - /// - /// A transport channel used by the netcode - /// - [Serializable] - public class UNetChannel - { - /// - /// The name of the channel - /// -#if UNITY_EDITOR - [ReadOnly] -#endif - public byte Id; - - /// - /// The type of channel - /// - public QosType Type; - -#if UNITY_EDITOR - private class ReadOnlyAttribute : PropertyAttribute { } - - [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] - private class ReadOnlyDrawer : PropertyDrawer - { - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - // Saving previous GUI enabled value - var previousGUIState = GUI.enabled; - - // Disabling edit for property - GUI.enabled = false; - - // Drawing Property - EditorGUI.PropertyField(position, property, label); - - // Setting old GUI enabled value - GUI.enabled = previousGUIState; - } - } -#endif - } -} -#endif diff --git a/Runtime/Transports/UNET/UNetChannel.cs.meta b/Runtime/Transports/UNET/UNetChannel.cs.meta deleted file mode 100644 index 9605368..0000000 --- a/Runtime/Transports/UNET/UNetChannel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e864534da30ef604992c0ed33c75d3c6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Transports/UTP.meta b/Runtime/Transports/UTP.meta new file mode 100644 index 0000000..7fe18e1 --- /dev/null +++ b/Runtime/Transports/UTP.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 81887adf6d9ca40c9b70728b7018b6f5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/UTP/BatchedReceiveQueue.cs b/Runtime/Transports/UTP/BatchedReceiveQueue.cs new file mode 100644 index 0000000..c6a3f55 --- /dev/null +++ b/Runtime/Transports/UTP/BatchedReceiveQueue.cs @@ -0,0 +1,96 @@ +using System; +using Unity.Networking.Transport; + +namespace Unity.Netcode.Transports.UTP +{ + /// Queue for batched messages received through UTP. + /// This is meant as a companion to . + internal class BatchedReceiveQueue + { + private byte[] m_Data; + private int m_Offset; + private int m_Length; + + public bool IsEmpty => m_Length <= 0; + + /// + /// Construct a new receive queue from a returned by + /// when popping a data event. + /// + /// The to construct from. + public BatchedReceiveQueue(DataStreamReader reader) + { + m_Data = new byte[reader.Length]; + unsafe + { + fixed (byte* dataPtr = m_Data) + { + reader.ReadBytes(dataPtr, reader.Length); + } + } + + m_Offset = 0; + m_Length = reader.Length; + } + + /// + /// Push the entire data from a (as returned by popping an + /// event from a ) to the queue. + /// + /// The to push the data of. + public void PushReader(DataStreamReader reader) + { + // Resize the array and copy the existing data to the beginning if there's not enough + // room to copy the reader's data at the end of the existing data. + var available = m_Data.Length - (m_Offset + m_Length); + if (available < reader.Length) + { + if (m_Length > 0) + { + Array.Copy(m_Data, m_Offset, m_Data, 0, m_Length); + } + + m_Offset = 0; + + while (m_Data.Length - m_Length < reader.Length) + { + Array.Resize(ref m_Data, m_Data.Length * 2); + } + } + + unsafe + { + fixed (byte* dataPtr = m_Data) + { + reader.ReadBytes(dataPtr + m_Offset + m_Length, reader.Length); + } + } + + m_Length += reader.Length; + } + + /// Pop the next full message in the queue. + /// The message, or the default value if no more full messages. + public ArraySegment PopMessage() + { + if (m_Length < sizeof(int)) + { + return default; + } + + var messageLength = BitConverter.ToInt32(m_Data, m_Offset); + + if (m_Length - sizeof(int) < messageLength) + { + return default; + } + + var data = new ArraySegment(m_Data, m_Offset + sizeof(int), messageLength); + + m_Offset += sizeof(int) + messageLength; + m_Length -= sizeof(int) + messageLength; + + return data; + } + } +} diff --git a/Editor/NetworkAnimatorEditor.cs.meta b/Runtime/Transports/UTP/BatchedReceiveQueue.cs.meta similarity index 83% rename from Editor/NetworkAnimatorEditor.cs.meta rename to Runtime/Transports/UTP/BatchedReceiveQueue.cs.meta index a675853..d91b5eb 100644 --- a/Editor/NetworkAnimatorEditor.cs.meta +++ b/Runtime/Transports/UTP/BatchedReceiveQueue.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a32aeecf69a2542469927066f5b88005 +guid: e9ead10b891184bd5b8f2650fd66a5b1 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Transports/UTP/BatchedSendQueue.cs b/Runtime/Transports/UTP/BatchedSendQueue.cs new file mode 100644 index 0000000..e217e37 --- /dev/null +++ b/Runtime/Transports/UTP/BatchedSendQueue.cs @@ -0,0 +1,233 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Networking.Transport; + +namespace Unity.Netcode.Transports.UTP +{ + /// Queue for batched messages meant to be sent through UTP. + /// + /// Messages should be pushed on the queue with . To send batched + /// messages, call with the obtained from + /// . This will fill the writer with as many messages as + /// possible. If the send is successful, call to remove the data from the + /// queue. + /// + /// This is meant as a companion to , which should be used to + /// read messages sent with this queue. + /// + internal struct BatchedSendQueue : IDisposable + { + private NativeArray m_Data; + private NativeArray m_HeadTailIndices; + + /// Overhead that is added to each message in the queue. + public const int PerMessageOverhead = sizeof(int); + + // Indices into m_HeadTailIndicies. + private const int k_HeadInternalIndex = 0; + private const int k_TailInternalIndex = 1; + + /// Index of the first byte of the oldest data in the queue. + private int HeadIndex + { + get { return m_HeadTailIndices[k_HeadInternalIndex]; } + set { m_HeadTailIndices[k_HeadInternalIndex] = value; } + } + + /// Index one past the last byte of the most recent data in the queue. + private int TailIndex + { + get { return m_HeadTailIndices[k_TailInternalIndex]; } + set { m_HeadTailIndices[k_TailInternalIndex] = value; } + } + + public int Length => TailIndex - HeadIndex; + + public bool IsEmpty => HeadIndex == TailIndex; + + public bool IsCreated => m_Data.IsCreated; + + /// Construct a new empty send queue. + /// Maximum capacity of the send queue. + public BatchedSendQueue(int capacity) + { + m_Data = new NativeArray(capacity, Allocator.Persistent); + m_HeadTailIndices = new NativeArray(2, Allocator.Persistent); + + HeadIndex = 0; + TailIndex = 0; + } + + public void Dispose() + { + if (IsCreated) + { + m_Data.Dispose(); + m_HeadTailIndices.Dispose(); + } + } + + /// Append data at the tail of the queue. No safety checks. + private void AppendDataAtTail(ArraySegment data) + { + unsafe + { + var writer = new DataStreamWriter((byte*)m_Data.GetUnsafePtr() + TailIndex, m_Data.Length - TailIndex); + + writer.WriteInt(data.Count); + + fixed (byte* dataPtr = data.Array) + { + writer.WriteBytes(dataPtr + data.Offset, data.Count); + } + } + + TailIndex += sizeof(int) + data.Count; + } + + /// Append a new message to the queue. + /// Message to append to the queue. + /// + /// Whether the message was appended successfully. The only way it can fail is if there's + /// no more room in the queue. On failure, nothing is written to the queue. + /// + public bool PushMessage(ArraySegment message) + { + if (!IsCreated) + { + return false; + } + + // Check if there's enough room after the current tail index. + if (m_Data.Length - TailIndex >= sizeof(int) + message.Count) + { + AppendDataAtTail(message); + return true; + } + + // Check if there would be enough room if we moved data at the beginning of m_Data. + if (m_Data.Length - TailIndex + HeadIndex >= sizeof(int) + message.Count) + { + // Move the data back at the beginning of m_Data. + unsafe + { + UnsafeUtility.MemMove(m_Data.GetUnsafePtr(), (byte*)m_Data.GetUnsafePtr() + HeadIndex, Length); + } + + TailIndex = Length; + HeadIndex = 0; + + AppendDataAtTail(message); + return true; + } + + return false; + } + + /// + /// Fill as much of a as possible with data from the head of + /// the queue. Only full messages (and their length) are written to the writer. + /// + /// + /// This does NOT actually consume anything from the queue. That is, calling this method + /// does not reduce the length of the queue. Callers are expected to call + /// with the value returned by this method afterwards if the data can + /// be safely removed from the queue (e.g. if it was sent successfully). + /// + /// This method should not be used together with since this + /// could lead to a corrupted queue. + /// + /// The to write to. + /// How many bytes were written to the writer. + public int FillWriterWithMessages(ref DataStreamWriter writer) + { + if (!IsCreated || Length == 0) + { + return 0; + } + + unsafe + { + var reader = new DataStreamReader((byte*)m_Data.GetUnsafePtr() + HeadIndex, Length); + + var writerAvailable = writer.Capacity; + var readerOffset = 0; + + while (readerOffset < Length) + { + reader.SeekSet(readerOffset); + var messageLength = reader.ReadInt(); + + if (writerAvailable < sizeof(int) + messageLength) + { + break; + } + else + { + writer.WriteInt(messageLength); + + var messageOffset = HeadIndex + reader.GetBytesRead(); + writer.WriteBytes((byte*)m_Data.GetUnsafePtr() + messageOffset, messageLength); + + writerAvailable -= sizeof(int) + messageLength; + readerOffset += sizeof(int) + messageLength; + } + } + + return writer.Capacity - writerAvailable; + } + } + + /// + /// Fill the given with as many bytes from the queue as + /// possible, disregarding message boundaries. + /// + /// + /// This does NOT actually consume anything from the queue. That is, calling this method + /// does not reduce the length of the queue. Callers are expected to call + /// with the value returned by this method afterwards if the data can + /// be safely removed from the queue (e.g. if it was sent successfully). + /// + /// This method should not be used together with since + /// this could lead to reading messages from a corrupted queue. + /// + /// The to write to. + /// How many bytes were written to the writer. + public int FillWriterWithBytes(ref DataStreamWriter writer) + { + if (!IsCreated || Length == 0) + { + return 0; + } + + var copyLength = Math.Min(writer.Capacity, Length); + + unsafe + { + writer.WriteBytes((byte*)m_Data.GetUnsafePtr() + HeadIndex, copyLength); + } + + return copyLength; + } + + /// Consume a number of bytes from the head of the queue. + /// + /// This should only be called with a size that matches the last value returned by + /// . Anything else will result in a corrupted queue. + /// + /// Number of bytes to consume from the queue. + public void Consume(int size) + { + if (size >= Length) + { + HeadIndex = 0; + TailIndex = 0; + } + else + { + HeadIndex += size; + } + } + } +} diff --git a/Runtime/Core/SnapshotSystem.cs.meta b/Runtime/Transports/UTP/BatchedSendQueue.cs.meta similarity index 83% rename from Runtime/Core/SnapshotSystem.cs.meta rename to Runtime/Transports/UTP/BatchedSendQueue.cs.meta index 27ffd35..97da0be 100644 --- a/Runtime/Core/SnapshotSystem.cs.meta +++ b/Runtime/Transports/UTP/BatchedSendQueue.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c275febadb27c4d18b41218e3353b84b +guid: ddf8f97f695d740f297dc42242b76b8c MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Transports/UTP/NetworkMetricsContext.cs b/Runtime/Transports/UTP/NetworkMetricsContext.cs new file mode 100644 index 0000000..c871f63 --- /dev/null +++ b/Runtime/Transports/UTP/NetworkMetricsContext.cs @@ -0,0 +1,8 @@ +namespace Unity.Netcode.Transports.UTP +{ + public struct NetworkMetricsContext + { + public uint PacketSentCount; + public uint PacketReceivedCount; + } +} diff --git a/Runtime/Core/IndexAllocator.cs.meta b/Runtime/Transports/UTP/NetworkMetricsContext.cs.meta similarity index 83% rename from Runtime/Core/IndexAllocator.cs.meta rename to Runtime/Transports/UTP/NetworkMetricsContext.cs.meta index b7c7632..7fa6423 100644 --- a/Runtime/Core/IndexAllocator.cs.meta +++ b/Runtime/Transports/UTP/NetworkMetricsContext.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: bd9e1475e8c8e4a6d935fe2409e3bd26 +guid: adb0270501ff1421896ce15cc75bd56a MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs b/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs new file mode 100644 index 0000000..d74fe94 --- /dev/null +++ b/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs @@ -0,0 +1,70 @@ +#if MULTIPLAYER_TOOLS +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 +using AOT; +using Unity.Burst; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Networking.Transport; +using UnityEngine; + +namespace Unity.Netcode.Transports.UTP +{ + [BurstCompile] + internal unsafe struct NetworkMetricsPipelineStage : INetworkPipelineStage + { + static TransportFunctionPointer ReceiveFunction = new TransportFunctionPointer(Receive); + static TransportFunctionPointer SendFunction = new TransportFunctionPointer(Send); + static TransportFunctionPointer InitializeConnectionFunction = new TransportFunctionPointer(InitializeConnection); + + public NetworkPipelineStage StaticInitialize(byte* staticInstanceBuffer, + int staticInstanceBufferLength, + NetworkSettings settings) + { + return new NetworkPipelineStage( + ReceiveFunction, + SendFunction, + InitializeConnectionFunction, + ReceiveCapacity: 0, + SendCapacity: 0, + HeaderCapacity: 0, + SharedStateCapacity: UnsafeUtility.SizeOf()); + } + + public int StaticSize => 0; + + [BurstCompile(DisableDirectCall = true)] + [MonoPInvokeCallback(typeof(NetworkPipelineStage.ReceiveDelegate))] + private static void Receive(ref NetworkPipelineContext networkPipelineContext, + ref InboundRecvBuffer inboundReceiveBuffer, + ref NetworkPipelineStage.Requests requests, + int systemHeaderSize) + { + var networkMetricContext = (NetworkMetricsContext*)networkPipelineContext.internalSharedProcessBuffer; + networkMetricContext->PacketReceivedCount++; + } + + [BurstCompile(DisableDirectCall = true)] + [MonoPInvokeCallback(typeof(NetworkPipelineStage.SendDelegate))] + private static int Send(ref NetworkPipelineContext networkPipelineContext, + ref InboundSendBuffer inboundSendBuffer, + ref NetworkPipelineStage.Requests requests, + int systemHeaderSize) + { + var networkMetricContext = (NetworkMetricsContext*)networkPipelineContext.internalSharedProcessBuffer; + networkMetricContext->PacketSentCount++; + return 0; + } + + [BurstCompile(DisableDirectCall = true)] + [MonoPInvokeCallback(typeof(NetworkPipelineStage.InitializeConnectionDelegate))] + private static void InitializeConnection(byte* staticInstanceBuffer, int staticInstanceBufferLength, + byte* sendProcessBuffer, int sendProcessBufferLength, byte* receiveProcessBuffer, int receiveProcessBufferLength, + byte* sharedProcessBuffer, int sharedProcessBufferLength) + { + var networkMetricContext = (NetworkMetricsContext*)sharedProcessBuffer; + networkMetricContext->PacketSentCount = 0; + networkMetricContext->PacketReceivedCount = 0; + } + } +} +#endif +#endif diff --git a/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs.meta b/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs.meta new file mode 100644 index 0000000..9479977 --- /dev/null +++ b/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52b1ce9f83ce049c59327064bf70cee8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/UTP/UnityTransport.cs b/Runtime/Transports/UTP/UnityTransport.cs new file mode 100644 index 0000000..83642df --- /dev/null +++ b/Runtime/Transports/UTP/UnityTransport.cs @@ -0,0 +1,1222 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using NetcodeNetworkEvent = Unity.Netcode.NetworkEvent; +using TransportNetworkEvent = Unity.Networking.Transport.NetworkEvent; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Collections; +using Unity.Networking.Transport; +using Unity.Networking.Transport.Relay; +using Unity.Networking.Transport.Utilities; + +namespace Unity.Netcode.Transports.UTP +{ + /// + /// Provides an interface that overrides the ability to create your own drivers and pipelines + /// + public interface INetworkStreamDriverConstructor + { + void CreateDriver( + UnityTransport transport, + out NetworkDriver driver, + out NetworkPipeline unreliableFragmentedPipeline, + out NetworkPipeline unreliableSequencedFragmentedPipeline, + out NetworkPipeline reliableSequencedPipeline); + } + + public static class ErrorUtilities + { + private const string k_NetworkSuccess = "Success"; + private const string k_NetworkIdMismatch = "NetworkId is invalid, likely caused by stale connection {0}."; + private const string k_NetworkVersionMismatch = "NetworkVersion is invalid, likely caused by stale connection {0}."; + private const string k_NetworkStateMismatch = "Sending data while connecting on connection {0} is not allowed."; + private const string k_NetworkPacketOverflow = "Unable to allocate packet due to buffer overflow."; + private const string k_NetworkSendQueueFull = "Currently unable to queue packet as there is too many in-flight packets. This could be because the send queue size ('Max Send Queue Size') is too small."; + private const string k_NetworkHeaderInvalid = "Invalid Unity Transport Protocol header."; + private const string k_NetworkDriverParallelForErr = "The parallel network driver needs to process a single unique connection per job, processing a single connection multiple times in a parallel for is not supported."; + private const string k_NetworkSendHandleInvalid = "Invalid NetworkInterface Send Handle. Likely caused by pipeline send data corruption."; + private const string k_NetworkArgumentMismatch = "Invalid NetworkEndpoint Arguments."; + + public static string ErrorToString(Networking.Transport.Error.StatusCode error, ulong connectionId) + { + switch (error) + { + case Networking.Transport.Error.StatusCode.Success: + return k_NetworkSuccess; + case Networking.Transport.Error.StatusCode.NetworkIdMismatch: + return string.Format(k_NetworkIdMismatch, connectionId); + case Networking.Transport.Error.StatusCode.NetworkVersionMismatch: + return string.Format(k_NetworkVersionMismatch, connectionId); + case Networking.Transport.Error.StatusCode.NetworkStateMismatch: + return string.Format(k_NetworkStateMismatch, connectionId); + case Networking.Transport.Error.StatusCode.NetworkPacketOverflow: + return k_NetworkPacketOverflow; + case Networking.Transport.Error.StatusCode.NetworkSendQueueFull: + return k_NetworkSendQueueFull; + case Networking.Transport.Error.StatusCode.NetworkHeaderInvalid: + return k_NetworkHeaderInvalid; + case Networking.Transport.Error.StatusCode.NetworkDriverParallelForErr: + return k_NetworkDriverParallelForErr; + case Networking.Transport.Error.StatusCode.NetworkSendHandleInvalid: + return k_NetworkSendHandleInvalid; + case Networking.Transport.Error.StatusCode.NetworkArgumentMismatch: + return k_NetworkArgumentMismatch; + } + + return $"Unknown ErrorCode {Enum.GetName(typeof(Networking.Transport.Error.StatusCode), error)}"; + } + } + + public partial class UnityTransport : NetworkTransport, INetworkStreamDriverConstructor + { + public enum ProtocolType + { + UnityTransport, + RelayUnityTransport, + } + + private enum State + { + Disconnected, + Listening, + Connected, + } + + public const int InitialMaxPacketQueueSize = 128; + public const int InitialMaxPayloadSize = 6 * 1024; + public const int InitialMaxSendQueueSize = 16 * InitialMaxPayloadSize; + + private static ConnectionAddressData s_DefaultConnectionAddressData = new ConnectionAddressData { Address = "127.0.0.1", Port = 7777, ServerListenAddress = string.Empty }; + +#pragma warning disable IDE1006 // Naming Styles + public static INetworkStreamDriverConstructor s_DriverConstructor; +#pragma warning restore IDE1006 // Naming Styles + public INetworkStreamDriverConstructor DriverConstructor => s_DriverConstructor ?? this; + + [Tooltip("Which protocol should be selected (Relay/Non-Relay).")] + [SerializeField] + private ProtocolType m_ProtocolType; + + [Tooltip("The maximum amount of packets that can be in the internal send/receive queues. Basically this is how many packets can be sent/received in a single update/frame.")] + [SerializeField] + private int m_MaxPacketQueueSize = InitialMaxPacketQueueSize; + + /// The maximum amount of packets that can be in the internal send/receive queues. + /// Basically this is how many packets can be sent/received in a single update/frame. + public int MaxPacketQueueSize + { + get => m_MaxPacketQueueSize; + set => m_MaxPacketQueueSize = value; + } + + [Tooltip("The maximum size of a payload that can be handled by the transport.")] + [SerializeField] + private int m_MaxPayloadSize = InitialMaxPayloadSize; + + /// The maximum size of a payload that can be handled by the transport. + public int MaxPayloadSize + { + get => m_MaxPayloadSize; + set => m_MaxPayloadSize = value; + } + + [Tooltip("The maximum size in bytes of the transport send queue. The send queue accumulates messages for batching and stores messages when other internal send queues are full. If you routinely observe an error about too many in-flight packets, try increasing this.")] + [SerializeField] + private int m_MaxSendQueueSize = InitialMaxSendQueueSize; + + /// The maximum size in bytes of the transport send queue. + /// + /// The send queue accumulates messages for batching and stores messages when other internal + /// send queues are full. If you routinely observe an error about too many in-flight packets, + /// try increasing this. + /// + public int MaxSendQueueSize + { + get => m_MaxSendQueueSize; + set => m_MaxSendQueueSize = value; + } + + [Tooltip("Timeout in milliseconds after which a heartbeat is sent if there is no activity.")] + [SerializeField] + private int m_HeartbeatTimeoutMS = NetworkParameterConstants.HeartbeatTimeoutMS; + + /// Timeout in milliseconds after which a heartbeat is sent if there is no activity. + public int HeartbeatTimeoutMS + { + get => m_HeartbeatTimeoutMS; + set => m_HeartbeatTimeoutMS = value; + } + + [Tooltip("Timeout in milliseconds indicating how long we will wait until we send a new connection attempt.")] + [SerializeField] + private int m_ConnectTimeoutMS = NetworkParameterConstants.ConnectTimeoutMS; + + /// + /// Timeout in milliseconds indicating how long we will wait until we send a new connection attempt. + /// + public int ConnectTimeoutMS + { + get => m_ConnectTimeoutMS; + set => m_ConnectTimeoutMS = value; + } + + [Tooltip("The maximum amount of connection attempts we will try before disconnecting.")] + [SerializeField] + private int m_MaxConnectAttempts = NetworkParameterConstants.MaxConnectAttempts; + + /// The maximum amount of connection attempts we will try before disconnecting. + public int MaxConnectAttempts + { + get => m_MaxConnectAttempts; + set => m_MaxConnectAttempts = value; + } + + [Tooltip("Inactivity timeout after which a connection will be disconnected. The connection needs to receive data from the connected endpoint within this timeout. Note that with heartbeats enabled, simply not sending any data will not be enough to trigger this timeout (since heartbeats count as connection events).")] + [SerializeField] + private int m_DisconnectTimeoutMS = NetworkParameterConstants.DisconnectTimeoutMS; + + /// Inactivity timeout after which a connection will be disconnected. + /// + /// The connection needs to receive data from the connected endpoint within this timeout. + /// Note that with heartbeats enabled, simply not sending any data will not be enough to + /// trigger this timeout (since heartbeats count as connection events). + /// + public int DisconnectTimeoutMS + { + get => m_DisconnectTimeoutMS; + set => m_DisconnectTimeoutMS = value; + } + + [Serializable] + public struct ConnectionAddressData + { + [Tooltip("IP address of the server (address to which clients will connect to).")] + [SerializeField] + public string Address; + + [Tooltip("UDP port of the server.")] + [SerializeField] + public ushort Port; + + [Tooltip("IP address the server will listen on. If not provided, will use 'Address'.")] + [SerializeField] + public string ServerListenAddress; + + private static NetworkEndPoint ParseNetworkEndpoint(string ip, ushort port) + { + if (!NetworkEndPoint.TryParse(ip, port, out var endpoint)) + { + Debug.LogError($"Invalid network endpoint: {ip}:{port}."); + return default; + } + + return endpoint; + } + + public NetworkEndPoint ServerEndPoint => ParseNetworkEndpoint(Address, Port); + + public NetworkEndPoint ListenEndPoint => ParseNetworkEndpoint((ServerListenAddress == string.Empty) ? Address : ServerListenAddress, Port); + } + + public ConnectionAddressData ConnectionData = s_DefaultConnectionAddressData; + + [Serializable] + public struct SimulatorParameters + { + [Tooltip("Delay to add to every send and received packet (in milliseconds). Only applies in the editor and in development builds. The value is ignored in production builds.")] + [SerializeField] + public int PacketDelayMS; + + [Tooltip("Jitter (random variation) to add/substract to the packet delay (in milliseconds). Only applies in the editor and in development builds. The value is ignored in production builds.")] + [SerializeField] + public int PacketJitterMS; + + [Tooltip("Percentage of sent and received packets to drop. Only applies in the editor and in the editor and in developments builds.")] + [SerializeField] + public int PacketDropRate; + } + + public SimulatorParameters DebugSimulator = new SimulatorParameters + { + PacketDelayMS = 0, + PacketJitterMS = 0, + PacketDropRate = 0 + }; + + private State m_State = State.Disconnected; + private NetworkDriver m_Driver; + private NetworkSettings m_NetworkSettings; + private ulong m_ServerClientId; + + private NetworkPipeline m_UnreliableFragmentedPipeline; + private NetworkPipeline m_UnreliableSequencedFragmentedPipeline; + private NetworkPipeline m_ReliableSequencedPipeline; + + public override ulong ServerClientId => m_ServerClientId; + + public ProtocolType Protocol => m_ProtocolType; + + private RelayServerData m_RelayServerData; + + internal NetworkManager NetworkManager; + + /// + /// SendQueue dictionary is used to batch events instead of sending them immediately. + /// + private readonly Dictionary m_SendQueue = new Dictionary(); + + // Since reliable messages may be spread out over multiple transport payloads, it's possible + // to receive only parts of a message in an update. We thus keep the reliable receive queues + // around to avoid losing partial messages. + private readonly Dictionary m_ReliableReceiveQueues = new Dictionary(); + + private void InitDriver() + { + DriverConstructor.CreateDriver( + this, + out m_Driver, + out m_UnreliableFragmentedPipeline, + out m_UnreliableSequencedFragmentedPipeline, + out m_ReliableSequencedPipeline); + } + + private void DisposeInternals() + { + if (m_Driver.IsCreated) + { + m_Driver.Dispose(); + } + + m_NetworkSettings.Dispose(); + + foreach (var queue in m_SendQueue.Values) + { + queue.Dispose(); + } + + m_SendQueue.Clear(); + } + + private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery) + { + switch (delivery) + { + case NetworkDelivery.Unreliable: + return m_UnreliableFragmentedPipeline; + + case NetworkDelivery.UnreliableSequenced: + return m_UnreliableSequencedFragmentedPipeline; + + case NetworkDelivery.Reliable: + case NetworkDelivery.ReliableSequenced: + case NetworkDelivery.ReliableFragmentedSequenced: + return m_ReliableSequencedPipeline; + + default: + Debug.LogError($"Unknown {nameof(NetworkDelivery)} value: {delivery}"); + return NetworkPipeline.Null; + } + } + + private bool ClientBindAndConnect() + { + var serverEndpoint = default(NetworkEndPoint); + + if (m_ProtocolType == ProtocolType.RelayUnityTransport) + { + //This comparison is currently slow since RelayServerData does not implement a custom comparison operator that doesn't use + //reflection, but this does not live in the context of a performance-critical loop, it runs once at initial connection time. + if (m_RelayServerData.Equals(default(RelayServerData))) + { + Debug.LogError("You must call SetRelayServerData() at least once before calling StartRelayServer."); + return false; + } + + m_NetworkSettings.WithRelayParameters(ref m_RelayServerData, m_HeartbeatTimeoutMS); + } + else + { + serverEndpoint = ConnectionData.ServerEndPoint; + } + + InitDriver(); + + int result = m_Driver.Bind(NetworkEndPoint.AnyIpv4); + if (result != 0) + { + Debug.LogError("Client failed to bind"); + return false; + } + + var serverConnection = m_Driver.Connect(serverEndpoint); + m_ServerClientId = ParseClientId(serverConnection); + + return true; + } + + private bool ServerBindAndListen(NetworkEndPoint endPoint) + { + InitDriver(); + + int result = m_Driver.Bind(endPoint); + if (result != 0) + { + Debug.LogError("Server failed to bind"); + return false; + } + + result = m_Driver.Listen(); + if (result != 0) + { + Debug.LogError("Server failed to listen"); + return false; + } + + m_State = State.Listening; + return true; + } + + private static RelayAllocationId ConvertFromAllocationIdBytes(byte[] allocationIdBytes) + { + unsafe + { + fixed (byte* ptr = allocationIdBytes) + { + return RelayAllocationId.FromBytePointer(ptr, allocationIdBytes.Length); + } + } + } + + private static RelayHMACKey ConvertFromHMAC(byte[] hmac) + { + unsafe + { + fixed (byte* ptr = hmac) + { + return RelayHMACKey.FromBytePointer(ptr, RelayHMACKey.k_Length); + } + } + } + + private static RelayConnectionData ConvertConnectionData(byte[] connectionData) + { + unsafe + { + fixed (byte* ptr = connectionData) + { + return RelayConnectionData.FromBytePointer(ptr, RelayConnectionData.k_Length); + } + } + } + + internal void SetMaxPayloadSize(int maxPayloadSize) + { + m_MaxPayloadSize = maxPayloadSize; + } + + private void SetProtocol(ProtocolType inProtocol) + { + m_ProtocolType = inProtocol; + } + + public void SetRelayServerData(string ipv4Address, ushort port, byte[] allocationIdBytes, byte[] keyBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes = null, bool isSecure = false) + { + RelayConnectionData hostConnectionData; + + if (!NetworkEndPoint.TryParse(ipv4Address, port, out var serverEndpoint)) + { + Debug.LogError($"Invalid address {ipv4Address}:{port}"); + + // We set this to default to cause other checks to fail to state you need to call this + // function again. + m_RelayServerData = default; + return; + } + + var allocationId = ConvertFromAllocationIdBytes(allocationIdBytes); + var key = ConvertFromHMAC(keyBytes); + var connectionData = ConvertConnectionData(connectionDataBytes); + + if (hostConnectionDataBytes != null) + { + hostConnectionData = ConvertConnectionData(hostConnectionDataBytes); + } + else + { + hostConnectionData = connectionData; + } + + m_RelayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationId, ref connectionData, ref hostConnectionData, ref key, isSecure); + m_RelayServerData.ComputeNewNonce(); + + SetProtocol(ProtocolType.RelayUnityTransport); + } + + /// Set the relay server data for the host. + /// IP address of the relay server. + /// UDP port of the relay server. + /// Allocation ID as a byte array. + /// Allocation key as a byte array. + /// Connection data as a byte array. + /// Whether the connection is secure (uses DTLS). + public void SetHostRelayData(string ipAddress, ushort port, byte[] allocationId, byte[] key, byte[] connectionData, bool isSecure = false) + { + SetRelayServerData(ipAddress, port, allocationId, key, connectionData, null, isSecure); + } + + /// Set the relay server data for the host. + /// IP address of the relay server. + /// UDP port of the relay server. + /// Allocation ID as a byte array. + /// Allocation key as a byte array. + /// Connection data as a byte array. + /// Host's connection data as a byte array. + /// Whether the connection is secure (uses DTLS). + public void SetClientRelayData(string ipAddress, ushort port, byte[] allocationId, byte[] key, byte[] connectionData, byte[] hostConnectionData, bool isSecure = false) + { + SetRelayServerData(ipAddress, port, allocationId, key, connectionData, hostConnectionData, isSecure); + } + + /// + /// Sets IP and Port information. This will be ignored if using the Unity Relay and you should call + /// + public void SetConnectionData(string ipv4Address, ushort port, string listenAddress = null) + { + ConnectionData = new ConnectionAddressData + { + Address = ipv4Address, + Port = port, + ServerListenAddress = listenAddress ?? string.Empty + }; + + SetProtocol(ProtocolType.UnityTransport); + } + + /// + /// Sets IP and Port information. This will be ignored if using the Unity Relay and you should call + /// + public void SetConnectionData(NetworkEndPoint endPoint, NetworkEndPoint listenEndPoint = default) + { + string serverAddress = endPoint.Address.Split(':')[0]; + + string listenAddress = string.Empty; + if (listenEndPoint != default) + { + listenAddress = listenEndPoint.Address.Split(':')[0]; + if (endPoint.Port != listenEndPoint.Port) + { + Debug.LogError($"Port mismatch between server and listen endpoints ({endPoint.Port} vs {listenEndPoint.Port})."); + } + } + + SetConnectionData(serverAddress, endPoint.Port, listenAddress); + } + + /// Set the parameters for the debug simulator. + /// Packet delay in milliseconds. + /// Packet jitter in milliseconds. + /// Packet drop percentage. + public void SetDebugSimulatorParameters(int packetDelay, int packetJitter, int dropRate) + { + if (m_Driver.IsCreated) + { + Debug.LogError("SetDebugSimulatorParameters() must be called before StartClient() or StartServer()."); + return; + } + + DebugSimulator = new SimulatorParameters + { + PacketDelayMS = packetDelay, + PacketJitterMS = packetJitter, + PacketDropRate = dropRate + }; + } + + private bool StartRelayServer() + { + //This comparison is currently slow since RelayServerData does not implement a custom comparison operator that doesn't use + //reflection, but this does not live in the context of a performance-critical loop, it runs once at initial connection time. + if (m_RelayServerData.Equals(default(RelayServerData))) + { + Debug.LogError("You must call SetRelayServerData() at least once before calling StartRelayServer."); + return false; + } + else + { + m_NetworkSettings.WithRelayParameters(ref m_RelayServerData, m_HeartbeatTimeoutMS); + return ServerBindAndListen(NetworkEndPoint.AnyIpv4); + } + } + + // Send as many batched messages from the queue as possible. + private void SendBatchedMessages(SendTarget sendTarget, BatchedSendQueue queue) + { + var clientId = sendTarget.ClientId; + var connection = ParseClientId(clientId); + var pipeline = sendTarget.NetworkPipeline; + + while (!queue.IsEmpty) + { + var result = m_Driver.BeginSend(pipeline, connection, out var writer); + if (result != (int)Networking.Transport.Error.StatusCode.Success) + { + Debug.LogError("Error sending the message: " + + ErrorUtilities.ErrorToString((Networking.Transport.Error.StatusCode)result, clientId)); + return; + } + + // We don't attempt to send entire payloads over the reliable pipeline. Instead we + // fragment it manually. This is safe and easy to do since the reliable pipeline + // basically implements a stream, so as long as we separate the different messages + // in the stream (the send queue does that automatically) we are sure they'll be + // reassembled properly at the other end. This allows us to lift the limit of ~44KB + // on reliable payloads (because of the reliable window size). + var written = pipeline == m_ReliableSequencedPipeline ? queue.FillWriterWithBytes(ref writer) : queue.FillWriterWithMessages(ref writer); + + result = m_Driver.EndSend(writer); + if (result == written) + { + // Batched message was sent successfully. Remove it from the queue. + queue.Consume(written); + } + else + { + // Some error occured. If it's just the UTP queue being full, then don't log + // anything since that's okay (the unsent message(s) are still in the queue + // and we'll retry sending the later). Otherwise log the error and remove the + // message from the queue (we don't want to resend it again since we'll likely + // just get the same error again). + if (result != (int)Networking.Transport.Error.StatusCode.NetworkSendQueueFull) + { + Debug.LogError("Error sending the message: " + ErrorUtilities.ErrorToString((Networking.Transport.Error.StatusCode)result, clientId)); + queue.Consume(written); + } + + return; + } + } + } + + private bool AcceptConnection() + { + var connection = m_Driver.Accept(); + + if (connection == default) + { + return false; + } + + InvokeOnTransportEvent(NetcodeNetworkEvent.Connect, + ParseClientId(connection), + default, + Time.realtimeSinceStartup); + + return true; + + } + + private void ReceiveMessages(ulong clientId, NetworkPipeline pipeline, DataStreamReader dataReader) + { + BatchedReceiveQueue queue; + if (pipeline == m_ReliableSequencedPipeline) + { + if (m_ReliableReceiveQueues.TryGetValue(clientId, out queue)) + { + queue.PushReader(dataReader); + } + else + { + queue = new BatchedReceiveQueue(dataReader); + m_ReliableReceiveQueues[clientId] = queue; + } + } + else + { + queue = new BatchedReceiveQueue(dataReader); + } + + while (!queue.IsEmpty) + { + var message = queue.PopMessage(); + if (message == default) + { + // Only happens if there's only a partial message in the queue (rare). + break; + } + + InvokeOnTransportEvent(NetcodeNetworkEvent.Data, clientId, message, Time.realtimeSinceStartup); + } + } + + private bool ProcessEvent() + { + var eventType = m_Driver.PopEvent(out var networkConnection, out var reader, out var pipeline); + var clientId = ParseClientId(networkConnection); + + switch (eventType) + { + case TransportNetworkEvent.Type.Connect: + { + InvokeOnTransportEvent(NetcodeNetworkEvent.Connect, + clientId, + default, + Time.realtimeSinceStartup); + + m_State = State.Connected; + return true; + } + case TransportNetworkEvent.Type.Disconnect: + { + // Handle cases where we're a client receiving a Disconnect event. The + // meaning of the event depends on our current state. If we were connected + // then it means we got disconnected. If we were disconnected means that our + // connection attempt has failed. + if (m_State == State.Connected) + { + m_State = State.Disconnected; + m_ServerClientId = default; + } + else if (m_State == State.Disconnected) + { + Debug.LogError("Failed to connect to server."); + m_ServerClientId = default; + } + + m_ReliableReceiveQueues.Remove(clientId); + ClearSendQueuesForClientId(clientId); + + InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect, + clientId, + default, + Time.realtimeSinceStartup); + + return true; + } + case TransportNetworkEvent.Type.Data: + { + ReceiveMessages(clientId, pipeline, reader); + return true; + } + } + + return false; + } + + private void Update() + { + if (m_Driver.IsCreated) + { + foreach (var kvp in m_SendQueue) + { + SendBatchedMessages(kvp.Key, kvp.Value); + } + + m_Driver.ScheduleUpdate().Complete(); + + while (AcceptConnection() && m_Driver.IsCreated) + { + ; + } + + while (ProcessEvent() && m_Driver.IsCreated) + { + ; + } + +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + if (NetworkManager) + { + ExtractNetworkMetrics(); + } +#endif + } + } + + private void OnDestroy() + { + DisposeInternals(); + } + +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + private void ExtractNetworkMetrics() + { + if (NetworkManager.IsServer) + { + var ngoConnectionIds = NetworkManager.ConnectedClients.Keys; + foreach (var ngoConnectionId in ngoConnectionIds) + { + if (ngoConnectionId == 0 && NetworkManager.IsHost) + { + continue; + } + var transportClientId = NetworkManager.ClientIdToTransportId(ngoConnectionId); + ExtractNetworkMetricsForClient(transportClientId); + } + } + else + { + if (m_ServerClientId != 0) + { + ExtractNetworkMetricsForClient(m_ServerClientId); + } + } + } + + private void ExtractNetworkMetricsForClient(ulong transportClientId) + { + var networkConnection = ParseClientId(transportClientId); + ExtractNetworkMetricsFromPipeline(m_UnreliableFragmentedPipeline, networkConnection); + ExtractNetworkMetricsFromPipeline(m_UnreliableSequencedFragmentedPipeline, networkConnection); + ExtractNetworkMetricsFromPipeline(m_ReliableSequencedPipeline, networkConnection); + + var rttValue = NetworkManager.IsServer ? 0 : ExtractRtt(networkConnection); + NetworkMetrics.UpdateRttToServer(rttValue); + + var packetLoss = NetworkManager.IsServer ? 0 : ExtractPacketLoss(networkConnection); + NetworkMetrics.UpdatePacketLoss(packetLoss); + } + + private void ExtractNetworkMetricsFromPipeline(NetworkPipeline pipeline, NetworkConnection networkConnection) + { + //Don't need to dispose of the buffers, they are filled with data pointers. + m_Driver.GetPipelineBuffers(pipeline, + NetworkPipelineStageCollection.GetStageId(typeof(NetworkMetricsPipelineStage)), + networkConnection, + out _, + out _, + out var sharedBuffer); + + unsafe + { + var networkMetricsContext = (NetworkMetricsContext*)sharedBuffer.GetUnsafePtr(); + + NetworkMetrics.TrackPacketSent(networkMetricsContext->PacketSentCount); + NetworkMetrics.TrackPacketReceived(networkMetricsContext->PacketReceivedCount); + + networkMetricsContext->PacketSentCount = 0; + networkMetricsContext->PacketReceivedCount = 0; + } + } +#endif + + private int ExtractRtt(NetworkConnection networkConnection) + { + if (m_Driver.GetConnectionState(networkConnection) != NetworkConnection.State.Connected) + { + return 0; + } + + m_Driver.GetPipelineBuffers(m_ReliableSequencedPipeline, + NetworkPipelineStageCollection.GetStageId(typeof(ReliableSequencedPipelineStage)), + networkConnection, + out _, + out _, + out var sharedBuffer); + + unsafe + { + var sharedContext = (ReliableUtility.SharedContext*)sharedBuffer.GetUnsafePtr(); + + return sharedContext->RttInfo.LastRtt; + } + } + + private float ExtractPacketLoss(NetworkConnection networkConnection) + { + if (m_Driver.GetConnectionState(networkConnection) != NetworkConnection.State.Connected) + { + return 0f; + } + + m_Driver.GetPipelineBuffers(m_ReliableSequencedPipeline, + NetworkPipelineStageCollection.GetStageId(typeof(ReliableSequencedPipelineStage)), + networkConnection, + out _, + out _, + out var sharedBuffer); + + unsafe + { + var sharedContext = (ReliableUtility.SharedContext*)sharedBuffer.GetUnsafePtr(); + + var packetReceived = (float)sharedContext->stats.PacketsReceived; + var packetDropped = (float)sharedContext->stats.PacketsDropped; + var packetLoss = packetReceived > 0 ? packetDropped / packetReceived : 0; + + return packetLoss; + } + } + + private static unsafe ulong ParseClientId(NetworkConnection utpConnectionId) + { + return *(ulong*)&utpConnectionId; + } + + private static unsafe NetworkConnection ParseClientId(ulong netcodeConnectionId) + { + return *(NetworkConnection*)&netcodeConnectionId; + } + + private void ClearSendQueuesForClientId(ulong clientId) + { + // NativeList and manual foreach avoids any allocations. + using var keys = new NativeList(16, Allocator.Temp); + foreach (var key in m_SendQueue.Keys) + { + if (key.ClientId == clientId) + { + keys.Add(key); + } + } + + foreach (var target in keys) + { + m_SendQueue[target].Dispose(); + m_SendQueue.Remove(target); + } + } + + private void FlushSendQueuesForClientId(ulong clientId) + { + foreach (var kvp in m_SendQueue) + { + if (kvp.Key.ClientId == clientId) + { + SendBatchedMessages(kvp.Key, kvp.Value); + } + } + } + + public override void DisconnectLocalClient() + { + if (m_State == State.Connected) + { + FlushSendQueuesForClientId(m_ServerClientId); + + if (m_Driver.Disconnect(ParseClientId(m_ServerClientId)) == 0) + { + m_State = State.Disconnected; + + m_ReliableReceiveQueues.Remove(m_ServerClientId); + ClearSendQueuesForClientId(m_ServerClientId); + + // If we successfully disconnect we dispatch a local disconnect message + // this how uNET and other transports worked and so this is just keeping with the old behavior + // should be also noted on the client this will call shutdown on the NetworkManager and the Transport + InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect, + m_ServerClientId, + default, + Time.realtimeSinceStartup); + } + } + } + + public override void DisconnectRemoteClient(ulong clientId) + { + Debug.Assert(m_State == State.Listening, "DisconnectRemoteClient should be called on a listening server"); + + if (m_State == State.Listening) + { + FlushSendQueuesForClientId(clientId); + + m_ReliableReceiveQueues.Remove(clientId); + ClearSendQueuesForClientId(clientId); + + var connection = ParseClientId(clientId); + if (m_Driver.GetConnectionState(connection) != NetworkConnection.State.Disconnected) + { + m_Driver.Disconnect(connection); + } + } + } + + public override ulong GetCurrentRtt(ulong clientId) + { + // We don't know if this is getting called from inside NGO (which presumably knows to + // use the transport client ID) or from a user (which will be using the NGO client ID). + // So we just try both cases (ExtractRtt returns 0 for invalid connections). + + if (NetworkManager != null) + { + var transportId = NetworkManager.ClientIdToTransportId(clientId); + + var rtt = ExtractRtt(ParseClientId(transportId)); + if (rtt > 0) + { + return (ulong)rtt; + } + } + + return (ulong)ExtractRtt(ParseClientId(clientId)); + } + + public override void Initialize(NetworkManager networkManager = null) + { + Debug.Assert(sizeof(ulong) == UnsafeUtility.SizeOf(), "Netcode connection id size does not match UTP connection id size"); + + NetworkManager = networkManager; + + m_NetworkSettings = new NetworkSettings(Allocator.Persistent); + +#if !UNITY_WEBGL + // If the user sends a message of exactly m_MaxPayloadSize in length, we need to + // account for the overhead of its length when we store it in the send queue. + var fragmentationCapacity = m_MaxPayloadSize + BatchedSendQueue.PerMessageOverhead; + + m_NetworkSettings + .WithFragmentationStageParameters(payloadCapacity: fragmentationCapacity) + .WithBaselibNetworkInterfaceParameters( + receiveQueueCapacity: m_MaxPacketQueueSize, + sendQueueCapacity: m_MaxPacketQueueSize); +#endif + } + + public override NetcodeNetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime) + { + clientId = default; + payload = default; + receiveTime = default; + return NetcodeNetworkEvent.Nothing; + } + + public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) + { + if (payload.Count > m_MaxPayloadSize) + { + Debug.LogError($"Payload of size {payload.Count} larger than configured 'Max Payload Size' ({m_MaxPayloadSize})."); + return; + } + + var pipeline = SelectSendPipeline(networkDelivery); + + var sendTarget = new SendTarget(clientId, pipeline); + if (!m_SendQueue.TryGetValue(sendTarget, out var queue)) + { + queue = new BatchedSendQueue(Math.Max(m_MaxSendQueueSize, m_MaxPayloadSize)); + m_SendQueue.Add(sendTarget, queue); + } + + if (!queue.PushMessage(payload)) + { + if (pipeline == m_ReliableSequencedPipeline) + { + // If the message is sent reliably, then we're over capacity and we can't + // provide any reliability guarantees anymore. Disconnect the client since at + // this point they're bound to become desynchronized. + + var ngoClientId = NetworkManager?.TransportIdToClientId(clientId) ?? clientId; + Debug.LogError($"Couldn't add payload of size {payload.Count} to reliable send queue. " + + $"Closing connection {ngoClientId} as reliability guarantees can't be maintained. " + + $"Perhaps 'Max Send Queue Size' ({m_MaxSendQueueSize}) is too small for workload."); + + if (clientId == m_ServerClientId) + { + DisconnectLocalClient(); + } + else + { + DisconnectRemoteClient(clientId); + + // DisconnectRemoteClient doesn't notify SDK of disconnection. + InvokeOnTransportEvent(NetcodeNetworkEvent.Disconnect, + clientId, + default(ArraySegment), + Time.realtimeSinceStartup); + } + } + else + { + // If the message is sent unreliably, we can always just flush everything out + // to make space in the send queue. This is an expensive operation, but a user + // would need to send A LOT of unreliable traffic in one update to get here. + + m_Driver.ScheduleFlushSend(default).Complete(); + SendBatchedMessages(sendTarget, queue); + + // Don't check for failure. If it still doesn't work, there's nothing we can do + // at this point and the message is lost (it was sent unreliable anyway). + queue.PushMessage(payload); + } + } + } + + public override bool StartClient() + { + if (m_Driver.IsCreated) + { + return false; + } + + return ClientBindAndConnect(); + } + + public override bool StartServer() + { + if (m_Driver.IsCreated) + { + return false; + } + + switch (m_ProtocolType) + { + case ProtocolType.UnityTransport: + return ServerBindAndListen(ConnectionData.ListenEndPoint); + case ProtocolType.RelayUnityTransport: + return StartRelayServer(); + default: + return false; + } + } + + public override void Shutdown() + { + if (!m_Driver.IsCreated) + { + return; + } + + // Flush all send queues to the network. NGO can be configured to flush its message + // queue on shutdown. But this only calls the Send() method, which doesn't actually + // get anything to the network. + foreach (var kvp in m_SendQueue) + { + SendBatchedMessages(kvp.Key, kvp.Value); + } + + // The above flush only puts the message in UTP internal buffers, need the flush send + // job to execute to actually get things out on the wire. This will also ensure any + // disconnect messages are sent out. + m_Driver.ScheduleFlushSend(default).Complete(); + + DisposeInternals(); + + // We must reset this to zero because UTP actually re-uses clientIds if there is a clean disconnect + m_ServerClientId = 0; + } + + private void ConfigureSimulator() + { + m_NetworkSettings.WithSimulatorStageParameters( + maxPacketCount: 300, // TODO Is there any way to compute a better value? + maxPacketSize: NetworkParameterConstants.MTU, + packetDelayMs: DebugSimulator.PacketDelayMS, + packetJitterMs: DebugSimulator.PacketJitterMS, + packetDropPercentage: DebugSimulator.PacketDropRate + ); + } + + public void CreateDriver(UnityTransport transport, out NetworkDriver driver, + out NetworkPipeline unreliableFragmentedPipeline, + out NetworkPipeline unreliableSequencedFragmentedPipeline, + out NetworkPipeline reliableSequencedPipeline) + { +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + NetworkPipelineStageCollection.RegisterPipelineStage(new NetworkMetricsPipelineStage()); +#endif + var maxFrameTimeMS = 0; + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + maxFrameTimeMS = 100; + ConfigureSimulator(); +#endif + + m_NetworkSettings.WithNetworkConfigParameters( + maxConnectAttempts: transport.m_MaxConnectAttempts, + connectTimeoutMS: transport.m_ConnectTimeoutMS, + disconnectTimeoutMS: transport.m_DisconnectTimeoutMS, + heartbeatTimeoutMS: transport.m_HeartbeatTimeoutMS, + maxFrameTimeMS: maxFrameTimeMS); + + driver = NetworkDriver.Create(m_NetworkSettings); + +#if UNITY_EDITOR || DEVELOPMENT_BUILD + if (DebugSimulator.PacketDelayMS > 0 || DebugSimulator.PacketDropRate > 0) + { + unreliableFragmentedPipeline = driver.CreatePipeline( + typeof(FragmentationPipelineStage), + typeof(SimulatorPipelineStage), + typeof(SimulatorPipelineStageInSend) +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + , typeof(NetworkMetricsPipelineStage) +#endif + ); + unreliableSequencedFragmentedPipeline = driver.CreatePipeline( + typeof(FragmentationPipelineStage), + typeof(UnreliableSequencedPipelineStage), + typeof(SimulatorPipelineStage), + typeof(SimulatorPipelineStageInSend) +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + ,typeof(NetworkMetricsPipelineStage) +#endif + ); + reliableSequencedPipeline = driver.CreatePipeline( + typeof(ReliableSequencedPipelineStage), + typeof(SimulatorPipelineStage), + typeof(SimulatorPipelineStageInSend) +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + ,typeof(NetworkMetricsPipelineStage) +#endif + ); + } + else +#endif + { + unreliableFragmentedPipeline = driver.CreatePipeline( + typeof(FragmentationPipelineStage) +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + ,typeof(NetworkMetricsPipelineStage) +#endif + ); + unreliableSequencedFragmentedPipeline = driver.CreatePipeline( + typeof(FragmentationPipelineStage), + typeof(UnreliableSequencedPipelineStage) +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + ,typeof(NetworkMetricsPipelineStage) +#endif + ); + reliableSequencedPipeline = driver.CreatePipeline( + typeof(ReliableSequencedPipelineStage) +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + ,typeof(NetworkMetricsPipelineStage) +#endif + ); + } + } + + // -------------- Utility Types ------------------------------------------------------------------------------- + + + /// + /// Cached information about reliability mode with a certain client + /// + private struct SendTarget : IEquatable + { + public readonly ulong ClientId; + public readonly NetworkPipeline NetworkPipeline; + + public SendTarget(ulong clientId, NetworkPipeline networkPipeline) + { + ClientId = clientId; + NetworkPipeline = networkPipeline; + } + + public bool Equals(SendTarget other) + { + return ClientId == other.ClientId && NetworkPipeline.Equals(other.NetworkPipeline); + } + + public override bool Equals(object obj) + { + return obj is SendTarget other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (ClientId.GetHashCode() * 397) ^ NetworkPipeline.GetHashCode(); + } + } + } + } +} diff --git a/Runtime/Transports/UTP/UnityTransport.cs.meta b/Runtime/Transports/UTP/UnityTransport.cs.meta new file mode 100644 index 0000000..471eade --- /dev/null +++ b/Runtime/Transports/UTP/UnityTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6960e84d07fb87f47956e7a81d71c4e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/com.unity.netcode.runtime.asmdef b/Runtime/com.unity.netcode.runtime.asmdef index 956c39d..24a10f6 100644 --- a/Runtime/com.unity.netcode.runtime.asmdef +++ b/Runtime/com.unity.netcode.runtime.asmdef @@ -10,7 +10,9 @@ "Unity.Multiplayer.Tools.NetStats", "Unity.Multiplayer.Tools.NetStatsReporting", "Unity.Multiplayer.Tools.NetworkSolutionInterface", - "Unity.Collections" + "Unity.Networking.Transport", + "Unity.Collections", + "Unity.Burst" ], "allowUnsafeCode": true, "versionDefines": [ @@ -26,8 +28,8 @@ }, { "name": "com.unity.multiplayer.tools", - "expression": "1.0.0-pre.4", - "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_4" + "expression": "1.0.0-pre.7", + "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_7" } ] -} \ No newline at end of file +} diff --git a/TestHelpers/Runtime/NetcodeIntegrationTest.cs b/TestHelpers/Runtime/NetcodeIntegrationTest.cs index 17e4eff..e846914 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTest.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; +using System.Runtime.CompilerServices; using Object = UnityEngine.Object; @@ -111,6 +112,33 @@ public enum HostOrServer private NetworkManagerInstatiationMode m_NetworkManagerInstatiationMode; + private bool m_EnableVerboseDebug; + + /// + /// Used to display the various integration test + /// stages and can be used to log verbose information + /// for troubleshooting an integration test. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void VerboseDebug(string msg) + { + if (m_EnableVerboseDebug) + { + Debug.Log(msg); + } + } + + /// + /// Override this and return true if you need + /// to troubleshoot a hard to track bug within an + /// integration test. + /// + protected virtual bool OnSetVerboseDebug() + { + return false; + } + /// /// The very first thing invoked during the that /// determines how this integration test handles NetworkManager instantiation @@ -130,11 +158,17 @@ protected virtual void OnOneTimeSetup() [OneTimeSetUp] public void OneTimeSetup() { + m_EnableVerboseDebug = OnSetVerboseDebug(); + + VerboseDebug($"Entering {nameof(OneTimeSetup)}"); + m_NetworkManagerInstatiationMode = OnSetIntegrationTestMode(); // Enable NetcodeIntegrationTest auto-label feature NetcodeIntegrationTestHelpers.RegisterNetcodeIntegrationTest(true); OnOneTimeSetup(); + + VerboseDebug($"Exiting {nameof(OneTimeSetup)}"); } /// @@ -153,6 +187,8 @@ protected virtual IEnumerator OnSetup() [UnitySetUp] public IEnumerator SetUp() { + VerboseDebug($"Entering {nameof(SetUp)}"); + yield return OnSetup(); if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests && m_ServerNetworkManager == null || m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest) @@ -161,6 +197,7 @@ public IEnumerator SetUp() yield return StartServerAndClients(); } + VerboseDebug($"Exiting {nameof(SetUp)}"); } /// @@ -173,6 +210,7 @@ protected virtual void OnCreatePlayerPrefab() private void CreatePlayerPrefab() { + VerboseDebug($"Entering {nameof(CreatePlayerPrefab)}"); // Create playerPrefab m_PlayerPrefab = new GameObject("Player"); NetworkObject networkObject = m_PlayerPrefab.AddComponent(); @@ -181,6 +219,8 @@ private void CreatePlayerPrefab() NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); OnCreatePlayerPrefab(); + + VerboseDebug($"Exiting {nameof(CreatePlayerPrefab)}"); } /// @@ -207,6 +247,8 @@ protected void CreateServerAndClients() /// protected void CreateServerAndClients(int numberOfClients) { + VerboseDebug($"Entering {nameof(CreateServerAndClients)}"); + CreatePlayerPrefab(); // Create multiple NetworkManager instances @@ -235,6 +277,8 @@ protected void CreateServerAndClients(int numberOfClients) // Provides opportunity to allow child derived classes to // modify the NetworkManager's configuration before starting. OnServerAndClientsCreated(); + + VerboseDebug($"Exiting {nameof(CreateServerAndClients)}"); } /// @@ -272,6 +316,8 @@ protected IEnumerator StartServerAndClients() { if (CanStartServerAndClients()) { + VerboseDebug($"Entering {nameof(StartServerAndClients)}"); + // Start the instances and pass in our SceneManagerInitialization action that is invoked immediately after host-server // is started and after each client is started. if (!NetcodeIntegrationTestHelpers.Start(m_UseHost, m_ServerNetworkManager, m_ClientNetworkManagers)) @@ -290,11 +336,6 @@ protected IEnumerator StartServerAndClients() Assert.False(s_GlobalTimeoutHelper.TimedOut, $"{nameof(StartServerAndClients)} timed out waiting for all clients to be connected!"); - if (s_GlobalTimeoutHelper.TimedOut) - { - yield return null; - } - if (m_UseHost || m_ServerNetworkManager.IsHost) { // Add the server player instance to all m_ClientSidePlayerNetworkObjects entries @@ -332,6 +373,8 @@ protected IEnumerator StartServerAndClients() // Notification that at this time the server and client(s) are instantiated, // started, and connected on both sides. yield return OnServerAndClientsConnected(); + + VerboseDebug($"Exiting {nameof(StartServerAndClients)}"); } } @@ -398,6 +441,7 @@ private bool ClientSceneHandler_CanClientsLoad() /// protected void ShutdownAndCleanUp() { + VerboseDebug($"Entering {nameof(ShutdownAndCleanUp)}"); // Shutdown and clean up both of our NetworkManager instances try { @@ -427,6 +471,7 @@ protected void ShutdownAndCleanUp() // reset the m_ServerWaitForTick for the next test to initialize s_DefaultWaitForTick = new WaitForSeconds(1.0f / k_DefaultTickRate); + VerboseDebug($"Exiting {nameof(ShutdownAndCleanUp)}"); } /// @@ -441,12 +486,15 @@ protected virtual IEnumerator OnTearDown() [UnityTearDown] public IEnumerator TearDown() { + VerboseDebug($"Entering {nameof(TearDown)}"); yield return OnTearDown(); if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.PerTest) { ShutdownAndCleanUp(); } + + VerboseDebug($"Exiting {nameof(TearDown)}"); } /// @@ -462,6 +510,7 @@ protected virtual void OnOneTimeTearDown() [OneTimeTearDown] public void OneTimeTearDown() { + VerboseDebug($"Entering {nameof(OneTimeTearDown)}"); OnOneTimeTearDown(); if (m_NetworkManagerInstatiationMode == NetworkManagerInstatiationMode.AllTests) @@ -471,6 +520,8 @@ public void OneTimeTearDown() // Disable NetcodeIntegrationTest auto-label feature NetcodeIntegrationTestHelpers.RegisterNetcodeIntegrationTest(false); + + VerboseDebug($"Exiting {nameof(OneTimeTearDown)}"); } /// diff --git a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs index 7135030..ff733d7 100644 --- a/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs +++ b/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using Unity.Netcode.Transports.UTP; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; @@ -109,9 +110,7 @@ public void OnAfterHandleMessage(ref T message, ref NetworkContext context) w public enum InstanceTransport { SIP, -#if UTP_ADAPTER UTP -#endif } internal static IntegrationTestSceneHandler ClientSceneHandler = null; @@ -172,12 +171,10 @@ internal static NetworkTransport CreateInstanceTransport(InstanceTransport insta switch (instanceTransport) { case InstanceTransport.SIP: - default: return go.AddComponent(); -#if UTP_ADAPTER + default: case InstanceTransport.UTP: return go.AddComponent(); -#endif } } diff --git a/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef b/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef index 5390755..03c978b 100644 --- a/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef +++ b/TestHelpers/Runtime/com.unity.netcode.testhelpers.runtime.asmdef @@ -3,7 +3,6 @@ "rootNamespace": "Unity.Netcode.TestHelpers.Runtime", "references": [ "Unity.Netcode.Runtime", - "Unity.Netcode.Adapter.UTP", "Unity.Multiplayer.MetricTypes", "Unity.Multiplayer.NetStats", "Unity.Multiplayer.Tools.MetricTypes", @@ -18,15 +17,10 @@ "expression": "", "define": "MULTIPLAYER_TOOLS" }, - { - "name": "com.unity.netcode.adapter.utp", - "expression": "", - "define": "UTP_ADAPTER" - }, { "name": "com.unity.multiplayer.tools", - "expression": "1.0.0-pre.4", - "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_4" + "expression": "1.0.0-pre.7", + "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_7" } ] } \ No newline at end of file diff --git a/Tests/Editor/IndexAllocatorTests.cs b/Tests/Editor/IndexAllocatorTests.cs deleted file mode 100644 index 2a04a97..0000000 --- a/Tests/Editor/IndexAllocatorTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using NUnit.Framework; -using UnityEngine; - -namespace Unity.Netcode.EditorTests -{ - public class FixedAllocatorTest - { - [Test] - public void SimpleTest() - { - int pos; - - var allocator = new IndexAllocator(20000, 200); - allocator.DebugDisplay(); - - // allocate 20 bytes - Assert.IsTrue(allocator.Allocate(0, 20, out pos)); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // can't ask for negative amount of memory - Assert.IsFalse(allocator.Allocate(1, -20, out pos)); - Assert.IsTrue(allocator.Verify()); - - // can't ask for deallocation of negative index - Assert.IsFalse(allocator.Deallocate(-1)); - Assert.IsTrue(allocator.Verify()); - - // can't ask for the same index twice - Assert.IsFalse(allocator.Allocate(0, 20, out pos)); - Assert.IsTrue(allocator.Verify()); - - // allocate another 20 bytes - Assert.IsTrue(allocator.Allocate(1, 20, out pos)); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // allocate a third 20 bytes - Assert.IsTrue(allocator.Allocate(2, 20, out pos)); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // deallocate 0 - Assert.IsTrue(allocator.Deallocate(0)); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // deallocate 1 - allocator.Deallocate(1); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // deallocate 2 - allocator.Deallocate(2); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // allocate 50 bytes - Assert.IsTrue(allocator.Allocate(0, 50, out pos)); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // allocate another 50 bytes - Assert.IsTrue(allocator.Allocate(1, 50, out pos)); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // allocate a third 50 bytes - Assert.IsTrue(allocator.Allocate(2, 50, out pos)); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // deallocate 1, a block in the middle this time - allocator.Deallocate(1); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - - // allocate a smaller one in its place - allocator.Allocate(1, 25, out pos); - allocator.DebugDisplay(); - Assert.IsTrue(allocator.Verify()); - } - - [Test] - public void ReuseTest() - { - int count = 100; - bool[] used = new bool[count]; - int[] pos = new int[count]; - int iterations = 10000; - - var allocator = new IndexAllocator(20000, 200); - - for (int i = 0; i < iterations; i++) - { - int index = Random.Range(0, count); - if (used[index]) - { - Assert.IsTrue(allocator.Deallocate(index)); - used[index] = false; - } - else - { - int position; - int length = 10 * Random.Range(1, 10); - Assert.IsTrue(allocator.Allocate(index, length, out position)); - pos[index] = position; - used[index] = true; - } - Assert.IsTrue(allocator.Verify()); - } - allocator.DebugDisplay(); - } - - } -} diff --git a/Tests/Editor/IndexAllocatorTests.cs.meta b/Tests/Editor/IndexAllocatorTests.cs.meta deleted file mode 100644 index 760a4cc..0000000 --- a/Tests/Editor/IndexAllocatorTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 85ac488e1432d49668c711fa625a0743 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Editor/NetworkManagerConfigurationTests.cs b/Tests/Editor/NetworkManagerConfigurationTests.cs index 7f36b2c..a5e9532 100644 --- a/Tests/Editor/NetworkManagerConfigurationTests.cs +++ b/Tests/Editor/NetworkManagerConfigurationTests.cs @@ -34,5 +34,49 @@ public void NestedNetworkManagerCheck() // Clean up Object.DestroyImmediate(parent); } + + public enum NetworkObjectPlacement + { + Root, // Added to the same root GameObject + Child // Added to a child GameObject + } + + [Test] + public void NetworkObjectNotAllowed([Values] NetworkObjectPlacement networkObjectPlacement) + { + var gameObject = new GameObject(nameof(NetworkManager)); + var targetforNetworkObject = gameObject; + + if (networkObjectPlacement == NetworkObjectPlacement.Child) + { + var childGameObject = new GameObject($"{nameof(NetworkManager)}-Child"); + childGameObject.transform.parent = targetforNetworkObject.transform; + targetforNetworkObject = childGameObject; + } + + var networkManager = gameObject.AddComponent(); + + // Trap for the error message generated when a NetworkObject is discovered on the same GameObject or any children under it + LogAssert.Expect(LogType.Error, NetworkManagerHelper.Singleton.NetworkManagerAndNetworkObjectNotAllowedMessage()); + + // Add the NetworkObject + var networkObject = targetforNetworkObject.AddComponent(); + + // Since this is an in-editor test, we must force this invocation + NetworkManagerHelper.Singleton.CheckAndNotifyUserNetworkObjectRemoved(networkManager, true); + + // Validate that the NetworkObject has been removed + if (networkObjectPlacement == NetworkObjectPlacement.Root) + { + Assert.IsNull(networkManager.gameObject.GetComponent(), $"There is still a {nameof(NetworkObject)} on {nameof(NetworkManager)}'s GameObject!"); + } + else + { + Assert.IsNull(networkManager.gameObject.GetComponentInChildren(), $"There is still a {nameof(NetworkObject)} on {nameof(NetworkManager)}'s child GameObject!"); + } + + // Clean up + Object.DestroyImmediate(gameObject); + } } } diff --git a/Tests/Editor/NetworkVar.meta b/Tests/Editor/NetworkVar.meta new file mode 100644 index 0000000..8382298 --- /dev/null +++ b/Tests/Editor/NetworkVar.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ed3be13d96c34bc4b8676ce550cee041 +timeCreated: 1647861659 \ No newline at end of file diff --git a/Tests/Editor/NetworkVar/NetworkVarTests.cs b/Tests/Editor/NetworkVar/NetworkVarTests.cs new file mode 100644 index 0000000..61bc147 --- /dev/null +++ b/Tests/Editor/NetworkVar/NetworkVarTests.cs @@ -0,0 +1,41 @@ +using NUnit.Framework; + +namespace Unity.Netcode.EditorTests.NetworkVar +{ + public class NetworkVarTests + { + [Test] + public void TestAssignmentUnchanged() + { + var intVar = new NetworkVariable(); + + intVar.Value = 314159265; + + intVar.OnValueChanged += (value, newValue) => + { + Assert.Fail("OnValueChanged was invoked when setting the same value"); + }; + + intVar.Value = 314159265; + } + + [Test] + public void TestAssignmentChanged() + { + var intVar = new NetworkVariable(); + + intVar.Value = 314159265; + + var changed = false; + + intVar.OnValueChanged += (value, newValue) => + { + changed = true; + }; + + intVar.Value = 314159266; + + Assert.True(changed); + } + } +} diff --git a/Tests/Editor/NetworkVar/NetworkVarTests.cs.meta b/Tests/Editor/NetworkVar/NetworkVarTests.cs.meta new file mode 100644 index 0000000..a282884 --- /dev/null +++ b/Tests/Editor/NetworkVar/NetworkVarTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a7cdd8c1251f4352b1f1d4825dc85182 +timeCreated: 1647861669 \ No newline at end of file diff --git a/Tests/Editor/Serialization/FastBufferReaderTests.cs b/Tests/Editor/Serialization/FastBufferReaderTests.cs index 1286dc1..0c469a2 100644 --- a/Tests/Editor/Serialization/FastBufferReaderTests.cs +++ b/Tests/Editor/Serialization/FastBufferReaderTests.cs @@ -393,6 +393,34 @@ public void WhenCreatingAReaderFromAnEmptyBuffer_LengthIsZero() } } + [Test] + public void WhenCreatingNewFastBufferReader_IsInitializedIsTrue() + { + var array = new NativeArray(100, Allocator.Temp); + var reader = new FastBufferReader(array, Allocator.Temp); + Assert.AreEqual(true, reader.IsInitialized); + reader.Dispose(); + array.Dispose(); + } + + [Test] + public void WhenDisposingFastBufferReader_IsInitializedIsFalse() + { + var array = new NativeArray(100, Allocator.Temp); + var reader = new FastBufferReader(array, Allocator.Temp); + reader.Dispose(); + Assert.AreEqual(false, reader.IsInitialized); + array.Dispose(); + Assert.AreEqual(false, reader.IsInitialized); + } + + [Test] + public void WhenUsingDefaultFastBufferReader_IsInitializedIsFalse() + { + FastBufferReader writer = default; + Assert.AreEqual(false, writer.IsInitialized); + } + [Test] public void WhenCallingReadByteWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() { diff --git a/Tests/Editor/Serialization/FastBufferWriterTests.cs b/Tests/Editor/Serialization/FastBufferWriterTests.cs index 6b141e6..1512c50 100644 --- a/Tests/Editor/Serialization/FastBufferWriterTests.cs +++ b/Tests/Editor/Serialization/FastBufferWriterTests.cs @@ -891,6 +891,29 @@ public void WhenCreatingNewFastBufferWriter_MaxCapacityIsCorrect() writer.Dispose(); } + [Test] + public void WhenCreatingNewFastBufferWriter_IsInitializedIsTrue() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + Assert.AreEqual(true, writer.IsInitialized); + writer.Dispose(); + } + + [Test] + public void WhenDisposingFastBufferWriter_IsInitializedIsFalse() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + writer.Dispose(); + Assert.AreEqual(false, writer.IsInitialized); + } + + [Test] + public void WhenUsingDefaultFastBufferWriter_IsInitializedIsFalse() + { + FastBufferWriter writer = default; + Assert.AreEqual(false, writer.IsInitialized); + } + [Test] public void WhenRequestingWritePastBoundsForNonGrowingWriter_TryBeginWriteReturnsFalse() { diff --git a/Tests/Editor/SnapshotRttTests.cs b/Tests/Editor/SnapshotRttTests.cs deleted file mode 100644 index 697af30..0000000 --- a/Tests/Editor/SnapshotRttTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using NUnit.Framework; - -namespace Unity.Netcode.EditorTests -{ - public class SnapshotRttTests - { - private const double k_Epsilon = 0.0001; - - [Test] - public void TestBasicRtt() - { - var snapshot = new SnapshotSystem(null, new NetworkConfig(), null); - var client1 = snapshot.GetConnectionRtt(0); - - client1.NotifySend(0, 0.0); - client1.NotifySend(1, 10.0); - - client1.NotifyAck(1, 15.0); - - client1.NotifySend(2, 20.0); - client1.NotifySend(3, 30.0); - client1.NotifySend(4, 32.0); - - client1.NotifyAck(4, 38.0); - client1.NotifyAck(3, 40.0); - - ConnectionRtt.Rtt ret = client1.GetRtt(); - Assert.True(ret.AverageSec < 7.0 + k_Epsilon); - Assert.True(ret.AverageSec > 7.0 - k_Epsilon); - Assert.True(ret.WorstSec < 10.0 + k_Epsilon); - Assert.True(ret.WorstSec > 10.0 - k_Epsilon); - Assert.True(ret.BestSec < 5.0 + k_Epsilon); - Assert.True(ret.BestSec > 5.0 - k_Epsilon); - - // note: `last` latency is latest received Ack, not latest sent sequence. - Assert.True(ret.LastSec < 10.0 + k_Epsilon); - Assert.True(ret.LastSec > 10.0 - k_Epsilon); - } - - [Test] - public void TestEdgeCasesRtt() - { - var snapshot = new SnapshotSystem(null, new NetworkConfig(), null); - var client1 = snapshot.GetConnectionRtt(0); - var iterationCount = NetworkConfig.RttWindowSize * 3; - var extraCount = NetworkConfig.RttWindowSize * 2; - - // feed in some messages - for (var iteration = 0; iteration < iterationCount; iteration++) - { - client1.NotifySend(iteration, 25.0 * iteration); - } - // ack some random ones in there (1 out of each 9), always 7.0 later - for (var iteration = 0; iteration < iterationCount; iteration += 9) - { - client1.NotifyAck(iteration, 25.0 * iteration + 7.0); - } - // ack some unused key, to check it doesn't throw off the values - for (var iteration = iterationCount; iteration < iterationCount + extraCount; iteration++) - { - client1.NotifyAck(iteration, 42.0); - } - - ConnectionRtt.Rtt ret = client1.GetRtt(); - Assert.True(ret.AverageSec < 7.0 + k_Epsilon); - Assert.True(ret.AverageSec > 7.0 - k_Epsilon); - Assert.True(ret.WorstSec < 7.0 + k_Epsilon); - Assert.True(ret.WorstSec > 7.0 - k_Epsilon); - Assert.True(ret.BestSec < 7.0 + k_Epsilon); - Assert.True(ret.BestSec > 7.0 - k_Epsilon); - } - } -} diff --git a/Tests/Editor/SnapshotRttTests.cs.meta b/Tests/Editor/SnapshotRttTests.cs.meta deleted file mode 100644 index 8cb7310..0000000 --- a/Tests/Editor/SnapshotRttTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a05afab7f08d44c07b2c5e144ba0b45a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Editor/SnapshotTests.cs b/Tests/Editor/SnapshotTests.cs deleted file mode 100644 index 7fc465e..0000000 --- a/Tests/Editor/SnapshotTests.cs +++ /dev/null @@ -1,363 +0,0 @@ -using System; -using System.Collections.Generic; -using Unity.Collections; -using UnityEngine; -using NUnit.Framework; -using Random = System.Random; - -namespace Unity.Netcode.EditorTests -{ - public class SnapshotTests - { - private SnapshotSystem m_SendSnapshot; - private SnapshotSystem m_RecvSnapshot; - - private NetworkTimeSystem m_SendTimeSystem; - private NetworkTickSystem m_SendTickSystem; - private NetworkTimeSystem m_RecvTimeSystem; - private NetworkTickSystem m_RecvTickSystem; - - private int m_SpawnedObjectCount; - private int m_DespawnedObjectCount; - private int m_NextSequence; - private uint m_TicksPerSec = 15; - private int m_MinSpawns; - private int m_MinDespawns; - - private bool m_ExpectSpawns; - private bool m_ExpectDespawns; - private bool m_LoseNextMessage; - private bool m_PassBackResponses; - - public void Prepare() - { - PrepareSendSideSnapshot(); - PrepareRecvSideSnapshot(); - } - - public void AdvanceOneTickSendSide() - { - m_SendTimeSystem.Advance(1.0f / m_TicksPerSec); - m_SendTickSystem.UpdateTick(m_SendTimeSystem.LocalTime, m_SendTimeSystem.ServerTime); - m_SendSnapshot.NetworkUpdate(NetworkUpdateStage.EarlyUpdate); - } - - public void AdvanceOneTickRecvSide() - { - m_RecvTimeSystem.Advance(1.0f / m_TicksPerSec); - m_RecvTickSystem.UpdateTick(m_RecvTimeSystem.LocalTime, m_RecvTimeSystem.ServerTime); - m_RecvSnapshot.NetworkUpdate(NetworkUpdateStage.EarlyUpdate); - } - - public void AdvanceOneTick() - { - AdvanceOneTickSendSide(); - AdvanceOneTickRecvSide(); - } - - internal int SpawnObject(SnapshotSpawnCommand command) - { - m_SpawnedObjectCount++; - return 0; - } - - internal int DespawnObject(SnapshotDespawnCommand command) - { - m_DespawnedObjectCount++; - return 0; - } - - internal int SendMessage(ref SnapshotDataMessage message, NetworkDelivery delivery, ulong clientId) - { - if (!m_PassBackResponses) - { - // we're not ack'ing anything, so those should stay 0 - Debug.Assert(message.Ack.LastReceivedSequence == 0); - } - - Debug.Assert(message.Ack.ReceivedSequenceMask == 0); - Debug.Assert(message.Sequence == m_NextSequence); // sequence has to be the expected one - - if (m_ExpectSpawns) - { - Debug.Assert(message.Spawns.Length >= m_MinSpawns); // there has to be multiple spawns per SnapshotMessage - } - else - { - Debug.Assert(message.Spawns.Length == 0); // Spawns were not expected - } - - if (m_ExpectDespawns) - { - Debug.Assert(message.Despawns.Length >= m_MinDespawns); // there has to be multiple despawns per SnapshotMessage - } - else - { - Debug.Assert(message.Despawns.IsEmpty); // this test should not have despawns - } - - Debug.Assert(message.Entries.Length == 0); - - m_NextSequence++; - - if (!m_LoseNextMessage) - { - using var writer = new FastBufferWriter(1024, Allocator.Temp); - message.Serialize(writer); - using var reader = new FastBufferReader(writer, Allocator.Temp); - var context = new NetworkContext { SenderId = 0, Timestamp = 0.0f, SystemOwner = new Tuple(m_RecvSnapshot, 0) }; - var newMessage = new SnapshotDataMessage(); - newMessage.Deserialize(reader, ref context); - newMessage.Handle(ref context); - } - - return 0; - } - - internal int SendMessageRecvSide(ref SnapshotDataMessage message, NetworkDelivery delivery, ulong clientId) - { - if (m_PassBackResponses) - { - using var writer = new FastBufferWriter(1024, Allocator.Temp); - message.Serialize(writer); - using var reader = new FastBufferReader(writer, Allocator.Temp); - var context = new NetworkContext { SenderId = 0, Timestamp = 0.0f, SystemOwner = new Tuple(m_SendSnapshot, 1) }; - var newMessage = new SnapshotDataMessage(); - newMessage.Deserialize(reader, ref context); - newMessage.Handle(ref context); - } - - return 0; - } - - - private void PrepareSendSideSnapshot() - { - var config = new NetworkConfig(); - - m_SendTickSystem = new NetworkTickSystem(m_TicksPerSec, 0.0, 0.0); - m_SendTimeSystem = new NetworkTimeSystem(0.2, 0.2, 1.0); - - config.UseSnapshotDelta = false; - config.UseSnapshotSpawn = true; - - m_SendSnapshot = new SnapshotSystem(null, config, m_SendTickSystem); - - m_SendSnapshot.IsServer = true; - m_SendSnapshot.IsConnectedClient = false; - m_SendSnapshot.ServerClientId = 0; - m_SendSnapshot.ConnectedClientsId.Clear(); - m_SendSnapshot.ConnectedClientsId.Add(0); - m_SendSnapshot.ConnectedClientsId.Add(1); - m_SendSnapshot.MockSendMessage = SendMessage; - m_SendSnapshot.MockSpawnObject = SpawnObject; - m_SendSnapshot.MockDespawnObject = DespawnObject; - } - - private void PrepareRecvSideSnapshot() - { - var config = new NetworkConfig(); - - m_RecvTickSystem = new NetworkTickSystem(m_TicksPerSec, 0.0, 0.0); - m_RecvTimeSystem = new NetworkTimeSystem(0.2, 0.2, 1.0); - - config.UseSnapshotDelta = false; - config.UseSnapshotSpawn = true; - - m_RecvSnapshot = new SnapshotSystem(null, config, m_RecvTickSystem); - - m_RecvSnapshot.IsServer = false; - m_RecvSnapshot.IsConnectedClient = true; - m_RecvSnapshot.ServerClientId = 0; - m_RecvSnapshot.ConnectedClientsId.Clear(); - m_SendSnapshot.ConnectedClientsId.Add(0); - m_SendSnapshot.ConnectedClientsId.Add(1); - m_RecvSnapshot.MockSendMessage = SendMessageRecvSide; - m_RecvSnapshot.MockSpawnObject = SpawnObject; - m_RecvSnapshot.MockDespawnObject = DespawnObject; - } - - private void SendSpawnToSnapshot(ulong objectId) - { - SnapshotSpawnCommand command = default; - // identity - command.NetworkObjectId = objectId; - // archetype - command.GlobalObjectIdHash = 0; - command.IsSceneObject = true; - // parameters - command.IsPlayerObject = false; - command.OwnerClientId = 0; - command.ParentNetworkId = 0; - command.ObjectPosition = default; - command.ObjectRotation = default; - command.ObjectScale = new Vector3(1.0f, 1.0f, 1.0f); - command.TargetClientIds = new List { 1 }; - m_SendSnapshot.Spawn(command); - } - - private void SendDespawnToSnapshot(ulong objectId) - { - SnapshotDespawnCommand command = default; - // identity - command.NetworkObjectId = objectId; - command.TargetClientIds = new List { 1 }; - m_SendSnapshot.Despawn(command); - } - - [Test] - public void TestSnapshotSpawn() - { - Prepare(); - - m_SpawnedObjectCount = 0; - m_NextSequence = 0; - m_ExpectSpawns = true; - m_ExpectDespawns = false; - m_MinSpawns = 2; // many spawns are to be sent together - m_LoseNextMessage = false; - m_PassBackResponses = false; - - var ticksToRun = 20; - - // spawns one more than current buffer size - var objectsToSpawn = m_SendSnapshot.SpawnsBufferCount + 1; - - for (int i = 0; i < objectsToSpawn; i++) - { - SendSpawnToSnapshot((ulong)i); - } - - for (int i = 0; i < ticksToRun; i++) - { - AdvanceOneTick(); - } - - Debug.Assert(m_SpawnedObjectCount == objectsToSpawn); - Debug.Assert(m_SendSnapshot.SpawnsBufferCount > objectsToSpawn); // spawn buffer should have grown - } - - [Test] - public void TestSnapshotSpawnDespawns() - { - Prepare(); - - // test that buffers actually shrink and will grow back to needed size - m_SendSnapshot.ReduceBufferUsage(); - m_RecvSnapshot.ReduceBufferUsage(); - Debug.Assert(m_SendSnapshot.SpawnsBufferCount == 1); - Debug.Assert(m_SendSnapshot.DespawnsBufferCount == 1); - Debug.Assert(m_RecvSnapshot.SpawnsBufferCount == 1); - Debug.Assert(m_RecvSnapshot.DespawnsBufferCount == 1); - - m_SpawnedObjectCount = 0; - m_DespawnedObjectCount = 0; - - m_NextSequence = 0; - m_ExpectSpawns = true; - m_ExpectDespawns = false; - m_MinDespawns = 2; // many despawns are to be sent together - m_LoseNextMessage = false; - m_PassBackResponses = false; - - var ticksToRun = 20; - - // spawns one more than current buffer size - var objectsToSpawn = 10; - - for (int i = 0; i < objectsToSpawn; i++) - { - SendSpawnToSnapshot((ulong)i); - } - - for (int i = 0; i < ticksToRun; i++) - { - AdvanceOneTick(); - } - - for (int i = 0; i < objectsToSpawn; i++) - { - SendDespawnToSnapshot((ulong)i); - } - - m_ExpectSpawns = true; // the un'acked spawns will still be present - m_MinSpawns = 1; // but we don't really care how they are grouped then - m_ExpectDespawns = true; - - for (int i = 0; i < ticksToRun; i++) - { - AdvanceOneTick(); - } - - Debug.Assert(m_DespawnedObjectCount == objectsToSpawn); - } - - [Test] - public void TestSnapshotMessageLoss() - { - var r = new Random(); - Prepare(); - - m_SpawnedObjectCount = 0; - m_NextSequence = 0; - m_ExpectSpawns = true; - m_ExpectDespawns = false; - m_MinSpawns = 1; - m_LoseNextMessage = false; - m_PassBackResponses = false; - - var ticksToRun = 10; - - for (int i = 0; i < ticksToRun; i++) - { - m_LoseNextMessage = (r.Next() % 2) > 0; - - SendSpawnToSnapshot((ulong)i); - AdvanceOneTick(); - } - - m_LoseNextMessage = false; - AdvanceOneTick(); - AdvanceOneTick(); - - Debug.Assert(m_SpawnedObjectCount == ticksToRun); - } - - [Test] - public void TestSnapshotAcks() - { - Prepare(); - - m_SpawnedObjectCount = 0; - m_NextSequence = 0; - m_ExpectSpawns = true; - m_ExpectDespawns = false; - m_MinSpawns = 1; - m_LoseNextMessage = false; - m_PassBackResponses = true; - - var objectsToSpawn = 10; - - for (int i = 0; i < objectsToSpawn; i++) - { - SendSpawnToSnapshot((ulong)i); - } - AdvanceOneTickSendSide(); // let's tick the send multiple time, to check it still tries to send - AdvanceOneTick(); - - m_ExpectSpawns = false; // all spawns should have made it back and forth and be absent from next messages - AdvanceOneTick(); - - for (int i = 0; i < objectsToSpawn; i++) - { - SendDespawnToSnapshot((ulong)i); - } - - m_ExpectDespawns = true; // we should now be seeing despawns - AdvanceOneTickSendSide(); // let's tick the send multiple time, to check it still tries to send - AdvanceOneTick(); - - Debug.Assert(m_SpawnedObjectCount == objectsToSpawn); - } - } -} diff --git a/Tests/Editor/SnapshotTests.cs.meta b/Tests/Editor/SnapshotTests.cs.meta deleted file mode 100644 index 294c508..0000000 --- a/Tests/Editor/SnapshotTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3d41788be1de34b7c8bcfce6a2877754 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Tests/Editor/Transports.meta b/Tests/Editor/Transports.meta new file mode 100644 index 0000000..3ec56fb --- /dev/null +++ b/Tests/Editor/Transports.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f4ecf3bb8c5654c1aae7f73d21e8c56e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Transports/BatchedReceiveQueueTests.cs b/Tests/Editor/Transports/BatchedReceiveQueueTests.cs new file mode 100644 index 0000000..4bbc5be --- /dev/null +++ b/Tests/Editor/Transports/BatchedReceiveQueueTests.cs @@ -0,0 +1,193 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using Unity.Netcode.Transports.UTP; +using Unity.Networking.Transport; + +namespace Unity.Netcode.EditorTests +{ + public class BatchedReceiveQueueTests + { + [Test] + public void BatchedReceiveQueue_EmptyReader() + { + var data = new NativeArray(0, Allocator.Temp); + + var reader = new DataStreamReader(data); + var q = new BatchedReceiveQueue(reader); + Assert.AreEqual(default(ArraySegment), q.PopMessage()); + Assert.True(q.IsEmpty); + } + + [Test] + public void BatchedReceiveQueue_SingleMessage() + { + var dataLength = sizeof(int) + 1; + + var data = new NativeArray(dataLength, Allocator.Temp); + + var writer = new DataStreamWriter(data); + writer.WriteInt(1); + writer.WriteByte((byte)42); + + var reader = new DataStreamReader(data); + var q = new BatchedReceiveQueue(reader); + + Assert.False(q.IsEmpty); + + var message = q.PopMessage(); + Assert.AreEqual(1, message.Count); + Assert.AreEqual((byte)42, message.Array[message.Offset]); + + Assert.AreEqual(default(ArraySegment), q.PopMessage()); + Assert.True(q.IsEmpty); + } + + [Test] + public void BatchedReceiveQueue_MultipleMessages() + { + var dataLength = (sizeof(int) + 1) * 2; + + var data = new NativeArray(dataLength, Allocator.Temp); + + var writer = new DataStreamWriter(data); + writer.WriteInt(1); + writer.WriteByte((byte)42); + writer.WriteInt(1); + writer.WriteByte((byte)142); + + var reader = new DataStreamReader(data); + var q = new BatchedReceiveQueue(reader); + + Assert.False(q.IsEmpty); + + var message1 = q.PopMessage(); + Assert.AreEqual(1, message1.Count); + Assert.AreEqual((byte)42, message1.Array[message1.Offset]); + + var message2 = q.PopMessage(); + Assert.AreEqual(1, message2.Count); + Assert.AreEqual((byte)142, message2.Array[message2.Offset]); + + Assert.AreEqual(default(ArraySegment), q.PopMessage()); + Assert.True(q.IsEmpty); + } + + [Test] + public void BatchedReceiveQueue_PartialMessage() + { + var dataLength = sizeof(int); + + var data = new NativeArray(dataLength, Allocator.Temp); + + var writer = new DataStreamWriter(data); + writer.WriteInt(42); + + var reader = new DataStreamReader(data); + var q = new BatchedReceiveQueue(reader); + + Assert.False(q.IsEmpty); + Assert.AreEqual(default(ArraySegment), q.PopMessage()); + } + + [Test] + public void BatchedReceiveQueue_PushReader_ToFilledQueue() + { + var data1Length = sizeof(int); + var data2Length = sizeof(byte); + + var data1 = new NativeArray(data1Length, Allocator.Temp); + var data2 = new NativeArray(data2Length, Allocator.Temp); + + var writer1 = new DataStreamWriter(data1); + writer1.WriteInt(1); + var writer2 = new DataStreamWriter(data2); + writer2.WriteByte(42); + + var reader1 = new DataStreamReader(data1); + var reader2 = new DataStreamReader(data2); + + var q = new BatchedReceiveQueue(reader1); + + Assert.False(q.IsEmpty); + + q.PushReader(reader2); + + Assert.False(q.IsEmpty); + + var message = q.PopMessage(); + Assert.AreEqual(1, message.Count); + Assert.AreEqual((byte)42, message.Array[message.Offset]); + + Assert.AreEqual(default(ArraySegment), q.PopMessage()); + Assert.True(q.IsEmpty); + } + + [Test] + public void BatchedReceiveQueue_PushReader_ToPartiallyFilledQueue() + { + var dataLength = sizeof(int) + 1; + + var data = new NativeArray(dataLength, Allocator.Temp); + + var writer = new DataStreamWriter(data); + writer.WriteInt(1); + writer.WriteByte((byte)42); + + var reader = new DataStreamReader(data); + var q = new BatchedReceiveQueue(reader); + + reader = new DataStreamReader(data); + q.PushReader(reader); + + var message = q.PopMessage(); + Assert.AreEqual(1, message.Count); + Assert.AreEqual((byte)42, message.Array[message.Offset]); + + reader = new DataStreamReader(data); + q.PushReader(reader); + + message = q.PopMessage(); + Assert.AreEqual(1, message.Count); + Assert.AreEqual((byte)42, message.Array[message.Offset]); + + message = q.PopMessage(); + Assert.AreEqual(1, message.Count); + Assert.AreEqual((byte)42, message.Array[message.Offset]); + + Assert.AreEqual(default(ArraySegment), q.PopMessage()); + Assert.True(q.IsEmpty); + } + + [Test] + public void BatchedReceiveQueue_PushReader_ToEmptyQueue() + { + var dataLength = sizeof(int) + 1; + + var data = new NativeArray(dataLength, Allocator.Temp); + + var writer = new DataStreamWriter(data); + writer.WriteInt(1); + writer.WriteByte((byte)42); + + var reader = new DataStreamReader(data); + var q = new BatchedReceiveQueue(reader); + + Assert.False(q.IsEmpty); + + q.PopMessage(); + + Assert.True(q.IsEmpty); + + reader = new DataStreamReader(data); + q.PushReader(reader); + + var message = q.PopMessage(); + Assert.AreEqual(1, message.Count); + Assert.AreEqual((byte)42, message.Array[message.Offset]); + + Assert.AreEqual(default(ArraySegment), q.PopMessage()); + Assert.True(q.IsEmpty); + } + } +} diff --git a/Tests/Editor/Transports/BatchedReceiveQueueTests.cs.meta b/Tests/Editor/Transports/BatchedReceiveQueueTests.cs.meta new file mode 100644 index 0000000..7d0297b --- /dev/null +++ b/Tests/Editor/Transports/BatchedReceiveQueueTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aabb21b30a80142ea86e59d1b4d5c587 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Transports/BatchedSendQueueTests.cs b/Tests/Editor/Transports/BatchedSendQueueTests.cs new file mode 100644 index 0000000..34a8a53 --- /dev/null +++ b/Tests/Editor/Transports/BatchedSendQueueTests.cs @@ -0,0 +1,266 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using Unity.Netcode.Transports.UTP; +using Unity.Networking.Transport; + +namespace Unity.Netcode.EditorTests +{ + public class BatchedSendQueueTests + { + private const int k_TestQueueCapacity = 1024; + private const int k_TestMessageSize = 42; + + private ArraySegment m_TestMessage; + + private void AssertIsTestMessage(NativeArray data) + { + var reader = new DataStreamReader(data); + Assert.AreEqual(k_TestMessageSize, reader.ReadInt()); + for (int i = 0; i < k_TestMessageSize; i++) + { + Assert.AreEqual(m_TestMessage.Array[i], reader.ReadByte()); + } + } + + [OneTimeSetUp] + public void InitializeTestMessage() + { + var data = new byte[k_TestMessageSize]; + for (int i = 0; i < k_TestMessageSize; i++) + { + data[i] = (byte)i; + } + m_TestMessage = new ArraySegment(data); + } + + [Test] + public void BatchedSendQueue_EmptyOnCreation() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + + Assert.AreEqual(0, q.Length); + Assert.True(q.IsEmpty); + } + + [Test] + public void BatchedSendQueue_NotCreatedAfterDispose() + { + var q = new BatchedSendQueue(k_TestQueueCapacity); + q.Dispose(); + Assert.False(q.IsCreated); + } + + [Test] + public void BatchedSendQueue_PushMessageReturnValue() + { + // Will fit a single test message, but not two (with overhead included). + var queueCapacity = (k_TestMessageSize * 2) + BatchedSendQueue.PerMessageOverhead; + + using var q = new BatchedSendQueue(queueCapacity); + + Assert.True(q.PushMessage(m_TestMessage)); + Assert.False(q.PushMessage(m_TestMessage)); + } + + [Test] + public void BatchedSendQueue_LengthIncreasedAfterPush() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + + q.PushMessage(m_TestMessage); + Assert.AreEqual(k_TestMessageSize + BatchedSendQueue.PerMessageOverhead, q.Length); + } + + [Test] + public void BatchedSendQueue_PushedMessageGeneratesCopy() + { + var messageLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + var queueCapacity = messageLength * 2; + + using var q = new BatchedSendQueue(queueCapacity); + using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); + + q.PushMessage(m_TestMessage); + q.PushMessage(m_TestMessage); + + q.Consume(messageLength); + Assert.IsTrue(q.PushMessage(m_TestMessage)); + Assert.AreEqual(queueCapacity, q.Length); + } + + [Test] + public void BatchedSendQueue_FillWriterWithMessages_ReturnValue() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); + + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + var filled = q.FillWriterWithMessages(ref writer); + Assert.AreEqual(k_TestMessageSize + BatchedSendQueue.PerMessageOverhead, filled); + } + + [Test] + public void BatchedSendQueue_FillWriterWithMessages_NoopIfNoPushedMessages() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(0, q.FillWriterWithMessages(ref writer)); + } + + [Test] + public void BatchedSendQueue_FillWriterWithMessages_NoopIfNotEnoughCapacity() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(2, Allocator.Temp); + + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(0, q.FillWriterWithMessages(ref writer)); + } + + [Test] + public void BatchedSendQueue_FillWriterWithMessages_SinglePushedMessage() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); + + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + q.FillWriterWithMessages(ref writer); + AssertIsTestMessage(data); + } + + [Test] + public void BatchedSendQueue_FillWriterWithMessages_MultiplePushedMessages() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); + + q.PushMessage(m_TestMessage); + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + q.FillWriterWithMessages(ref writer); + + var messageLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + AssertIsTestMessage(data); + AssertIsTestMessage(data.GetSubArray(messageLength, messageLength)); + } + + [Test] + public void BatchedSendQueue_FillWriterWithMessages_PartialPushedMessages() + { + var messageLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(messageLength, Allocator.Temp); + + q.PushMessage(m_TestMessage); + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(messageLength, q.FillWriterWithMessages(ref writer)); + AssertIsTestMessage(data); + } + + [Test] + public void BatchedSendQueue_FillWriterWithBytes_NoopIfNoData() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(0, q.FillWriterWithBytes(ref writer)); + } + + [Test] + public void BatchedSendQueue_FillWriterWithBytes_WriterCapacityMoreThanLength() + { + var dataLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(k_TestQueueCapacity, Allocator.Temp); + + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(dataLength, q.FillWriterWithBytes(ref writer)); + AssertIsTestMessage(data); + } + + [Test] + public void BatchedSendQueue_FillWriterWithBytes_WriterCapacityLessThanLength() + { + var dataLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(dataLength, Allocator.Temp); + + q.PushMessage(m_TestMessage); + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(dataLength, q.FillWriterWithBytes(ref writer)); + AssertIsTestMessage(data); + } + + [Test] + public void BatchedSendQueue_FillWriterWithBytes_WriterCapacityEqualToLength() + { + var dataLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + + using var q = new BatchedSendQueue(k_TestQueueCapacity); + using var data = new NativeArray(dataLength, Allocator.Temp); + + q.PushMessage(m_TestMessage); + + var writer = new DataStreamWriter(data); + Assert.AreEqual(dataLength, q.FillWriterWithBytes(ref writer)); + AssertIsTestMessage(data); + } + + [Test] + public void BatchedSendQueue_ConsumeLessThanLength() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + + q.PushMessage(m_TestMessage); + q.PushMessage(m_TestMessage); + + var messageLength = k_TestMessageSize + BatchedSendQueue.PerMessageOverhead; + q.Consume(messageLength); + Assert.AreEqual(messageLength, q.Length); + } + + [Test] + public void BatchedSendQueue_ConsumeExactLength() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + + q.PushMessage(m_TestMessage); + + q.Consume(k_TestMessageSize + BatchedSendQueue.PerMessageOverhead); + Assert.AreEqual(0, q.Length); + Assert.True(q.IsEmpty); + } + + [Test] + public void BatchedSendQueue_ConsumeMoreThanLength() + { + using var q = new BatchedSendQueue(k_TestQueueCapacity); + + q.PushMessage(m_TestMessage); + + q.Consume(k_TestQueueCapacity); + Assert.AreEqual(0, q.Length); + Assert.True(q.IsEmpty); + } + } +} diff --git a/Tests/Editor/Transports/BatchedSendQueueTests.cs.meta b/Tests/Editor/Transports/BatchedSendQueueTests.cs.meta new file mode 100644 index 0000000..8107703 --- /dev/null +++ b/Tests/Editor/Transports/BatchedSendQueueTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 51a68dc80bf18443180f3600eb5890d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Transports/UnityTransportTests.cs b/Tests/Editor/Transports/UnityTransportTests.cs new file mode 100644 index 0000000..20b83b0 --- /dev/null +++ b/Tests/Editor/Transports/UnityTransportTests.cs @@ -0,0 +1,84 @@ +using NUnit.Framework; +using Unity.Netcode.Transports.UTP; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + public class UnityTransportTests + { + // Check that starting a server doesn't immediately result in faulted tasks. + [Test] + public void BasicInitServer() + { + UnityTransport transport = new GameObject().AddComponent(); + transport.Initialize(); + + Assert.True(transport.StartServer()); + + transport.Shutdown(); + } + + // Check that starting a client doesn't immediately result in faulted tasks. + [Test] + public void BasicInitClient() + { + UnityTransport transport = new GameObject().AddComponent(); + transport.Initialize(); + + Assert.True(transport.StartClient()); + + transport.Shutdown(); + } + + // Check that we can't restart a server. + [Test] + public void NoRestartServer() + { + UnityTransport transport = new GameObject().AddComponent(); + transport.Initialize(); + + transport.StartServer(); + Assert.False(transport.StartServer()); + + transport.Shutdown(); + } + + // Check that we can't restart a client. + [Test] + public void NoRestartClient() + { + UnityTransport transport = new GameObject().AddComponent(); + transport.Initialize(); + + transport.StartClient(); + Assert.False(transport.StartClient()); + + transport.Shutdown(); + } + + // Check that we can't start both a server and client on the same transport. + [Test] + public void NotBothServerAndClient() + { + UnityTransport transport; + + // Start server then client. + transport = new GameObject().AddComponent(); + transport.Initialize(); + + transport.StartServer(); + Assert.False(transport.StartClient()); + + transport.Shutdown(); + + // Start client then server. + transport = new GameObject().AddComponent(); + transport.Initialize(); + + transport.StartClient(); + Assert.False(transport.StartServer()); + + transport.Shutdown(); + } + } +} diff --git a/Tests/Editor/Transports/UnityTransportTests.cs.meta b/Tests/Editor/Transports/UnityTransportTests.cs.meta new file mode 100644 index 0000000..8399287 --- /dev/null +++ b/Tests/Editor/Transports/UnityTransportTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b0137a26ef0140f0bf5167c09eecb96 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/com.unity.netcode.editortests.asmdef b/Tests/Editor/com.unity.netcode.editortests.asmdef index f0e437d..49fd726 100644 --- a/Tests/Editor/com.unity.netcode.editortests.asmdef +++ b/Tests/Editor/com.unity.netcode.editortests.asmdef @@ -9,7 +9,8 @@ "Unity.Multiplayer.MetricTypes", "Unity.Multiplayer.NetStats", "Unity.Multiplayer.Tools.MetricTypes", - "Unity.Multiplayer.Tools.NetStats" + "Unity.Multiplayer.Tools.NetStats", + "Unity.Networking.Transport" ], "optionalUnityReferences": [ "TestAssemblies" diff --git a/Tests/Runtime/ClientOnlyConnectionTests.cs b/Tests/Runtime/ClientOnlyConnectionTests.cs new file mode 100644 index 0000000..97ea137 --- /dev/null +++ b/Tests/Runtime/ClientOnlyConnectionTests.cs @@ -0,0 +1,78 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode.TestHelpers.Runtime; +#if UNITY_UNET_PRESENT +using Unity.Netcode.Transports.UNET; +#else +using Unity.Netcode.Transports.UTP; +#endif + +namespace Unity.Netcode.RuntimeTests +{ + public class ClientOnlyConnectionTests + { + private NetworkManager m_ClientNetworkManager; + private GameObject m_NetworkManagerGameObject; + private WaitForSeconds m_DefaultWaitForTick = new WaitForSeconds(1.0f / 30); + private bool m_WasDisconnected; + private TimeoutHelper m_TimeoutHelper; + + [SetUp] + public void Setup() + { + m_WasDisconnected = false; + m_NetworkManagerGameObject = new GameObject(); + m_ClientNetworkManager = m_NetworkManagerGameObject.AddComponent(); + m_ClientNetworkManager.NetworkConfig = new NetworkConfig(); +#if UNITY_UNET_PRESENT + m_TimeoutHelper = new TimeoutHelper(30); + m_ClientNetworkManager.NetworkConfig.NetworkTransport = m_NetworkManagerGameObject.AddComponent(); +#else + // Default is 1000ms per connection attempt and 60 connection attempts (60s) + // Currently there is no easy way to set these values other than in-editor + m_TimeoutHelper = new TimeoutHelper(70); + m_ClientNetworkManager.NetworkConfig.NetworkTransport = m_NetworkManagerGameObject.AddComponent(); +#endif + } + + [UnityTest] + public IEnumerator ClientFailsToConnect() + { + // Wait for the disconnected event + m_ClientNetworkManager.OnClientDisconnectCallback += ClientNetworkManager_OnClientDisconnectCallback; + + // Only start the client (so it will timeout) + m_ClientNetworkManager.StartClient(); + +#if !UNITY_UNET_PRESENT + // Unity Transport throws an error when it times out + LogAssert.Expect(LogType.Error, "Failed to connect to server."); +#endif + yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => m_WasDisconnected, m_TimeoutHelper); + Assert.False(m_TimeoutHelper.TimedOut, "Timed out waiting for client to timeout waiting to connect!"); + + // Shutdown the client + m_ClientNetworkManager.Shutdown(); + + // Wait for a tick + yield return m_DefaultWaitForTick; + } + + private void ClientNetworkManager_OnClientDisconnectCallback(ulong clientId) + { + m_WasDisconnected = true; + } + + [TearDown] + public void TearDown() + { + if (m_NetworkManagerGameObject != null) + { + Object.DestroyImmediate(m_NetworkManagerGameObject); + } + } + } +} + diff --git a/Tests/Runtime/ClientOnlyConnectionTests.cs.meta b/Tests/Runtime/ClientOnlyConnectionTests.cs.meta new file mode 100644 index 0000000..f6aaab9 --- /dev/null +++ b/Tests/Runtime/ClientOnlyConnectionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 639fe2161ab25c54f94ce6eb991e668a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Components/NetworkVariableTestComponent.cs b/Tests/Runtime/Components/NetworkVariableTestComponent.cs index 78045c3..b887067 100644 --- a/Tests/Runtime/Components/NetworkVariableTestComponent.cs +++ b/Tests/Runtime/Components/NetworkVariableTestComponent.cs @@ -103,46 +103,6 @@ private void InitializeTest() m_NetworkVariableUInt = new NetworkVariable(1); m_NetworkVariableUShort = new NetworkVariable(1); - m_NetworkVariableBool = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableByte = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableColor = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableColor32 = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableDouble = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableFloat = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableInt = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableLong = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableSByte = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableQuaternion = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableShort = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableVector4 = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableVector3 = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableVector2 = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableRay = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableULong = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableUInt = new NetworkVariable(NetworkVariableReadPermission.Everyone); - m_NetworkVariableUShort = new NetworkVariable(NetworkVariableReadPermission.Everyone); - - - // NetworkVariable Value Type and NetworkVariableSettings Constructor Test Coverage - m_NetworkVariableBool = new NetworkVariable(NetworkVariableReadPermission.Everyone, true); - m_NetworkVariableByte = new NetworkVariable(NetworkVariableReadPermission.Everyone, 0); - m_NetworkVariableColor = new NetworkVariable(NetworkVariableReadPermission.Everyone, new Color(1, 1, 1, 1)); - m_NetworkVariableColor32 = new NetworkVariable(NetworkVariableReadPermission.Everyone, new Color32(1, 1, 1, 1)); - m_NetworkVariableDouble = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1.0); - m_NetworkVariableFloat = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1.0f); - m_NetworkVariableInt = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1); - m_NetworkVariableLong = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1); - m_NetworkVariableSByte = new NetworkVariable(NetworkVariableReadPermission.Everyone, 0); - m_NetworkVariableQuaternion = new NetworkVariable(NetworkVariableReadPermission.Everyone, Quaternion.identity); - m_NetworkVariableShort = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1); - m_NetworkVariableVector4 = new NetworkVariable(NetworkVariableReadPermission.Everyone, new Vector4(1, 1, 1, 1)); - m_NetworkVariableVector3 = new NetworkVariable(NetworkVariableReadPermission.Everyone, new Vector3(1, 1, 1)); - m_NetworkVariableVector2 = new NetworkVariable(NetworkVariableReadPermission.Everyone, new Vector2(1, 1)); - m_NetworkVariableRay = new NetworkVariable(NetworkVariableReadPermission.Everyone, new Ray()); - m_NetworkVariableULong = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1); - m_NetworkVariableUInt = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1); - m_NetworkVariableUShort = new NetworkVariable(NetworkVariableReadPermission.Everyone, 1); - // Use this nifty class: NetworkVariableHelper // Tracks if NetworkVariable changed invokes the OnValueChanged callback for the given instance type Bool_Var = new NetworkVariableHelper(m_NetworkVariableBool); @@ -193,7 +153,7 @@ private void Update() { if (EnableTesting) { - //Added timeout functionality for near future changes to NetworkVariables and the Snapshot system + //Added timeout functionality for near future changes to NetworkVariables if (!m_FinishedTests && m_ChangesAppliedToNetworkVariables) { //We finish testing if all NetworkVariables changed their value or we timed out waiting for diff --git a/Tests/Runtime/Metrics/ConnectionMetricsTests.cs b/Tests/Runtime/Metrics/ConnectionMetricsTests.cs new file mode 100644 index 0000000..9f678f1 --- /dev/null +++ b/Tests/Runtime/Metrics/ConnectionMetricsTests.cs @@ -0,0 +1,69 @@ +#if MULTIPLAYER_TOOLS +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.TestHelpers.Runtime; +using Unity.Netcode.TestHelpers.Runtime.Metrics; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + [TestFixture(ClientCount.OneClient, HostOrServer.Host)] + [TestFixture(ClientCount.TwoClients, HostOrServer.Host)] + [TestFixture(ClientCount.OneClient, HostOrServer.Server)] + [TestFixture(ClientCount.TwoClients, HostOrServer.Server)] + public class ConnectionMetricsTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => m_ClientCount; + + private int m_ClientCount; + + public enum ClientCount + { + OneClient = 1, + TwoClients, + } + + public ConnectionMetricsTests(ClientCount clientCount, HostOrServer hostOrServer) + : base(hostOrServer) + { + m_ClientCount = (int)clientCount; + } + + private int GetClientCountForFixture() + { + return m_ClientCount + ((m_UseHost) ? 1 : 0); + } + + [UnityTest] + public IEnumerator UpdateConnectionCountOnServer() + { + var waitForGaugeValues = new WaitForGaugeMetricValues((m_ServerNetworkManager.NetworkMetrics as NetworkMetrics).Dispatcher, NetworkMetricTypes.ConnectedClients); + + yield return waitForGaugeValues.WaitForMetricsReceived(); + + var value = waitForGaugeValues.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(GetClientCountForFixture(), value); + } + + [UnityTest] + public IEnumerator UpdateConnectionCountOnClient() + { + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + var waitForGaugeValues = new WaitForGaugeMetricValues((clientNetworkManager.NetworkMetrics as NetworkMetrics).Dispatcher, NetworkMetricTypes.ConnectedClients); + + yield return waitForGaugeValues.WaitForMetricsReceived(); + + var value = waitForGaugeValues.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(1, value); + } + } + } +} + +#endif +#endif diff --git a/Tests/Runtime/Metrics/ConnectionMetricsTests.cs.meta b/Tests/Runtime/Metrics/ConnectionMetricsTests.cs.meta new file mode 100644 index 0000000..bc0d812 --- /dev/null +++ b/Tests/Runtime/Metrics/ConnectionMetricsTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1845aef61dbb4f2b9d2be9145262ab90 +timeCreated: 1647023529 \ No newline at end of file diff --git a/Tests/Runtime/Metrics/MessagingMetricsTests.cs b/Tests/Runtime/Metrics/MessagingMetricsTests.cs index 31c3e00..082c6b6 100644 --- a/Tests/Runtime/Metrics/MessagingMetricsTests.cs +++ b/Tests/Runtime/Metrics/MessagingMetricsTests.cs @@ -39,7 +39,7 @@ public IEnumerator TrackNetworkMessageSentMetric() var networkMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); - // We should have 1 NamedMessage and some potential SnapshotMessage + // We should have 1 NamedMessage Assert.That(networkMessageSentMetricValues, Has.Exactly(1).Matches(x => x.Name == nameof(NamedMessage))); } @@ -85,7 +85,7 @@ public IEnumerator TrackNetworkMessageReceivedMetric() yield return waitForMetricValues.WaitForMetricsReceived(); var networkMessageReceivedValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); - // We should have 1 NamedMessage and some potential SnapshotMessage + // We should have 1 NamedMessage Assert.That(networkMessageReceivedValues, Has.Exactly(1).Matches(x => x.Name == nameof(NamedMessage))); } diff --git a/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs b/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs index 436be78..1679607 100644 --- a/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs +++ b/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs @@ -189,6 +189,76 @@ public IEnumerator TrackMultipleNetworkObjectDestroySentMetric() Assert.AreEqual(1, objectDestroyedSentMetricValues.Select(x => x.BytesCount).Distinct().Count()); Assert.That(objectDestroyedSentMetricValues.Select(x => x.BytesCount), Has.All.Not.EqualTo(0)); } + +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + [UnityTest] + public IEnumerator TrackNetworkObjectCountAfterSpawnOnServer() + { + SpawnNetworkObject(); + + var waitForGaugeValues = new WaitForGaugeMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NetworkObjects); + + yield return s_DefaultWaitForTick; + yield return waitForGaugeValues.WaitForMetricsReceived(); + + var value = waitForGaugeValues.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(3, value); + } + + [UnityTest] + public IEnumerator TrackNetworkObjectCountAfterSpawnOnClient() + { + SpawnNetworkObject(); + + //By default, we have 2 network objects + //There's a slight delay between the spawn on the server and the spawn on the client + //We want to have metrics when the value is different than the 2 default one to confirm the client has the new value + var waitForGaugeValues = new WaitForGaugeMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.NetworkObjects, metric => (int)metric != 2); + + yield return waitForGaugeValues.WaitForMetricsReceived(); + + var value = waitForGaugeValues.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(3, value); + } + + [UnityTest] + public IEnumerator TrackNetworkObjectCountAfterDespawnOnServer() + { + var objectList = Server.SpawnManager.SpawnedObjectsList; + for (int i = objectList.Count - 1; i >= 0; --i) + { + objectList.ElementAt(i).Despawn(); + } + + var waitForGaugeValues = new WaitForGaugeMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NetworkObjects); + + yield return s_DefaultWaitForTick; + yield return waitForGaugeValues.WaitForMetricsReceived(); + + var value = waitForGaugeValues.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(0, value); + } + + [UnityTest] + public IEnumerator TrackNetworkObjectCountAfterDespawnOnClient() + { + var objectList = Server.SpawnManager.SpawnedObjectsList; + for (int i = objectList.Count - 1; i >= 0; --i) + { + objectList.ElementAt(i).Despawn(); + } + + //By default, we have 2 network objects + //There's a slight delay between the spawn on the server and the spawn on the client + //We want to have metrics when the value is different than the 2 default one to confirm the client has the new value + var waitForGaugeValues = new WaitForGaugeMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.NetworkObjects, metric => (int)metric != 2); + + yield return waitForGaugeValues.WaitForMetricsReceived(); + + var value = waitForGaugeValues.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(0, value); + } +#endif } } #endif diff --git a/Tests/Runtime/Metrics/PacketLossMetricsTests.cs b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs new file mode 100644 index 0000000..e838832 --- /dev/null +++ b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs @@ -0,0 +1,89 @@ +#if MULTIPLAYER_TOOLS +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + +using System; +using System.Collections; +using NUnit.Framework; +using Unity.Collections; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.TestHelpers.Runtime; +using Unity.Netcode.TestHelpers.Runtime.Metrics; +using Unity.Netcode.Transports.UTP; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + public class PacketLossMetricsTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + private readonly int m_PacketLossRate = 25; + private int m_DropInterval = 5; + + public PacketLossMetricsTests() + : base(HostOrServer.Server) + {} + + protected override void OnOneTimeSetup() + { + m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.UTP; + } + + protected override void OnServerAndClientsCreated() + { + var clientTransport = (UnityTransport)m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport; + clientTransport.SetDebugSimulatorParameters(0, 0, m_PacketLossRate); + + base.OnServerAndClientsCreated(); + } + + [UnityTest] + public IEnumerator TrackPacketLossAsServer() + { + var waitForPacketLossMetric = new WaitForGaugeMetricValues((m_ServerNetworkManager.NetworkMetrics as NetworkMetrics).Dispatcher, + NetworkMetricTypes.PacketLoss, + metric => metric == 0.0d); + + for (int i = 0; i < 1000; ++i) + { + using (var writer = new FastBufferWriter(sizeof(byte), Allocator.Persistent)) + { + writer.WriteByteSafe(42); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage("Test", m_ServerNetworkManager.ConnectedClientsIds, writer); + } + } + + yield return waitForPacketLossMetric.WaitForMetricsReceived(); + + var packetLossValue = waitForPacketLossMetric.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(0d, packetLossValue); + } + + [UnityTest] + public IEnumerator TrackPacketLossAsClient() + { + double packetLossRate = m_PacketLossRate/100d; + var clientNetworkManager = m_ClientNetworkManagers[0]; + var waitForPacketLossMetric = new WaitForGaugeMetricValues((clientNetworkManager.NetworkMetrics as NetworkMetrics).Dispatcher, + NetworkMetricTypes.PacketLoss, + metric => Math.Abs(metric - packetLossRate) < Double.Epsilon); + + for (int i = 0; i < 1000; ++i) + { + using (var writer = new FastBufferWriter(sizeof(byte), Allocator.Persistent)) + { + writer.WriteByteSafe(42); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage("Test", m_ServerNetworkManager.ConnectedClientsIds, writer); + } + } + + yield return waitForPacketLossMetric.WaitForMetricsReceived(); + + var packetLossValue = waitForPacketLossMetric.AssertMetricValueHaveBeenFound(); + Assert.AreEqual(packetLossRate, packetLossValue); + } + } +} + +#endif +#endif diff --git a/Tests/Runtime/Metrics/PacketLossMetricsTests.cs.meta b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs.meta new file mode 100644 index 0000000..d48f305 --- /dev/null +++ b/Tests/Runtime/Metrics/PacketLossMetricsTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 12e64da4670d49a4a89da38d18e64396 +timeCreated: 1648133968 \ No newline at end of file diff --git a/Tests/Runtime/Metrics/PacketMetricsTests.cs b/Tests/Runtime/Metrics/PacketMetricsTests.cs index 14448e9..6a0003a 100644 --- a/Tests/Runtime/Metrics/PacketMetricsTests.cs +++ b/Tests/Runtime/Metrics/PacketMetricsTests.cs @@ -1,5 +1,5 @@ #if MULTIPLAYER_TOOLS -#if MULTIPLAYER_TOOLS_1_0_0_PRE_4 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 using System.Collections; using NUnit.Framework; using Unity.Collections; @@ -15,11 +15,7 @@ internal class PacketMetricsTests : SingleClientMetricTestBase protected override void OnOneTimeSetup() { -#if UTP_ADAPTER m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.UTP; -#else - m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.SIP; -#endif base.OnOneTimeSetup(); } diff --git a/Tests/Runtime/Metrics/RttMetricsTests.cs b/Tests/Runtime/Metrics/RttMetricsTests.cs index 29a5e76..aad9ae5 100644 --- a/Tests/Runtime/Metrics/RttMetricsTests.cs +++ b/Tests/Runtime/Metrics/RttMetricsTests.cs @@ -1,5 +1,5 @@ #if MULTIPLAYER_TOOLS -#if MULTIPLAYER_TOOLS_1_0_0_PRE_4 +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 using System.Collections; using System.Collections.Generic; @@ -46,11 +46,7 @@ public RttMetricsTests(ClientCount numberOfClients) /// protected override void OnOneTimeSetup() { -#if UTP_ADAPTER m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.UTP; -#else - m_NetworkTransport = NetcodeIntegrationTestHelpers.InstanceTransport.SIP; -#endif } [UnityTest] @@ -92,7 +88,7 @@ public IEnumerator TrackRttMetricClientToServer() foreach (var clientGaugeMetricValue in clientGaugeMetricValues) { var rttValue = clientGaugeMetricValue.AssertMetricValueHaveBeenFound(); - Assert.That(rttValue, Is.GreaterThanOrEqualTo(1f)); + Assert.That(rttValue, Is.GreaterThanOrEqualTo(1e-3f)); } } } diff --git a/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs b/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs index eb46c00..108e37e 100644 --- a/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs +++ b/Tests/Runtime/NetworkAnimator/NetworkAnimatorTests.cs @@ -1,3 +1,4 @@ +#if COM_UNITY_MODULES_ANIMATION using System.Collections; using System.Collections.Generic; using NUnit.Framework; @@ -235,3 +236,4 @@ public IEnumerator AnimationStateSyncTestWithOverride() } } } +#endif // COM_UNITY_MODULES_ANIMATION diff --git a/Tests/Runtime/NetworkManagerTransportTests.cs b/Tests/Runtime/NetworkManagerTransportTests.cs new file mode 100644 index 0000000..79dc1e0 --- /dev/null +++ b/Tests/Runtime/NetworkManagerTransportTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + public class NetworkManagerTransportTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + private bool m_CanStartServerAndClients = false; + + public NetworkManagerTransportTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + protected override IEnumerator OnSetup() + { + m_CanStartServerAndClients = false; + return base.OnSetup(); + } + + protected override bool CanStartServerAndClients() + { + return m_CanStartServerAndClients; + } + + /// + /// Validate that if the NetworkTransport fails to start the NetworkManager + /// will not continue the startup process and will shut itself down. + /// + /// if true it will test the client side + [UnityTest] + public IEnumerator DoesNotStartWhenTransportFails([Values] bool testClient) + { + // The error message we should expect + var messageToCheck = ""; + if (!testClient) + { + Object.DestroyImmediate(m_ServerNetworkManager.NetworkConfig.NetworkTransport); + m_ServerNetworkManager.NetworkConfig.NetworkTransport = m_ServerNetworkManager.gameObject.AddComponent(); + m_ServerNetworkManager.NetworkConfig.NetworkTransport.Initialize(m_ServerNetworkManager); + // The error message we should expect + messageToCheck = $"Server is shutting down due to network transport start failure of {m_ServerNetworkManager.NetworkConfig.NetworkTransport.GetType().Name}!"; + } + else + { + foreach (var client in m_ClientNetworkManagers) + { + Object.DestroyImmediate(client.NetworkConfig.NetworkTransport); + client.NetworkConfig.NetworkTransport = client.gameObject.AddComponent(); + client.NetworkConfig.NetworkTransport.Initialize(m_ServerNetworkManager); + } + // The error message we should expect + messageToCheck = $"Client is shutting down due to network transport start failure of {m_ClientNetworkManagers[0].NetworkConfig.NetworkTransport.GetType().Name}!"; + } + + // Trap for the nested NetworkManager exception + LogAssert.Expect(LogType.Error, messageToCheck); + m_CanStartServerAndClients = true; + // Due to other errors, we must not send clients if testing the server-host side + // We can test both server and client(s) when testing client-side only + if (testClient) + { + NetcodeIntegrationTestHelpers.Start(m_UseHost, m_ServerNetworkManager, m_ClientNetworkManagers); + yield return s_DefaultWaitForTick; + foreach (var client in m_ClientNetworkManagers) + { + Assert.False(client.IsListening); + Assert.False(client.IsConnectedClient); + } + } + else + { + NetcodeIntegrationTestHelpers.Start(m_UseHost, m_ServerNetworkManager, new NetworkManager[] { }); + yield return s_DefaultWaitForTick; + Assert.False(m_ServerNetworkManager.IsListening); + } + } + } + + /// + /// Does nothing but simulate a transport that failed to start + /// + public class FailedTransport : TestingNetworkTransport + { + public override void Shutdown() + { + } + + public override ulong ServerClientId => 0; + + public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime) + { + clientId = 0; + payload = new ArraySegment(); + receiveTime = 0; + return NetworkEvent.Nothing; + } + public override bool StartClient() + { + // Simulate failure, always return false + return false; + } + public override bool StartServer() + { + // Simulate failure, always return false + return false; + } + public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) + { + } + + public override void DisconnectRemoteClient(ulong clientId) + { + } + + public override void Initialize(NetworkManager networkManager = null) + { + } + public override ulong GetCurrentRtt(ulong clientId) + { + return 0; + } + public override void DisconnectLocalClient() + { + } + } +} diff --git a/Tests/Runtime/NetworkManagerTransportTests.cs.meta b/Tests/Runtime/NetworkManagerTransportTests.cs.meta new file mode 100644 index 0000000..134ba9f --- /dev/null +++ b/Tests/Runtime/NetworkManagerTransportTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 798d76599e527b245a14b7cc9cfd2607 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs index 59da25f..c7f106d 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs @@ -1,78 +1,55 @@ using System.Collections; -using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; using Unity.Netcode.TestHelpers.Runtime; -using Object = UnityEngine.Object; + namespace Unity.Netcode.RuntimeTests { - public class NetworkObjectDontDestroyWithOwnerTests + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + public class NetworkObjectDontDestroyWithOwnerTests : NetcodeIntegrationTest { - [UnityTest] - public IEnumerator DontDestroyWithOwnerTest() - { - // create server and client instances - NetcodeIntegrationTestHelpers.Create(1, out NetworkManager server, out NetworkManager[] clients); - - // create prefab - var gameObject = new GameObject("ClientOwnedObject"); - var networkObject = gameObject.AddComponent(); - networkObject.DontDestroyWithOwner = true; - NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); - - server.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() - { - Prefab = gameObject - }); - - for (int i = 0; i < clients.Length; i++) - { - clients[i].NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() - { - Prefab = gameObject - }); - } + private const int k_NumberObjectsToSpawn = 32; + protected override int NumberOfClients => 1; - // start server and connect clients - NetcodeIntegrationTestHelpers.Start(false, server, clients); + protected GameObject m_PrefabToSpawn; - // wait for connection on client side - yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(clients); + public NetworkObjectDontDestroyWithOwnerTests(HostOrServer hostOrServer) : base(hostOrServer) { } - // wait for connection on server side - yield return NetcodeIntegrationTestHelpers.WaitForClientConnectedToServer(server); - - // network objects - var networkObjects = new List(); + protected override void OnServerAndClientsCreated() + { + m_PrefabToSpawn = CreateNetworkObjectPrefab("ClientOwnedObject"); + m_PrefabToSpawn.GetComponent().DontDestroyWithOwner = true; + } - // create instances - for (int i = 0; i < 32; i++) - { - var no = Object.Instantiate(gameObject).GetComponent(); - no.NetworkManagerOwner = server; - networkObjects.Add(no); - no.SpawnWithOwnership(clients[0].LocalClientId); - } + [UnityTest] + public IEnumerator DontDestroyWithOwnerTest() + { + var client = m_ClientNetworkManagers[0]; + var clientId = client.LocalClientId; + var networkObjects = SpawnObjects(m_PrefabToSpawn, m_ClientNetworkManagers[0], k_NumberObjectsToSpawn); - // wait for object spawn on client - yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => clients[0].SpawnManager.SpawnedObjects.Count == 32); + // wait for object spawn on client to reach k_NumberObjectsToSpawn + 1 (k_NumberObjectsToSpawn and 1 for the player) + yield return WaitForConditionOrTimeOut(() => client.SpawnManager.GetClientOwnedObjects(clientId).Count() == k_NumberObjectsToSpawn + 1); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for client to have 33 NetworkObjects spawned! Only {client.SpawnManager.GetClientOwnedObjects(clientId).Count()} were assigned!"); // disconnect the client that owns all the clients - NetcodeIntegrationTestHelpers.StopOneClient(clients[0]); + NetcodeIntegrationTestHelpers.StopOneClient(client); + var remainingClients = Mathf.Max(0, TotalClients - 1); // wait for disconnect - yield return NetcodeIntegrationTest.WaitForConditionOrTimeOut(() => server.ConnectedClients.Count == 0); + yield return WaitForConditionOrTimeOut(() => m_ServerNetworkManager.ConnectedClients.Count == remainingClients); + Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client to disconnect!"); for (int i = 0; i < networkObjects.Count; i++) { + var networkObject = networkObjects[i].GetComponent(); // ensure ownership was transferred back - Assert.That(networkObjects[i].OwnerClientId == server.ServerClientId); + Assert.That(networkObject.OwnerClientId == m_ServerNetworkManager.LocalClientId); } - - // cleanup - NetcodeIntegrationTestHelpers.Destroy(); } } } diff --git a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs index ffe624c..dbfc24d 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectNetworkClientOwnedObjectsTests.cs @@ -42,7 +42,7 @@ public IEnumerator ChangeOwnershipOwnedObjectsAddTest() yield return s_DefaultWaitForTick; // The object is owned by server - Assert.False(m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].OwnedObjects.Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); + Assert.False(m_ServerNetworkManager.SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); // Change the ownership serverObject.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); @@ -51,8 +51,8 @@ public IEnumerator ChangeOwnershipOwnedObjectsAddTest() yield return s_DefaultWaitForTick; // Ensure it's now added to the list - Assert.True(m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].OwnedObjects.Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); - + Assert.True(m_ClientNetworkManagers[0].SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); + Assert.True(m_ServerNetworkManager.SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId).Any(x => x.NetworkObjectId == serverObject.NetworkObjectId)); } } } diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs index 2bf8c02..e2d2fe4 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs @@ -1,4 +1,6 @@ using System.Collections; +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; @@ -9,118 +11,77 @@ namespace Unity.Netcode.RuntimeTests public class NetworkObjectOwnershipComponent : NetworkBehaviour { public bool OnLostOwnershipFired = false; - public ulong CachedOwnerIdOnLostOwnership = 0; + public bool OnGainedOwnershipFired = false; public override void OnLostOwnership() { OnLostOwnershipFired = true; - CachedOwnerIdOnLostOwnership = OwnerClientId; } - public bool OnGainedOwnershipFired = false; - public ulong CachedOwnerIdOnGainedOwnership = 0; - public override void OnGainedOwnership() { OnGainedOwnershipFired = true; - CachedOwnerIdOnGainedOwnership = OwnerClientId; } - } - - [TestFixture(true)] - [TestFixture(false)] - public class NetworkObjectOwnershipTests - { - private const int k_ClientInstanceCount = 1; - - private NetworkManager m_ServerNetworkManager; - private NetworkManager[] m_ClientNetworkManagers; - - private GameObject m_DummyPrefab; - private GameObject m_DummyGameObject; - private readonly bool m_IsHost; - - public NetworkObjectOwnershipTests(bool isHost) + public void ResetFlags() { - m_IsHost = isHost; + OnLostOwnershipFired = false; + OnGainedOwnershipFired = false; } + } - [UnitySetUp] - public IEnumerator Setup() - { - // we need at least 1 client for tests - Assert.That(k_ClientInstanceCount, Is.GreaterThan(0)); - - // create NetworkManager instances - Assert.That(NetcodeIntegrationTestHelpers.Create(k_ClientInstanceCount, out m_ServerNetworkManager, out m_ClientNetworkManagers)); - Assert.That(m_ServerNetworkManager, Is.Not.Null); - Assert.That(m_ClientNetworkManagers, Is.Not.Null); - Assert.That(m_ClientNetworkManagers.Length, Is.EqualTo(k_ClientInstanceCount)); - - // create and register our ad-hoc DummyPrefab (we'll spawn it later during tests) - m_DummyPrefab = new GameObject("DummyPrefabPrototype"); - m_DummyPrefab.AddComponent(); - m_DummyPrefab.AddComponent(); - NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(m_DummyPrefab.GetComponent()); - m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab { Prefab = m_DummyPrefab }); - foreach (var clientNetworkManager in m_ClientNetworkManagers) - { - clientNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab { Prefab = m_DummyPrefab }); - } + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + public class NetworkObjectOwnershipTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 9; - // start server and client NetworkManager instances - Assert.That(NetcodeIntegrationTestHelpers.Start(m_IsHost, m_ServerNetworkManager, m_ClientNetworkManagers)); + private GameObject m_OwnershipPrefab; + private GameObject m_OwnershipObject; + private NetworkObject m_OwnershipNetworkObject; - // wait for connection on client side - yield return NetcodeIntegrationTestHelpers.WaitForClientsConnected(m_ClientNetworkManagers); + public NetworkObjectOwnershipTests(HostOrServer hostOrServer) : base(hostOrServer) { } - // wait for connection on server side - yield return NetcodeIntegrationTestHelpers.WaitForClientConnectedToServer(m_ServerNetworkManager); + protected override void OnServerAndClientsCreated() + { + m_OwnershipPrefab = CreateNetworkObjectPrefab("OnwershipPrefab"); + m_OwnershipPrefab.AddComponent(); + base.OnServerAndClientsCreated(); } - [TearDown] - public void Teardown() + [Test] + public void TestPlayerIsOwned() { - NetcodeIntegrationTestHelpers.Destroy(); + var clientOwnedObjects = m_ClientNetworkManagers[0].SpawnManager.GetClientOwnedObjects(m_ClientNetworkManagers[0].LocalClientId); - if (m_DummyGameObject != null) - { - Object.DestroyImmediate(m_DummyGameObject); - } + var clientPlayerObject = clientOwnedObjects.Where((c) => c.IsLocalPlayer).FirstOrDefault(); + Assert.NotNull(clientPlayerObject, $"Client Id {m_ClientNetworkManagers[0].LocalClientId} does not have its local player marked as an owned object!"); - if (m_DummyPrefab != null) - { - Object.DestroyImmediate(m_DummyPrefab); - } + clientPlayerObject = m_ClientNetworkManagers[0].LocalClient.OwnedObjects.Where((c) => c.IsLocalPlayer).FirstOrDefault(); + Assert.NotNull(clientPlayerObject, $"Client Id {m_ClientNetworkManagers[0].LocalClientId} does not have its local player marked as an owned object using local client!"); } [UnityTest] public IEnumerator TestOwnershipCallbacks() { - m_DummyGameObject = Object.Instantiate(m_DummyPrefab); - var dummyNetworkObject = m_DummyGameObject.GetComponent(); - Assert.That(dummyNetworkObject, Is.Not.Null); - - dummyNetworkObject.NetworkManagerOwner = m_ServerNetworkManager; - dummyNetworkObject.Spawn(); - var dummyNetworkObjectId = dummyNetworkObject.NetworkObjectId; - Assert.That(dummyNetworkObjectId, Is.GreaterThan(0)); + m_OwnershipObject = SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager); + m_OwnershipNetworkObject = m_OwnershipObject.GetComponent(); yield return NetcodeIntegrationTestHelpers.WaitForMessageOfType(m_ClientNetworkManagers[0]); - Assert.That(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(dummyNetworkObjectId)); + var ownershipNetworkObjectId = m_OwnershipNetworkObject.NetworkObjectId; + Assert.That(ownershipNetworkObjectId, Is.GreaterThan(0)); + Assert.That(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId)); foreach (var clientNetworkManager in m_ClientNetworkManagers) { - Assert.That(clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(dummyNetworkObjectId)); + Assert.That(clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId)); } - // Verifies that removing the ownership when the default (server) is already set does not cause - // a Key Not Found Exception - m_ServerNetworkManager.SpawnManager.RemoveOwnership(dummyNetworkObject); + // Verifies that removing the ownership when the default (server) is already set does not cause a Key Not Found Exception + m_ServerNetworkManager.SpawnManager.RemoveOwnership(m_OwnershipNetworkObject); - var serverObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[dummyNetworkObjectId]; - var clientObject = m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[dummyNetworkObjectId]; + var serverObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId]; + var clientObject = m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[ownershipNetworkObjectId]; Assert.That(serverObject, Is.Not.Null); Assert.That(clientObject, Is.Not.Null); @@ -129,25 +90,164 @@ public IEnumerator TestOwnershipCallbacks() Assert.That(serverComponent, Is.Not.Null); Assert.That(clientComponent, Is.Not.Null); - - Assert.That(serverObject.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.ServerClientId)); - Assert.That(clientObject.OwnerClientId, Is.EqualTo(m_ClientNetworkManagers[0].ServerClientId)); + Assert.That(serverObject.OwnerClientId, Is.EqualTo(NetworkManager.ServerClientId)); + Assert.That(clientObject.OwnerClientId, Is.EqualTo(NetworkManager.ServerClientId)); Assert.That(m_ServerNetworkManager.ConnectedClients.ContainsKey(m_ClientNetworkManagers[0].LocalClientId)); - serverObject.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); - - yield return NetcodeIntegrationTestHelpers.WaitForMessageOfType(m_ClientNetworkManagers[0]); + serverObject.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + Assert.That(serverComponent.OnLostOwnershipFired); + Assert.That(serverComponent.OwnerClientId, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); Assert.That(clientComponent.OnGainedOwnershipFired); - Assert.That(clientComponent.CachedOwnerIdOnGainedOwnership, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); - serverObject.ChangeOwnership(m_ServerNetworkManager.ServerClientId); + Assert.That(clientComponent.OwnerClientId, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); - yield return NetcodeIntegrationTestHelpers.WaitForMessageOfType(m_ClientNetworkManagers[0]); + serverComponent.ResetFlags(); + clientComponent.ResetFlags(); - Assert.That(serverObject.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); + serverObject.ChangeOwnership(NetworkManager.ServerClientId); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + + Assert.That(serverComponent.OnGainedOwnershipFired); + Assert.That(serverComponent.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); Assert.That(clientComponent.OnLostOwnershipFired); - Assert.That(clientComponent.CachedOwnerIdOnLostOwnership, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); + Assert.That(clientComponent.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); + } + + /// + /// Verifies that switching ownership between several clients works properly + /// + [UnityTest] + public IEnumerator TestOwnershipCallbacksSeveralClients() + { + // Build our message hook entries tables so we can determine if all clients received spawn or ownership messages + var messageHookEntriesForSpawn = new List(); + var messageHookEntriesForOwnership = new List(); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + var messageHook = new MessageHookEntry(clientNetworkManager); + messageHook.AssignMessageType(); + messageHookEntriesForSpawn.Add(messageHook); + messageHook = new MessageHookEntry(clientNetworkManager); + messageHook.AssignMessageType(); + messageHookEntriesForOwnership.Add(messageHook); + } + // Used to determine if all clients received the CreateObjectMessage + var spawnMessageHooks = new MessageHooksConditional(messageHookEntriesForSpawn); + + // Used to determine if all clients received the ChangeOwnershipMessage + var ownershipMessageHooks = new MessageHooksConditional(messageHookEntriesForOwnership); + + // Spawn our test object from server with server ownership + m_OwnershipObject = SpawnObject(m_OwnershipPrefab, m_ServerNetworkManager); + m_OwnershipNetworkObject = m_OwnershipObject.GetComponent(); + + // Wait for all clients to receive the CreateObjectMessage + yield return WaitForConditionOrTimeOut(spawnMessageHooks); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for all clients to receive the {nameof(CreateObjectMessage)} message."); + + // Validate the NetworkObjectId and that the server and all clients have this NetworkObject + var ownershipNetworkObjectId = m_OwnershipNetworkObject.NetworkObjectId; + Assert.That(ownershipNetworkObjectId, Is.GreaterThan(0)); + Assert.That(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId)); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + Assert.That(clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(ownershipNetworkObjectId)); + } + + // Verifies that removing the ownership when the default (server) is already set does not cause a Key Not Found Exception + m_ServerNetworkManager.SpawnManager.RemoveOwnership(m_OwnershipNetworkObject); + var serverObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[ownershipNetworkObjectId]; + Assert.That(serverObject, Is.Not.Null); + + var clientObjects = new List(); + for (int i = 0; i < NumberOfClients; i++) + { + var clientObject = m_ClientNetworkManagers[i].SpawnManager.SpawnedObjects[ownershipNetworkObjectId]; + Assert.That(clientObject, Is.Not.Null); + clientObjects.Add(clientObject); + } + + // Verify the server side component + var serverComponent = serverObject.GetComponent(); + Assert.That(serverComponent, Is.Not.Null); + Assert.That(serverObject.OwnerClientId, Is.EqualTo(NetworkManager.ServerClientId)); + + // Verify the clients components + for (int i = 0; i < NumberOfClients; i++) + { + var clientComponent = clientObjects[i].GetComponent(); + Assert.That(clientComponent.OwnerClientId, Is.EqualTo(NetworkManager.ServerClientId)); + clientComponent.ResetFlags(); + } + + // After the 1st client has been given ownership to the object, this will be used to make sure each previous owner properly received the remove ownership message + var previousClientComponent = (NetworkObjectOwnershipComponent)null; + for (int clientIndex = 0; clientIndex < NumberOfClients; clientIndex++) + { + var clientObject = clientObjects[clientIndex]; + var clientId = m_ClientNetworkManagers[clientIndex].LocalClientId; + + Assert.That(m_ServerNetworkManager.ConnectedClients.ContainsKey(clientId)); + serverObject.ChangeOwnership(clientId); + yield return s_DefaultWaitForTick; + Assert.That(serverComponent.OnLostOwnershipFired); + Assert.That(serverComponent.OwnerClientId, Is.EqualTo(clientId)); + // Wait for all clients to receive the CreateObjectMessage + yield return WaitForConditionOrTimeOut(ownershipMessageHooks); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for all clients to receive the {nameof(ChangeOwnershipMessage)} message."); + + var previousNetworkManager = m_ServerNetworkManager; + if (previousClientComponent != null) + { + // Once we have a previousClientComponent, we want to verify the server is keeping track for the removal of ownership in the OwnershipToObjectsTable + Assert.That(!m_ServerNetworkManager.SpawnManager.OwnershipToObjectsTable[m_ServerNetworkManager.LocalClientId].ContainsKey(serverObject.NetworkObjectId)); + previousNetworkManager = previousClientComponent.NetworkManager; + Assert.That(previousClientComponent.OnLostOwnershipFired); + Assert.That(previousClientComponent.OwnerClientId, Is.EqualTo(clientId)); + } + + // Assure the previous owner is no longer in the local table of the previous owner. + Assert.That(!previousNetworkManager.SpawnManager.OwnershipToObjectsTable[previousNetworkManager.LocalClientId].ContainsKey(serverObject.NetworkObjectId)); + + var currentClientComponent = clientObjects[clientIndex].GetComponent(); + Assert.That(currentClientComponent.OnGainedOwnershipFired); + + // Possibly the more important part of this test: + // Check to make sure all other non-former or current ownership clients are synchronized to each ownership change + for (int i = 0; i < NumberOfClients; i++) + { + var clientComponent = clientObjects[i].GetComponent(); + Assert.That(clientComponent, Is.Not.Null); + Assert.That(clientComponent.OwnerClientId, Is.EqualTo(clientId)); + clientComponent.ResetFlags(); + } + // We must reset this for each iteration in order to make sure all clients receive the ChangeOwnershipMessage + ownershipMessageHooks.Reset(); + + // Set the current owner client to the previous one + previousClientComponent = currentClientComponent; + } + + // Now change ownership back to the server + serverObject.ChangeOwnership(NetworkManager.ServerClientId); + yield return WaitForConditionOrTimeOut(ownershipMessageHooks); + Assert.False(s_GlobalTimeoutHelper.TimedOut, $"Timed out waiting for all clients to receive the {nameof(ChangeOwnershipMessage)} message (back to server)."); + + Assert.That(serverComponent.OnGainedOwnershipFired); + Assert.That(serverComponent.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); + Assert.That(previousClientComponent.OnLostOwnershipFired); + + // Make sure all client-side versions of the object is once again owned by the server + for (int i = 0; i < NumberOfClients; i++) + { + var clientComponent = clientObjects[i].GetComponent(); + Assert.That(clientComponent, Is.Not.Null); + Assert.That(clientComponent.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); + clientComponent.ResetFlags(); + } + serverComponent.ResetFlags(); } } } diff --git a/Tests/Runtime/NetworkSpawnManagerTests.cs b/Tests/Runtime/NetworkSpawnManagerTests.cs index a3882d3..a84e64f 100644 --- a/Tests/Runtime/NetworkSpawnManagerTests.cs +++ b/Tests/Runtime/NetworkSpawnManagerTests.cs @@ -8,7 +8,7 @@ namespace Unity.Netcode.RuntimeTests { public class NetworkSpawnManagerTests : NetcodeIntegrationTest { - private ulong serverSideClientId => m_ServerNetworkManager.ServerClientId; + private ulong serverSideClientId => NetworkManager.ServerClientId; private ulong clientSideClientId => m_ClientNetworkManagers[0].LocalClientId; private ulong otherClientSideClientId => m_ClientNetworkManagers[1].LocalClientId; diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs index 4dfc6e4..dd9936a 100644 --- a/Tests/Runtime/NetworkVariableTests.cs +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -6,9 +6,229 @@ using Unity.Collections; using Unity.Netcode.TestHelpers.Runtime; using Random = UnityEngine.Random; +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); + } + + [TestFixtureSource(nameof(TestDataSource))] + public class NetworkVariablePermissionTests : NetcodeIntegrationTest + { + public static IEnumerable TestDataSource() + { + foreach (HostOrServer hostOrServer in Enum.GetValues(typeof(HostOrServer))) + { + yield return new TestFixtureData(hostOrServer); + } + } + + protected override int NumberOfClients => 3; + + public NetworkVariablePermissionTests(HostOrServer hostOrServer) + : base(hostOrServer) + { + } + + private GameObject m_TestObjPrefab; + private ulong m_TestObjId = 0; + + protected override void OnServerAndClientsCreated() + { + m_TestObjPrefab = CreateNetworkObjectPrefab($"[{nameof(NetworkVariablePermissionTests)}.{nameof(m_TestObjPrefab)}]"); + var testComp = m_TestObjPrefab.AddComponent(); + } + + protected override IEnumerator OnServerAndClientsConnected() + { + m_TestObjId = SpawnObject(m_TestObjPrefab, m_ServerNetworkManager).GetComponent().NetworkObjectId; + yield return null; + } + + private IEnumerator WaitForPositionsAreEqual(NetworkVariable netvar, Vector3 expected) + { + yield return WaitForConditionOrTimeOut(() => netvar.Value == expected); + Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut); + } + + private IEnumerator WaitForOwnerWritableAreEqualOnAll() + { + yield return WaitForConditionOrTimeOut(CheckOwnerWritableAreEqualOnAll); + Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut); + } + + private bool CheckOwnerWritableAreEqualOnAll() + { + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompServer = testObjServer.GetComponent(); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + var testObjClient = clientNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + if (testObjServer.OwnerClientId != testObjClient.OwnerClientId || + testCompServer.OwnerWritable_Position.Value != testCompClient.OwnerWritable_Position.Value || + testCompServer.OwnerWritable_Position.ReadPerm != testCompClient.OwnerWritable_Position.ReadPerm || + testCompServer.OwnerWritable_Position.WritePerm != testCompClient.OwnerWritable_Position.WritePerm) + { + return false; + } + } + return true; + } + + private IEnumerator WaitForServerWritableAreEqualOnAll() + { + yield return WaitForConditionOrTimeOut(CheckServerWritableAreEqualOnAll); + Assert.IsFalse(s_GlobalTimeoutHelper.TimedOut); + } + + private bool CheckServerWritableAreEqualOnAll() + { + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompServer = testObjServer.GetComponent(); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + var testObjClient = clientNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + if (testCompServer.ServerWritable_Position.Value != testCompClient.ServerWritable_Position.Value || + testCompServer.ServerWritable_Position.ReadPerm != testCompClient.ServerWritable_Position.ReadPerm || + testCompServer.ServerWritable_Position.WritePerm != testCompClient.ServerWritable_Position.WritePerm) + { + return false; + } + } + return true; + } + + [UnityTest] + public IEnumerator ServerChangesOwnerWritableNetVar() + { + yield return WaitForOwnerWritableAreEqualOnAll(); + + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompServer = testObjServer.GetComponent(); + + var oldValue = testCompServer.OwnerWritable_Position.Value; + var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); + + testCompServer.OwnerWritable_Position.Value = newValue; + yield return WaitForPositionsAreEqual(testCompServer.OwnerWritable_Position, newValue); + + yield return WaitForOwnerWritableAreEqualOnAll(); + } + + [UnityTest] + public IEnumerator ServerChangesServerWritableNetVar() + { + yield return WaitForServerWritableAreEqualOnAll(); + + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompServer = testObjServer.GetComponent(); + + var oldValue = testCompServer.ServerWritable_Position.Value; + var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); + + testCompServer.ServerWritable_Position.Value = newValue; + yield return WaitForPositionsAreEqual(testCompServer.ServerWritable_Position, newValue); + + yield return WaitForServerWritableAreEqualOnAll(); + } + + [UnityTest] + public IEnumerator ClientChangesOwnerWritableNetVar() + { + yield return WaitForOwnerWritableAreEqualOnAll(); + + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + + int clientManagerIndex = m_ClientNetworkManagers.Length - 1; + var newOwnerClientId = m_ClientNetworkManagers[clientManagerIndex].LocalClientId; + testObjServer.ChangeOwnership(newOwnerClientId); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + + yield return WaitForOwnerWritableAreEqualOnAll(); + + var testObjClient = m_ClientNetworkManagers[clientManagerIndex].SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + + var oldValue = testCompClient.OwnerWritable_Position.Value; + var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); + + testCompClient.OwnerWritable_Position.Value = newValue; + yield return WaitForPositionsAreEqual(testCompClient.OwnerWritable_Position, newValue); + + yield return WaitForOwnerWritableAreEqualOnAll(); + } + + [UnityTest] + public IEnumerator ClientCannotChangeServerWritableNetVar() + { + yield return WaitForServerWritableAreEqualOnAll(); + + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompServer = testObjServer.GetComponent(); + + int clientManagerIndex = m_ClientNetworkManagers.Length - 1; + var newOwnerClientId = m_ClientNetworkManagers[clientManagerIndex].LocalClientId; + testObjServer.ChangeOwnership(newOwnerClientId); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + + yield return WaitForServerWritableAreEqualOnAll(); + + var testObjClient = m_ClientNetworkManagers[clientManagerIndex].SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + + var oldValue = testCompClient.ServerWritable_Position.Value; + var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); + + Assert.That(() => testCompClient.ServerWritable_Position.Value = newValue, Throws.TypeOf()); + yield return WaitForPositionsAreEqual(testCompServer.ServerWritable_Position, oldValue); + + yield return WaitForServerWritableAreEqualOnAll(); + + testCompServer.ServerWritable_Position.Value = newValue; + yield return WaitForPositionsAreEqual(testCompServer.ServerWritable_Position, newValue); + + yield return WaitForServerWritableAreEqualOnAll(); + } + + [UnityTest] + public IEnumerator ServerCannotChangeOwnerWritableNetVar() + { + yield return WaitForOwnerWritableAreEqualOnAll(); + + var testObjServer = m_ServerNetworkManager.SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompServer = testObjServer.GetComponent(); + + int clientManagerIndex = m_ClientNetworkManagers.Length - 1; + var newOwnerClientId = m_ClientNetworkManagers[clientManagerIndex].LocalClientId; + testObjServer.ChangeOwnership(newOwnerClientId); + yield return NetcodeIntegrationTestHelpers.WaitForTicks(m_ServerNetworkManager, 2); + + yield return WaitForOwnerWritableAreEqualOnAll(); + + var oldValue = testCompServer.OwnerWritable_Position.Value; + var newValue = oldValue + new Vector3(Random.Range(0, 100.0f), Random.Range(0, 100.0f), Random.Range(0, 100.0f)); + + Assert.That(() => testCompServer.OwnerWritable_Position.Value = newValue, Throws.TypeOf()); + yield return WaitForPositionsAreEqual(testCompServer.OwnerWritable_Position, oldValue); + + yield return WaitForOwnerWritableAreEqualOnAll(); + + var testObjClient = m_ClientNetworkManagers[clientManagerIndex].SpawnManager.SpawnedObjects[m_TestObjId]; + var testCompClient = testObjClient.GetComponent(); + + testCompClient.OwnerWritable_Position.Value = newValue; + yield return WaitForPositionsAreEqual(testCompClient.OwnerWritable_Position, newValue); + + yield return WaitForOwnerWritableAreEqualOnAll(); + } + } + public struct TestStruct : INetworkSerializable, IEquatable { public uint SomeInt; @@ -95,8 +315,6 @@ public class NetworkVariableTests : NetcodeIntegrationTest private const int k_TestVal2 = 222; private const int k_TestVal3 = 333; - private const int k_TestKey1 = 0x0f0f; - private static List s_ClientNetworkVariableTestInstances = new List(); public static void ClientNetworkVariableTestSpawned(NetworkVariableTest networkVariableTest) { @@ -111,7 +329,7 @@ public static void ClientNetworkVariableTestSpawned(NetworkVariableTest networkV private NetworkListTestPredicate m_NetworkListPredicateHandler; - private bool m_EnsureLengthSafety; + private readonly bool m_EnsureLengthSafety; public NetworkVariableTests(bool ensureLengthSafety) { diff --git a/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs index 443d8df..7f98b62 100644 --- a/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs +++ b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs @@ -1,3 +1,4 @@ +#if COM_UNITY_MODULES_PHYSICS2D using System.Collections; using NUnit.Framework; using Unity.Netcode.Components; @@ -76,3 +77,4 @@ public IEnumerator TestRigidbodyKinematicEnableDisable() } } } +#endif // COM_UNITY_MODULES_PHYSICS2D diff --git a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs index fb7b8e8..02ae245 100644 --- a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs +++ b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs @@ -1,3 +1,4 @@ +#if COM_UNITY_MODULES_PHYSICS using System.Collections; using NUnit.Framework; using Unity.Netcode.Components; @@ -75,3 +76,4 @@ public IEnumerator TestRigidbodyKinematicEnableDisable() } } } +#endif // COM_UNITY_MODULES_PHYSICS diff --git a/Tests/Runtime/PlayerObjectTests.cs b/Tests/Runtime/PlayerObjectTests.cs new file mode 100644 index 0000000..6b52c4f --- /dev/null +++ b/Tests/Runtime/PlayerObjectTests.cs @@ -0,0 +1,51 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Netcode.TestHelpers.Runtime; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + public class PlayerObjectTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + protected GameObject m_NewPlayerToSpawn; + + public PlayerObjectTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + protected override void OnServerAndClientsCreated() + { + m_NewPlayerToSpawn = CreateNetworkObjectPrefab("NewPlayerInstance"); + base.OnServerAndClientsCreated(); + } + + [UnityTest] + public IEnumerator SpawnAndReplaceExistingPlayerObject() + { + // Get the server-side player NetworkObject + var originalPlayer = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ClientNetworkManagers[0].LocalClientId]; + // Get the client-side player NetworkObject + var playerLocalClient = m_ClientNetworkManagers[0].LocalClient.PlayerObject; + + // Create a new player prefab instance + var newPlayer = Object.Instantiate(m_NewPlayerToSpawn); + var newPlayerNetworkObject = newPlayer.GetComponent(); + newPlayerNetworkObject.NetworkManagerOwner = m_ServerNetworkManager; + // Spawn this instance as a new player object for the client who already has an assigned player object + newPlayerNetworkObject.SpawnAsPlayerObject(m_ClientNetworkManagers[0].LocalClientId); + + // Make sure server-side changes are detected, the original player object should no longer be marked as a player + // and the local new player object should. + yield return WaitForConditionOrTimeOut(() => !originalPlayer.IsPlayerObject && newPlayerNetworkObject.IsPlayerObject); + Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for server-side player object to change!"); + + // Make sure the client-side changes are the same + yield return WaitForConditionOrTimeOut(() => m_ClientNetworkManagers[0].LocalClient.PlayerObject != playerLocalClient && !playerLocalClient.IsPlayerObject + && m_ClientNetworkManagers[0].LocalClient.PlayerObject.IsPlayerObject); + Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out waiting for client-side player object to change!"); + } + } +} diff --git a/Tests/Runtime/PlayerObjectTests.cs.meta b/Tests/Runtime/PlayerObjectTests.cs.meta new file mode 100644 index 0000000..1e25743 --- /dev/null +++ b/Tests/Runtime/PlayerObjectTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee94262f06e591e45a9382582013cf7a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/TransformInterpolationTests.cs b/Tests/Runtime/TransformInterpolationTests.cs index cedd2f2..2f61a35 100644 --- a/Tests/Runtime/TransformInterpolationTests.cs +++ b/Tests/Runtime/TransformInterpolationTests.cs @@ -16,10 +16,15 @@ public class TransformInterpolationObject : NetworkBehaviour private void Update() { + // Since the local position is transformed from local to global and vice-versa on the server and client + // it may accumulate some error. We allow an error of 0.01 over the range of 1000 used in this test. + // This requires precision to 5 digits, so it doesn't weaken the test, while preventing spurious failures + const float maxRoundingError = 0.01f; + // Check the position of the nested object on the client if (CheckPosition) { - if (transform.position.y < 0.0f || transform.position.y > 100.0f) + if (transform.position.y < -maxRoundingError || transform.position.y > 100.0f + maxRoundingError) { Debug.LogError($"Interpolation failure. transform.position.y is {transform.position.y}. Should be between 0.0 and 100.0"); } diff --git a/Tests/Runtime/Transport.meta b/Tests/Runtime/Transports.meta similarity index 100% rename from Tests/Runtime/Transport.meta rename to Tests/Runtime/Transports.meta diff --git a/Tests/Runtime/Transport/DummyTransport.cs b/Tests/Runtime/Transports/DummyTransport.cs similarity index 100% rename from Tests/Runtime/Transport/DummyTransport.cs rename to Tests/Runtime/Transports/DummyTransport.cs diff --git a/Tests/Runtime/Transport/DummyTransport.cs.meta b/Tests/Runtime/Transports/DummyTransport.cs.meta similarity index 100% rename from Tests/Runtime/Transport/DummyTransport.cs.meta rename to Tests/Runtime/Transports/DummyTransport.cs.meta diff --git a/Tests/Runtime/Transport/SIPTransportTests.cs b/Tests/Runtime/Transports/SIPTransportTests.cs similarity index 100% rename from Tests/Runtime/Transport/SIPTransportTests.cs rename to Tests/Runtime/Transports/SIPTransportTests.cs diff --git a/Tests/Runtime/Transport/SIPTransportTests.cs.meta b/Tests/Runtime/Transports/SIPTransportTests.cs.meta similarity index 100% rename from Tests/Runtime/Transport/SIPTransportTests.cs.meta rename to Tests/Runtime/Transports/SIPTransportTests.cs.meta diff --git a/Tests/Runtime/Transports/UnityTransportConnectionTests.cs b/Tests/Runtime/Transports/UnityTransportConnectionTests.cs new file mode 100644 index 0000000..df1f094 --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportConnectionTests.cs @@ -0,0 +1,383 @@ +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Unity.Netcode.Transports.UTP; +using UnityEngine; +using UnityEngine.TestTools; +using static Unity.Netcode.RuntimeTests.UnityTransportTestHelpers; + +namespace Unity.Netcode.RuntimeTests +{ + public class UnityTransportConnectionTests + { + // For tests using multiple clients. + private const int k_NumClients = 5; + private UnityTransport m_Server; + private UnityTransport[] m_Clients = new UnityTransport[k_NumClients]; + private List m_ServerEvents; + private List[] m_ClientsEvents = new List[k_NumClients]; + + [UnityTearDown] + public IEnumerator Cleanup() + { + if (m_Server) + { + m_Server.Shutdown(); + UnityEngine.Object.DestroyImmediate(m_Server); + } + + foreach (var transport in m_Clients) + { + if (transport) + { + transport.Shutdown(); + UnityEngine.Object.DestroyImmediate(transport); + } + } + + foreach (var transportEvents in m_ClientsEvents) + { + transportEvents?.Clear(); + } + + yield return null; + } + + // Check connection with a single client. + [UnityTest] + public IEnumerator ConnectSingleClient() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + // Check we've received Connect event on server too. + Assert.AreEqual(1, m_ServerEvents.Count); + Assert.AreEqual(NetworkEvent.Connect, m_ServerEvents[0].Type); + + yield return null; + } + + // Check connection with multiple clients. + [UnityTest] + public IEnumerator ConnectMultipleClients() + { + InitializeTransport(out m_Server, out m_ServerEvents); + m_Server.StartServer(); + + for (int i = 0; i < k_NumClients; i++) + { + InitializeTransport(out m_Clients[i], out m_ClientsEvents[i]); + m_Clients[i].StartClient(); + } + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[k_NumClients - 1]); + + // Check that every client received a Connect event. + Assert.True(m_ClientsEvents.All(evs => evs.Count == 1)); + Assert.True(m_ClientsEvents.All(evs => evs[0].Type == NetworkEvent.Connect)); + + // Check we've received Connect events on server too. + Assert.AreEqual(k_NumClients, m_ServerEvents.Count); + Assert.True(m_ServerEvents.All(ev => ev.Type == NetworkEvent.Connect)); + + yield return null; + } + + // Check server disconnection with a single client. + [UnityTest] + public IEnumerator ServerDisconnectSingleClient() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + m_Server.DisconnectRemoteClient(m_ServerEvents[0].ClientID); + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ClientsEvents[0]); + + yield return null; + } + + // Check server disconnection with multiple clients. + [UnityTest] + public IEnumerator ServerDisconnectMultipleClients() + { + InitializeTransport(out m_Server, out m_ServerEvents); + m_Server.StartServer(); + + for (int i = 0; i < k_NumClients; i++) + { + InitializeTransport(out m_Clients[i], out m_ClientsEvents[i]); + m_Clients[i].StartClient(); + } + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[k_NumClients - 1]); + + // Disconnect a single client. + m_Server.DisconnectRemoteClient(m_ServerEvents[0].ClientID); + + // Need to manually wait since we don't know which client will get the Disconnect. + yield return new WaitForSeconds(MaxNetworkEventWaitTime); + + // Check that we received a Disconnect event on only one client. + Assert.AreEqual(1, m_ClientsEvents.Count(evs => evs.Count == 2 && evs[1].Type == NetworkEvent.Disconnect)); + + // Disconnect all the other clients. + for (int i = 1; i < k_NumClients; i++) + { + m_Server.DisconnectRemoteClient(m_ServerEvents[i].ClientID); + } + + // Need to manually wait since we don't know which client got the Disconnect. + yield return new WaitForSeconds(MaxNetworkEventWaitTime); + + // Check that all clients got a Disconnect event. + Assert.True(m_ClientsEvents.All(evs => evs.Count == 2)); + Assert.True(m_ClientsEvents.All(evs => evs[1].Type == NetworkEvent.Disconnect)); + + yield return null; + } + + // Check client disconnection from a single client. + [UnityTest] + public IEnumerator ClientDisconnectSingleClient() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + m_Clients[0].DisconnectLocalClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents); + } + + // Check client disconnection with multiple clients. + [UnityTest] + public IEnumerator ClientDisconnectMultipleClients() + { + InitializeTransport(out m_Server, out m_ServerEvents); + m_Server.StartServer(); + + for (int i = 0; i < k_NumClients; i++) + { + InitializeTransport(out m_Clients[i], out m_ClientsEvents[i]); + m_Clients[i].StartClient(); + } + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[k_NumClients - 1]); + + // Disconnect a single client. + m_Clients[0].DisconnectLocalClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents); + + // Disconnect all the other clients. + for (int i = 1; i < k_NumClients; i++) + { + m_Clients[i].DisconnectLocalClient(); + } + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents); + + // Check that we got the correct number of Disconnect events on the server. + Assert.AreEqual(k_NumClients * 2, m_ServerEvents.Count); + Assert.AreEqual(k_NumClients, m_ServerEvents.Count(e => e.Type == NetworkEvent.Disconnect)); + + yield return null; + } + + // Check that server re-disconnects are no-ops. + [UnityTest] + public IEnumerator RepeatedServerDisconnectsNoop() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + m_Server.DisconnectRemoteClient(m_ServerEvents[0].ClientID); + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ClientsEvents[0]); + + var previousServerEventsCount = m_ServerEvents.Count; + var previousClientEventsCount = m_ClientsEvents[0].Count; + + m_Server.DisconnectRemoteClient(m_ServerEvents[0].ClientID); + + // Need to wait manually since no event should be generated. + yield return new WaitForSeconds(MaxNetworkEventWaitTime); + + // Check we haven't received anything else on the client or server. + Assert.AreEqual(m_ServerEvents.Count, previousServerEventsCount); + Assert.AreEqual(m_ClientsEvents[0].Count, previousClientEventsCount); + + yield return null; + } + + // Check that client re-disconnects are no-ops. + [UnityTest] + public IEnumerator RepeatedClientDisconnectsNoop() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + m_Clients[0].DisconnectLocalClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents); + + var previousServerEventsCount = m_ServerEvents.Count; + var previousClientEventsCount = m_ClientsEvents[0].Count; + + m_Clients[0].DisconnectLocalClient(); + + // Need to wait manually since no event should be generated. + yield return new WaitForSeconds(MaxNetworkEventWaitTime); + + // Check we haven't received anything else on the client or server. + Assert.AreEqual(m_ServerEvents.Count, previousServerEventsCount); + Assert.AreEqual(m_ClientsEvents[0].Count, previousClientEventsCount); + + yield return null; + } + + // Check connection with different server/listen addresses. + [UnityTest] + public IEnumerator DifferentServerAndListenAddresses() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.SetConnectionData("127.0.0.1", 10042, "0.0.0.0"); + m_Clients[0].SetConnectionData("127.0.0.1", 10042); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + // Check we've received Connect event on server too. + Assert.AreEqual(1, m_ServerEvents.Count); + Assert.AreEqual(NetworkEvent.Connect, m_ServerEvents[0].Type); + + yield return null; + } + + // Check server disconnection with data in send queue. + [UnityTest] + public IEnumerator ServerDisconnectWithDataInQueue() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + // Wait for the client to connect before we disconnect the client + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + var data = new ArraySegment(new byte[] { 42 }); + m_Server.Send(m_ServerEvents[0].ClientID, data, NetworkDelivery.Unreliable); + + m_Server.DisconnectRemoteClient(m_ServerEvents[0].ClientID); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ClientsEvents[0]); + + if (m_ClientsEvents[0].Count >= 3) + { + Assert.AreEqual(NetworkEvent.Disconnect, m_ClientsEvents[0][2].Type); + } + else + { + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ClientsEvents[0]); + } + } + + // Check client disconnection with data in send queue. + [UnityTest] + public IEnumerator ClientDisconnectWithDataInQueue() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + + m_Server.StartServer(); + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ServerEvents); + + var data = new ArraySegment(new byte[] { 42 }); + m_Clients[0].Send(m_Clients[0].ServerClientId, data, NetworkDelivery.Unreliable); + + m_Clients[0].DisconnectLocalClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + if (m_ServerEvents.Count >= 3) + { + Assert.AreEqual(NetworkEvent.Disconnect, m_ServerEvents[2].Type); + } + else + { + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents); + } + } + + // Check that a server can disconnect a client after another client has disconnected. + [UnityTest] + public IEnumerator ServerDisconnectAfterClientDisconnect() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Clients[0], out m_ClientsEvents[0]); + InitializeTransport(out m_Clients[1], out m_ClientsEvents[1]); + + m_Server.StartServer(); + + m_Clients[0].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[0]); + + m_Clients[1].StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_ClientsEvents[1]); + + m_Clients[0].DisconnectLocalClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ServerEvents); + + // Pick the client ID of the still connected client. + var clientId = m_ServerEvents[0].ClientID; + if (m_ServerEvents[2].ClientID == clientId) + { + clientId = m_ServerEvents[1].ClientID; + } + + m_Server.DisconnectRemoteClient(clientId); + + yield return WaitForNetworkEvent(NetworkEvent.Disconnect, m_ClientsEvents[1]); + + yield return null; + } + } +} diff --git a/Tests/Runtime/Transports/UnityTransportConnectionTests.cs.meta b/Tests/Runtime/Transports/UnityTransportConnectionTests.cs.meta new file mode 100644 index 0000000..fbdf1b9 --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportConnectionTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a6b90810a6304cd98c615c9828888a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Transports/UnityTransportDriverClient.cs b/Tests/Runtime/Transports/UnityTransportDriverClient.cs new file mode 100644 index 0000000..e61ce4b --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportDriverClient.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using System.Collections; +using Unity.Networking.Transport; +using Unity.Networking.Transport.Utilities; +using Unity.Netcode.Transports.UTP; +using UnityEngine; + +using UTPNetworkEvent = Unity.Networking.Transport.NetworkEvent; +using static Unity.Netcode.RuntimeTests.UnityTransportTestHelpers; + +namespace Unity.Netcode.RuntimeTests +{ + // Thin wrapper around a UTP NetworkDriver that can act as a client to a UnityTransport server. + // In particular that means the pipelines are set up the same way as in UnityTransport. + // + // The only reason it's defined as a MonoBehaviour is that OnDestroy is the only reliable way + // to get the driver's Dispose method called from a UnityTest. Making it disposable would be + // the preferred solution, but that doesn't always mesh well with coroutines. + public class UnityTransportDriverClient : MonoBehaviour + { + private NetworkDriver m_Driver; + public NetworkDriver Driver => m_Driver; + + private NetworkConnection m_Connection; + + private NetworkPipeline m_UnreliableSequencedPipeline; + private NetworkPipeline m_ReliableSequencedPipeline; + private NetworkPipeline m_ReliableSequencedFragmentedPipeline; + + public NetworkPipeline UnreliableSequencedPipeline => m_UnreliableSequencedPipeline; + public NetworkPipeline ReliableSequencedPipeline => m_ReliableSequencedPipeline; + public NetworkPipeline ReliableSequencedFragmentedPipeline => m_ReliableSequencedFragmentedPipeline; + + private NetworkPipeline m_LastEventPipeline; + public NetworkPipeline LastEventPipeline => m_LastEventPipeline; + + private void Awake() + { + var maxCap = UnityTransport.InitialMaxPayloadSize + 128; + + var settings = new NetworkSettings(); + settings.WithFragmentationStageParameters(payloadCapacity: maxCap); + + var fragParams = new FragmentationUtility.Parameters() { PayloadCapacity = maxCap }; + + m_Driver = NetworkDriver.Create(settings); + + m_UnreliableSequencedPipeline = m_Driver.CreatePipeline(typeof(UnreliableSequencedPipelineStage)); + m_ReliableSequencedPipeline = m_Driver.CreatePipeline(typeof(ReliableSequencedPipelineStage)); + m_ReliableSequencedFragmentedPipeline = m_Driver.CreatePipeline(typeof(FragmentationPipelineStage), typeof(ReliableSequencedPipelineStage)); + } + + private void Update() + { + m_Driver.ScheduleUpdate().Complete(); + } + + private void OnDestroy() + { + if (m_Driver.IsCreated) + { + m_Driver.Dispose(); + } + } + + public void Connect() + { + var endpoint = NetworkEndPoint.LoopbackIpv4; + endpoint.Port = 7777; + + m_Connection = m_Driver.Connect(endpoint); + } + + // Wait for the given event to be generated by the client's driver. + public IEnumerator WaitForNetworkEvent(UTPNetworkEvent.Type type) + { + float startTime = Time.realtimeSinceStartup; + + while (Time.realtimeSinceStartup - startTime < MaxNetworkEventWaitTime) + { + UTPNetworkEvent.Type eventType = m_Driver.PopEvent(out _, out _, out m_LastEventPipeline); + if (eventType != UTPNetworkEvent.Type.Empty) + { + Assert.AreEqual(type, eventType); + yield break; + } + + yield return null; + } + + Assert.Fail("Timed out while waiting for network event."); + } + } +} diff --git a/Tests/Runtime/Transports/UnityTransportDriverClient.cs.meta b/Tests/Runtime/Transports/UnityTransportDriverClient.cs.meta new file mode 100644 index 0000000..e3a021a --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportDriverClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a087bb407f16c402dbc67a2ab9386eab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Transports/UnityTransportTestHelpers.cs b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs new file mode 100644 index 0000000..254cc86 --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using Unity.Netcode.Transports.UTP; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + public static class UnityTransportTestHelpers + { + // Half a second might seem like a very long time to wait for a network event, but in CI + // many of the machines are underpowered (e.g. old Android devices or Macs) and there are + // sometimes very high lag spikes. PS4 and Switch are particularly sensitive in this regard + // so we allow even more time for these platforms. + public const float MaxNetworkEventWaitTime = 0.5f; + + // Wait for an event to appear in the given event list (must be the very next event). + public static IEnumerator WaitForNetworkEvent(NetworkEvent type, List events, float timeout = MaxNetworkEventWaitTime) + { + int initialCount = events.Count; + float startTime = Time.realtimeSinceStartup; + + while (Time.realtimeSinceStartup - startTime < timeout) + { + if (events.Count > initialCount) + { + Assert.AreEqual(type, events[initialCount].Type); + yield break; + } + + yield return new WaitForSeconds(0.01f); + } + + Assert.Fail("Timed out while waiting for network event."); + } + + // Common code to initialize a UnityTransport that logs its events. + public static void InitializeTransport(out UnityTransport transport, out List events, int maxPayloadSize = UnityTransport.InitialMaxPayloadSize) + { + var logger = new TransportEventLogger(); + events = logger.Events; + + transport = new GameObject().AddComponent(); + transport.OnTransportEvent += logger.HandleEvent; + transport.SetMaxPayloadSize(maxPayloadSize); + transport.Initialize(); + } + + // Information about an event generated by a transport (basically just the parameters that + // are normally passed along to a TransportEventDelegate). + public struct TransportEvent + { + public NetworkEvent Type; + public ulong ClientID; + public ArraySegment Data; + public float ReceiveTime; + } + // Utility class that logs events generated by a UnityTransport. Set it up by adding the + // HandleEvent method as an OnTransportEvent delegate of the transport. The list of events + // (in order in which they were generated) can be accessed through the Events property. + public class TransportEventLogger + { + private readonly List m_Events = new List(); + public List Events => m_Events; + public void HandleEvent(NetworkEvent type, ulong clientID, ArraySegment data, float receiveTime) + { + // Copy the data since the backing array will be reused for future messages. + if (data != default(ArraySegment)) + { + var dataCopy = new byte[data.Count]; + Array.Copy(data.Array, data.Offset, dataCopy, 0, data.Count); + data = new ArraySegment(dataCopy); + } + m_Events.Add(new TransportEvent + { + Type = type, + ClientID = clientID, + Data = data, + ReceiveTime = receiveTime + }); + } + } + } +} diff --git a/Tests/Runtime/Transports/UnityTransportTestHelpers.cs.meta b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs.meta new file mode 100644 index 0000000..2bef2ea --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportTestHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3af9d71bc7c414cebad947e6340de223 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Transports/UnityTransportTests.cs b/Tests/Runtime/Transports/UnityTransportTests.cs new file mode 100644 index 0000000..5dec849 --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportTests.cs @@ -0,0 +1,461 @@ +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Unity.Netcode.Transports.UTP; +using UnityEngine; +using UnityEngine.TestTools; +using static Unity.Netcode.RuntimeTests.UnityTransportTestHelpers; + +namespace Unity.Netcode.RuntimeTests +{ + public class UnityTransportTests + { + // No need to test all reliable delivery methods since they all map to the same pipeline. + private static readonly NetworkDelivery[] k_DeliveryParameters = + { + NetworkDelivery.Unreliable, + NetworkDelivery.UnreliableSequenced, + NetworkDelivery.Reliable + }; + + private UnityTransport m_Server, m_Client1, m_Client2; + private List m_ServerEvents, m_Client1Events, m_Client2Events; + + [UnityTearDown] + public IEnumerator Cleanup() + { + if (m_Server) + { + m_Server.Shutdown(); + + // Need to destroy the GameObject (all assigned components will get destroyed too) + UnityEngine.Object.DestroyImmediate(m_Server.gameObject); + } + + if (m_Client1) + { + m_Client1.Shutdown(); + + // Need to destroy the GameObject (all assigned components will get destroyed too) + UnityEngine.Object.DestroyImmediate(m_Client1.gameObject); + } + + if (m_Client2) + { + m_Client2.Shutdown(); + + // Need to destroy the GameObject (all assigned components will get destroyed too) + UnityEngine.Object.DestroyImmediate(m_Client2.gameObject); + } + + m_ServerEvents?.Clear(); + m_Client1Events?.Clear(); + m_Client2Events?.Clear(); + + yield return null; + } + + // Check if can make a simple data exchange. + [UnityTest] + public IEnumerator PingPong([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var ping = new ArraySegment(Encoding.ASCII.GetBytes("ping")); + m_Client1.Send(m_Client1.ServerClientId, ping, delivery); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + Assert.That(m_ServerEvents[1].Data, Is.EquivalentTo(Encoding.ASCII.GetBytes("ping"))); + + var pong = new ArraySegment(Encoding.ASCII.GetBytes("pong")); + m_Server.Send(m_ServerEvents[0].ClientID, pong, delivery); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_Client1Events); + + Assert.That(m_Client1Events[1].Data, Is.EquivalentTo(Encoding.ASCII.GetBytes("pong"))); + + yield return null; + } + + // Check if can make a simple data exchange (both ways at a time). + [UnityTest] + public IEnumerator PingPongSimultaneous([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var ping = new ArraySegment(Encoding.ASCII.GetBytes("ping")); + m_Server.Send(m_ServerEvents[0].ClientID, ping, delivery); + m_Client1.Send(m_Client1.ServerClientId, ping, delivery); + + // Once one event is in the other should be too. + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + Assert.That(m_ServerEvents[1].Data, Is.EquivalentTo(Encoding.ASCII.GetBytes("ping"))); + Assert.That(m_Client1Events[1].Data, Is.EquivalentTo(Encoding.ASCII.GetBytes("ping"))); + + var pong = new ArraySegment(Encoding.ASCII.GetBytes("pong")); + m_Server.Send(m_ServerEvents[0].ClientID, pong, delivery); + m_Client1.Send(m_Client1.ServerClientId, pong, delivery); + + // Once one event is in the other should be too. + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + Assert.That(m_ServerEvents[2].Data, Is.EquivalentTo(Encoding.ASCII.GetBytes("pong"))); + Assert.That(m_Client1Events[2].Data, Is.EquivalentTo(Encoding.ASCII.GetBytes("pong"))); + + yield return null; + } + + // Test is ignored on Switch, PS4, and PS5 because on these platforms the OS buffers for + // loopback traffic are too small for the amount of data sent in a single update here. + [UnityTest] + [UnityPlatform(exclude = new[] { RuntimePlatform.Switch, RuntimePlatform.PS4, RuntimePlatform.PS5 })] + public IEnumerator SendMaximumPayloadSize([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + // We want something that's over the old limit of ~44KB for reliable payloads. + var payloadSize = 64 * 1024; + + InitializeTransport(out m_Server, out m_ServerEvents, payloadSize); + InitializeTransport(out m_Client1, out m_Client1Events, payloadSize); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var payloadData = new byte[payloadSize]; + for (int i = 0; i < payloadData.Length; i++) + { + payloadData[i] = (byte)i; + } + + var payload = new ArraySegment(payloadData); + m_Client1.Send(m_Client1.ServerClientId, payload, delivery); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents, MaxNetworkEventWaitTime * 4); + + Assert.AreEqual(payloadSize, m_ServerEvents[1].Data.Count); + + var receivedArray = m_ServerEvents[1].Data.Array; + var receivedArrayOffset = m_ServerEvents[1].Data.Offset; + for (int i = 0; i < payloadSize; i++) + { + Assert.AreEqual(payloadData[i], receivedArray[receivedArrayOffset + i]); + } + + yield return null; + } + + // Check making multiple sends to a client in a single frame. + [UnityTest] + public IEnumerator MultipleSendsSingleFrame([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var data1 = new ArraySegment(new byte[] { 11 }); + m_Client1.Send(m_Client1.ServerClientId, data1, delivery); + + var data2 = new ArraySegment(new byte[] { 22 }); + m_Client1.Send(m_Client1.ServerClientId, data2, delivery); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + Assert.AreEqual(3, m_ServerEvents.Count); + Assert.AreEqual(NetworkEvent.Data, m_ServerEvents[2].Type); + + Assert.AreEqual(11, m_ServerEvents[1].Data.First()); + Assert.AreEqual(22, m_ServerEvents[2].Data.First()); + + yield return null; + } + + // Check sending data to multiple clients. + [UnityTest] + public IEnumerator SendMultipleClients([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + InitializeTransport(out m_Client2, out m_Client2Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + m_Client2.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + if (m_Client2Events.Count == 0) + { + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client2Events); + } + + // Ensure we got both Connect events. + Assert.AreEqual(2, m_ServerEvents.Count); + + var data1 = new ArraySegment(new byte[] { 11 }); + m_Server.Send(m_ServerEvents[0].ClientID, data1, delivery); + + var data2 = new ArraySegment(new byte[] { 22 }); + m_Server.Send(m_ServerEvents[1].ClientID, data2, delivery); + + // Once one has received its data, the other should have too. + yield return WaitForNetworkEvent(NetworkEvent.Data, m_Client1Events); + + // Do make sure the other client got its Data event. + Assert.AreEqual(2, m_Client2Events.Count); + Assert.AreEqual(NetworkEvent.Data, m_Client2Events[1].Type); + + byte c1Data = m_Client1Events[1].Data.First(); + byte c2Data = m_Client2Events[1].Data.First(); + Assert.That((c1Data == 11 && c2Data == 22) || (c1Data == 22 && c2Data == 11)); + + yield return null; + } + + // Check receiving data from multiple clients. + [UnityTest] + public IEnumerator ReceiveMultipleClients([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + InitializeTransport(out m_Client2, out m_Client2Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + m_Client2.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + if (m_Client2Events.Count == 0) + { + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client2Events); + } + + var data1 = new ArraySegment(new byte[] { 11 }); + m_Client1.Send(m_Client1.ServerClientId, data1, delivery); + + var data2 = new ArraySegment(new byte[] { 22 }); + m_Client2.Send(m_Client2.ServerClientId, data2, delivery); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + // Make sure we got both data messages. + Assert.AreEqual(4, m_ServerEvents.Count); + Assert.AreEqual(NetworkEvent.Data, m_ServerEvents[3].Type); + + byte sData1 = m_ServerEvents[2].Data.First(); + byte sData2 = m_ServerEvents[3].Data.First(); + Assert.That((sData1 == 11 && sData2 == 22) || (sData1 == 22 && sData2 == 11)); + + yield return null; + } + + // Check that we get disconnected when overflowing the reliable send queue. + [UnityTest] + public IEnumerator DisconnectOnReliableSendQueueOverflow() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + m_Server.Shutdown(); + + var numSends = (UnityTransport.InitialMaxSendQueueSize / 1024); + + for (int i = 0; i < numSends; i++) + { + var payload = new ArraySegment(new byte[1024]); + m_Client1.Send(m_Client1.ServerClientId, payload, NetworkDelivery.Reliable); + } + + LogAssert.Expect(LogType.Error, "Couldn't add payload of size 1024 to reliable send queue. " + + $"Closing connection {m_Client1.ServerClientId} as reliability guarantees can't be maintained. " + + $"Perhaps 'Max Send Queue Size' ({UnityTransport.InitialMaxSendQueueSize}) is too small for workload."); + + Assert.AreEqual(2, m_Client1Events.Count); + Assert.AreEqual(NetworkEvent.Disconnect, m_Client1Events[1].Type); + + yield return null; + } + + // Check that it's fine to overflow the unreliable send queue (traffic is flushed on overflow). + // Test is ignored on Switch, PS4, and PS5 because on these platforms the OS buffers for + // loopback traffic are too small for the amount of data sent in a single update here. + [UnityTest] + [UnityPlatform(exclude = new[] { RuntimePlatform.Switch, RuntimePlatform.PS4, RuntimePlatform.PS5 })] + public IEnumerator SendCompletesOnUnreliableSendQueueOverflow() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var numSends = (UnityTransport.InitialMaxSendQueueSize / 1024) + 1; + + for (int i = 0; i < numSends; i++) + { + var payload = new ArraySegment(new byte[1024]); + m_Client1.Send(m_Client1.ServerClientId, payload, NetworkDelivery.Unreliable); + } + + // Manually wait. This ends up generating quite a bit of packets and it might take a + // while for everything to make it to the server. + yield return new WaitForSeconds(numSends * 0.02f); + + // Extra event is the connect event. + Assert.AreEqual(numSends + 1, m_ServerEvents.Count); + + for (int i = 1; i <= numSends; i++) + { + Assert.AreEqual(NetworkEvent.Data, m_ServerEvents[i].Type); + Assert.AreEqual(1024, m_ServerEvents[i].Data.Count); + } + + yield return null; + } + + // Check that simulator parameters are effective. We only check with the drop rate, because + // that's easy to check and we only really want to make sure the simulator parameters are + // configured properly (the simulator pipeline stage is already well-tested in UTP). + [UnityTest] + [UnityPlatform(include = new[] { RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor, RuntimePlatform.LinuxEditor })] + public IEnumerator SimulatorParametersAreEffective() + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.SetDebugSimulatorParameters(0, 0, 100); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var data = new ArraySegment(new byte[] { 42 }); + m_Client1.Send(m_Client1.ServerClientId, data, NetworkDelivery.Reliable); + + yield return new WaitForSeconds(MaxNetworkEventWaitTime); + + Assert.AreEqual(1, m_ServerEvents.Count); + + yield return null; + } + + // Check that RTT is reported correctly. + [UnityTest] + [UnityPlatform(include = new[] { RuntimePlatform.OSXEditor, RuntimePlatform.WindowsEditor, RuntimePlatform.LinuxEditor })] + public IEnumerator CurrentRttReportedCorrectly() + { + const int simulatedRtt = 25; + + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.SetDebugSimulatorParameters(simulatedRtt, 0, 0); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var data = new ArraySegment(new byte[] { 42 }); + m_Client1.Send(m_Client1.ServerClientId, data, NetworkDelivery.Reliable); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents, + timeout: MaxNetworkEventWaitTime + (2 * simulatedRtt)); + + Assert.GreaterOrEqual(m_Client1.GetCurrentRtt(m_Client1.ServerClientId), simulatedRtt); + + yield return null; + } + + [UnityTest] + public IEnumerator SendQueuesFlushedOnShutdown([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var data = new ArraySegment(new byte[] { 42 }); + m_Client1.Send(m_Client1.ServerClientId, data, delivery); + + m_Client1.Shutdown(); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + yield return null; + } + + [UnityTest] + public IEnumerator SendQueuesFlushedOnLocalClientDisconnect([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var data = new ArraySegment(new byte[] { 42 }); + m_Client1.Send(m_Client1.ServerClientId, data, delivery); + + m_Client1.DisconnectLocalClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_ServerEvents); + + yield return null; + } + + [UnityTest] + public IEnumerator SendQueuesFlushedOnRemoteClientDisconnect([ValueSource("k_DeliveryParameters")] NetworkDelivery delivery) + { + InitializeTransport(out m_Server, out m_ServerEvents); + InitializeTransport(out m_Client1, out m_Client1Events); + + m_Server.StartServer(); + m_Client1.StartClient(); + + yield return WaitForNetworkEvent(NetworkEvent.Connect, m_Client1Events); + + var data = new ArraySegment(new byte[] { 42 }); + m_Server.Send(m_Client1.ServerClientId, data, delivery); + + m_Server.DisconnectRemoteClient(m_ServerEvents[0].ClientID); + + yield return WaitForNetworkEvent(NetworkEvent.Data, m_Client1Events); + + yield return null; + } + } +} diff --git a/Tests/Runtime/Transports/UnityTransportTests.cs.meta b/Tests/Runtime/Transports/UnityTransportTests.cs.meta new file mode 100644 index 0000000..c27f4e2 --- /dev/null +++ b/Tests/Runtime/Transports/UnityTransportTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66e054c3619ad4bd982e351eb8d63087 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef index 3a54749..59b5f84 100644 --- a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef +++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef @@ -9,7 +9,7 @@ "Unity.Multiplayer.NetStats", "Unity.Multiplayer.Tools.MetricTypes", "Unity.Multiplayer.Tools.NetStats", - "Unity.Netcode.Adapter.UTP", + "Unity.Networking.Transport", "ClientNetworkTransform", "Unity.Netcode.TestHelpers.Runtime" ], @@ -27,15 +27,10 @@ "expression": "(0,2022.2.0a5)", "define": "UNITY_UNET_PRESENT" }, - { - "name": "com.unity.netcode.adapter.utp", - "expression": "", - "define": "UTP_ADAPTER" - }, { "name": "com.unity.multiplayer.tools", - "expression": "1.0.0-pre.4", - "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_4" + "expression": "1.0.0-pre.7", + "define": "MULTIPLAYER_TOOLS_1_0_0_PRE_7" } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 2b58a63..b65d2f1 100644 --- a/package.json +++ b/package.json @@ -2,25 +2,22 @@ "name": "com.unity.netcode.gameobjects", "displayName": "Netcode for GameObjects", "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", - "version": "1.0.0-pre.6", + "version": "1.0.0-pre.7", "unity": "2020.3", "dependencies": { - "com.unity.modules.animation": "1.0.0", - "com.unity.modules.physics": "1.0.0", - "com.unity.modules.physics2d": "1.0.0", "com.unity.nuget.mono-cecil": "1.10.1", - "com.unity.collections": "1.1.0" + "com.unity.transport": "1.0.0" }, "upm": { - "changelog": "### Added,- NetworkAnimator now properly synchrhonizes all animation layers as well as runtime-adjusted weighting between them (#1765),- Added first set of tests for NetworkAnimator - parameter syncing, trigger set / reset, override network animator (#1735),### Changed,### Fixed,- Fixed an issue where sometimes the first client to connect to the server could see messages from the server as coming from itself. (#1683),- Fixed an issue where clients seemed to be able to send messages to ClientId 1, but these messages would actually still go to the server (id 0) instead of that client. (#1683),- Improved clarity of error messaging when a client attempts to send a message to a destination other than the server, which isn't allowed. (#1683),- Disallowed async keyword in RPCs (#1681),- Fixed an issue where Alpha release versions of Unity (version 2022.2.0a5 and later) will not compile due to the UNet Transport no longer existing (#1678),- Fixed messages larger than 64k being written with incorrectly truncated message size in header (#1686) (credit: @kaen),- Fixed overloading RPC methods causing collisions and failing on IL2CPP targets. (#1694),- Fixed spawn flow to propagate `IsSceneObject` down to children NetworkObjects, decouple implicit relationship between object spawning & `IsSceneObject` flag (#1685),- Fixed error when serializing ConnectionApprovalMessage with scene management disabled when one or more objects is hidden via the CheckObjectVisibility delegate (#1720),- Fixed CheckObjectVisibility delegate not being properly invoked for connecting clients when Scene Management is enabled. (#1680),- Fixed NetworkList to properly call INetworkSerializable's NetworkSerialize() method (#1682),- Fixed NetworkVariables containing more than 1300 bytes of data (such as large NetworkLists) no longer cause an OverflowException (the limit on data size is now whatever limit the chosen transport imposes on fragmented NetworkDelivery mechanisms) (#1725),- Fixed ServerRpcParams and ClientRpcParams must be the last parameter of an RPC in order to function properly. Added a compile-time check to ensure this is the case and trigger an error if they're placed elsewhere (#1721),- Fixed FastBufferReader being created with a length of 1 if provided an input of length 0 (#1724),- Fixed The NetworkConfig's checksum hash includes the NetworkTick so that clients with a different tickrate than the server are identified and not allowed to connect (#1728),- Fixed OwnedObjects not being properly modified when using ChangeOwnership (#1731),- Improved performance in NetworkAnimator (#1735),- Removed the \"always sync\" network animator (aka \"autosend\") parameters (#1746)" + "changelog": "### Added\n\n- Added editor only check prior to entering into play mode if the currently open and active scene is in the build list and if not displays a dialog box asking the user if they would like to automatically add it prior to entering into play mode. (#1828)\n- Added `UnityTransport` implementation and `com.unity.transport` package dependency (#1823)\n- Added `NetworkVariableWritePermission` to `NetworkVariableBase` and implemented `Owner` client writable netvars. (#1762)\n- `UnityTransport` settings can now be set programmatically. (#1845)\n- `FastBufferWriter` and Reader IsInitialized property. (#1859)\n\n### Changed\n\n- Updated `UnityTransport` dependency on `com.unity.transport` to 1.0.0 (#1849)\n\n### Removed\n\n- Removed `SnapshotSystem` (#1852)\n- Removed `com.unity.modules.animation`, `com.unity.modules.physics` and `com.unity.modules.physics2d` dependencies from the package (#1812)\n- Removed `com.unity.collections` dependency from the package (#1849)\n\n### Fixed\n- Fixed in-scene placed NetworkObjects not being found/ignored after a client disconnects and then reconnects. (#1850)\n- Fixed issue where `UnityTransport` send queues were not flushed when calling `DisconnectLocalClient` or `DisconnectRemoteClient`. (#1847)\n- Fixed NetworkBehaviour dependency verification check for an existing NetworkObject not searching from root parent transform relative GameObject. (#1841)\n- Fixed issue where entries were not being removed from the NetworkSpawnManager.OwnershipToObjectsTable. (#1838)\n- Fixed ClientRpcs would always send to all connected clients by default as opposed to only sending to the NetworkObject's Observers list by default. (#1836)\n- Fixed clarity for NetworkSceneManager client side notification when it receives a scene hash value that does not exist in its local hash table. (#1828)\n- Fixed client throws a key not found exception when it times out using UNet or UTP. (#1821)\n- Fixed network variable updates are no longer limited to 32,768 bytes when NetworkConfig.EnsureNetworkVariableLengthSafety is enabled. The limits are now determined by what the transport can send in a message. (#1811)\n- Fixed in-scene NetworkObjects get destroyed if a client fails to connect and shuts down the NetworkManager. (#1809)\n- Fixed user never being notified in the editor that a NetworkBehaviour requires a NetworkObject to function properly. (#1808)\n- Fixed PlayerObjects and dynamically spawned NetworkObjects not being added to the NetworkClient's OwnedObjects (#1801)\n- Fixed issue where NetworkManager would continue starting even if the NetworkTransport selected failed. (#1780)\n- Fixed issue when spawning new player if an already existing player exists it does not remove IsPlayer from the previous player (#1779)\n- Fixed lack of notification that NetworkManager and NetworkObject cannot be added to the same GameObject with in-editor notifications (#1777)\n- Fixed parenting warning printing for false positives (#1855)" }, "upmCi": { - "footprint": "86275b5331ab1e0e0a176d177bcd429a69c058af" + "footprint": "98c91bebf56ec13b76dfa5cf9d5d22946f7529cd" }, "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "cc3c088aad2bfb1b3da51137057df65b882cad45" + "revision": "16420648a7790c64f917e000f7d0cdbe57350835" }, "samples": [ {