commit 22d877d1b2400ecb1d80469077e4328e5c8c0dd1 Author: Unity Technologies <@unity> Date: Sun Dec 20 00:00:00 2020 +0000 com.unity.netcode.gameobjects@1.0.0-pre.2 # Changelog All notable changes to this project will be documented in this file. 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.2] - 2020-12-20 ### Added - Associated Known Issues for the 1.0.0-pre.1 release in the changelog ### Changed - Updated label for `1.0.0-pre.1` changelog section ## [1.0.0-pre.1] - 2020-12-20 ### Added - Added `ClientNetworkTransform` sample to the SDK package (#1168) - Added `Bootstrap` sample to the SDK package (#1140) - Enhanced `NetworkSceneManager` implementation with additive scene loading capabilities (#1080, #955, #913) - `NetworkSceneManager.OnSceneEvent` provides improved scene event notificaitons - Enhanced `NetworkTransform` implementation with per axis/component based and threshold based state replication (#1042, #1055, #1061, #1084, #1101) - Added a jitter-resistent `BufferedLinearInterpolator` for `NetworkTransform` (#1060) - Implemented `NetworkPrefabHandler` that provides support for object pooling and `NetworkPrefab` overrides (#1073, #1004, #977, #905,#749, #727) - Implemented auto `NetworkObject` transform parent synchronization at runtime over the network (#855) - Adopted Unity C# Coding Standards in the codebase with `.editorconfig` ruleset (#666, #670) - When a client tries to spawn a `NetworkObject` an exception is thrown to indicate unsupported behavior. (#981) - Added a `NetworkTime` and `NetworkTickSystem` which allows for improved control over time and ticks. (#845) - Added a `OnNetworkDespawn` function to `NetworkObject` which gets called when a `NetworkObject` gets despawned and can be overriden. (#865) - Added `SnapshotSystem` that would allow variables and spawn/despawn messages to be sent in blocks (#805, #852, #862, #963, #1012, #1013, #1021, #1040, #1062, #1064, #1083, #1091, #1111, #1129, #1166, #1192) - Disabled by default for now, except spawn/despawn messages - Will leverage unreliable messages with eventual consistency - `NetworkBehaviour` and `NetworkObject`'s `NetworkManager` instances can now be overriden (#762) - Added metrics reporting for the new network profiler if the Multiplayer Tools package is present (#1104, #1089, #1096, #1086, #1072, #1058, #960, #897, #891, #878) - `NetworkBehaviour.IsSpawned` a quick (and stable) way to determine if the associated NetworkObject is spawned (#1190) - Added `NetworkRigidbody` and `NetworkRigidbody2D` components to support networking `Rigidbody` and `Rigidbody2D` components (#1202, #1175) - Added `NetworkObjectReference` and `NetworkBehaviourReference` structs which allow to sending `NetworkObject/Behaviours` over RPCs/`NetworkVariable`s (#1173) - Added `NetworkAnimator` component to support networking `Animator` component (#1281, #872) ### Changed - Bumped minimum Unity version, renamed package as "Unity Netcode for GameObjects", replaced `MLAPI` namespace and its variants with `Unity.Netcode` namespace and per asm-def variants (#1007, #1009, #1015, #1017, #1019, #1025, #1026, #1065) - Minimum Unity version: - 2019.4 → 2020.3+ - Package rename: - Display name: `MLAPI Networking Library` → `Netcode for GameObjects` - Name: `com.unity.multiplayer.mlapi` → `com.unity.netcode.gameobjects` - Updated package description - All `MLAPI.x` namespaces are replaced with `Unity.Netcode` - `MLAPI.Messaging` → `Unity.Netcode` - `MLAPI.Connection` → `Unity.Netcode` - `MLAPI.Logging` → `Unity.Netcode` - `MLAPI.SceneManagement` → `Unity.Netcode` - and other `MLAPI.x` variants to `Unity.Netcode` - All assembly definitions are renamed with `Unity.Netcode.x` variants - `Unity.Multiplayer.MLAPI.Runtime` → `Unity.Netcode.Runtime` - `Unity.Multiplayer.MLAPI.Editor` → `Unity.Netcode.Editor` - and other `Unity.Multiplayer.MLAPI.x` variants to `Unity.Netcode.x` variants - Renamed `Prototyping` namespace and assembly definition to `Components` (#1145) - Changed `NetworkObject.Despawn(bool destroy)` API to default to `destroy = true` for better usability (#1217) - Scene registration in `NetworkManager` is now replaced by Build Setttings → Scenes in Build List (#1080) - `NetworkSceneManager.SwitchScene` has been replaced by `NetworkSceneManager.LoadScene` (#955) - `NetworkManager, NetworkConfig, and NetworkSceneManager` scene registration replaced with scenes in build list (#1080) - `GlobalObjectIdHash` replaced `PrefabHash` and `PrefabHashGenerator` for stability and consistency (#698) - `NetworkStart` has been renamed to `OnNetworkSpawn`. (#865) - Network variable cleanup - eliminated shared mode, variables are server-authoritative (#1059, #1074) - `NetworkManager` and other systems are no longer singletons/statics (#696, #705, #706, #737, #738, #739, #746, #747, #763, #765, #766, #783, #784, #785, #786, #787, #788) - Changed `INetworkSerializable.NetworkSerialize` method signature to use `BufferSerializer` instead of `NetworkSerializer` (#1187) - Changed `CustomMessagingManager`'s methods to use `FastBufferWriter` and `FastBufferReader` instead of `Stream` (#1187) - Reduced internal runtime allocations by removing LINQ calls and replacing managed lists/arrays with native collections (#1196) ### Removed - Removed `NetworkNavMeshAgent` (#1150) - Removed `NetworkDictionary`, `NetworkSet` (#1149) - Removed `NetworkVariableSettings` (#1097) - Removed predefined `NetworkVariable` types (#1093) - Removed `NetworkVariableBool`, `NetworkVariableByte`, `NetworkVariableSByte`, `NetworkVariableUShort`, `NetworkVariableShort`, `NetworkVariableUInt`, `NetworkVariableInt`, `NetworkVariableULong`, `NetworkVariableLong`, `NetworkVariableFloat`, `NetworkVariableDouble`, `NetworkVariableVector2`, `NetworkVariableVector3`, `NetworkVariableVector4`, `NetworkVariableColor`, `NetworkVariableColor32`, `NetworkVariableRay`, `NetworkVariableQuaternion` - Removed `NetworkChannel` and `MultiplexTransportAdapter` (#1133) - Removed ILPP backend for 2019.4, minimum required version is 2020.3+ (#895) - `NetworkManager.NetworkConfig` had the following properties removed: (#1080) - Scene Registrations no longer exists - Allow Runtime Scene Changes was no longer needed and was removed - Removed the NetworkObject.Spawn payload parameter (#1005) - Removed `ProfilerCounter`, the original MLAPI network profiler, and the built-in network profiler module (2020.3). A replacement can now be found in the Multiplayer Tools package. (#1048) - Removed UNet RelayTransport and related relay functionality in UNetTransport (#1081) - Removed `UpdateStage` parameter from `ServerRpcSendParams` and `ClientRpcSendParams` (#1187) - Removed `NetworkBuffer`, `NetworkWriter`, `NetworkReader`, `NetworkSerializer`, `PooledNetworkBuffer`, `PooledNetworkWriter`, and `PooledNetworkReader` (#1187) - Removed `EnableNetworkVariable` in `NetworkConfig`, it is always enabled now (#1179) - Removed `NetworkTransform`'s FixedSendsPerSecond, AssumeSyncedSends, InterpolateServer, ExtrapolatePosition, MaxSendsToExtrapolate, Channel, EnableNonProvokedResendChecks, DistanceSendrate (#1060) (#826) (#1042, #1055, #1061, #1084, #1101) - Removed `NetworkManager`'s `StopServer()`, `StopClient()` and `StopHost()` methods and replaced with single `NetworkManager.Shutdown()` method for all (#1108) ### Fixed - Fixed ServerRpc ownership check to `Debug.LogError` instead of `Debug.LogWarning` (#1126) - Fixed `NetworkObject.OwnerClientId` property changing before `NetworkBehaviour.OnGainedOwnership()` callback (#1092) - Fixed `NetworkBehaviourILPP` to iterate over all types in an assembly (#803) - Fixed cross-asmdef RPC ILPP by importing types into external assemblies (#678) - Fixed `NetworkManager` shutdown when quitting the application or switching scenes (#1011) - Now `NetworkManager` shutdowns correctly and despawns existing `NetworkObject`s - Fixed Only one `PlayerPrefab` can be selected on `NetworkManager` inspector UI in the editor (#676) - Fixed connection approval not being triggered for host (#675) - Fixed various situations where messages could be processed in an invalid order, resulting in errors (#948, #1187, #1218) - Fixed `NetworkVariable`s being default-initialized on the client instead of being initialized with the desired value (#1266) - Improved runtime performance and reduced GC pressure (#1187) - Fixed #915 - clients are receiving data from objects not visible to them (#1099) - Fixed `NetworkTransform`'s "late join" issues, `NetworkTransform` now uses `NetworkVariable`s instead of RPCs (#826) - Throw an exception for silent failure when a client tries to get another player's `PlayerObject`, it is now only allowed on the server-side (#844) ### Known Issues - `NetworkVariable` does not serialize `INetworkSerializable` types through their `NetworkSerialize` implementation - `NetworkObjects` marked as `DontDestroyOnLoad` are disabled during some network scene transitions - `NetworkTransform` interpolates from the origin when switching Local Space synchronization - Exceptions thrown in `OnNetworkSpawn` user code for an object will prevent the callback in other objects - Cannot send an array of `INetworkSerializable` in RPCs - ILPP generation fails with special characters in project path ## [0.2.0] - 2021-06-03 WIP version increment to pass package validation checks. Changelog & final version number TBD. ## [0.1.1] - 2021-06-01 This is hotfix v0.1.1 for the initial experimental Unity MLAPI Package. ### Changed - Fixed issue with the Unity Registry package version missing some fixes from the v0.1.0 release. ## [0.1.0] - 2021-03-23 This is the initial experimental Unity MLAPI Package, v0.1.0. ### Added - Refactored a new standard for Remote Procedure Call (RPC) in MLAPI which provides increased performance, significantly reduced boilerplate code, and extensibility for future-proofed code. MLAPI RPC includes `ServerRpc` and `ClientRpc` to execute logic on the server and client-side. This provides a single performant unified RPC solution, replacing MLAPI Convenience and Performance RPC (see [here](#removed-features)). - Added standarized serialization types, including built-in and custom serialization flows. See [RFC #2](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0002-serializable-types.md) for details. - `INetworkSerializable` interface replaces `IBitWritable`. - Added `NetworkSerializer`..., which is the main aggregator that implements serialization code for built-in supported types and holds `NetworkReader` and `NetworkWriter` instances internally. - Added a Network Update Loop infrastructure that aids Netcode systems to update (such as RPC queue and transport) outside of the standard `MonoBehaviour` event cycle. See [RFC #8](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0008-network-update-loop.md) and the following details: - It uses Unity's [low-level Player Loop API](https://docs.unity3d.com/ScriptReference/LowLevel.PlayerLoop.html) and allows for registering `INetworkUpdateSystem`s with `NetworkUpdate` methods to be executed at specific `NetworkUpdateStage`s, which may also be before or after `MonoBehaviour`-driven game logic execution. - You will typically interact with `NetworkUpdateLoop` for registration and `INetworkUpdateSystem` for implementation. - `NetworkVariable`s are now tick-based using the `NetworkTickSystem`, tracking time through network interactions and syncs. - Added message batching to handle consecutive RPC requests sent to the same client. `RpcBatcher` sends batches based on requests from the `RpcQueueProcessing`, by batch size threshold or immediately. - [GitHub 494](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/494): Added a constraint to allow one `NetworkObject` per `GameObject`, set through the `DisallowMultipleComponent` attribute. - Integrated MLAPI with the Unity Profiler for versions 2020.2 and later: - Added new profiler modules for MLAPI that report important network data. - Attached the profiler to a remote player to view network data over the wire. - A test project is available for building and experimenting with MLAPI features. This project is available in the MLAPI GitHub [testproject folder](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/tree/release/0.1.0/testproject). - Added a [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) new GitHub repository to accept extensions from the MLAPI community. Current extensions include moved MLAPI features for lag compensation (useful for Server Authoritative actions) and `TrackedObject`. ### Changed - [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): MLAPI now uses the Unity Package Manager for installation management. - Added functionality and usability to `NetworkVariable`, previously called `NetworkVar`. Updates enhance options and fully replace the need for `SyncedVar`s. - [GitHub 507](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/507): Reimplemented `NetworkAnimator`, which synchronizes animation states for networked objects. - GitHub [444](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/444) and [455](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/455): Channels are now represented as bytes instead of strings. For users of previous versions of MLAPI, this release renames APIs due to refactoring. All obsolete marked APIs have been removed as per [GitHub 513](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/513) and [GitHub 514](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/514). | Previous MLAPI Versions | V 0.1.0 Name | | -- | -- | | `NetworkingManager` | `NetworkManager` | | `NetworkedObject` | `NetworkObject` | | `NetworkedBehaviour` | `NetworkBehaviour` | | `NetworkedClient` | `NetworkClient` | | `NetworkedPrefab` | `NetworkPrefab` | | `NetworkedVar` | `NetworkVariable` | | `NetworkedTransform` | `NetworkTransform` | | `NetworkedAnimator` | `NetworkAnimator` | | `NetworkedAnimatorEditor` | `NetworkAnimatorEditor` | | `NetworkedNavMeshAgent` | `NetworkNavMeshAgent` | | `SpawnManager` | `NetworkSpawnManager` | | `BitStream` | `NetworkBuffer` | | `BitReader` | `NetworkReader` | | `BitWriter` | `NetworkWriter` | | `NetEventType` | `NetworkEventType` | | `ChannelType` | `NetworkDelivery` | | `Channel` | `NetworkChannel` | | `Transport` | `NetworkTransport` | | `NetworkedDictionary` | `NetworkDictionary` | | `NetworkedList` | `NetworkList` | | `NetworkedSet` | `NetworkSet` | | `MLAPIConstants` | `NetworkConstants` | | `UnetTransport` | `UNetTransport` | ### Fixed - [GitHub 460](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/460): Fixed an issue for RPC where the host-server was not receiving RPCs from the host-client and vice versa without the loopback flag set in `NetworkingManager`. - Fixed an issue where data in the Profiler was incorrectly aggregated and drawn, which caused the profiler data to increment indefinitely instead of resetting each frame. - Fixed an issue the client soft-synced causing PlayMode client-only scene transition issues, caused when running the client in the editor and the host as a release build. Users may have encountered a soft sync of `NetworkedInstanceId` issues in the `SpawnManager.ClientCollectSoftSyncSceneObjectSweep` method. - [GitHub 458](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/458): Fixed serialization issues in `NetworkList` and `NetworkDictionary` when running in Server mode. - [GitHub 498](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/498): Fixed numerical precision issues to prevent not a number (NaN) quaternions. - [GitHub 438](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/438): Fixed booleans by reaching or writing bytes instead of bits. - [GitHub 519](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/519): Fixed an issue where calling `Shutdown()` before making `NetworkManager.Singleton = null` is null on `NetworkManager.OnDestroy()`. ### Removed With a new release of MLAPI in Unity, some features have been removed: - SyncVars have been removed from MLAPI. Use `NetworkVariable`s in place of this functionality. - [GitHub 527](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/527): Lag compensation systems and `TrackedObject` have moved to the new [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) repo. - [GitHub 509](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/509): Encryption has been removed from MLAPI. The `Encryption` option in `NetworkConfig` on the `NetworkingManager` is not available in this release. This change will not block game creation or running. A current replacement for this functionality is not available, and may be developed in future releases. See the following changes: - Removed `SecuritySendFlags` from all APIs. - Removed encryption, cryptography, and certificate configurations from APIs including `NetworkManager` and `NetworkConfig`. - Removed "hail handshake", including `NetworkManager` implementation and `NetworkConstants` entries. - Modified `RpcQueue` and `RpcBatcher` internals to remove encryption and authentication from reading and writing. - Removed the previous MLAPI Profiler editor window from Unity versions 2020.2 and later. - Removed previous MLAPI Convenience and Performance RPC APIs with the new standard RPC API. See [RFC #1](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0001-std-rpc-api.md) for details. - [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): Removed the MLAPI Installer. ### Known Issues - `NetworkNavMeshAgent` does not synchronize mesh data, Agent Size, Steering, Obstacle Avoidance, or Path Finding settings. It only synchronizes the destination and velocity, not the path to the destination. - For `RPC`, methods with a `ClientRpc` or `ServerRpc` suffix which are not marked with [ServerRpc] or [ClientRpc] will cause a compiler error. - For `NetworkAnimator`, Animator Overrides are not supported. Triggers do not work. - For `NetworkVariable`, the `NetworkDictionary` `List` and `Set` must use the `reliableSequenced` channel. - `NetworkObjects`s are supported but when spawning a prefab with nested child network objects you have to manually call spawn on them - `NetworkTransform` have the following issues: - Replicated objects may have jitter. - The owner is always authoritative about the object's position. - Scale is not synchronized. - Connection Approval is not called on the host client. - For `NamedMessages`, always use `NetworkBuffer` as the underlying stream for sending named and unnamed messages. - For `NetworkManager`, connection management is limited. Use `IsServer`, `IsClient`, `IsConnectedClient`, or other code to check if MLAPI connected correctly. ## [0.0.1-preview.1] - 2020-12-20 This was an internally-only-used version of the Unity MLAPI Package diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..19aec06 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,235 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +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.2] - 2020-12-20 + +### Added + +- Associated Known Issues for the 1.0.0-pre.1 release in the changelog + +### Changed + +- Updated label for `1.0.0-pre.1` changelog section + +## [1.0.0-pre.1] - 2020-12-20 + +### Added + +- Added `ClientNetworkTransform` sample to the SDK package (#1168) +- Added `Bootstrap` sample to the SDK package (#1140) +- Enhanced `NetworkSceneManager` implementation with additive scene loading capabilities (#1080, #955, #913) + - `NetworkSceneManager.OnSceneEvent` provides improved scene event notificaitons +- Enhanced `NetworkTransform` implementation with per axis/component based and threshold based state replication (#1042, #1055, #1061, #1084, #1101) +- Added a jitter-resistent `BufferedLinearInterpolator` for `NetworkTransform` (#1060) +- Implemented `NetworkPrefabHandler` that provides support for object pooling and `NetworkPrefab` overrides (#1073, #1004, #977, #905,#749, #727) +- Implemented auto `NetworkObject` transform parent synchronization at runtime over the network (#855) +- Adopted Unity C# Coding Standards in the codebase with `.editorconfig` ruleset (#666, #670) +- When a client tries to spawn a `NetworkObject` an exception is thrown to indicate unsupported behavior. (#981) +- Added a `NetworkTime` and `NetworkTickSystem` which allows for improved control over time and ticks. (#845) +- Added a `OnNetworkDespawn` function to `NetworkObject` which gets called when a `NetworkObject` gets despawned and can be overriden. (#865) +- Added `SnapshotSystem` that would allow variables and spawn/despawn messages to be sent in blocks (#805, #852, #862, #963, #1012, #1013, #1021, #1040, #1062, #1064, #1083, #1091, #1111, #1129, #1166, #1192) + - Disabled by default for now, except spawn/despawn messages + - Will leverage unreliable messages with eventual consistency +- `NetworkBehaviour` and `NetworkObject`'s `NetworkManager` instances can now be overriden (#762) +- Added metrics reporting for the new network profiler if the Multiplayer Tools package is present (#1104, #1089, #1096, #1086, #1072, #1058, #960, #897, #891, #878) +- `NetworkBehaviour.IsSpawned` a quick (and stable) way to determine if the associated NetworkObject is spawned (#1190) +- Added `NetworkRigidbody` and `NetworkRigidbody2D` components to support networking `Rigidbody` and `Rigidbody2D` components (#1202, #1175) +- Added `NetworkObjectReference` and `NetworkBehaviourReference` structs which allow to sending `NetworkObject/Behaviours` over RPCs/`NetworkVariable`s (#1173) +- Added `NetworkAnimator` component to support networking `Animator` component (#1281, #872) + +### Changed + +- Bumped minimum Unity version, renamed package as "Unity Netcode for GameObjects", replaced `MLAPI` namespace and its variants with `Unity.Netcode` namespace and per asm-def variants (#1007, #1009, #1015, #1017, #1019, #1025, #1026, #1065) + - Minimum Unity version: + - 2019.4 → 2020.3+ + - Package rename: + - Display name: `MLAPI Networking Library` → `Netcode for GameObjects` + - Name: `com.unity.multiplayer.mlapi` → `com.unity.netcode.gameobjects` + - Updated package description + - All `MLAPI.x` namespaces are replaced with `Unity.Netcode` + - `MLAPI.Messaging` → `Unity.Netcode` + - `MLAPI.Connection` → `Unity.Netcode` + - `MLAPI.Logging` → `Unity.Netcode` + - `MLAPI.SceneManagement` → `Unity.Netcode` + - and other `MLAPI.x` variants to `Unity.Netcode` + - All assembly definitions are renamed with `Unity.Netcode.x` variants + - `Unity.Multiplayer.MLAPI.Runtime` → `Unity.Netcode.Runtime` + - `Unity.Multiplayer.MLAPI.Editor` → `Unity.Netcode.Editor` + - and other `Unity.Multiplayer.MLAPI.x` variants to `Unity.Netcode.x` variants +- Renamed `Prototyping` namespace and assembly definition to `Components` (#1145) +- Changed `NetworkObject.Despawn(bool destroy)` API to default to `destroy = true` for better usability (#1217) +- Scene registration in `NetworkManager` is now replaced by Build Setttings → Scenes in Build List (#1080) +- `NetworkSceneManager.SwitchScene` has been replaced by `NetworkSceneManager.LoadScene` (#955) +- `NetworkManager, NetworkConfig, and NetworkSceneManager` scene registration replaced with scenes in build list (#1080) +- `GlobalObjectIdHash` replaced `PrefabHash` and `PrefabHashGenerator` for stability and consistency (#698) +- `NetworkStart` has been renamed to `OnNetworkSpawn`. (#865) +- Network variable cleanup - eliminated shared mode, variables are server-authoritative (#1059, #1074) +- `NetworkManager` and other systems are no longer singletons/statics (#696, #705, #706, #737, #738, #739, #746, #747, #763, #765, #766, #783, #784, #785, #786, #787, #788) +- Changed `INetworkSerializable.NetworkSerialize` method signature to use `BufferSerializer` instead of `NetworkSerializer` (#1187) +- Changed `CustomMessagingManager`'s methods to use `FastBufferWriter` and `FastBufferReader` instead of `Stream` (#1187) +- Reduced internal runtime allocations by removing LINQ calls and replacing managed lists/arrays with native collections (#1196) + +### Removed + +- Removed `NetworkNavMeshAgent` (#1150) +- Removed `NetworkDictionary`, `NetworkSet` (#1149) +- Removed `NetworkVariableSettings` (#1097) +- Removed predefined `NetworkVariable` types (#1093) + - Removed `NetworkVariableBool`, `NetworkVariableByte`, `NetworkVariableSByte`, `NetworkVariableUShort`, `NetworkVariableShort`, `NetworkVariableUInt`, `NetworkVariableInt`, `NetworkVariableULong`, `NetworkVariableLong`, `NetworkVariableFloat`, `NetworkVariableDouble`, `NetworkVariableVector2`, `NetworkVariableVector3`, `NetworkVariableVector4`, `NetworkVariableColor`, `NetworkVariableColor32`, `NetworkVariableRay`, `NetworkVariableQuaternion` +- Removed `NetworkChannel` and `MultiplexTransportAdapter` (#1133) +- Removed ILPP backend for 2019.4, minimum required version is 2020.3+ (#895) +- `NetworkManager.NetworkConfig` had the following properties removed: (#1080) + - Scene Registrations no longer exists + - Allow Runtime Scene Changes was no longer needed and was removed +- Removed the NetworkObject.Spawn payload parameter (#1005) +- Removed `ProfilerCounter`, the original MLAPI network profiler, and the built-in network profiler module (2020.3). A replacement can now be found in the Multiplayer Tools package. (#1048) +- Removed UNet RelayTransport and related relay functionality in UNetTransport (#1081) +- Removed `UpdateStage` parameter from `ServerRpcSendParams` and `ClientRpcSendParams` (#1187) +- Removed `NetworkBuffer`, `NetworkWriter`, `NetworkReader`, `NetworkSerializer`, `PooledNetworkBuffer`, `PooledNetworkWriter`, and `PooledNetworkReader` (#1187) +- Removed `EnableNetworkVariable` in `NetworkConfig`, it is always enabled now (#1179) +- Removed `NetworkTransform`'s FixedSendsPerSecond, AssumeSyncedSends, InterpolateServer, ExtrapolatePosition, MaxSendsToExtrapolate, Channel, EnableNonProvokedResendChecks, DistanceSendrate (#1060) (#826) (#1042, #1055, #1061, #1084, #1101) +- Removed `NetworkManager`'s `StopServer()`, `StopClient()` and `StopHost()` methods and replaced with single `NetworkManager.Shutdown()` method for all (#1108) + +### Fixed + +- Fixed ServerRpc ownership check to `Debug.LogError` instead of `Debug.LogWarning` (#1126) +- Fixed `NetworkObject.OwnerClientId` property changing before `NetworkBehaviour.OnGainedOwnership()` callback (#1092) +- Fixed `NetworkBehaviourILPP` to iterate over all types in an assembly (#803) +- Fixed cross-asmdef RPC ILPP by importing types into external assemblies (#678) +- Fixed `NetworkManager` shutdown when quitting the application or switching scenes (#1011) + - Now `NetworkManager` shutdowns correctly and despawns existing `NetworkObject`s +- Fixed Only one `PlayerPrefab` can be selected on `NetworkManager` inspector UI in the editor (#676) +- Fixed connection approval not being triggered for host (#675) +- Fixed various situations where messages could be processed in an invalid order, resulting in errors (#948, #1187, #1218) +- Fixed `NetworkVariable`s being default-initialized on the client instead of being initialized with the desired value (#1266) +- Improved runtime performance and reduced GC pressure (#1187) +- Fixed #915 - clients are receiving data from objects not visible to them (#1099) +- Fixed `NetworkTransform`'s "late join" issues, `NetworkTransform` now uses `NetworkVariable`s instead of RPCs (#826) +- Throw an exception for silent failure when a client tries to get another player's `PlayerObject`, it is now only allowed on the server-side (#844) + +### Known Issues + +- `NetworkVariable` does not serialize `INetworkSerializable` types through their `NetworkSerialize` implementation +- `NetworkObjects` marked as `DontDestroyOnLoad` are disabled during some network scene transitions +- `NetworkTransform` interpolates from the origin when switching Local Space synchronization +- Exceptions thrown in `OnNetworkSpawn` user code for an object will prevent the callback in other objects +- Cannot send an array of `INetworkSerializable` in RPCs +- ILPP generation fails with special characters in project path + +## [0.2.0] - 2021-06-03 + +WIP version increment to pass package validation checks. Changelog & final version number TBD. + +## [0.1.1] - 2021-06-01 + +This is hotfix v0.1.1 for the initial experimental Unity MLAPI Package. + +### Changed + +- Fixed issue with the Unity Registry package version missing some fixes from the v0.1.0 release. + +## [0.1.0] - 2021-03-23 + +This is the initial experimental Unity MLAPI Package, v0.1.0. + +### Added + +- Refactored a new standard for Remote Procedure Call (RPC) in MLAPI which provides increased performance, significantly reduced boilerplate code, and extensibility for future-proofed code. MLAPI RPC includes `ServerRpc` and `ClientRpc` to execute logic on the server and client-side. This provides a single performant unified RPC solution, replacing MLAPI Convenience and Performance RPC (see [here](#removed-features)). +- Added standarized serialization types, including built-in and custom serialization flows. See [RFC #2](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0002-serializable-types.md) for details. +- `INetworkSerializable` interface replaces `IBitWritable`. +- Added `NetworkSerializer`..., which is the main aggregator that implements serialization code for built-in supported types and holds `NetworkReader` and `NetworkWriter` instances internally. +- Added a Network Update Loop infrastructure that aids Netcode systems to update (such as RPC queue and transport) outside of the standard `MonoBehaviour` event cycle. See [RFC #8](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0008-network-update-loop.md) and the following details: + - It uses Unity's [low-level Player Loop API](https://docs.unity3d.com/ScriptReference/LowLevel.PlayerLoop.html) and allows for registering `INetworkUpdateSystem`s with `NetworkUpdate` methods to be executed at specific `NetworkUpdateStage`s, which may also be before or after `MonoBehaviour`-driven game logic execution. + - You will typically interact with `NetworkUpdateLoop` for registration and `INetworkUpdateSystem` for implementation. + - `NetworkVariable`s are now tick-based using the `NetworkTickSystem`, tracking time through network interactions and syncs. +- Added message batching to handle consecutive RPC requests sent to the same client. `RpcBatcher` sends batches based on requests from the `RpcQueueProcessing`, by batch size threshold or immediately. +- [GitHub 494](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/494): Added a constraint to allow one `NetworkObject` per `GameObject`, set through the `DisallowMultipleComponent` attribute. +- Integrated MLAPI with the Unity Profiler for versions 2020.2 and later: + - Added new profiler modules for MLAPI that report important network data. + - Attached the profiler to a remote player to view network data over the wire. +- A test project is available for building and experimenting with MLAPI features. This project is available in the MLAPI GitHub [testproject folder](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/tree/release/0.1.0/testproject). +- Added a [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) new GitHub repository to accept extensions from the MLAPI community. Current extensions include moved MLAPI features for lag compensation (useful for Server Authoritative actions) and `TrackedObject`. + +### Changed + +- [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): MLAPI now uses the Unity Package Manager for installation management. +- Added functionality and usability to `NetworkVariable`, previously called `NetworkVar`. Updates enhance options and fully replace the need for `SyncedVar`s. +- [GitHub 507](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/507): Reimplemented `NetworkAnimator`, which synchronizes animation states for networked objects. +- GitHub [444](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/444) and [455](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/455): Channels are now represented as bytes instead of strings. + +For users of previous versions of MLAPI, this release renames APIs due to refactoring. All obsolete marked APIs have been removed as per [GitHub 513](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/513) and [GitHub 514](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/514). + +| Previous MLAPI Versions | V 0.1.0 Name | +| -- | -- | +| `NetworkingManager` | `NetworkManager` | +| `NetworkedObject` | `NetworkObject` | +| `NetworkedBehaviour` | `NetworkBehaviour` | +| `NetworkedClient` | `NetworkClient` | +| `NetworkedPrefab` | `NetworkPrefab` | +| `NetworkedVar` | `NetworkVariable` | +| `NetworkedTransform` | `NetworkTransform` | +| `NetworkedAnimator` | `NetworkAnimator` | +| `NetworkedAnimatorEditor` | `NetworkAnimatorEditor` | +| `NetworkedNavMeshAgent` | `NetworkNavMeshAgent` | +| `SpawnManager` | `NetworkSpawnManager` | +| `BitStream` | `NetworkBuffer` | +| `BitReader` | `NetworkReader` | +| `BitWriter` | `NetworkWriter` | +| `NetEventType` | `NetworkEventType` | +| `ChannelType` | `NetworkDelivery` | +| `Channel` | `NetworkChannel` | +| `Transport` | `NetworkTransport` | +| `NetworkedDictionary` | `NetworkDictionary` | +| `NetworkedList` | `NetworkList` | +| `NetworkedSet` | `NetworkSet` | +| `MLAPIConstants` | `NetworkConstants` | +| `UnetTransport` | `UNetTransport` | + +### Fixed + +- [GitHub 460](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/460): Fixed an issue for RPC where the host-server was not receiving RPCs from the host-client and vice versa without the loopback flag set in `NetworkingManager`. +- Fixed an issue where data in the Profiler was incorrectly aggregated and drawn, which caused the profiler data to increment indefinitely instead of resetting each frame. +- Fixed an issue the client soft-synced causing PlayMode client-only scene transition issues, caused when running the client in the editor and the host as a release build. Users may have encountered a soft sync of `NetworkedInstanceId` issues in the `SpawnManager.ClientCollectSoftSyncSceneObjectSweep` method. +- [GitHub 458](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/458): Fixed serialization issues in `NetworkList` and `NetworkDictionary` when running in Server mode. +- [GitHub 498](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/498): Fixed numerical precision issues to prevent not a number (NaN) quaternions. +- [GitHub 438](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/438): Fixed booleans by reaching or writing bytes instead of bits. +- [GitHub 519](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/519): Fixed an issue where calling `Shutdown()` before making `NetworkManager.Singleton = null` is null on `NetworkManager.OnDestroy()`. + +### Removed + +With a new release of MLAPI in Unity, some features have been removed: + +- SyncVars have been removed from MLAPI. Use `NetworkVariable`s in place of this functionality. +- [GitHub 527](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/527): Lag compensation systems and `TrackedObject` have moved to the new [MLAPI Community Contributions](https://github.com/Unity-Technologies/mlapi-community-contributions/tree/master/com.mlapi.contrib.extensions) repo. +- [GitHub 509](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/509): Encryption has been removed from MLAPI. The `Encryption` option in `NetworkConfig` on the `NetworkingManager` is not available in this release. This change will not block game creation or running. A current replacement for this functionality is not available, and may be developed in future releases. See the following changes: + - Removed `SecuritySendFlags` from all APIs. + - Removed encryption, cryptography, and certificate configurations from APIs including `NetworkManager` and `NetworkConfig`. + - Removed "hail handshake", including `NetworkManager` implementation and `NetworkConstants` entries. + - Modified `RpcQueue` and `RpcBatcher` internals to remove encryption and authentication from reading and writing. +- Removed the previous MLAPI Profiler editor window from Unity versions 2020.2 and later. +- Removed previous MLAPI Convenience and Performance RPC APIs with the new standard RPC API. See [RFC #1](https://github.com/Unity-Technologies/com.unity.multiplayer.rfcs/blob/master/text/0001-std-rpc-api.md) for details. +- [GitHub 520](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/520): Removed the MLAPI Installer. + +### Known Issues + +- `NetworkNavMeshAgent` does not synchronize mesh data, Agent Size, Steering, Obstacle Avoidance, or Path Finding settings. It only synchronizes the destination and velocity, not the path to the destination. +- For `RPC`, methods with a `ClientRpc` or `ServerRpc` suffix which are not marked with [ServerRpc] or [ClientRpc] will cause a compiler error. +- For `NetworkAnimator`, Animator Overrides are not supported. Triggers do not work. +- For `NetworkVariable`, the `NetworkDictionary` `List` and `Set` must use the `reliableSequenced` channel. +- `NetworkObjects`s are supported but when spawning a prefab with nested child network objects you have to manually call spawn on them +- `NetworkTransform` have the following issues: + - Replicated objects may have jitter. + - The owner is always authoritative about the object's position. + - Scale is not synchronized. +- Connection Approval is not called on the host client. +- For `NamedMessages`, always use `NetworkBuffer` as the underlying stream for sending named and unnamed messages. +- For `NetworkManager`, connection management is limited. Use `IsServer`, `IsClient`, `IsConnectedClient`, or other code to check if MLAPI connected correctly. + +## [0.0.1-preview.1] - 2020-12-20 + +This was an internally-only-used version of the Unity MLAPI Package diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..3d20b83 --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d8f80d04925759d41a7bbee0259b3c39 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components.meta b/Components.meta new file mode 100644 index 0000000..d4eb0da --- /dev/null +++ b/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8b267eb841a574dc083ac248a95d4443 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/AssemblyInfo.cs b/Components/AssemblyInfo.cs new file mode 100644 index 0000000..98d32ca --- /dev/null +++ b/Components/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Runtime.CompilerServices; + +#if UNITY_EDITOR +[assembly: InternalsVisibleTo("Unity.Netcode.EditorTests")] +[assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")] +[assembly: InternalsVisibleTo("Unity.Netcode.Editor")] +[assembly: InternalsVisibleTo("TestProject.EditorTests")] +[assembly: InternalsVisibleTo("TestProject.RuntimeTests")] +#endif +[assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")] diff --git a/Components/AssemblyInfo.cs.meta b/Components/AssemblyInfo.cs.meta new file mode 100644 index 0000000..36524ba --- /dev/null +++ b/Components/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b8086dc75d86473f9e3c928dd773733 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/Interpolator.meta b/Components/Interpolator.meta new file mode 100644 index 0000000..15aee65 --- /dev/null +++ b/Components/Interpolator.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8eb56856ab05d41fa9e422a92acbc109 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/Interpolator/BufferedLinearInterpolator.cs b/Components/Interpolator/BufferedLinearInterpolator.cs new file mode 100644 index 0000000..d0e9026 --- /dev/null +++ b/Components/Interpolator/BufferedLinearInterpolator.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Netcode +{ + + /// + /// Solves for incoming values that are jittered + /// Partially solves for message loss. Unclamped lerping helps hide this, but not completely + /// + /// + internal abstract class BufferedLinearInterpolator where T : struct + { + private struct BufferedItem + { + public T Item; + public double TimeSent; + + public BufferedItem(T item, double timeSent) + { + Item = item; + TimeSent = timeSent; + } + } + + + private const double k_SmallValue = 9.999999439624929E-11; // copied from Vector3's equal operator + + private T m_InterpStartValue; + private T m_CurrentInterpValue; + private T m_InterpEndValue; + + private double m_EndTimeConsumed; + private double m_StartTimeConsumed; + + private readonly List m_Buffer = new List(k_BufferCountLimit); + + // Buffer consumption scenarios + // Perfect case consumption + // | 1 | 2 | 3 | + // | 2 | 3 | 4 | consume 1 + // | 3 | 4 | 5 | consume 2 + // | 4 | 5 | 6 | consume 3 + // | 5 | 6 | 7 | consume 4 + // jittered case + // | 1 | 2 | 3 | + // | 2 | 3 | | consume 1 + // | 3 | | | consume 2 + // | 4 | 5 | 6 | consume 3 + // | 5 | 6 | 7 | consume 4 + // bursted case (assuming max count is 5) + // | 1 | 2 | 3 | + // | 2 | 3 | | consume 1 + // | 3 | | | consume 2 + // | | | | consume 3 + // | | | | + // | 4 | 5 | 6 | 7 | 8 | --> consume all and teleport to last value <8> --> this is the nuclear option, ideally this example would consume 4 and 5 + // instead of jumping to 8, but since in OnValueChange we don't yet have an updated server time (updated in pre-update) to know which value + // we should keep and which we should drop, we don't have enough information to do this. Another thing would be to not have the burst in the first place. + + // Constant absolute value for max buffer count instead of dynamic time based value. This is in case we have very low tick rates, so + // that we don't have a very small buffer because of this. + private const int k_BufferCountLimit = 100; + private BufferedItem m_LastBufferedItemReceived; + private int m_NbItemsReceivedThisFrame; + + private int m_LifetimeConsumedCount; + + private bool InvalidState => m_Buffer.Count == 0 && m_LifetimeConsumedCount == 0; + + public void ResetTo(T targetValue, double serverTime) + { + m_LifetimeConsumedCount = 1; + m_InterpStartValue = targetValue; + m_InterpEndValue = targetValue; + m_CurrentInterpValue = targetValue; + m_Buffer.Clear(); + m_EndTimeConsumed = 0.0d; + m_StartTimeConsumed = 0.0d; + + Update(0, serverTime, serverTime); + } + + + // todo if I have value 1, 2, 3 and I'm treating 1 to 3, I shouldn't interpolate between 1 and 3, I should interpolate from 1 to 2, then from 2 to 3 to get the best path + private void TryConsumeFromBuffer(double renderTime, double serverTime) + { + int consumedCount = 0; + // only consume if we're ready + + // this operation was measured as one of our most expensive, and we should put some thought into this. + // NetworkTransform has (currently) 7 buffered linear interpolators (3 position, 3 scale, 1 rot), and + // each has its own independent buffer and 'm_endTimeConsume'. That means every frame I have to do 7x + // these checks vs. if we tracked these values in a unified way + if (renderTime >= m_EndTimeConsumed) + { + BufferedItem? itemToInterpolateTo = null; + // assumes we're using sequenced messages for netvar syncing + // buffer contains oldest values first, iterating from end to start to remove elements from list while iterating + + // calling m_Buffer.Count shows up hot in the profiler. + for (int i = m_Buffer.Count - 1; i >= 0; i--) // todo stretch: consume ahead if we see we're missing values due to packet loss + { + var bufferedValue = m_Buffer[i]; + // Consume when ready and interpolate to last value we can consume. This can consume multiple values from the buffer + if (bufferedValue.TimeSent <= serverTime) + { + if (!itemToInterpolateTo.HasValue || bufferedValue.TimeSent > itemToInterpolateTo.Value.TimeSent) + { + if (m_LifetimeConsumedCount == 0) + { + // if interpolator not initialized, teleport to first value when available + m_StartTimeConsumed = bufferedValue.TimeSent; + m_InterpStartValue = bufferedValue.Item; + } + else if (consumedCount == 0) + { + // Interpolating to new value, end becomes start. We then look in our buffer for a new end. + m_StartTimeConsumed = m_EndTimeConsumed; + m_InterpStartValue = m_InterpEndValue; + } + + if (bufferedValue.TimeSent > m_EndTimeConsumed) + { + itemToInterpolateTo = bufferedValue; + m_EndTimeConsumed = bufferedValue.TimeSent; + m_InterpEndValue = bufferedValue.Item; + } + } + + m_Buffer.RemoveAt(i); + consumedCount++; + m_LifetimeConsumedCount++; + } + } + } + } + + /// + /// Convenience version of 'Update' mainly for testing + /// the reason we don't want to always call this version is so that on the calling side we can compute + /// the renderTime once for the many things being interpolated (and the many interpolators per object) + /// + /// time since call + /// current server time + public T Update(float deltaTime, NetworkTime serverTime) + { + return Update(deltaTime, serverTime.TimeTicksAgo(1).Time, serverTime.Time); + } + + /// + /// Call to update the state of the interpolators before reading out + /// + /// time since last call + /// our current time + /// current server time + public T Update(float deltaTime, double renderTime, double serverTime) + { + TryConsumeFromBuffer(renderTime, serverTime); + + if (InvalidState) + { + throw new InvalidOperationException("trying to update interpolator when no data has been added to it yet"); + } + + // Interpolation example to understand the math below + // 4 4.5 6 6.5 + // | | | | + // A render B Server + + if (m_LifetimeConsumedCount >= 1) // shouldn't interpolate between default values, let's wait to receive data first, should only interpolate between real measurements + { + float t = 1.0f; + double range = m_EndTimeConsumed - m_StartTimeConsumed; + if (range > k_SmallValue) + { + t = (float)((renderTime - m_StartTimeConsumed) / range); + + 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}"); + } + + if (t > 3.0f) // max extrapolation + { + // TODO this causes issues with teleport, investigate + // todo make this configurable + t = 1.0f; + } + } + + var target = InterpolateUnclamped(m_InterpStartValue, m_InterpEndValue, t); + float maxInterpTime = 0.1f; + m_CurrentInterpValue = Interpolate(m_CurrentInterpValue, target, deltaTime / maxInterpTime); // second interpolate to smooth out extrapolation jumps + } + + m_NbItemsReceivedThisFrame = 0; + return m_CurrentInterpValue; + } + + public void AddMeasurement(T newMeasurement, double sentTime) + { + m_NbItemsReceivedThisFrame++; + + // This situation can happen after a game is paused. When starting to receive again, the server will have sent a bunch of messages in the meantime + // instead of going through thousands of value updates just to get a big teleport, we're giving up on interpolation and teleporting to the latest value + if (m_NbItemsReceivedThisFrame > k_BufferCountLimit) + { + if (m_LastBufferedItemReceived.TimeSent < sentTime) + { + m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime); + ResetTo(newMeasurement, sentTime); + } + + return; + } + + if (sentTime > m_EndTimeConsumed || m_LifetimeConsumedCount == 0) // treat only if value is newer than the one being interpolated to right now + { + m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime); + m_Buffer.Add(m_LastBufferedItemReceived); + } + } + + public T GetInterpolatedValue() + { + return m_CurrentInterpValue; + } + + protected abstract T Interpolate(T start, T end, float time); + protected abstract T InterpolateUnclamped(T start, T end, float time); + } + + + internal class BufferedLinearInterpolatorFloat : BufferedLinearInterpolator + { + protected override float InterpolateUnclamped(float start, float end, float time) + { + return Mathf.LerpUnclamped(start, end, time); + } + + protected override float Interpolate(float start, float end, float time) + { + return Mathf.Lerp(start, end, time); + } + } + + internal class BufferedLinearInterpolatorQuaternion : BufferedLinearInterpolator + { + protected override Quaternion InterpolateUnclamped(Quaternion start, Quaternion end, float time) + { + return Quaternion.SlerpUnclamped(start, end, time); + } + + protected override Quaternion Interpolate(Quaternion start, Quaternion end, float time) + { + return Quaternion.SlerpUnclamped(start, end, time); + } + } +} diff --git a/Components/Interpolator/BufferedLinearInterpolator.cs.meta b/Components/Interpolator/BufferedLinearInterpolator.cs.meta new file mode 100644 index 0000000..88cd489 --- /dev/null +++ b/Components/Interpolator/BufferedLinearInterpolator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a12ebf95bdb4445d9a16e4b6adadb6aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/NetworkAnimator.cs b/Components/NetworkAnimator.cs new file mode 100644 index 0000000..7116b4e --- /dev/null +++ b/Components/NetworkAnimator.cs @@ -0,0 +1,454 @@ +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace Unity.Netcode.Components +{ + /// + /// A prototype component for syncing animations + /// + [AddComponentMenu("Netcode/" + nameof(NetworkAnimator))] + [RequireComponent(typeof(Animator))] + public class NetworkAnimator : NetworkBehaviour + { + internal struct AnimationMessage : INetworkSerializable + { + public int StateHash; // if non-zero, then Play() this animation, skipping transitions + public float NormalizedTime; + public byte[] Parameters; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref StateHash); + serializer.SerializeValue(ref NormalizedTime); + serializer.SerializeValue(ref Parameters); + } + } + + internal struct AnimationParametersMessage : INetworkSerializable + { + public byte[] Parameters; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Parameters); + } + } + + internal struct AnimationTriggerMessage : INetworkSerializable + { + public int Hash; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref Hash); + + } + } + + [SerializeField] private Animator m_Animator; + [SerializeField] private uint m_ParameterSendBits; + [SerializeField] private float m_SendRate = 0.1f; + + public Animator Animator + { + get { return m_Animator; } + set + { + m_Animator = value; + ResetParameterOptions(); + } + } + + /* + * AutoSend is the ability to select which parameters linked to this animator + * get replicated on a regular basis regardless of a state change. The thinking + * behind this is that many of the parameters people use are usually booleans + * which result in a state change and thus would cause a full sync of state. + * Thus if you really care about a parameter syncing then you need to be explict + * by selecting it in the inspector when an NetworkAnimator is selected. + */ + public void SetParameterAutoSend(int index, bool value) + { + if (value) + { + m_ParameterSendBits |= (uint)(1 << index); + } + else + { + m_ParameterSendBits &= (uint)(~(1 << index)); + } + } + + public bool GetParameterAutoSend(int index) + { + return (m_ParameterSendBits & (uint)(1 << index)) != 0; + } + + // Animators only support up to 32 params + public static int K_MaxAnimationParams = 32; + + private int m_TransitionHash; + private double m_NextSendTime = 0.0f; + + private int m_AnimationHash; + public int AnimationHash { get => m_AnimationHash; } + + private unsafe struct AnimatorParamCache + { + public int Hash; + public int Type; + public fixed byte Value[4]; // this is a max size of 4 bytes + } + + // 128bytes per Animator + private FastBufferWriter m_ParameterWriter = new FastBufferWriter(K_MaxAnimationParams * sizeof(float), Allocator.Persistent); + private NativeArray m_CachedAnimatorParameters; + + // We cache these values because UnsafeUtility.EnumToInt use direct IL that allows a nonboxing conversion + private struct AnimationParamEnumWrapper + { + public static readonly int AnimatorControllerParameterInt; + public static readonly int AnimatorControllerParameterFloat; + public static readonly int AnimatorControllerParameterBool; + + static AnimationParamEnumWrapper() + { + AnimatorControllerParameterInt = UnsafeUtility.EnumToInt(AnimatorControllerParameterType.Int); + AnimatorControllerParameterFloat = UnsafeUtility.EnumToInt(AnimatorControllerParameterType.Float); + AnimatorControllerParameterBool = UnsafeUtility.EnumToInt(AnimatorControllerParameterType.Bool); + } + } + + internal void ResetParameterOptions() + { + + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfoServer("ResetParameterOptions"); + } + + m_ParameterSendBits = 0; + } + + private bool sendMessagesAllowed + { + get + { + return IsServer && NetworkObject.IsSpawned; + } + } + + public override void OnDestroy() + { + if (m_CachedAnimatorParameters.IsCreated) + { + m_CachedAnimatorParameters.Dispose(); + } + + m_ParameterWriter.Dispose(); + } + + public override void OnNetworkSpawn() + { + var parameters = m_Animator.parameters; + m_CachedAnimatorParameters = new NativeArray(parameters.Length, Allocator.Persistent); + + m_AnimationHash = -1; + + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + + if (m_Animator.IsParameterControlledByCurve(parameter.nameHash)) + { + //we are ignoring parameters that are controlled by animation curves - syncing the layer states indirectly syncs the values that are driven by the animation curves + continue; + } + + var cacheParam = new AnimatorParamCache(); + + cacheParam.Type = UnsafeUtility.EnumToInt(parameter.type); + cacheParam.Hash = parameter.nameHash; + unsafe + { + switch (parameter.type) + { + case AnimatorControllerParameterType.Float: + var value = m_Animator.GetFloat(cacheParam.Hash); + UnsafeUtility.WriteArrayElement(cacheParam.Value, 0, value); + break; + case AnimatorControllerParameterType.Int: + var valueInt = m_Animator.GetInteger(cacheParam.Hash); + UnsafeUtility.WriteArrayElement(cacheParam.Value, 0, valueInt); + + break; + case AnimatorControllerParameterType.Bool: + var valueBool = m_Animator.GetBool(cacheParam.Hash); + UnsafeUtility.WriteArrayElement(cacheParam.Value, 0, valueBool); + break; + case AnimatorControllerParameterType.Trigger: + default: + break; + } + } + + m_CachedAnimatorParameters[i] = cacheParam; + } + } + + private void FixedUpdate() + { + if (!sendMessagesAllowed) + { + return; + } + + int stateHash; + float normalizedTime; + if (!CheckAnimStateChanged(out stateHash, out normalizedTime)) + { + // We only want to check and send if we don't have any other state to since + // as we will sync all params as part of the state sync + CheckAndSend(); + + return; + } + + var animMsg = new AnimationMessage(); + animMsg.StateHash = stateHash; + animMsg.NormalizedTime = normalizedTime; + + m_ParameterWriter.Seek(0); + m_ParameterWriter.Truncate(); + + WriteParameters(m_ParameterWriter, false); + animMsg.Parameters = m_ParameterWriter.ToArray(); + + SendAnimStateClientRpc(animMsg); + } + + private void CheckAndSend() + { + var networkTime = NetworkManager.ServerTime.Time; + if (sendMessagesAllowed && m_SendRate != 0 && m_NextSendTime < networkTime) + { + m_NextSendTime = networkTime + m_SendRate; + + m_ParameterWriter.Seek(0); + m_ParameterWriter.Truncate(); + + if (WriteParameters(m_ParameterWriter, true)) + { + // we then sync the params we care about + var animMsg = new AnimationParametersMessage() + { + Parameters = m_ParameterWriter.ToArray() + }; + + SendParamsClientRpc(animMsg); + } + } + } + + private bool CheckAnimStateChanged(out int stateHash, out float normalizedTime) + { + stateHash = 0; + normalizedTime = 0; + + if (m_Animator.IsInTransition(0)) + { + AnimatorTransitionInfo tt = m_Animator.GetAnimatorTransitionInfo(0); + if (tt.fullPathHash != m_TransitionHash) + { + // first time in this transition + m_TransitionHash = tt.fullPathHash; + m_AnimationHash = 0; + return true; + } + return false; + } + + AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(0); + if (st.fullPathHash != m_AnimationHash) + { + // first time in this animation state + if (m_AnimationHash != 0) + { + // came from another animation directly - from Play() + stateHash = st.fullPathHash; + normalizedTime = st.normalizedTime; + } + m_TransitionHash = 0; + m_AnimationHash = st.fullPathHash; + return true; + } + return false; + } + + private unsafe bool WriteParameters(FastBufferWriter writer, bool autoSend) + { + if (m_CachedAnimatorParameters == null) + { + return false; + } + + for (int i = 0; i < m_CachedAnimatorParameters.Length; i++) + { + if (autoSend && !GetParameterAutoSend(i)) + { + continue; + } + + ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef(m_CachedAnimatorParameters.GetUnsafePtr(), i); + var hash = cacheValue.Hash; + + if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterInt) + { + var valueInt = m_Animator.GetInteger(hash); + fixed (void* value = cacheValue.Value) + { + var oldValue = UnsafeUtility.AsRef(value); + if (valueInt != oldValue) + { + UnsafeUtility.WriteArrayElement(value, 0, valueInt); + BytePacker.WriteValuePacked(writer, (uint)valueInt); + } + } + } + else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterBool) + { + var valueBool = m_Animator.GetBool(hash); + fixed (void* value = cacheValue.Value) + { + var oldValue = UnsafeUtility.AsRef(value); + if (valueBool != oldValue) + { + UnsafeUtility.WriteArrayElement(value, 0, valueBool); + writer.WriteValueSafe(valueBool); + } + } + } + else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat) + { + var valueFloat = m_Animator.GetFloat(hash); + fixed (void* value = cacheValue.Value) + { + var oldValue = UnsafeUtility.AsRef(value); + if (valueFloat != oldValue) + { + UnsafeUtility.WriteArrayElement(value, 0, valueFloat); + + writer.WriteValueSafe(valueFloat); + } + } + } + } + + // If we do not write any values to the writer then we should not send any data + return writer.Length > 0; + } + + private unsafe void ReadParameters(FastBufferReader reader, bool autoSend) + { + if (m_CachedAnimatorParameters == null) + { + return; + } + + for (int i = 0; i < m_CachedAnimatorParameters.Length; i++) + { + if (autoSend && !GetParameterAutoSend(i)) + { + continue; + } + ref var cacheValue = ref UnsafeUtility.ArrayElementAsRef(m_CachedAnimatorParameters.GetUnsafePtr(), i); + var hash = cacheValue.Hash; + + if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterInt) + { + ByteUnpacker.ReadValuePacked(reader, out int newValue); + m_Animator.SetInteger(hash, newValue); + fixed (void* value = cacheValue.Value) + { + UnsafeUtility.WriteArrayElement(value, 0, newValue); + } + } + else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterBool) + { + reader.ReadValueSafe(out bool newBoolValue); + m_Animator.SetBool(hash, newBoolValue); + fixed (void* value = cacheValue.Value) + { + UnsafeUtility.WriteArrayElement(value, 0, newBoolValue); + } + } + else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat) + { + reader.ReadValueSafe(out float newFloatValue); + m_Animator.SetFloat(hash, newFloatValue); + fixed (void* value = cacheValue.Value) + { + UnsafeUtility.WriteArrayElement(value, 0, newFloatValue); + } + } + } + } + + [ClientRpc] + private unsafe void SendParamsClientRpc(AnimationParametersMessage animSnapshot, ClientRpcParams clientRpcParams = default) + { + if (animSnapshot.Parameters != null) + { + // We use a fixed value here to avoid the copy of data from the byte buffer since we own the data + fixed (byte* parameters = animSnapshot.Parameters) + { + var reader = new FastBufferReader(parameters, Allocator.None, animSnapshot.Parameters.Length); + ReadParameters(reader, true); + } + } + } + + [ClientRpc] + private unsafe void SendAnimStateClientRpc(AnimationMessage animSnapshot, ClientRpcParams clientRpcParams = default) + { + if (animSnapshot.StateHash != 0) + { + m_AnimationHash = animSnapshot.StateHash; + m_Animator.Play(animSnapshot.StateHash, 0, animSnapshot.NormalizedTime); + } + + if (animSnapshot.Parameters != null && animSnapshot.Parameters.Length != 0) + { + // We use a fixed value here to avoid the copy of data from the byte buffer since we own the data + fixed (byte* parameters = animSnapshot.Parameters) + { + var reader = new FastBufferReader(parameters, Allocator.None, animSnapshot.Parameters.Length); + ReadParameters(reader, false); + } + } + } + + [ClientRpc] + private void SendAnimTriggerClientRpc(AnimationTriggerMessage animSnapshot, ClientRpcParams clientRpcParams = default) + { + m_Animator.SetTrigger(animSnapshot.Hash); + } + + public void SetTrigger(string triggerName) + { + SetTrigger(Animator.StringToHash(triggerName)); + } + + public void SetTrigger(int hash) + { + var animMsg = new AnimationTriggerMessage(); + animMsg.Hash = hash; + + if (IsServer) + { + SendAnimTriggerClientRpc(animMsg); + } + } + } +} diff --git a/Components/NetworkAnimator.cs.meta b/Components/NetworkAnimator.cs.meta new file mode 100644 index 0000000..5b5fe3e --- /dev/null +++ b/Components/NetworkAnimator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8d0727d5ae3244e3b569694d3912374 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/NetworkRigidbody.cs b/Components/NetworkRigidbody.cs new file mode 100644 index 0000000..08c225f --- /dev/null +++ b/Components/NetworkRigidbody.cs @@ -0,0 +1,80 @@ +using UnityEngine; + +namespace Unity.Netcode.Components +{ + /// + /// NetworkRigidbody allows for the use of on network objects. By controlling the kinematic + /// mode of the rigidbody and disabling it on all peers but the authoritative one. + /// + [RequireComponent(typeof(Rigidbody))] + [RequireComponent(typeof(NetworkTransform))] + public class NetworkRigidbody : NetworkBehaviour + { + private Rigidbody m_Rigidbody; + private NetworkTransform m_NetworkTransform; + + private bool m_OriginalKinematic; + private RigidbodyInterpolation m_OriginalInterpolation; + + // Used to cache the authority state of this rigidbody during the last frame + private bool m_IsAuthority; + + /// + /// Gets a bool value indicating whether this on this peer currently holds authority. + /// + private bool HasAuthority => m_NetworkTransform.CanCommitToTransform; + + private void Awake() + { + m_Rigidbody = GetComponent(); + m_NetworkTransform = GetComponent(); + } + + private void FixedUpdate() + { + if (NetworkManager.IsListening) + { + if (HasAuthority != m_IsAuthority) + { + m_IsAuthority = HasAuthority; + UpdateRigidbodyKinematicMode(); + } + } + } + + // Puts the rigidbody in a kinematic non-interpolated mode on everyone but the server. + private void UpdateRigidbodyKinematicMode() + { + if (m_IsAuthority == false) + { + m_OriginalKinematic = m_Rigidbody.isKinematic; + m_Rigidbody.isKinematic = true; + + m_OriginalInterpolation = m_Rigidbody.interpolation; + // Set interpolation to none, the NetworkTransform component interpolates the position of the object. + m_Rigidbody.interpolation = RigidbodyInterpolation.None; + } + else + { + // Resets the rigidbody back to it's non replication only state. Happens on shutdown and when authority is lost + m_Rigidbody.isKinematic = m_OriginalKinematic; + m_Rigidbody.interpolation = m_OriginalInterpolation; + } + } + + /// + public override void OnNetworkSpawn() + { + m_IsAuthority = HasAuthority; + m_OriginalKinematic = m_Rigidbody.isKinematic; + m_OriginalInterpolation = m_Rigidbody.interpolation; + UpdateRigidbodyKinematicMode(); + } + + /// + public override void OnNetworkDespawn() + { + UpdateRigidbodyKinematicMode(); + } + } +} diff --git a/Components/NetworkRigidbody.cs.meta b/Components/NetworkRigidbody.cs.meta new file mode 100644 index 0000000..6f22401 --- /dev/null +++ b/Components/NetworkRigidbody.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6c0be61502bb534f922ebb746851216 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/NetworkRigidbody2D.cs b/Components/NetworkRigidbody2D.cs new file mode 100644 index 0000000..e0997da --- /dev/null +++ b/Components/NetworkRigidbody2D.cs @@ -0,0 +1,81 @@ +using UnityEngine; + +namespace Unity.Netcode.Components +{ + /// + /// NetworkRigidbody allows for the use of on network objects. By controlling the kinematic + /// mode of the rigidbody and disabling it on all peers but the authoritative one. + /// + [RequireComponent(typeof(Rigidbody2D))] + [RequireComponent(typeof(NetworkTransform))] + public class NetworkRigidbody2D : NetworkBehaviour + { + private Rigidbody2D m_Rigidbody; + private NetworkTransform m_NetworkTransform; + + private bool m_OriginalKinematic; + private RigidbodyInterpolation2D m_OriginalInterpolation; + + // Used to cache the authority state of this rigidbody during the last frame + private bool m_IsAuthority; + + /// + /// Gets a bool value indicating whether this on this peer currently holds authority. + /// + private bool HasAuthority => m_NetworkTransform.CanCommitToTransform; + + private void Awake() + { + m_Rigidbody = GetComponent(); + m_NetworkTransform = GetComponent(); + } + + private void FixedUpdate() + { + if (NetworkManager.IsListening) + { + if (HasAuthority != m_IsAuthority) + { + m_IsAuthority = HasAuthority; + UpdateRigidbodyKinematicMode(); + } + } + } + + // Puts the rigidbody in a kinematic non-interpolated mode on everyone but the server. + private void UpdateRigidbodyKinematicMode() + { + if (m_IsAuthority == false) + { + m_OriginalKinematic = m_Rigidbody.isKinematic; + m_Rigidbody.isKinematic = true; + + m_OriginalInterpolation = m_Rigidbody.interpolation; + // Set interpolation to none, the NetworkTransform component interpolates the position of the object. + m_Rigidbody.interpolation = RigidbodyInterpolation2D.None; + } + else + { + // Resets the rigidbody back to it's non replication only state. Happens on shutdown and when authority is lost + m_Rigidbody.isKinematic = m_OriginalKinematic; + m_Rigidbody.interpolation = m_OriginalInterpolation; + } + } + + /// + public override void OnNetworkSpawn() + { + m_IsAuthority = HasAuthority; + m_OriginalKinematic = m_Rigidbody.isKinematic; + m_OriginalInterpolation = m_Rigidbody.interpolation; + UpdateRigidbodyKinematicMode(); + } + + /// + public override void OnNetworkDespawn() + { + m_IsAuthority = false; + UpdateRigidbodyKinematicMode(); + } + } +} diff --git a/Components/NetworkRigidbody2D.cs.meta b/Components/NetworkRigidbody2D.cs.meta new file mode 100644 index 0000000..f285546 --- /dev/null +++ b/Components/NetworkRigidbody2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80d7c879794dfda4687da0e400131852 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs new file mode 100644 index 0000000..831d1e2 --- /dev/null +++ b/Components/NetworkTransform.cs @@ -0,0 +1,900 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Random = UnityEngine.Random; + +namespace Unity.Netcode.Components +{ + /// + /// A component for syncing transforms + /// NetworkTransform will read the underlying transform and replicate it to clients. + /// The replicated value will be automatically be interpolated (if active) and applied to the underlying GameObject's transform + /// + [DisallowMultipleComponent] + [AddComponentMenu("Netcode/" + nameof(NetworkTransform))] + [DefaultExecutionOrder(100000)] // this is needed to catch the update time after the transform was updated by user scripts + public class NetworkTransform : NetworkBehaviour + { + public const float PositionThresholdDefault = .001f; + public const float RotAngleThresholdDefault = .01f; + public const float ScaleThresholdDefault = .01f; + public delegate (Vector3 pos, Quaternion rotOut, Vector3 scale) OnClientRequestChangeDelegate(Vector3 pos, Quaternion rot, Vector3 scale); + public OnClientRequestChangeDelegate OnClientRequestChange; + + internal struct NetworkTransformState : INetworkSerializable + { + private const int k_InLocalSpaceBit = 0; + private const int k_PositionXBit = 1; + private const int k_PositionYBit = 2; + private const int k_PositionZBit = 3; + private const int k_RotAngleXBit = 4; + private const int k_RotAngleYBit = 5; + private const int k_RotAngleZBit = 6; + private const int k_ScaleXBit = 7; + private const int k_ScaleYBit = 8; + private const int k_ScaleZBit = 9; + private const int k_TeleportingBit = 10; + + // 11-15: + private ushort m_Bitset; + + public bool InLocalSpace + { + get => (m_Bitset & (1 << k_InLocalSpaceBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_InLocalSpaceBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_InLocalSpaceBit)); } + } + } + + // Position + public bool HasPositionX + { + get => (m_Bitset & (1 << k_PositionXBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionXBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionXBit)); } + } + } + + public bool HasPositionY + { + get => (m_Bitset & (1 << k_PositionYBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionYBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionYBit)); } + } + } + + public bool HasPositionZ + { + get => (m_Bitset & (1 << k_PositionZBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_PositionZBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_PositionZBit)); } + } + } + + // RotAngles + public bool HasRotAngleX + { + get => (m_Bitset & (1 << k_RotAngleXBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleXBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleXBit)); } + } + } + + public bool HasRotAngleY + { + get => (m_Bitset & (1 << k_RotAngleYBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleYBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleYBit)); } + } + } + + public bool HasRotAngleZ + { + get => (m_Bitset & (1 << k_RotAngleZBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_RotAngleZBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_RotAngleZBit)); } + } + } + + // Scale + public bool HasScaleX + { + get => (m_Bitset & (1 << k_ScaleXBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleXBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleXBit)); } + } + } + + public bool HasScaleY + { + get => (m_Bitset & (1 << k_ScaleYBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleYBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleYBit)); } + } + } + + public bool HasScaleZ + { + get => (m_Bitset & (1 << k_ScaleZBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_ScaleZBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_ScaleZBit)); } + } + } + + public bool IsTeleportingNextFrame + { + get => (m_Bitset & (1 << k_TeleportingBit)) != 0; + set + { + if (value) { m_Bitset = (ushort)(m_Bitset | (1 << k_TeleportingBit)); } + else { m_Bitset = (ushort)(m_Bitset & ~(1 << k_TeleportingBit)); } + } + } + + public float PositionX, PositionY, PositionZ; + public float RotAngleX, RotAngleY, RotAngleZ; + public float ScaleX, ScaleY, ScaleZ; + public double SentTime; + + public Vector3 Position + { + get { return new Vector3(PositionX, PositionY, PositionZ); } + set + { + PositionX = value.x; + PositionY = value.y; + PositionZ = value.z; + } + } + + public Vector3 Rotation + { + get { return new Vector3(RotAngleX, RotAngleY, RotAngleZ); } + set + { + RotAngleX = value.x; + RotAngleY = value.y; + RotAngleZ = value.z; + } + } + + public Vector3 Scale + { + get { return new Vector3(ScaleX, ScaleY, ScaleZ); } + set + { + ScaleX = value.x; + ScaleY = value.y; + ScaleZ = value.z; + } + } + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref SentTime); + // InLocalSpace + HasXXX Bits + serializer.SerializeValue(ref m_Bitset); + // Position Values + if (HasPositionX) + { + serializer.SerializeValue(ref PositionX); + } + + if (HasPositionY) + { + serializer.SerializeValue(ref PositionY); + } + + if (HasPositionZ) + { + serializer.SerializeValue(ref PositionZ); + } + + // RotAngle Values + if (HasRotAngleX) + { + serializer.SerializeValue(ref RotAngleX); + } + + if (HasRotAngleY) + { + serializer.SerializeValue(ref RotAngleY); + } + + if (HasRotAngleZ) + { + serializer.SerializeValue(ref RotAngleZ); + } + + // Scale Values + if (HasScaleX) + { + serializer.SerializeValue(ref ScaleX); + } + + if (HasScaleY) + { + serializer.SerializeValue(ref ScaleY); + } + + if (HasScaleZ) + { + serializer.SerializeValue(ref ScaleZ); + } + } + } + + public bool SyncPositionX = true, SyncPositionY = true, SyncPositionZ = true; + public bool SyncRotAngleX = true, SyncRotAngleY = true, SyncRotAngleZ = true; + public bool SyncScaleX = true, SyncScaleY = true, SyncScaleZ = true; + + public float PositionThreshold = PositionThresholdDefault; + public float RotAngleThreshold = RotAngleThresholdDefault; + public float ScaleThreshold = ScaleThresholdDefault; + + /// + /// Sets whether this transform should sync in local space or in world space. + /// This is important to set since reparenting this transform could have issues, + /// if using world position (depending on who gets synced first: the parent or the child) + /// Having a child always at position 0,0,0 for example will have less possibilities of desync than when using world positions + /// + [Tooltip("Sets whether this transform should sync in local space or in world space")] + public bool InLocalSpace = false; + + public bool Interpolate = true; + + /// + /// Used to determine who can write to this transform. Server only for this transform. + /// Changing this value alone in a child implementation will not allow you to create a NetworkTransform which can be written to by clients. See the ClientNetworkTransform Sample + /// in the package samples for how to implement a NetworkTransform with client write support. + /// 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; + protected bool m_CachedIsServer; + protected NetworkManager m_CachedNetworkManager; + + private readonly NetworkVariable m_ReplicatedNetworkState = new NetworkVariable(new NetworkTransformState()); + + private NetworkTransformState m_LocalAuthoritativeNetworkState; + + private NetworkTransformState m_PrevNetworkState; + + private const int k_DebugDrawLineTime = 10; + + private bool m_HasSentLastValue = false; // used to send one last value, so clients can make the difference between lost replication data (clients extrapolate) and no more data to send. + + + private BufferedLinearInterpolator m_PositionXInterpolator; // = new BufferedLinearInterpolatorFloat(); + private BufferedLinearInterpolator m_PositionYInterpolator; // = new BufferedLinearInterpolatorFloat(); + private BufferedLinearInterpolator m_PositionZInterpolator; // = new BufferedLinearInterpolatorFloat(); + private BufferedLinearInterpolator m_RotationInterpolator; // = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value + private BufferedLinearInterpolator m_ScaleXInterpolator; // = new BufferedLinearInterpolatorFloat(); + private BufferedLinearInterpolator m_ScaleYInterpolator; // = new BufferedLinearInterpolatorFloat(); + private BufferedLinearInterpolator m_ScaleZInterpolator; // = new BufferedLinearInterpolatorFloat(); + private readonly List> m_AllFloatInterpolators = new List>(6); + + private Transform m_Transform; // cache the transform component to reduce unnecessary bounce between managed and native + private int m_LastSentTick; + private NetworkTransformState m_LastSentState; + + /// + /// Tries updating the server authoritative transform, only if allowed. + /// If this called server side, this will commit directly. + /// If no update is needed, nothing will be sent. This method should still be called every update, it'll self manage when it should and shouldn't send + /// + /// + /// + protected void TryCommitTransformToServer(Transform transformToCommit, double dirtyTime) + { + var isDirty = ApplyTransformToNetworkState(ref m_LocalAuthoritativeNetworkState, dirtyTime, transformToCommit); + TryCommit(isDirty); + } + + private void TryCommitValuesToServer(Vector3 position, Vector3 rotation, Vector3 scale, double dirtyTime) + { + var isDirty = ApplyTransformToNetworkStateWithInfo(ref m_LocalAuthoritativeNetworkState, dirtyTime, position, rotation, scale); + + TryCommit(isDirty.isDirty); + } + + private void TryCommit(bool isDirty) + { + void Send(NetworkTransformState stateToSend) + { + if (m_CachedIsServer) + { + // server RPC takes a few frames to execute server side, we want this to execute immediately + CommitLocallyAndReplicate(stateToSend); + } + else + { + CommitTransformServerRpc(stateToSend); + } + } + + // if dirty, send + // if not dirty anymore, but hasn't sent last value for limiting extrapolation, still set isDirty + // if not dirty and has already sent last value, don't do anything + // extrapolation works by using last two values. if it doesn't receive anything anymore, it'll continue to extrapolate. + // This is great in case there's message loss, not so great if we just don't have new values to send. + // the following will send one last "copied" value so unclamped interpolation tries to extrapolate between two identical values, effectively + // making it immobile. + if (isDirty) + { + Send(m_LocalAuthoritativeNetworkState); + m_HasSentLastValue = false; + m_LastSentTick = m_CachedNetworkManager.LocalTime.Tick; + m_LastSentState = m_LocalAuthoritativeNetworkState; + } + else if (!m_HasSentLastValue && m_CachedNetworkManager.LocalTime.Tick >= m_LastSentTick + 1) // check for state.IsDirty since update can happen more than once per tick. No need for client, RPCs will just queue up + { + m_LastSentState.SentTime = m_CachedNetworkManager.LocalTime.Time; // time 1+ tick later + Send(m_LastSentState); + m_HasSentLastValue = true; + } + } + + [ServerRpc(RequireOwnership = false)] + private void CommitTransformServerRpc(NetworkTransformState networkState, ServerRpcParams serverParams = default) + { + if (serverParams.Receive.SenderClientId == OwnerClientId) // RPC call when not authorized to write could happen during the RTT interval during which a server's ownership change hasn't reached the client yet + { + CommitLocallyAndReplicate(networkState); + } + } + + private void CommitLocallyAndReplicate(NetworkTransformState networkState) + { + m_ReplicatedNetworkState.Value = networkState; + AddInterpolatedState(networkState); + } + + private void ResetInterpolatedStateToCurrentAuthoritativeState() + { + var serverTime = NetworkManager.ServerTime.Time; + m_PositionXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionX, serverTime); + m_PositionYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionY, serverTime); + m_PositionZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.PositionZ, serverTime); + + m_RotationInterpolator.ResetTo(Quaternion.Euler(m_LocalAuthoritativeNetworkState.Rotation), serverTime); + + m_ScaleXInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleX, serverTime); + m_ScaleYInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleY, serverTime); + m_ScaleZInterpolator.ResetTo(m_LocalAuthoritativeNetworkState.ScaleZ, serverTime); + } + + // updates `NetworkState` properties if they need to and returns a `bool` indicating whether or not there was any changes made + // returned boolean would be useful to change encapsulating `NetworkVariable`'s dirty state, e.g. ReplNetworkState.SetDirty(isDirty); + internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) + { + return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, transformToUse).isDirty; + } + + private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse) + { + var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position; + var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles; + var scale = transformToUse.localScale; + return ApplyTransformToNetworkStateWithInfo(ref networkState, dirtyTime, position, rotAngles, scale); + } + + private (bool isDirty, bool isPositionDirty, bool isRotationDirty, bool isScaleDirty) ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, double dirtyTime, Vector3 position, Vector3 rotAngles, Vector3 scale) + { + var isDirty = false; + var isPositionDirty = false; + var isRotationDirty = false; + var isScaleDirty = false; + + // hasPositionZ set to false when it should be true? + + if (InLocalSpace != networkState.InLocalSpace) + { + networkState.InLocalSpace = InLocalSpace; + isDirty = true; + } + + // we assume that if x, y or z are dirty then we'll have to send all 3 anyway, so for efficiency + // we skip doing the (quite expensive) Math.Approximately() and check against PositionThreshold + // this still is overly costly and could use more improvements. + // + // (ditto for scale components) + if (SyncPositionX && + Mathf.Abs(networkState.PositionX - position.x) > PositionThreshold) + { + networkState.PositionX = position.x; + networkState.HasPositionX = true; + isPositionDirty = true; + } + + if (SyncPositionY && + Mathf.Abs(networkState.PositionY - position.y) > PositionThreshold) + { + networkState.PositionY = position.y; + networkState.HasPositionY = true; + isPositionDirty = true; + } + + if (SyncPositionZ && + Mathf.Abs(networkState.PositionZ - position.z) > PositionThreshold) + { + networkState.PositionZ = position.z; + networkState.HasPositionZ = true; + isPositionDirty = true; + } + + if (SyncRotAngleX && + Mathf.Abs(networkState.RotAngleX - rotAngles.x) > RotAngleThreshold) + { + networkState.RotAngleX = rotAngles.x; + networkState.HasRotAngleX = true; + isRotationDirty = true; + } + + if (SyncRotAngleY && + Mathf.Abs(networkState.RotAngleY - rotAngles.y) > RotAngleThreshold) + { + networkState.RotAngleY = rotAngles.y; + networkState.HasRotAngleY = true; + isRotationDirty = true; + } + + if (SyncRotAngleZ && + Mathf.Abs(networkState.RotAngleZ - rotAngles.z) > RotAngleThreshold) + { + networkState.RotAngleZ = rotAngles.z; + networkState.HasRotAngleZ = true; + isRotationDirty = true; + } + + if (SyncScaleX && + Mathf.Abs(networkState.ScaleX - scale.x) > ScaleThreshold) + { + networkState.ScaleX = scale.x; + networkState.HasScaleX = true; + isScaleDirty = true; + } + + if (SyncScaleY && + Mathf.Abs(networkState.ScaleY - scale.y) > ScaleThreshold) + { + networkState.ScaleY = scale.y; + networkState.HasScaleY = true; + isScaleDirty = true; + } + + if (SyncScaleZ && + Mathf.Abs(networkState.ScaleZ - scale.z) > ScaleThreshold) + { + networkState.ScaleZ = scale.z; + networkState.HasScaleZ = true; + isScaleDirty = true; + } + + isDirty |= isPositionDirty || isRotationDirty || isScaleDirty; + + if (isDirty) + { + networkState.SentTime = dirtyTime; + } + + return (isDirty, isPositionDirty, isRotationDirty, isScaleDirty); + } + + private void ApplyInterpolatedNetworkStateToTransform(NetworkTransformState networkState, Transform transformToUpdate) + { + m_PrevNetworkState = networkState; + + var interpolatedPosition = InLocalSpace ? transformToUpdate.localPosition : transformToUpdate.position; + + // todo: we should store network state w/ quats vs. euler angles + var interpolatedRotAngles = InLocalSpace ? transformToUpdate.localEulerAngles : transformToUpdate.eulerAngles; + var interpolatedScale = transformToUpdate.localScale; + + // InLocalSpace Read + InLocalSpace = networkState.InLocalSpace; + // Position Read + if (SyncPositionX) + { + interpolatedPosition.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.x : m_PositionXInterpolator.GetInterpolatedValue(); + } + + if (SyncPositionY) + { + interpolatedPosition.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.y : m_PositionYInterpolator.GetInterpolatedValue(); + } + + if (SyncPositionZ) + { + interpolatedPosition.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Position.z : m_PositionZInterpolator.GetInterpolatedValue(); + } + + // again, we should be using quats here + if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ) + { + var eulerAngles = m_RotationInterpolator.GetInterpolatedValue().eulerAngles; + if (SyncRotAngleX) + { + interpolatedRotAngles.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.x : eulerAngles.x; + } + + if (SyncRotAngleY) + { + interpolatedRotAngles.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.y : eulerAngles.y; + } + + if (SyncRotAngleZ) + { + interpolatedRotAngles.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Rotation.z : eulerAngles.z; + } + } + + // Scale Read + if (SyncScaleX) + { + interpolatedScale.x = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.x : m_ScaleXInterpolator.GetInterpolatedValue(); + } + + if (SyncScaleY) + { + interpolatedScale.y = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.y : m_ScaleYInterpolator.GetInterpolatedValue(); + } + + if (SyncScaleZ) + { + interpolatedScale.z = networkState.IsTeleportingNextFrame || !Interpolate ? networkState.Scale.z : m_ScaleZInterpolator.GetInterpolatedValue(); + } + + // Position Apply + if (SyncPositionX || SyncPositionY || SyncPositionZ) + { + if (InLocalSpace) + { + transformToUpdate.localPosition = interpolatedPosition; + } + else + { + transformToUpdate.position = interpolatedPosition; + } + + m_PrevNetworkState.Position = interpolatedPosition; + } + + // RotAngles Apply + if (SyncRotAngleX || SyncRotAngleY || SyncRotAngleZ) + { + if (InLocalSpace) + { + transformToUpdate.localRotation = Quaternion.Euler(interpolatedRotAngles); + } + else + { + transformToUpdate.rotation = Quaternion.Euler(interpolatedRotAngles); + } + + m_PrevNetworkState.Rotation = interpolatedRotAngles; + } + + // Scale Apply + if (SyncScaleX || SyncScaleY || SyncScaleZ) + { + transformToUpdate.localScale = interpolatedScale; + m_PrevNetworkState.Scale = interpolatedScale; + } + } + + private void AddInterpolatedState(NetworkTransformState newState) + { + var sentTime = newState.SentTime; + + if (newState.HasPositionX) + { + m_PositionXInterpolator.AddMeasurement(newState.PositionX, sentTime); + } + + if (newState.HasPositionY) + { + m_PositionYInterpolator.AddMeasurement(newState.PositionY, sentTime); + } + + if (newState.HasPositionZ) + { + m_PositionZInterpolator.AddMeasurement(newState.PositionZ, sentTime); + } + + m_RotationInterpolator.AddMeasurement(Quaternion.Euler(newState.Rotation), sentTime); + + if (newState.HasScaleX) + { + m_ScaleXInterpolator.AddMeasurement(newState.ScaleX, sentTime); + } + + if (newState.HasScaleY) + { + m_ScaleYInterpolator.AddMeasurement(newState.ScaleY, sentTime); + } + + if (newState.HasScaleZ) + { + m_ScaleZInterpolator.AddMeasurement(newState.ScaleZ, sentTime); + } + } + + private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState) + { + if (!NetworkObject.IsSpawned) + { + // todo MTT-849 should never happen but yet it does! maybe revisit/dig after NetVar updates and snapshot system lands? + return; + } + + if (CanCommitToTransform) + { + // we're the authority, we ignore incoming changes + return; + } + + Debug.DrawLine(newState.Position, newState.Position + Vector3.up + Vector3.left, Color.green, 10, false); + + AddInterpolatedState(newState); + + if (m_CachedNetworkManager.LogLevel == LogLevel.Developer) + { + var pos = new Vector3(newState.PositionX, newState.PositionY, newState.PositionZ); + Debug.DrawLine(pos, pos + Vector3.up + Vector3.left * Random.Range(0.5f, 2f), Color.green, k_DebugDrawLineTime, false); + } + } + + private void Awake() + { + // we only want to create our interpolators during Awake so that, when pooled, we do not create tons + // of gc thrash each time objects wink out and are re-used + m_PositionXInterpolator = new BufferedLinearInterpolatorFloat(); + m_PositionYInterpolator = new BufferedLinearInterpolatorFloat(); + m_PositionZInterpolator = new BufferedLinearInterpolatorFloat(); + m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion(); // rotation is a single Quaternion since each euler axis will affect the quaternion's final value + m_ScaleXInterpolator = new BufferedLinearInterpolatorFloat(); + m_ScaleYInterpolator = new BufferedLinearInterpolatorFloat(); + m_ScaleZInterpolator = new BufferedLinearInterpolatorFloat(); + + if (m_AllFloatInterpolators.Count == 0) + { + m_AllFloatInterpolators.Add(m_PositionXInterpolator); + m_AllFloatInterpolators.Add(m_PositionYInterpolator); + m_AllFloatInterpolators.Add(m_PositionZInterpolator); + m_AllFloatInterpolators.Add(m_ScaleXInterpolator); + m_AllFloatInterpolators.Add(m_ScaleYInterpolator); + m_AllFloatInterpolators.Add(m_ScaleZInterpolator); + } + } + + public override void OnNetworkSpawn() + { + // must set up m_Transform in OnNetworkSpawn because it's possible an object spawns but is disabled + // and thus awake won't be called. + // TODO: investigate further on not sending data for something that is not enabled + m_Transform = transform; + m_ReplicatedNetworkState.OnValueChanged += OnNetworkStateChanged; + + CanCommitToTransform = IsServer; + m_CachedIsServer = IsServer; + m_CachedNetworkManager = NetworkManager; + + if (CanCommitToTransform) + { + TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time); + } + m_LocalAuthoritativeNetworkState = m_ReplicatedNetworkState.Value; + + // crucial we do this to reset the interpolators so that recycled objects when using a pool will + // not have leftover interpolator state from the previous object + Initialize(); + } + + public override void OnNetworkDespawn() + { + m_ReplicatedNetworkState.OnValueChanged -= OnNetworkStateChanged; + } + + public override void OnGainedOwnership() + { + Initialize(); + } + + public override void OnLostOwnership() + { + Initialize(); + } + + private void Initialize() + { + ResetInterpolatedStateToCurrentAuthoritativeState(); // useful for late joining + + if (CanCommitToTransform) + { + m_ReplicatedNetworkState.SetDirty(true); + } + else + { + ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); + } + } + + #region state set + + /// + /// Directly sets a state on the authoritative transform. + /// This will override any changes made previously to the transform + /// This isn't resistant to network jitter. Server side changes due to this method won't be interpolated. + /// The parameters are broken up into pos / rot / scale on purpose so that the caller can perturb + /// just the desired one(s) + /// + /// new position to move to. Can be null + /// new rotation to rotate to. Can be null + /// new scale to scale to. Can be null + /// Should other clients interpolate this change or not. True by default + /// new scale to scale to. Can be null + /// + public void SetState(Vector3? posIn = null, Quaternion? rotIn = null, Vector3? scaleIn = null, bool shouldGhostsInterpolate = true) + { + if (!IsOwner) + { + throw new Exception("Trying to set a state on a not owned transform"); + } + + if (m_CachedNetworkManager && !(m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening)) + { + return; + } + + Vector3 pos = posIn == null ? transform.position : (Vector3)posIn; + Quaternion rot = rotIn == null ? transform.rotation : (Quaternion)rotIn; + Vector3 scale = scaleIn == null ? transform.localScale : (Vector3)scaleIn; + + if (!CanCommitToTransform) + { + if (!m_CachedIsServer) + { + SetStateServerRpc(pos, rot, scale, shouldGhostsInterpolate); + } + } + else + { + m_Transform.position = pos; + m_Transform.rotation = rot; + m_Transform.localScale = scale; + m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldGhostsInterpolate; + } + } + + [ServerRpc] + private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool shouldTeleport) + { + // server has received this RPC request to move change transform. Give the server a chance to modify or + // even reject the move + if (OnClientRequestChange != null) + { + (pos, rot, scale) = OnClientRequestChange(pos, rot, scale); + } + m_Transform.position = pos; + m_Transform.rotation = rot; + m_Transform.localScale = scale; + m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = shouldTeleport; + } + #endregion + + // todo this is currently in update, to be able to catch any transform changes. A FixedUpdate mode could be added to be less intense, but it'd be + // conditional to users only making transform update changes in FixedUpdate. + protected virtual void Update() + { + if (!NetworkObject.IsSpawned) + { + return; + } + + if (CanCommitToTransform) + { + if (m_CachedIsServer) + { + TryCommitTransformToServer(m_Transform, m_CachedNetworkManager.LocalTime.Time); + } + + m_PrevNetworkState = m_LocalAuthoritativeNetworkState; + } + + // apply interpolated value + if (m_CachedNetworkManager.IsConnectedClient || m_CachedNetworkManager.IsListening) + { + // eventually, we could hoist this calculation so that it happens once for all objects, not once per object + var cachedDeltaTime = Time.deltaTime; + var serverTime = NetworkManager.ServerTime; + var cachedServerTime = serverTime.Time; + var cachedRenderTime = serverTime.TimeTicksAgo(1).Time; + + foreach (var interpolator in m_AllFloatInterpolators) + { + interpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); + } + + m_RotationInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime); + + if (!CanCommitToTransform) + { +#if NGO_TRANSFORM_DEBUG + if (m_CachedNetworkManager.LogLevel == LogLevel.Developer) + { + // TODO: This should be a component gizmo - not some debug draw based on log level + var interpolatedPosition = new Vector3(m_PositionXInterpolator.GetInterpolatedValue(), m_PositionYInterpolator.GetInterpolatedValue(), m_PositionZInterpolator.GetInterpolatedValue()); + Debug.DrawLine(interpolatedPosition, interpolatedPosition + Vector3.up, Color.magenta, k_DebugDrawLineTime, false); + + // try to update previously consumed NetworkState + // if we have any changes, that means made some updates locally + // we apply the latest ReplNetworkState again to revert our changes + var oldStateDirtyInfo = ApplyTransformToNetworkStateWithInfo(ref m_PrevNetworkState, 0, m_Transform); + + // there are several bugs in this code, as we the message is dumped out under odd circumstances + // For Matt, it would trigger when an object's rotation was perturbed by colliding with another + // object vs. explicitly rotating it + if (oldStateDirtyInfo.isPositionDirty || oldStateDirtyInfo.isScaleDirty || (oldStateDirtyInfo.isRotationDirty && SyncRotAngleX && SyncRotAngleY && SyncRotAngleZ)) + { + // ignoring rotation dirty since quaternions will mess with euler angles, making this impossible to determine if the change to a single axis comes + // from an unauthorized transform change or euler to quaternion conversion artifacts. + var dirtyField = oldStateDirtyInfo.isPositionDirty ? "position" : oldStateDirtyInfo.isRotationDirty ? "rotation" : "scale"; + Debug.LogWarning($"A local change to {dirtyField} without authority detected, reverting back to latest interpolated network state!", this); + } + } +#endif + + // Apply updated interpolated value + ApplyInterpolatedNetworkStateToTransform(m_ReplicatedNetworkState.Value, m_Transform); + } + } + + m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; + } + + /// + /// Teleports the transform to the given values without interpolating + /// + public void Teleport(Vector3 newPosition, Quaternion newRotation, Vector3 newScale) + { + if (!CanCommitToTransform) + { + throw new Exception("Teleport not allowed"); + } + + var newRotationEuler = newRotation.eulerAngles; + var stateToSend = m_LocalAuthoritativeNetworkState; + stateToSend.IsTeleportingNextFrame = true; + stateToSend.Position = newPosition; + stateToSend.Rotation = newRotationEuler; + stateToSend.Scale = newScale; + ApplyInterpolatedNetworkStateToTransform(stateToSend, transform); + // set teleport flag in state to signal to ghosts not to interpolate + m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = true; + // check server side + TryCommitValuesToServer(newPosition, newRotationEuler, newScale, m_CachedNetworkManager.LocalTime.Time); + m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; + } + } +} diff --git a/Components/NetworkTransform.cs.meta b/Components/NetworkTransform.cs.meta new file mode 100644 index 0000000..4023b38 --- /dev/null +++ b/Components/NetworkTransform.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e96cb6065543e43c4a752faaa1468eb1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/com.unity.netcode.components.asmdef b/Components/com.unity.netcode.components.asmdef new file mode 100644 index 0000000..6c16972 --- /dev/null +++ b/Components/com.unity.netcode.components.asmdef @@ -0,0 +1,17 @@ +{ + "name": "Unity.Netcode.Components", + "rootNamespace": "Unity.Netcode.Components", + "references": [ + "Unity.Netcode.Runtime", + "Unity.Collections" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Components/com.unity.netcode.components.asmdef.meta b/Components/com.unity.netcode.components.asmdef.meta new file mode 100644 index 0000000..3dcf24d --- /dev/null +++ b/Components/com.unity.netcode.components.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3b8ed52f1b5c64994af4c4e0aa4b6c4b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Documentation~/Index.md b/Documentation~/Index.md new file mode 100644 index 0000000..616406c --- /dev/null +++ b/Documentation~/Index.md @@ -0,0 +1,31 @@ +# About Netcode for GameObjects + +Unity Netcode for GameObjects is a high-level networking library built to abstract networking. This allows developers to focus on the game rather than low level protocols and networking frameworks. + +## Guides + +See guides below to install Unity Netcode for GameObjects, set up your project, and get started with your first networked game: + +* [Documentation](https://docs-multiplayer.unity3d.com/docs/getting-started/about-mlapi) +* [Installation](https://docs-multiplayer.unity3d.com/docs/migration/install) +* [First Steps](https://docs-multiplayer.unity3d.com/docs/tutorials/helloworld/helloworldintro) +* [API Reference](https://docs-multiplayer.unity3d.com/docs/mlapi-api/introduction) + +# Technical details + +## Requirements + +This version of Netcode for GameObjects is compatible with the following Unity versions and platforms: + +* 2020.3 and later +* Windows, Mac, Linux platforms + +## Document revision history + +|Date|Reason| +|---|---| +|March 10, 2021|Document created. Matches package version 0.1.0| +|June 1, 2021|Update and add links for additional content. Matches patch version 0.1.0 and hotfixes.| +|June 3, 2021|Update document to acknowledge Unity min version change. Matches package version 0.2.0| +|August 5, 2021|Update product/package name| +|September 9,2021|Updated the links and name of the file.| \ No newline at end of file diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..0679703 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0388707d01c6e18409986ad2fadf6faa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/AssemblyInfo.cs b/Editor/AssemblyInfo.cs new file mode 100644 index 0000000..3437d8c --- /dev/null +++ b/Editor/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Unity.Netcode.EditorTests")] diff --git a/Editor/AssemblyInfo.cs.meta b/Editor/AssemblyInfo.cs.meta new file mode 100644 index 0000000..5b2f096 --- /dev/null +++ b/Editor/AssemblyInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4b3ac10f0d62417080a83d7e17407dd3 +timeCreated: 1631656133 \ No newline at end of file diff --git a/Editor/CodeGen.meta b/Editor/CodeGen.meta new file mode 100644 index 0000000..0da94fd --- /dev/null +++ b/Editor/CodeGen.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bbb4974b4302f435b9f4663c64d8f803 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CodeGen/CodeGenHelpers.cs b/Editor/CodeGen/CodeGenHelpers.cs new file mode 100644 index 0000000..390d190 --- /dev/null +++ b/Editor/CodeGen/CodeGenHelpers.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using Unity.CompilationPipeline.Common.Diagnostics; +using Unity.CompilationPipeline.Common.ILPostProcessing; +using UnityEngine; + +namespace Unity.Netcode.Editor.CodeGen +{ + internal static class CodeGenHelpers + { + public const string RuntimeAssemblyName = "Unity.Netcode.Runtime"; + + public static readonly string NetworkBehaviour_FullName = typeof(NetworkBehaviour).FullName; + public static readonly string INetworkMessage_FullName = typeof(INetworkMessage).FullName; + public static readonly string ServerRpcAttribute_FullName = typeof(ServerRpcAttribute).FullName; + public static readonly string ClientRpcAttribute_FullName = typeof(ClientRpcAttribute).FullName; + public static readonly string ServerRpcParams_FullName = typeof(ServerRpcParams).FullName; + public static readonly string ClientRpcParams_FullName = typeof(ClientRpcParams).FullName; + public static readonly string INetworkSerializable_FullName = typeof(INetworkSerializable).FullName; + public static readonly string UnityColor_FullName = typeof(Color).FullName; + public static readonly string UnityColor32_FullName = typeof(Color32).FullName; + public static readonly string UnityVector2_FullName = typeof(Vector2).FullName; + public static readonly string UnityVector3_FullName = typeof(Vector3).FullName; + public static readonly string UnityVector4_FullName = typeof(Vector4).FullName; + public static readonly string UnityQuaternion_FullName = typeof(Quaternion).FullName; + public static readonly string UnityRay_FullName = typeof(Ray).FullName; + public static readonly string UnityRay2D_FullName = typeof(Ray2D).FullName; + + public static uint Hash(this MethodDefinition methodDefinition) + { + var sigArr = Encoding.UTF8.GetBytes($"{methodDefinition.Module.Name} / {methodDefinition.FullName}"); + var sigLen = sigArr.Length; + unsafe + { + fixed (byte* sigPtr = sigArr) + { + return XXHash.Hash32(sigPtr, sigLen); + } + } + } + + public static bool IsSubclassOf(this TypeDefinition typeDefinition, string classTypeFullName) + { + if (!typeDefinition.IsClass) + { + return false; + } + + var baseTypeRef = typeDefinition.BaseType; + while (baseTypeRef != null) + { + if (baseTypeRef.FullName == classTypeFullName) + { + return true; + } + + try + { + baseTypeRef = baseTypeRef.Resolve().BaseType; + } + catch + { + return false; + } + } + + return false; + } + + public static bool HasInterface(this TypeReference typeReference, string interfaceTypeFullName) + { + if (typeReference.IsArray) + { + return false; + } + + try + { + var typeDef = typeReference.Resolve(); + var typeFaces = typeDef.Interfaces; + return typeFaces.Any(iface => iface.InterfaceType.FullName == interfaceTypeFullName); + } + catch + { + return false; + } + } + + public static bool IsSerializable(this TypeReference typeReference) + { + var typeSystem = typeReference.Module.TypeSystem; + + // C# primitives + if (typeReference == typeSystem.Boolean) + { + return true; + } + + if (typeReference == typeSystem.Char) + { + return true; + } + + if (typeReference == typeSystem.SByte) + { + return true; + } + + if (typeReference == typeSystem.Byte) + { + return true; + } + + if (typeReference == typeSystem.Int16) + { + return true; + } + + if (typeReference == typeSystem.UInt16) + { + return true; + } + + if (typeReference == typeSystem.Int32) + { + return true; + } + + if (typeReference == typeSystem.UInt32) + { + return true; + } + + if (typeReference == typeSystem.Int64) + { + return true; + } + + if (typeReference == typeSystem.UInt64) + { + return true; + } + + if (typeReference == typeSystem.Single) + { + return true; + } + + if (typeReference == typeSystem.Double) + { + return true; + } + + if (typeReference == typeSystem.String) + { + return true; + } + + // Unity primitives + if (typeReference.FullName == UnityColor_FullName) + { + return true; + } + + if (typeReference.FullName == UnityColor32_FullName) + { + return true; + } + + if (typeReference.FullName == UnityVector2_FullName) + { + return true; + } + + if (typeReference.FullName == UnityVector3_FullName) + { + return true; + } + + if (typeReference.FullName == UnityVector4_FullName) + { + return true; + } + + if (typeReference.FullName == UnityQuaternion_FullName) + { + return true; + } + + if (typeReference.FullName == UnityRay_FullName) + { + return true; + } + + if (typeReference.FullName == UnityRay2D_FullName) + { + return true; + } + + // Enum + if (typeReference.GetEnumAsInt() != null) + { + return true; + } + + // INetworkSerializable + if (typeReference.HasInterface(INetworkSerializable_FullName)) + { + return true; + } + + // Static array + if (typeReference.IsArray) + { + return typeReference.GetElementType().IsSerializable(); + } + + return false; + } + + public static TypeReference GetEnumAsInt(this TypeReference typeReference) + { + if (typeReference.IsArray) + { + return null; + } + + try + { + var typeDef = typeReference.Resolve(); + return typeDef.IsEnum ? typeDef.GetEnumUnderlyingType() : null; + } + catch + { + return null; + } + } + + public static void AddError(this List diagnostics, string message) + { + diagnostics.AddError((SequencePoint)null, message); + } + + public static void AddError(this List diagnostics, MethodDefinition methodDefinition, string message) + { + diagnostics.AddError(methodDefinition.DebugInformation.SequencePoints.FirstOrDefault(), message); + } + + public static void AddError(this List diagnostics, SequencePoint sequencePoint, string message) + { + diagnostics.Add(new DiagnosticMessage + { + DiagnosticType = DiagnosticType.Error, + File = sequencePoint?.Document.Url.Replace($"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}", ""), + Line = sequencePoint?.StartLine ?? 0, + Column = sequencePoint?.StartColumn ?? 0, + MessageData = $" - {message}" + }); + } + + public static void RemoveRecursiveReferences(this ModuleDefinition moduleDefinition) + { + // Weird behavior from Cecil: When importing a reference to a specific implementation of a generic + // method, it's importing the main module as a reference into itself. This causes Unity to have issues + // when attempting to iterate the assemblies to discover unit tests, as it goes into infinite recursion + // and eventually hits a stack overflow. I wasn't able to find any way to stop Cecil from importing the module + // into itself, so at the end of it all, we're just going to go back and remove it again. + var moduleName = moduleDefinition.Name; + if (moduleName.EndsWith(".dll") || moduleName.EndsWith(".exe")) + { + moduleName = moduleName.Substring(0, moduleName.Length - 4); + } + + foreach (var reference in moduleDefinition.AssemblyReferences) + { + var referenceName = reference.Name.Split(',')[0]; + if (referenceName.EndsWith(".dll") || referenceName.EndsWith(".exe")) + { + referenceName = referenceName.Substring(0, referenceName.Length - 4); + } + + if (moduleName == referenceName) + { + try + { + moduleDefinition.AssemblyReferences.Remove(reference); + break; + } + catch (Exception) + { + // + } + } + } + } + + public static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly, out PostProcessorAssemblyResolver assemblyResolver) + { + assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly); + var readerParameters = new ReaderParameters + { + SymbolStream = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData), + SymbolReaderProvider = new PortablePdbReaderProvider(), + AssemblyResolver = assemblyResolver, + ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(), + ReadingMode = ReadingMode.Immediate + }; + + var assemblyDefinition = AssemblyDefinition.ReadAssembly(new MemoryStream(compiledAssembly.InMemoryAssembly.PeData), readerParameters); + + //apparently, it will happen that when we ask to resolve a type that lives inside Unity.Netcode.Runtime, and we + //are also postprocessing Unity.Netcode.Runtime, type resolving will fail, because we do not actually try to resolve + //inside the assembly we are processing. Let's make sure we do that, so that we can use postprocessor features inside + //Unity.Netcode.Runtime itself as well. + assemblyResolver.AddAssemblyDefinitionBeingOperatedOn(assemblyDefinition); + + return assemblyDefinition; + } + } +} diff --git a/Editor/CodeGen/CodeGenHelpers.cs.meta b/Editor/CodeGen/CodeGenHelpers.cs.meta new file mode 100644 index 0000000..1c99993 --- /dev/null +++ b/Editor/CodeGen/CodeGenHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e5541b3bca0e43b48c2e694fffef5b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CodeGen/INetworkMessageILPP.cs b/Editor/CodeGen/INetworkMessageILPP.cs new file mode 100644 index 0000000..336c1d2 --- /dev/null +++ b/Editor/CodeGen/INetworkMessageILPP.cs @@ -0,0 +1,281 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using Unity.CompilationPipeline.Common.Diagnostics; +using Unity.CompilationPipeline.Common.ILPostProcessing; +using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor; +using MethodAttributes = Mono.Cecil.MethodAttributes; + +namespace Unity.Netcode.Editor.CodeGen +{ + + internal sealed class INetworkMessageILPP : ILPPInterface + { + public override ILPPInterface GetInstance() => this; + + public override bool WillProcess(ICompiledAssembly compiledAssembly) => + compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName || + compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == CodeGenHelpers.RuntimeAssemblyName); + + private readonly List m_Diagnostics = new List(); + + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) + { + if (!WillProcess(compiledAssembly)) + { + return null; + } + + + m_Diagnostics.Clear(); + + // read + var assemblyDefinition = CodeGenHelpers.AssemblyDefinitionFor(compiledAssembly, out var resolver); + if (assemblyDefinition == null) + { + m_Diagnostics.AddError($"Cannot read assembly definition: {compiledAssembly.Name}"); + return null; + } + + // process + var mainModule = assemblyDefinition.MainModule; + if (mainModule != null) + { + if (ImportReferences(mainModule)) + { + var types = mainModule.GetTypes() + .Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkMessage_FullName) && !t.Resolve().IsAbstract) + .ToList(); + // process `INetworkMessage` types + if (types.Count == 0) + { + return null; + } + + try + { + CreateModuleInitializer(assemblyDefinition, types); + } + catch (Exception e) + { + m_Diagnostics.AddError((e.ToString() + e.StackTrace.ToString()).Replace("\n", "|").Replace("\r", "|")); + } + } + else + { + m_Diagnostics.AddError($"Cannot import references into main module: {mainModule.Name}"); + } + } + else + { + m_Diagnostics.AddError($"Cannot get main module from assembly definition: {compiledAssembly.Name}"); + } + + mainModule.RemoveRecursiveReferences(); + + // write + var pe = new MemoryStream(); + var pdb = new MemoryStream(); + + var writerParameters = new WriterParameters + { + SymbolWriterProvider = new PortablePdbWriterProvider(), + SymbolStream = pdb, + WriteSymbols = true + }; + + assemblyDefinition.Write(pe, writerParameters); + + return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics); + } + + + private TypeReference m_FastBufferReader_TypeRef; + private TypeReference m_NetworkContext_TypeRef; + private TypeReference m_MessagingSystem_MessageWithHandler_TypeRef; + private MethodReference m_MessagingSystem_MessageHandler_Constructor_TypeRef; + private FieldReference m_ILPPMessageProvider___network_message_types_FieldRef; + private FieldReference m_MessagingSystem_MessageWithHandler_MessageType_FieldRef; + private FieldReference m_MessagingSystem_MessageWithHandler_Handler_FieldRef; + private MethodReference m_Type_GetTypeFromHandle_MethodRef; + + private MethodReference m_List_Add_MethodRef; + + private bool ImportReferences(ModuleDefinition moduleDefinition) + { + m_FastBufferReader_TypeRef = moduleDefinition.ImportReference(typeof(FastBufferReader)); + m_NetworkContext_TypeRef = moduleDefinition.ImportReference(typeof(NetworkContext)); + m_MessagingSystem_MessageHandler_Constructor_TypeRef = + moduleDefinition.ImportReference(typeof(MessagingSystem.MessageHandler).GetConstructors()[0]); + + var messageWithHandlerType = typeof(MessagingSystem.MessageWithHandler); + m_MessagingSystem_MessageWithHandler_TypeRef = + moduleDefinition.ImportReference(messageWithHandlerType); + foreach (var fieldInfo in messageWithHandlerType.GetFields()) + { + switch (fieldInfo.Name) + { + case nameof(MessagingSystem.MessageWithHandler.MessageType): + m_MessagingSystem_MessageWithHandler_MessageType_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + case nameof(MessagingSystem.MessageWithHandler.Handler): + m_MessagingSystem_MessageWithHandler_Handler_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + } + } + + var typeType = typeof(Type); + foreach (var methodInfo in typeType.GetMethods()) + { + switch (methodInfo.Name) + { + case nameof(Type.GetTypeFromHandle): + m_Type_GetTypeFromHandle_MethodRef = moduleDefinition.ImportReference(methodInfo); + break; + } + } + + var ilppMessageProviderType = typeof(ILPPMessageProvider); + foreach (var fieldInfo in ilppMessageProviderType.GetFields(BindingFlags.Static | BindingFlags.NonPublic)) + { + switch (fieldInfo.Name) + { + case nameof(ILPPMessageProvider.__network_message_types): + m_ILPPMessageProvider___network_message_types_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + } + } + + var listType = typeof(List); + foreach (var methodInfo in listType.GetMethods()) + { + switch (methodInfo.Name) + { + case nameof(List.Add): + m_List_Add_MethodRef = moduleDefinition.ImportReference(methodInfo); + break; + } + } + + + return true; + } + + private MethodReference GetNetworkMessageRecieveHandler(TypeDefinition typeDefinition) + { + SequencePoint typeSequence = null; + foreach (var method in typeDefinition.Methods) + { + var resolved = method.Resolve(); + var methodSequence = resolved.DebugInformation.SequencePoints.FirstOrDefault(); + if (typeSequence == null || methodSequence.StartLine < typeSequence.StartLine) + { + typeSequence = methodSequence; + } + + if (resolved.IsStatic && resolved.IsPublic && resolved.Name == "Receive" && resolved.Parameters.Count == 2 + && !resolved.Parameters[0].IsIn + && !resolved.Parameters[0].ParameterType.IsByReference + && resolved.Parameters[0].ParameterType.Resolve() == + m_FastBufferReader_TypeRef.Resolve() + && resolved.Parameters[1].IsIn + && resolved.Parameters[1].ParameterType.IsByReference + && resolved.Parameters[1].ParameterType.GetElementType().Resolve() == m_NetworkContext_TypeRef.Resolve() + && resolved.ReturnType == resolved.Module.TypeSystem.Void) + { + return method; + } + } + + m_Diagnostics.AddError(typeSequence, $"Class {typeDefinition.FullName} does not implement required method: `public static void Receive(FastBufferReader, in NetworkContext)`"); + return null; + } + + private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinition) + { + var staticCtorMethodDef = typeDefinition.GetStaticConstructor(); + if (staticCtorMethodDef == null) + { + staticCtorMethodDef = new MethodDefinition( + ".cctor", // Static Constructor (constant-constructor) + MethodAttributes.HideBySig | + MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName | + MethodAttributes.Static, + typeDefinition.Module.TypeSystem.Void); + staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + typeDefinition.Methods.Add(staticCtorMethodDef); + } + + return staticCtorMethodDef; + } + + private void CreateInstructionsToRegisterType(ILProcessor processor, List instructions, TypeReference type, MethodReference receiveMethod) + { + // MessagingSystem.__network_message_types.Add(new MessagingSystem.MessageWithHandler{MessageType=typeof(type), Handler=type.Receive}); + processor.Body.Variables.Add(new VariableDefinition(m_MessagingSystem_MessageWithHandler_TypeRef)); + int messageWithHandlerLocIdx = processor.Body.Variables.Count - 1; + + instructions.Add(processor.Create(OpCodes.Ldsfld, m_ILPPMessageProvider___network_message_types_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldloca, messageWithHandlerLocIdx)); + instructions.Add(processor.Create(OpCodes.Initobj, m_MessagingSystem_MessageWithHandler_TypeRef)); + + // tmp.MessageType = typeof(type); + instructions.Add(processor.Create(OpCodes.Ldloca, messageWithHandlerLocIdx)); + instructions.Add(processor.Create(OpCodes.Ldtoken, type)); + instructions.Add(processor.Create(OpCodes.Call, m_Type_GetTypeFromHandle_MethodRef)); + instructions.Add(processor.Create(OpCodes.Stfld, m_MessagingSystem_MessageWithHandler_MessageType_FieldRef)); + + // tmp.Handler = type.Receive + instructions.Add(processor.Create(OpCodes.Ldloca, messageWithHandlerLocIdx)); + instructions.Add(processor.Create(OpCodes.Ldnull)); + + instructions.Add(processor.Create(OpCodes.Ldftn, receiveMethod)); + instructions.Add(processor.Create(OpCodes.Newobj, m_MessagingSystem_MessageHandler_Constructor_TypeRef)); + instructions.Add(processor.Create(OpCodes.Stfld, m_MessagingSystem_MessageWithHandler_Handler_FieldRef)); + + // ILPPMessageProvider.__network_message_types.Add(tmp); + instructions.Add(processor.Create(OpCodes.Ldloc, messageWithHandlerLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, m_List_Add_MethodRef)); + } + + // Creates a static module constructor (which is executed when the module is loaded) that registers all the + // message types in the assembly with MessagingSystem. + // This is the same behavior as annotating a static method with [ModuleInitializer] in standardized + // C# (that attribute doesn't exist in Unity, but the static module constructor still works) + // https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-5.0 + // https://web.archive.org/web/20100212140402/http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx + private void CreateModuleInitializer(AssemblyDefinition assembly, List networkMessageTypes) + { + foreach (var typeDefinition in assembly.MainModule.Types) + { + if (typeDefinition.FullName == "") + { + var staticCtorMethodDef = GetOrCreateStaticConstructor(typeDefinition); + + var processor = staticCtorMethodDef.Body.GetILProcessor(); + + var instructions = new List(); + + foreach (var type in networkMessageTypes) + { + var receiveMethod = GetNetworkMessageRecieveHandler(type); + if (receiveMethod == null) + { + continue; + } + CreateInstructionsToRegisterType(processor, instructions, type, receiveMethod); + } + + instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction)); + break; + } + } + } + } +} diff --git a/Editor/CodeGen/INetworkMessageILPP.cs.meta b/Editor/CodeGen/INetworkMessageILPP.cs.meta new file mode 100644 index 0000000..47c5ba2 --- /dev/null +++ b/Editor/CodeGen/INetworkMessageILPP.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a754504752d649bb8131df8756bd764e +timeCreated: 1631666359 \ No newline at end of file diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs b/Editor/CodeGen/NetworkBehaviourILPP.cs new file mode 100644 index 0000000..3b4b6a1 --- /dev/null +++ b/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -0,0 +1,1310 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using Unity.Collections; +using Unity.CompilationPipeline.Common.Diagnostics; +using Unity.CompilationPipeline.Common.ILPostProcessing; +using UnityEngine; +using MethodAttributes = Mono.Cecil.MethodAttributes; +using ParameterAttributes = Mono.Cecil.ParameterAttributes; +using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor; + +namespace Unity.Netcode.Editor.CodeGen +{ + + internal sealed class NetworkBehaviourILPP : ILPPInterface + { + private const string k_ReadValueMethodName = nameof(FastBufferReader.ReadValueSafe); + private const string k_WriteValueMethodName = nameof(FastBufferWriter.WriteValueSafe); + + public override ILPPInterface GetInstance() => this; + + public override bool WillProcess(ICompiledAssembly compiledAssembly) => compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == CodeGenHelpers.RuntimeAssemblyName); + + private readonly List m_Diagnostics = new List(); + + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) + { + if (!WillProcess(compiledAssembly)) + { + return null; + } + + + m_Diagnostics.Clear(); + + // read + var assemblyDefinition = CodeGenHelpers.AssemblyDefinitionFor(compiledAssembly, out m_AssemblyResolver); + if (assemblyDefinition == null) + { + m_Diagnostics.AddError($"Cannot read assembly definition: {compiledAssembly.Name}"); + return null; + } + + // process + var mainModule = assemblyDefinition.MainModule; + if (mainModule != null) + { + m_MainModule = mainModule; + if (ImportReferences(mainModule)) + { + // process `NetworkBehaviour` types + try + { + mainModule.GetTypes() + .Where(t => t.IsSubclassOf(CodeGenHelpers.NetworkBehaviour_FullName)) + .ToList() + .ForEach(b => ProcessNetworkBehaviour(b, compiledAssembly.Defines)); + } + catch (Exception e) + { + m_Diagnostics.AddError((e.ToString() + e.StackTrace.ToString()).Replace("\n", "|").Replace("\r", "|")); + } + } + else + { + m_Diagnostics.AddError($"Cannot import references into main module: {mainModule.Name}"); + } + } + else + { + m_Diagnostics.AddError($"Cannot get main module from assembly definition: {compiledAssembly.Name}"); + } + + // write + var pe = new MemoryStream(); + var pdb = new MemoryStream(); + + var writerParameters = new WriterParameters + { + SymbolWriterProvider = new PortablePdbWriterProvider(), + SymbolStream = pdb, + WriteSymbols = true + }; + + assemblyDefinition.Write(pe, writerParameters); + + return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics); + } + + private ModuleDefinition m_MainModule; + private PostProcessorAssemblyResolver m_AssemblyResolver; + + private MethodReference m_Debug_LogError_MethodRef; + private TypeReference m_NetworkManager_TypeRef; + private MethodReference m_NetworkManager_getLocalClientId_MethodRef; + private MethodReference m_NetworkManager_getIsListening_MethodRef; + private MethodReference m_NetworkManager_getIsHost_MethodRef; + private MethodReference m_NetworkManager_getIsServer_MethodRef; + private MethodReference m_NetworkManager_getIsClient_MethodRef; + private FieldReference m_NetworkManager_LogLevel_FieldRef; + private FieldReference m_NetworkManager_rpc_func_table_FieldRef; + private MethodReference m_NetworkManager_rpc_func_table_Add_MethodRef; + private FieldReference m_NetworkManager_rpc_name_table_FieldRef; + private MethodReference m_NetworkManager_rpc_name_table_Add_MethodRef; + private TypeReference m_NetworkBehaviour_TypeRef; + private MethodReference m_NetworkBehaviour_SendServerRpc_MethodRef; + private MethodReference m_NetworkBehaviour_SendClientRpc_MethodRef; + private FieldReference m_NetworkBehaviour_rpc_exec_stage_FieldRef; + private MethodReference m_NetworkBehaviour_getNetworkManager_MethodRef; + private MethodReference m_NetworkBehaviour_getOwnerClientId_MethodRef; + private MethodReference m_NetworkHandlerDelegateCtor_MethodRef; + private TypeReference m_RpcParams_TypeRef; + private FieldReference m_RpcParams_Server_FieldRef; + private FieldReference m_RpcParams_Client_FieldRef; + private TypeReference m_ServerRpcParams_TypeRef; + private FieldReference m_ServerRpcParams_Receive_FieldRef; + private FieldReference m_ServerRpcParams_Receive_SenderClientId_FieldRef; + private TypeReference m_ClientRpcParams_TypeRef; + + private TypeReference m_FastBufferWriter_TypeRef; + private MethodReference m_FastBufferWriter_Constructor; + private MethodReference m_FastBufferWriter_Dispose; + private Dictionary m_FastBufferWriter_WriteValue_MethodRefs = new Dictionary(); + private List m_FastBufferWriter_ExtensionMethodRefs = new List(); + + private TypeReference m_FastBufferReader_TypeRef; + private Dictionary m_FastBufferReader_ReadValue_MethodRefs = new Dictionary(); + private List m_FastBufferReader_ExtensionMethodRefs = new List(); + + private const string k_Debug_LogError = nameof(Debug.LogError); + private const string k_NetworkManager_LocalClientId = nameof(NetworkManager.LocalClientId); + private const string k_NetworkManager_IsListening = nameof(NetworkManager.IsListening); + private const string k_NetworkManager_IsHost = nameof(NetworkManager.IsHost); + private const string k_NetworkManager_IsServer = nameof(NetworkManager.IsServer); + private const string k_NetworkManager_IsClient = nameof(NetworkManager.IsClient); + private const string k_NetworkManager_LogLevel = nameof(NetworkManager.LogLevel); + private const string k_NetworkManager_rpc_func_table = nameof(NetworkManager.__rpc_func_table); + private const string k_NetworkManager_rpc_name_table = nameof(NetworkManager.__rpc_name_table); + + private const string k_NetworkBehaviour_rpc_exec_stage = nameof(NetworkBehaviour.__rpc_exec_stage); + private const string k_NetworkBehaviour_SendServerRpc = nameof(NetworkBehaviour.__sendServerRpc); + private const string k_NetworkBehaviour_SendClientRpc = nameof(NetworkBehaviour.__sendClientRpc); + private const string k_NetworkBehaviour_NetworkManager = nameof(NetworkBehaviour.NetworkManager); + private const string k_NetworkBehaviour_OwnerClientId = nameof(NetworkBehaviour.OwnerClientId); + + private const string k_RpcAttribute_Delivery = nameof(RpcAttribute.Delivery); + private const string k_ServerRpcAttribute_RequireOwnership = nameof(ServerRpcAttribute.RequireOwnership); + private const string k_RpcParams_Server = nameof(__RpcParams.Server); + private const string k_RpcParams_Client = nameof(__RpcParams.Client); + private const string k_ServerRpcParams_Receive = nameof(ServerRpcParams.Receive); + private const string k_ServerRpcReceiveParams_SenderClientId = nameof(ServerRpcReceiveParams.SenderClientId); + + private bool ImportReferences(ModuleDefinition moduleDefinition) + { + var debugType = typeof(Debug); + foreach (var methodInfo in debugType.GetMethods()) + { + switch (methodInfo.Name) + { + case k_Debug_LogError: + if (methodInfo.GetParameters().Length == 1) + { + m_Debug_LogError_MethodRef = moduleDefinition.ImportReference(methodInfo); + } + + break; + } + } + + var networkManagerType = typeof(NetworkManager); + m_NetworkManager_TypeRef = moduleDefinition.ImportReference(networkManagerType); + foreach (var propertyInfo in networkManagerType.GetProperties()) + { + switch (propertyInfo.Name) + { + case k_NetworkManager_LocalClientId: + m_NetworkManager_getLocalClientId_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + break; + case k_NetworkManager_IsListening: + m_NetworkManager_getIsListening_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + break; + case k_NetworkManager_IsHost: + m_NetworkManager_getIsHost_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + break; + case k_NetworkManager_IsServer: + m_NetworkManager_getIsServer_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + break; + case k_NetworkManager_IsClient: + m_NetworkManager_getIsClient_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + break; + } + } + + foreach (var fieldInfo in networkManagerType.GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + { + switch (fieldInfo.Name) + { + case k_NetworkManager_LogLevel: + m_NetworkManager_LogLevel_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + case k_NetworkManager_rpc_func_table: + m_NetworkManager_rpc_func_table_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_NetworkManager_rpc_func_table_Add_MethodRef = moduleDefinition.ImportReference(fieldInfo.FieldType.GetMethod("Add")); + break; + case k_NetworkManager_rpc_name_table: + m_NetworkManager_rpc_name_table_FieldRef = moduleDefinition.ImportReference(fieldInfo); + m_NetworkManager_rpc_name_table_Add_MethodRef = moduleDefinition.ImportReference(fieldInfo.FieldType.GetMethod("Add")); + break; + } + } + + var networkBehaviourType = typeof(NetworkBehaviour); + m_NetworkBehaviour_TypeRef = moduleDefinition.ImportReference(networkBehaviourType); + foreach (var propertyInfo in networkBehaviourType.GetProperties()) + { + switch (propertyInfo.Name) + { + case k_NetworkBehaviour_NetworkManager: + m_NetworkBehaviour_getNetworkManager_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + break; + case k_NetworkBehaviour_OwnerClientId: + m_NetworkBehaviour_getOwnerClientId_MethodRef = moduleDefinition.ImportReference(propertyInfo.GetMethod); + break; + } + } + + foreach (var methodInfo in networkBehaviourType.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + { + switch (methodInfo.Name) + { + case k_NetworkBehaviour_SendServerRpc: + m_NetworkBehaviour_SendServerRpc_MethodRef = moduleDefinition.ImportReference(methodInfo); + break; + case k_NetworkBehaviour_SendClientRpc: + m_NetworkBehaviour_SendClientRpc_MethodRef = moduleDefinition.ImportReference(methodInfo); + break; + } + } + + foreach (var fieldInfo in networkBehaviourType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + { + switch (fieldInfo.Name) + { + case k_NetworkBehaviour_rpc_exec_stage: + m_NetworkBehaviour_rpc_exec_stage_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + } + } + + var networkHandlerDelegateType = typeof(NetworkManager.RpcReceiveHandler); + m_NetworkHandlerDelegateCtor_MethodRef = moduleDefinition.ImportReference(networkHandlerDelegateType.GetConstructor(new[] { typeof(object), typeof(IntPtr) })); + + var rpcParamsType = typeof(__RpcParams); + m_RpcParams_TypeRef = moduleDefinition.ImportReference(rpcParamsType); + foreach (var fieldInfo in rpcParamsType.GetFields()) + { + switch (fieldInfo.Name) + { + case k_RpcParams_Server: + m_RpcParams_Server_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + case k_RpcParams_Client: + m_RpcParams_Client_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + } + } + + var serverRpcParamsType = typeof(ServerRpcParams); + m_ServerRpcParams_TypeRef = moduleDefinition.ImportReference(serverRpcParamsType); + foreach (var fieldInfo in serverRpcParamsType.GetFields()) + { + switch (fieldInfo.Name) + { + case k_ServerRpcParams_Receive: + foreach (var recvFieldInfo in fieldInfo.FieldType.GetFields()) + { + switch (recvFieldInfo.Name) + { + case k_ServerRpcReceiveParams_SenderClientId: + m_ServerRpcParams_Receive_SenderClientId_FieldRef = moduleDefinition.ImportReference(recvFieldInfo); + break; + } + } + + m_ServerRpcParams_Receive_FieldRef = moduleDefinition.ImportReference(fieldInfo); + break; + } + } + + var clientRpcParamsType = typeof(ClientRpcParams); + m_ClientRpcParams_TypeRef = moduleDefinition.ImportReference(clientRpcParamsType); + + var fastBufferWriterType = typeof(FastBufferWriter); + m_FastBufferWriter_TypeRef = moduleDefinition.ImportReference(fastBufferWriterType); + + m_FastBufferWriter_Constructor = moduleDefinition.ImportReference( + fastBufferWriterType.GetConstructor(new[] { typeof(int), typeof(Allocator), typeof(int) })); + m_FastBufferWriter_Dispose = moduleDefinition.ImportReference(fastBufferWriterType.GetMethod("Dispose")); + + var fastBufferReaderType = typeof(FastBufferReader); + m_FastBufferReader_TypeRef = moduleDefinition.ImportReference(fastBufferReaderType); + + // Find all extension methods for FastBufferReader and FastBufferWriter to enable user-implemented + // methods to be called. + var assemblies = new List(); + assemblies.Add(m_MainModule.Assembly); + foreach (var reference in m_MainModule.AssemblyReferences) + { + assemblies.Add(m_AssemblyResolver.Resolve(reference)); + } + + var extensionConstructor = + moduleDefinition.ImportReference(typeof(ExtensionAttribute).GetConstructor(new Type[] { })); + foreach (var assembly in assemblies) + { + foreach (var module in assembly.Modules) + { + foreach (var type in module.Types) + { + var resolvedType = type.Resolve(); + if (!resolvedType.IsSealed || !resolvedType.IsAbstract || resolvedType.IsNested) + { + continue; + } + foreach (var method in type.Methods) + { + if (!method.IsStatic) + { + continue; + } + + var isExtension = false; + + foreach (var attr in method.CustomAttributes) + { + if (attr.Constructor.Resolve() == extensionConstructor.Resolve()) + { + isExtension = true; + } + } + + if (!isExtension) + { + continue; + } + + var parameters = method.Parameters; + + if (parameters.Count == 2 + && parameters[0].ParameterType.Resolve() == m_FastBufferWriter_TypeRef.MakeByReferenceType().Resolve()) + { + m_FastBufferWriter_ExtensionMethodRefs.Add(m_MainModule.ImportReference(method)); + } + else if (parameters.Count == 2 + && parameters[0].ParameterType.Resolve() == m_FastBufferReader_TypeRef.MakeByReferenceType().Resolve()) + { + m_FastBufferReader_ExtensionMethodRefs.Add(m_MainModule.ImportReference(method)); + } + } + } + } + } + + return true; + } + + private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] assemblyDefines) + { + var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler)>(); + var rpcNames = new List<(uint RpcMethodId, string RpcMethodName)>(); + + bool isEditorOrDevelopment = assemblyDefines.Contains("UNITY_EDITOR") || assemblyDefines.Contains("DEVELOPMENT_BUILD"); + + foreach (var methodDefinition in typeDefinition.Methods) + { + var rpcAttribute = CheckAndGetRpcAttribute(methodDefinition); + if (rpcAttribute == null) + { + continue; + } + + var rpcMethodId = methodDefinition.Hash(); + if (rpcMethodId == 0) + { + continue; + } + + InjectWriteAndCallBlocks(methodDefinition, rpcAttribute, rpcMethodId); + + rpcHandlers.Add((rpcMethodId, GenerateStaticHandler(methodDefinition, rpcAttribute))); + + if (isEditorOrDevelopment) + { + rpcNames.Add((rpcMethodId, methodDefinition.Name)); + } + } + + if (rpcHandlers.Count > 0 || rpcNames.Count > 0) + { + var staticCtorMethodDef = typeDefinition.GetStaticConstructor(); + if (staticCtorMethodDef == null) + { + staticCtorMethodDef = new MethodDefinition( + ".cctor", // Static Constructor (constant-constructor) + MethodAttributes.HideBySig | + MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName | + MethodAttributes.Static, + typeDefinition.Module.TypeSystem.Void); + staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + typeDefinition.Methods.Add(staticCtorMethodDef); + } + + var instructions = new List(); + var processor = staticCtorMethodDef.Body.GetILProcessor(); + + foreach (var (rpcMethodId, rpcHandler) in rpcHandlers) + { + typeDefinition.Methods.Add(rpcHandler); + + // NetworkManager.__rpc_func_table.Add(RpcMethodId, HandleFunc); + instructions.Add(processor.Create(OpCodes.Ldsfld, m_NetworkManager_rpc_func_table_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); + instructions.Add(processor.Create(OpCodes.Ldnull)); + instructions.Add(processor.Create(OpCodes.Ldftn, rpcHandler)); + instructions.Add(processor.Create(OpCodes.Newobj, m_NetworkHandlerDelegateCtor_MethodRef)); + instructions.Add(processor.Create(OpCodes.Call, m_NetworkManager_rpc_func_table_Add_MethodRef)); + } + + foreach (var (rpcMethodId, rpcMethodName) in rpcNames) + { + // NetworkManager.__rpc_name_table.Add(RpcMethodId, RpcMethodName); + instructions.Add(processor.Create(OpCodes.Ldsfld, m_NetworkManager_rpc_name_table_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); + instructions.Add(processor.Create(OpCodes.Ldstr, rpcMethodName)); + instructions.Add(processor.Create(OpCodes.Call, m_NetworkManager_rpc_name_table_Add_MethodRef)); + } + + instructions.Reverse(); + instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction)); + } + + // override NetworkBehaviour.__getTypeName() method to return concrete type + { + var networkBehaviour_TypeDef = m_NetworkBehaviour_TypeRef.Resolve(); + var baseGetTypeNameMethod = networkBehaviour_TypeDef.Methods.First(p => p.Name.Equals(nameof(NetworkBehaviour.__getTypeName))); + + var newGetTypeNameMethod = new MethodDefinition( + nameof(NetworkBehaviour.__getTypeName), + (baseGetTypeNameMethod.Attributes & ~MethodAttributes.NewSlot) | MethodAttributes.ReuseSlot, + baseGetTypeNameMethod.ReturnType) + { + ImplAttributes = baseGetTypeNameMethod.ImplAttributes, + SemanticsAttributes = baseGetTypeNameMethod.SemanticsAttributes + }; + + var processor = newGetTypeNameMethod.Body.GetILProcessor(); + processor.Body.Instructions.Add(processor.Create(OpCodes.Ldstr, typeDefinition.Name)); + processor.Body.Instructions.Add(processor.Create(OpCodes.Ret)); + + typeDefinition.Methods.Add(newGetTypeNameMethod); + } + + m_MainModule.RemoveRecursiveReferences(); + } + + private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinition) + { + CustomAttribute rpcAttribute = null; + bool isServerRpc = false; + foreach (var customAttribute in methodDefinition.CustomAttributes) + { + var customAttributeType_FullName = customAttribute.AttributeType.FullName; + + if (customAttributeType_FullName == CodeGenHelpers.ServerRpcAttribute_FullName || + customAttributeType_FullName == CodeGenHelpers.ClientRpcAttribute_FullName) + { + bool isValid = true; + + if (methodDefinition.IsStatic) + { + m_Diagnostics.AddError(methodDefinition, "RPC method must not be static!"); + isValid = false; + } + + if (methodDefinition.IsAbstract) + { + m_Diagnostics.AddError(methodDefinition, "RPC method must not be abstract!"); + isValid = false; + } + + if (methodDefinition.ReturnType != methodDefinition.Module.TypeSystem.Void) + { + m_Diagnostics.AddError(methodDefinition, "RPC method must return `void`!"); + isValid = false; + } + + if (customAttributeType_FullName == CodeGenHelpers.ServerRpcAttribute_FullName && + !methodDefinition.Name.EndsWith("ServerRpc", StringComparison.OrdinalIgnoreCase)) + { + m_Diagnostics.AddError(methodDefinition, "ServerRpc method must end with 'ServerRpc' suffix!"); + isValid = false; + } + + if (customAttributeType_FullName == CodeGenHelpers.ClientRpcAttribute_FullName && + !methodDefinition.Name.EndsWith("ClientRpc", StringComparison.OrdinalIgnoreCase)) + { + m_Diagnostics.AddError(methodDefinition, "ClientRpc method must end with 'ClientRpc' suffix!"); + isValid = false; + } + + if (isValid) + { + isServerRpc = customAttributeType_FullName == CodeGenHelpers.ServerRpcAttribute_FullName; + rpcAttribute = customAttribute; + } + } + } + + if (rpcAttribute == null) + { + if (methodDefinition.Name.EndsWith("ServerRpc", StringComparison.OrdinalIgnoreCase)) + { + m_Diagnostics.AddError(methodDefinition, "ServerRpc method must be marked with 'ServerRpc' attribute!"); + } + else if (methodDefinition.Name.EndsWith("ClientRpc", StringComparison.OrdinalIgnoreCase)) + { + m_Diagnostics.AddError(methodDefinition, "ClientRpc method must be marked with 'ClientRpc' attribute!"); + } + + return null; + } + // Checks for IsSerializable are moved to later as the check is now done by dynamically seeing if any valid + // serializer OR extension method exists for it. + return rpcAttribute; + } + + private MethodReference GetFastBufferWriterWriteMethod(string name, TypeReference paramType) + { + foreach (var method in m_FastBufferWriter_TypeRef.Resolve().Methods) + { + if (method.Name == name) + { + var parameters = method.Parameters; + + if (parameters.Count == 0 || (parameters.Count > 1 && !parameters[1].IsOptional)) + { + continue; + } + + if (parameters[0].ParameterType.IsArray != paramType.IsArray) + { + continue; + } + + var checkType = paramType.Resolve(); + if (paramType.IsArray) + { + checkType = paramType.GetElementType().Resolve(); + } + + if ( + (parameters[0].ParameterType.Resolve() == checkType + || (parameters[0].ParameterType.Resolve() == checkType.MakeByReferenceType().Resolve() && parameters[0].IsIn))) + { + return method; + } + if (method.HasGenericParameters && method.GenericParameters.Count == 1) + { + if (method.GenericParameters[0].HasConstraints) + { + foreach (var constraint in method.GenericParameters[0].Constraints) + { + var resolvedConstraint = constraint.Resolve(); + + if ( + (resolvedConstraint.IsInterface && + checkType.HasInterface(resolvedConstraint.FullName)) + || (resolvedConstraint.IsClass && + checkType.Resolve().IsSubclassOf(resolvedConstraint.FullName))) + { + var instanceMethod = new GenericInstanceMethod(method); + instanceMethod.GenericArguments.Add(checkType); + return instanceMethod; + } + } + } + } + } + } + + return null; + } + + private bool GetWriteMethodForParameter(TypeReference paramType, out MethodReference methodRef) + { + var assemblyQualifiedName = paramType.FullName + ", " + paramType.Resolve().Module.Assembly.FullName; + var foundMethodRef = m_FastBufferWriter_WriteValue_MethodRefs.TryGetValue(assemblyQualifiedName, out methodRef); + + if (!foundMethodRef) + { + foreach (var method in m_FastBufferWriter_ExtensionMethodRefs) + { + var parameters = method.Resolve().Parameters; + + if (method.Name == k_WriteValueMethodName) + { + if (parameters[1].IsIn) + { + if (parameters[1].ParameterType.Resolve() == paramType.MakeByReferenceType().Resolve() + && ((ByReferenceType)parameters[1].ParameterType).ElementType.IsArray == paramType.IsArray) + { + methodRef = method; + m_FastBufferWriter_WriteValue_MethodRefs[assemblyQualifiedName] = methodRef; + return true; + } + } + else + { + + if (parameters[1].ParameterType.Resolve() == paramType.Resolve() + && parameters[1].ParameterType.IsArray == paramType.IsArray) + { + methodRef = method; + m_FastBufferWriter_WriteValue_MethodRefs[assemblyQualifiedName] = methodRef; + return true; + } + } + } + } + + // Try NetworkSerializable first because INetworkSerializable may also be valid for WriteValueSafe + // and that would cause boxing if so. + var typeMethod = GetFastBufferWriterWriteMethod("WriteNetworkSerializable", paramType); + if (typeMethod == null) + { + typeMethod = GetFastBufferWriterWriteMethod(k_WriteValueMethodName, paramType); + } + if (typeMethod != null) + { + methodRef = m_MainModule.ImportReference(typeMethod); + m_FastBufferWriter_WriteValue_MethodRefs[assemblyQualifiedName] = methodRef; + foundMethodRef = true; + } + } + + return foundMethodRef; + } + private MethodReference GetFastBufferReaderReadMethod(string name, TypeReference paramType) + { + foreach (var method in m_FastBufferReader_TypeRef.Resolve().Methods) + { + var paramTypeDef = paramType.Resolve(); + if (method.Name == name) + { + var parameters = method.Parameters; + + if (parameters.Count == 0 || (parameters.Count > 1 && !parameters[1].IsOptional)) + { + continue; + } + + if (!parameters[0].IsOut) + { + return null; + } + + var methodParam = ((ByReferenceType)parameters[0].ParameterType).ElementType; + + if (methodParam.IsArray != paramType.IsArray) + { + continue; + } + + var checkType = paramType.Resolve(); + if (paramType.IsArray) + { + checkType = paramType.GetElementType().Resolve(); + } + + if (methodParam.Resolve() == checkType.Resolve() || methodParam.Resolve() == checkType.MakeByReferenceType().Resolve()) + { + return method; + } + if (method.HasGenericParameters && method.GenericParameters.Count == 1) + { + if (method.GenericParameters[0].HasConstraints) + { + foreach (var constraint in method.GenericParameters[0].Constraints) + { + var resolvedConstraint = constraint.Resolve(); + + if ( + (resolvedConstraint.IsInterface && + checkType.HasInterface(resolvedConstraint.FullName)) + || (resolvedConstraint.IsClass && + checkType.Resolve().IsSubclassOf(resolvedConstraint.FullName))) + { + var instanceMethod = new GenericInstanceMethod(method); + instanceMethod.GenericArguments.Add(checkType); + return instanceMethod; + } + } + } + } + } + } + + return null; + } + + private bool GetReadMethodForParameter(TypeReference paramType, out MethodReference methodRef) + { + var assemblyQualifiedName = paramType.FullName + ", " + paramType.Resolve().Module.Assembly.FullName; + + var foundMethodRef = m_FastBufferReader_ReadValue_MethodRefs.TryGetValue(assemblyQualifiedName, out methodRef); + if (!foundMethodRef) + { + foreach (var method in m_FastBufferReader_ExtensionMethodRefs) + { + var parameters = method.Resolve().Parameters; + if ( + method.Name == k_ReadValueMethodName + && parameters[1].IsOut + && parameters[1].ParameterType.Resolve() == paramType.MakeByReferenceType().Resolve() + && ((ByReferenceType)parameters[1].ParameterType).ElementType.IsArray == paramType.IsArray) + { + methodRef = method; + m_FastBufferReader_ReadValue_MethodRefs[assemblyQualifiedName] = methodRef; + return true; + } + } + + // Try NetworkSerializable first because INetworkSerializable may also be valid for ReadValueSafe + // and that would cause boxing if so. + var typeMethod = GetFastBufferReaderReadMethod("ReadNetworkSerializable", paramType); + if (typeMethod == null) + { + typeMethod = GetFastBufferReaderReadMethod(k_ReadValueMethodName, paramType); + } + if (typeMethod != null) + { + methodRef = m_MainModule.ImportReference(typeMethod); + m_FastBufferReader_ReadValue_MethodRefs[assemblyQualifiedName] = methodRef; + foundMethodRef = true; + } + } + + return foundMethodRef; + } + + private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomAttribute rpcAttribute, uint rpcMethodId) + { + var typeSystem = methodDefinition.Module.TypeSystem; + var instructions = new List(); + var processor = methodDefinition.Body.GetILProcessor(); + var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; + var requireOwnership = true; // default value MUST be = `ServerRpcAttribute.RequireOwnership` + var rpcDelivery = RpcDelivery.Reliable; // default value MUST be = `RpcAttribute.Delivery` + foreach (var attrField in rpcAttribute.Fields) + { + switch (attrField.Name) + { + case k_RpcAttribute_Delivery: + rpcDelivery = (RpcDelivery)attrField.Argument.Value; + break; + case k_ServerRpcAttribute_RequireOwnership: + requireOwnership = attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value; + break; + } + } + + var paramCount = methodDefinition.Parameters.Count; + var hasRpcParams = + paramCount > 0 && + ((isServerRpc && methodDefinition.Parameters[paramCount - 1].ParameterType.FullName == CodeGenHelpers.ServerRpcParams_FullName) || + (!isServerRpc && methodDefinition.Parameters[paramCount - 1].ParameterType.FullName == CodeGenHelpers.ClientRpcParams_FullName)); + + methodDefinition.Body.InitLocals = true; + // NetworkManager networkManager; + methodDefinition.Body.Variables.Add(new VariableDefinition(m_NetworkManager_TypeRef)); + int netManLocIdx = methodDefinition.Body.Variables.Count - 1; + // NetworkSerializer serializer; + methodDefinition.Body.Variables.Add(new VariableDefinition(m_FastBufferWriter_TypeRef)); + int serializerLocIdx = methodDefinition.Body.Variables.Count - 1; + + // XXXRpcParams + if (!hasRpcParams) + { + methodDefinition.Body.Variables.Add(new VariableDefinition(isServerRpc ? m_ServerRpcParams_TypeRef : m_ClientRpcParams_TypeRef)); + } + int rpcParamsIdx = !hasRpcParams ? methodDefinition.Body.Variables.Count - 1 : -1; + + { + var returnInstr = processor.Create(OpCodes.Ret); + var lastInstr = processor.Create(OpCodes.Nop); + + // networkManager = this.NetworkManager; + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_getNetworkManager_MethodRef)); + instructions.Add(processor.Create(OpCodes.Stloc, netManLocIdx)); + + // if (networkManager == null || !networkManager.IsListening) return; + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Brfalse, returnInstr)); + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsListening_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + + instructions.Add(returnInstr); + instructions.Add(lastInstr); + } + + { + var beginInstr = processor.Create(OpCodes.Nop); + var endInstr = processor.Create(OpCodes.Nop); + var lastInstr = processor.Create(OpCodes.Nop); + + // if (__rpc_exec_stage != __RpcExecStage.Server) -> ServerRpc + // if (__rpc_exec_stage != __RpcExecStage.Client) -> ClientRpc + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)(isServerRpc ? NetworkBehaviour.__RpcExecStage.Server : NetworkBehaviour.__RpcExecStage.Client))); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, 0)); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Brfalse, lastInstr)); + + // if (networkManager.IsClient || networkManager.IsHost) { ... } -> ServerRpc + // if (networkManager.IsServer || networkManager.IsHost) { ... } -> ClientRpc + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, isServerRpc ? m_NetworkManager_getIsClient_MethodRef : m_NetworkManager_getIsServer_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brtrue, beginInstr)); + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsHost_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brfalse, lastInstr)); + + instructions.Add(beginInstr); + + if (isServerRpc) + { + // ServerRpc + + if (requireOwnership) + { + var roReturnInstr = processor.Create(OpCodes.Ret); + var roLastInstr = processor.Create(OpCodes.Nop); + + // if (this.OwnerClientId != networkManager.LocalClientId) { ... } return; + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_getOwnerClientId_MethodRef)); + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add( + processor.Create(OpCodes.Callvirt, m_NetworkManager_getLocalClientId_MethodRef)); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, 0)); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Brfalse, roLastInstr)); + + var logNextInstr = processor.Create(OpCodes.Nop); + + // if (LogLevel.Normal > networkManager.LogLevel) + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkManager_LogLevel_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)LogLevel.Normal)); + instructions.Add(processor.Create(OpCodes.Cgt)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, 0)); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Brfalse, logNextInstr)); + + // Debug.LogError(...); + instructions.Add(processor.Create(OpCodes.Ldstr, + "Only the owner can invoke a ServerRpc that requires ownership!")); + instructions.Add(processor.Create(OpCodes.Call, m_Debug_LogError_MethodRef)); + + instructions.Add(logNextInstr); + + instructions.Add(roReturnInstr); + instructions.Add(roLastInstr); + } + } + + // var writer = new FastBufferWriter(1285, Allocator.Temp, 63985); + instructions.Add(processor.Create(OpCodes.Ldloca, serializerLocIdx)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, 1300 - sizeof(byte) - sizeof(ulong) - sizeof(uint) - sizeof(ushort))); + instructions.Add(processor.Create(OpCodes.Ldc_I4_2)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, 64000 - sizeof(byte) - sizeof(ulong) - sizeof(uint) - sizeof(ushort))); + instructions.Add(processor.Create(OpCodes.Call, m_FastBufferWriter_Constructor)); + + var firstInstruction = processor.Create(OpCodes.Nop); + instructions.Add(firstInstruction); + + // write method parameters into stream + for (int paramIndex = 0; paramIndex < paramCount; ++paramIndex) + { + var paramDef = methodDefinition.Parameters[paramIndex]; + var paramType = paramDef.ParameterType; + // ServerRpcParams + if (paramType.FullName == CodeGenHelpers.ServerRpcParams_FullName && isServerRpc && paramIndex == paramCount - 1) + { + continue; + } + // ClientRpcParams + if (paramType.FullName == CodeGenHelpers.ClientRpcParams_FullName && !isServerRpc && paramIndex == paramCount - 1) + { + continue; + } + + Instruction jumpInstruction = null; + + if (!paramType.IsValueType) + { + if (!GetWriteMethodForParameter(typeSystem.Boolean, out var boolMethodRef)) + { + m_Diagnostics.AddError(methodDefinition, $"Couldn't find boolean serializer! Something's wrong!"); + return; + } + + methodDefinition.Body.Variables.Add(new VariableDefinition(typeSystem.Boolean)); + int isSetLocalIndex = methodDefinition.Body.Variables.Count - 1; + + // bool isSet = (param != null); + instructions.Add(processor.Create(OpCodes.Ldarg, paramIndex + 1)); + instructions.Add(processor.Create(OpCodes.Ldnull)); + instructions.Add(processor.Create(OpCodes.Cgt_Un)); + instructions.Add(processor.Create(OpCodes.Stloc, isSetLocalIndex)); + + // writer.WriteValueSafe(isSet); + instructions.Add(processor.Create(OpCodes.Ldloca, serializerLocIdx)); + instructions.Add(processor.Create(OpCodes.Ldloca, isSetLocalIndex)); + instructions.Add(processor.Create(OpCodes.Call, boolMethodRef)); + + // if(isSet) { + jumpInstruction = processor.Create(OpCodes.Nop); + instructions.Add(processor.Create(OpCodes.Ldloc, isSetLocalIndex)); + instructions.Add(processor.Create(OpCodes.Brfalse, jumpInstruction)); + } + + var foundMethodRef = GetWriteMethodForParameter(paramType, out var methodRef); + if (foundMethodRef) + { + // writer.WriteNetworkSerializable(param) for INetworkSerializable, OR + // writer.WriteNetworkSerializable(param, -1, 0) for INetworkSerializable arrays, OR + // writer.WriteValueSafe(param) for value types, OR + // writer.WriteValueSafe(param, -1, 0) for arrays of value types, OR + // writer.WriteValueSafe(param, false) for strings + instructions.Add(processor.Create(OpCodes.Ldloca, serializerLocIdx)); + var method = methodRef.Resolve(); + var checkParameter = method.Parameters[0]; + var isExtensionMethod = false; + if (checkParameter.ParameterType.Resolve() == + m_FastBufferWriter_TypeRef.MakeByReferenceType().Resolve()) + { + isExtensionMethod = true; + checkParameter = method.Parameters[1]; + } + if (checkParameter.IsIn) + { + instructions.Add(processor.Create(OpCodes.Ldarga, paramIndex + 1)); + } + else + { + instructions.Add(processor.Create(OpCodes.Ldarg, paramIndex + 1)); + } + // Special handling for WriteValue() on arrays and strings since they have additional arguments. + if (paramType.IsArray + && ((!isExtensionMethod && methodRef.Parameters.Count == 3) + || (isExtensionMethod && methodRef.Parameters.Count == 4))) + { + instructions.Add(processor.Create(OpCodes.Ldc_I4_M1)); + instructions.Add(processor.Create(OpCodes.Ldc_I4_0)); + } + else if (paramType == typeSystem.String + && ((!isExtensionMethod && methodRef.Parameters.Count == 2) + || (isExtensionMethod && methodRef.Parameters.Count == 3))) + { + instructions.Add(processor.Create(OpCodes.Ldc_I4_0)); + } + instructions.Add(processor.Create(OpCodes.Call, methodRef)); + } + else + { + m_Diagnostics.AddError(methodDefinition, $"Don't know how to serialize {paramType.Name} - implement {nameof(INetworkSerializable)} or add an extension method for {nameof(FastBufferWriter)}.{k_WriteValueMethodName} to define serialization."); + continue; + } + + if (jumpInstruction != null) + { + instructions.Add(jumpInstruction); + } + } + + instructions.Add(endInstr); + + // __sendServerRpc(ref serializer, rpcMethodId, serverRpcParams, rpcDelivery) -> ServerRpc + // __sendClientRpc(ref serializer, rpcMethodId, clientRpcParams, rpcDelivery) -> ClientRpc + if (isServerRpc) + { + // ServerRpc + // __sendServerRpc(ref serializer, rpcMethodId, serverRpcParams, rpcDelivery); + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + + // serializer + instructions.Add(processor.Create(OpCodes.Ldloc, serializerLocIdx)); + + // rpcMethodId + instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); + + if (hasRpcParams) + { + // rpcParams + instructions.Add(processor.Create(OpCodes.Ldarg, paramCount)); + } + else + { + // default + instructions.Add(processor.Create(OpCodes.Ldloc, rpcParamsIdx)); + } + + // rpcDelivery + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)rpcDelivery)); + + // EndSendServerRpc + instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_SendServerRpc_MethodRef)); + } + else + { + // ClientRpc + // __sendClientRpc(ref serializer, rpcMethodId, clientRpcParams, rpcDelivery); + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + + // serializer + instructions.Add(processor.Create(OpCodes.Ldloc, serializerLocIdx)); + + // rpcMethodId + instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); + + if (hasRpcParams) + { + // rpcParams + instructions.Add(processor.Create(OpCodes.Ldarg, paramCount)); + } + else + { + // default + instructions.Add(processor.Create(OpCodes.Ldloc, rpcParamsIdx)); + } + + // rpcDelivery + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)rpcDelivery)); + + // EndSendClientRpc + instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_SendClientRpc_MethodRef)); + } + + { + // TODO: Figure out why try/catch here cause the try block not to execute at all. + // End try block + //instructions.Add(processor.Create(OpCodes.Leave, lastInstr)); + + // writer.Dispose(); + var handlerFirst = processor.Create(OpCodes.Ldloca, serializerLocIdx); + instructions.Add(handlerFirst); + instructions.Add(processor.Create(OpCodes.Call, m_FastBufferWriter_Dispose)); + + // End finally block + //instructions.Add(processor.Create(OpCodes.Endfinally)); + + // try { ... serialization code ... } finally { writer.Dispose(); } + /*var handler = new ExceptionHandler(ExceptionHandlerType.Finally) + { + TryStart = firstInstruction, + TryEnd = handlerFirst, + HandlerStart = handlerFirst, + HandlerEnd = lastInstr + }; + processor.Body.ExceptionHandlers.Add(handler);*/ + } + + instructions.Add(lastInstr); + } + + { + var returnInstr = processor.Create(OpCodes.Ret); + var lastInstr = processor.Create(OpCodes.Nop); + + // if (__rpc_exec_stage == __RpcExecStage.Server) -> ServerRpc + // if (__rpc_exec_stage == __RpcExecStage.Client) -> ClientRpc + instructions.Add(processor.Create(OpCodes.Ldarg_0)); + instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)(isServerRpc ? NetworkBehaviour.__RpcExecStage.Server : NetworkBehaviour.__RpcExecStage.Client))); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Brfalse, returnInstr)); + + // if (networkManager.IsServer || networkManager.IsHost) -> ServerRpc + // if (networkManager.IsClient || networkManager.IsHost) -> ClientRpc + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, isServerRpc ? m_NetworkManager_getIsServer_MethodRef : m_NetworkManager_getIsClient_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsHost_MethodRef)); + instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + + instructions.Add(returnInstr); + instructions.Add(lastInstr); + } + + instructions.Reverse(); + instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction)); + } + + private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition, CustomAttribute rpcAttribute) + { + var typeSystem = methodDefinition.Module.TypeSystem; + var nhandler = new MethodDefinition( + $"{methodDefinition.Name}__nhandler", + MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig, + methodDefinition.Module.TypeSystem.Void); + nhandler.Parameters.Add(new ParameterDefinition("target", ParameterAttributes.None, m_NetworkBehaviour_TypeRef)); + nhandler.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, m_FastBufferReader_TypeRef)); + nhandler.Parameters.Add(new ParameterDefinition("rpcParams", ParameterAttributes.None, m_RpcParams_TypeRef)); + + var processor = nhandler.Body.GetILProcessor(); + var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; + var requireOwnership = true; // default value MUST be = `ServerRpcAttribute.RequireOwnership` + foreach (var attrField in rpcAttribute.Fields) + { + switch (attrField.Name) + { + case k_ServerRpcAttribute_RequireOwnership: + requireOwnership = attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value; + break; + } + } + + nhandler.Body.InitLocals = true; + // NetworkManager networkManager; + nhandler.Body.Variables.Add(new VariableDefinition(m_NetworkManager_TypeRef)); + int netManLocIdx = nhandler.Body.Variables.Count - 1; + + { + var returnInstr = processor.Create(OpCodes.Ret); + var lastInstr = processor.Create(OpCodes.Nop); + + // networkManager = this.NetworkManager; + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Call, m_NetworkBehaviour_getNetworkManager_MethodRef); + processor.Emit(OpCodes.Stloc, netManLocIdx); + + // if (networkManager == null || !networkManager.IsListening) return; + processor.Emit(OpCodes.Ldloc, netManLocIdx); + processor.Emit(OpCodes.Brfalse, returnInstr); + processor.Emit(OpCodes.Ldloc, netManLocIdx); + processor.Emit(OpCodes.Callvirt, m_NetworkManager_getIsListening_MethodRef); + processor.Emit(OpCodes.Brtrue, lastInstr); + + processor.Append(returnInstr); + processor.Append(lastInstr); + } + + if (isServerRpc && requireOwnership) + { + var roReturnInstr = processor.Create(OpCodes.Ret); + var roLastInstr = processor.Create(OpCodes.Nop); + + // if (rpcParams.Server.Receive.SenderClientId != target.OwnerClientId) { ... } return; + processor.Emit(OpCodes.Ldarg_2); + processor.Emit(OpCodes.Ldfld, m_RpcParams_Server_FieldRef); + processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_FieldRef); + processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_SenderClientId_FieldRef); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Call, m_NetworkBehaviour_getOwnerClientId_MethodRef); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Ldc_I4, 0); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Brfalse, roLastInstr); + + var logNextInstr = processor.Create(OpCodes.Nop); + + // if (LogLevel.Normal > networkManager.LogLevel) + processor.Emit(OpCodes.Ldloc, netManLocIdx); + processor.Emit(OpCodes.Ldfld, m_NetworkManager_LogLevel_FieldRef); + processor.Emit(OpCodes.Ldc_I4, (int)LogLevel.Normal); + processor.Emit(OpCodes.Cgt); + processor.Emit(OpCodes.Ldc_I4, 0); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Brfalse, logNextInstr); + + // Debug.LogError(...); + processor.Emit(OpCodes.Ldstr, "Only the owner can invoke a ServerRpc that requires ownership!"); + processor.Emit(OpCodes.Call, m_Debug_LogError_MethodRef); + + processor.Append(logNextInstr); + + processor.Append(roReturnInstr); + processor.Append(roLastInstr); + } + + // read method parameters from stream + int paramCount = methodDefinition.Parameters.Count; + int[] paramLocalMap = new int[paramCount]; + for (int paramIndex = 0; paramIndex < paramCount; ++paramIndex) + { + var paramDef = methodDefinition.Parameters[paramIndex]; + var paramType = paramDef.ParameterType; + + // local variable + nhandler.Body.Variables.Add(new VariableDefinition(paramType)); + int localIndex = nhandler.Body.Variables.Count - 1; + paramLocalMap[paramIndex] = localIndex; + + // ServerRpcParams, ClientRpcParams + { + // ServerRpcParams + if (paramType.FullName == CodeGenHelpers.ServerRpcParams_FullName) + { + processor.Emit(OpCodes.Ldarg_2); + processor.Emit(OpCodes.Ldfld, m_RpcParams_Server_FieldRef); + processor.Emit(OpCodes.Stloc, localIndex); + continue; + } + + // ClientRpcParams + if (paramType.FullName == CodeGenHelpers.ClientRpcParams_FullName) + { + processor.Emit(OpCodes.Ldarg_2); + processor.Emit(OpCodes.Ldfld, m_RpcParams_Client_FieldRef); + processor.Emit(OpCodes.Stloc, localIndex); + continue; + } + } + + Instruction jumpInstruction = null; + + if (!paramType.IsValueType) + { + if (!GetReadMethodForParameter(typeSystem.Boolean, out var boolMethodRef)) + { + m_Diagnostics.AddError(methodDefinition, $"Couldn't find boolean deserializer! Something's wrong!"); + } + + // reader.ReadValueSafe(out bool isSet) + nhandler.Body.Variables.Add(new VariableDefinition(typeSystem.Boolean)); + int isSetLocalIndex = nhandler.Body.Variables.Count - 1; + processor.Emit(OpCodes.Ldarga, 1); + processor.Emit(OpCodes.Ldloca, isSetLocalIndex); + processor.Emit(OpCodes.Call, boolMethodRef); + + // paramType param = null; + processor.Emit(OpCodes.Ldnull); + processor.Emit(OpCodes.Stloc, localIndex); + + // if(isSet) { + jumpInstruction = processor.Create(OpCodes.Nop); + processor.Emit(OpCodes.Ldloc, isSetLocalIndex); + processor.Emit(OpCodes.Brfalse, jumpInstruction); + } + + var foundMethodRef = GetReadMethodForParameter(paramType, out var methodRef); + if (foundMethodRef) + { + // reader.ReadValueSafe(out localVar); + processor.Emit(OpCodes.Ldarga, 1); + processor.Emit(OpCodes.Ldloca, localIndex); + if (paramType == typeSystem.String) + { + processor.Emit(OpCodes.Ldc_I4_0); + } + processor.Emit(OpCodes.Call, methodRef); + } + else + { + m_Diagnostics.AddError(methodDefinition, $"Don't know how to deserialize {paramType.Name} - implement {nameof(INetworkSerializable)} or add an extension method for {nameof(FastBufferReader)}.{k_ReadValueMethodName} to define serialization."); + continue; + } + + if (jumpInstruction != null) + { + processor.Append(jumpInstruction); + } + } + + // NetworkBehaviour.__rpc_exec_stage = __RpcExecStage.Server; -> ServerRpc + // NetworkBehaviour.__rpc_exec_stage = __RpcExecStage.Client; -> ClientRpc + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldc_I4, (int)(isServerRpc ? NetworkBehaviour.__RpcExecStage.Server : NetworkBehaviour.__RpcExecStage.Client)); + processor.Emit(OpCodes.Stfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef); + + // NetworkBehaviour.XXXRpc(...); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Castclass, methodDefinition.DeclaringType); + Enumerable.Range(0, paramCount).ToList().ForEach(paramIndex => processor.Emit(OpCodes.Ldloc, paramLocalMap[paramIndex])); + processor.Emit(OpCodes.Callvirt, methodDefinition); + + // NetworkBehaviour.__rpc_exec_stage = __RpcExecStage.None; + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldc_I4, (int)NetworkBehaviour.__RpcExecStage.None); + processor.Emit(OpCodes.Stfld, m_NetworkBehaviour_rpc_exec_stage_FieldRef); + + processor.Emit(OpCodes.Ret); + return nhandler; + } + } +} diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs.meta b/Editor/CodeGen/NetworkBehaviourILPP.cs.meta new file mode 100644 index 0000000..9ff4309 --- /dev/null +++ b/Editor/CodeGen/NetworkBehaviourILPP.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf1c8b78182704372820a586c1c91d97 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CodeGen/PostProcessorAssemblyResolver.cs b/Editor/CodeGen/PostProcessorAssemblyResolver.cs new file mode 100644 index 0000000..4adbbfd --- /dev/null +++ b/Editor/CodeGen/PostProcessorAssemblyResolver.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Mono.Cecil; +using Unity.CompilationPipeline.Common.ILPostProcessing; + +namespace Unity.Netcode.Editor.CodeGen +{ + internal class PostProcessorAssemblyResolver : IAssemblyResolver + { + private readonly string[] m_AssemblyReferences; + private readonly Dictionary m_AssemblyCache = new Dictionary(); + private readonly ICompiledAssembly m_CompiledAssembly; + private AssemblyDefinition m_SelfAssembly; + + public PostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly) + { + m_CompiledAssembly = compiledAssembly; + m_AssemblyReferences = compiledAssembly.References; + } + + public void Dispose() { } + + public AssemblyDefinition Resolve(AssemblyNameReference name) => Resolve(name, new ReaderParameters(ReadingMode.Deferred)); + + public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + lock (m_AssemblyCache) + { + if (name.Name == m_CompiledAssembly.Name) + { + return m_SelfAssembly; + } + + var fileName = FindFile(name); + if (fileName == null) + { + return null; + } + + var lastWriteTime = File.GetLastWriteTime(fileName); + var cacheKey = $"{fileName}{lastWriteTime}"; + if (m_AssemblyCache.TryGetValue(cacheKey, out var result)) + { + return result; + } + + parameters.AssemblyResolver = this; + + var ms = MemoryStreamFor(fileName); + var pdb = $"{fileName}.pdb"; + if (File.Exists(pdb)) + { + parameters.SymbolStream = MemoryStreamFor(pdb); + } + + var assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters); + m_AssemblyCache.Add(cacheKey, assemblyDefinition); + + return assemblyDefinition; + } + } + + private string FindFile(AssemblyNameReference name) + { + var fileName = m_AssemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == $"{name.Name}.dll"); + if (fileName != null) + { + return fileName; + } + + // perhaps the type comes from an exe instead + fileName = m_AssemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == $"{name.Name}.exe"); + if (fileName != null) + { + return fileName; + } + + //Unfortunately the current ICompiledAssembly API only provides direct references. + //It is very much possible that a postprocessor ends up investigating a type in a directly + //referenced assembly, that contains a field that is not in a directly referenced assembly. + //if we don't do anything special for that situation, it will fail to resolve. We should fix this + //in the ILPostProcessing API. As a workaround, we rely on the fact here that the indirect references + //are always located next to direct references, so we search in all directories of direct references we + //got passed, and if we find the file in there, we resolve to it. + return m_AssemblyReferences + .Select(Path.GetDirectoryName) + .Distinct() + .Select(parentDir => Path.Combine(parentDir, $"{name.Name}.dll")) + .FirstOrDefault(File.Exists); + } + + private static MemoryStream MemoryStreamFor(string fileName) + { + return Retry(10, TimeSpan.FromSeconds(1), () => + { + byte[] byteArray; + using var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + byteArray = new byte[fileStream.Length]; + var readLength = fileStream.Read(byteArray, 0, (int)fileStream.Length); + if (readLength != fileStream.Length) + { + throw new InvalidOperationException("File read length is not full length of file."); + } + + return new MemoryStream(byteArray); + }); + } + + private static MemoryStream Retry(int retryCount, TimeSpan waitTime, Func func) + { + try + { + return func(); + } + catch (IOException) + { + if (retryCount == 0) + { + throw; + } + + Console.WriteLine($"Caught IO Exception, trying {retryCount} more times"); + Thread.Sleep(waitTime); + + return Retry(retryCount - 1, waitTime, func); + } + } + + public void AddAssemblyDefinitionBeingOperatedOn(AssemblyDefinition assemblyDefinition) + { + m_SelfAssembly = assemblyDefinition; + } + } +} diff --git a/Editor/CodeGen/PostProcessorAssemblyResolver.cs.meta b/Editor/CodeGen/PostProcessorAssemblyResolver.cs.meta new file mode 100644 index 0000000..1a05af5 --- /dev/null +++ b/Editor/CodeGen/PostProcessorAssemblyResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c247f4266b2864eb96e6a9ae6557d31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CodeGen/PostProcessorReflectionImporter.cs b/Editor/CodeGen/PostProcessorReflectionImporter.cs new file mode 100644 index 0000000..f96ba33 --- /dev/null +++ b/Editor/CodeGen/PostProcessorReflectionImporter.cs @@ -0,0 +1,22 @@ +using System.Linq; +using System.Reflection; +using Mono.Cecil; + +namespace Unity.Netcode.Editor.CodeGen +{ + internal class PostProcessorReflectionImporter : DefaultReflectionImporter + { + private const string k_SystemPrivateCoreLib = "System.Private.CoreLib"; + private readonly AssemblyNameReference m_CorrectCorlib; + + public PostProcessorReflectionImporter(ModuleDefinition module) : base(module) + { + m_CorrectCorlib = module.AssemblyReferences.FirstOrDefault(a => a.Name == "mscorlib" || a.Name == "netstandard" || a.Name == k_SystemPrivateCoreLib); + } + + public override AssemblyNameReference ImportReference(AssemblyName reference) + { + return m_CorrectCorlib != null && reference.Name == k_SystemPrivateCoreLib ? m_CorrectCorlib : base.ImportReference(reference); + } + } +} diff --git a/Editor/CodeGen/PostProcessorReflectionImporter.cs.meta b/Editor/CodeGen/PostProcessorReflectionImporter.cs.meta new file mode 100644 index 0000000..8dca5f1 --- /dev/null +++ b/Editor/CodeGen/PostProcessorReflectionImporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 484e8ad8c4dde382ea67036b32935ef1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CodeGen/PostProcessorReflectionImporterProvider.cs b/Editor/CodeGen/PostProcessorReflectionImporterProvider.cs new file mode 100644 index 0000000..81f80f0 --- /dev/null +++ b/Editor/CodeGen/PostProcessorReflectionImporterProvider.cs @@ -0,0 +1,12 @@ +using Mono.Cecil; + +namespace Unity.Netcode.Editor.CodeGen +{ + internal class PostProcessorReflectionImporterProvider : IReflectionImporterProvider + { + public IReflectionImporter GetReflectionImporter(ModuleDefinition moduleDefinition) + { + return new PostProcessorReflectionImporter(moduleDefinition); + } + } +} diff --git a/Editor/CodeGen/PostProcessorReflectionImporterProvider.cs.meta b/Editor/CodeGen/PostProcessorReflectionImporterProvider.cs.meta new file mode 100644 index 0000000..12a58b8 --- /dev/null +++ b/Editor/CodeGen/PostProcessorReflectionImporterProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9273a5dad109ab0783891e36c983080 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs new file mode 100644 index 0000000..1c3d846 --- /dev/null +++ b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.IO; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Unity.CompilationPipeline.Common.Diagnostics; +using Unity.CompilationPipeline.Common.ILPostProcessing; +using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor; + +namespace Unity.Netcode.Editor.CodeGen +{ + internal sealed class RuntimeAccessModifiersILPP : ILPPInterface + { + public override ILPPInterface GetInstance() => this; + + public override bool WillProcess(ICompiledAssembly compiledAssembly) => compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName; + + private readonly List m_Diagnostics = new List(); + + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) + { + if (!WillProcess(compiledAssembly)) + { + return null; + } + + m_Diagnostics.Clear(); + + // read + var assemblyDefinition = CodeGenHelpers.AssemblyDefinitionFor(compiledAssembly, out var unused); + if (assemblyDefinition == null) + { + m_Diagnostics.AddError($"Cannot read Netcode Runtime assembly definition: {compiledAssembly.Name}"); + return null; + } + + // process + var mainModule = assemblyDefinition.MainModule; + if (mainModule != null) + { + foreach (var typeDefinition in mainModule.Types) + { + if (!typeDefinition.IsClass) + { + continue; + } + + switch (typeDefinition.Name) + { + case nameof(NetworkManager): + ProcessNetworkManager(typeDefinition, compiledAssembly.Defines); + break; + case nameof(NetworkBehaviour): + ProcessNetworkBehaviour(typeDefinition); + break; + case nameof(__RpcParams): + typeDefinition.IsPublic = true; + break; + } + } + } + else + { + m_Diagnostics.AddError($"Cannot get main module from Netcode Runtime assembly definition: {compiledAssembly.Name}"); + } + + // write + var pe = new MemoryStream(); + var pdb = new MemoryStream(); + + var writerParameters = new WriterParameters + { + SymbolWriterProvider = new PortablePdbWriterProvider(), + SymbolStream = pdb, + WriteSymbols = true + }; + + assemblyDefinition.Write(pe, writerParameters); + + return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics); + } + + private void ProcessNetworkManager(TypeDefinition typeDefinition, string[] assemblyDefines) + { + foreach (var fieldDefinition in typeDefinition.Fields) + { + if (fieldDefinition.Name == nameof(NetworkManager.__rpc_func_table)) + { + fieldDefinition.IsPublic = true; + } + + if (fieldDefinition.Name == nameof(NetworkManager.RpcReceiveHandler)) + { + fieldDefinition.IsPublic = true; + } + + if (fieldDefinition.Name == nameof(NetworkManager.__rpc_name_table)) + { + fieldDefinition.IsPublic = true; + } + } + } + + private void ProcessNetworkBehaviour(TypeDefinition typeDefinition) + { + foreach (var nestedType in typeDefinition.NestedTypes) + { + if (nestedType.Name == nameof(NetworkBehaviour.__RpcExecStage)) + { + nestedType.IsNestedFamily = true; + } + } + + foreach (var fieldDefinition in typeDefinition.Fields) + { + if (fieldDefinition.Name == nameof(NetworkBehaviour.__rpc_exec_stage)) + { + fieldDefinition.IsFamily = true; + } + } + + foreach (var methodDefinition in typeDefinition.Methods) + { + if (methodDefinition.Name == nameof(NetworkBehaviour.__sendServerRpc) + || methodDefinition.Name == nameof(NetworkBehaviour.__sendClientRpc)) + { + methodDefinition.IsFamily = true; + } + } + } + } +} diff --git a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs.meta b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs.meta new file mode 100644 index 0000000..8feb576 --- /dev/null +++ b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c9f2f4b03d774432be69d4c2f53bd2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CodeGen/com.unity.netcode.editor.codegen.asmdef b/Editor/CodeGen/com.unity.netcode.editor.codegen.asmdef new file mode 100644 index 0000000..f9a5652 --- /dev/null +++ b/Editor/CodeGen/com.unity.netcode.editor.codegen.asmdef @@ -0,0 +1,19 @@ +{ + "name": "Unity.Netcode.Editor.CodeGen", + "rootNamespace": "Unity.Netcode.Editor.CodeGen", + "references": [ + "Unity.Netcode.Runtime" + ], + "includePlatforms": [ + "Editor" + ], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "Mono.Cecil.dll", + "Mono.Cecil.Mdb.dll", + "Mono.Cecil.Pdb.dll", + "Mono.Cecil.Rocks.dll" + ], + "autoReferenced": false +} \ No newline at end of file diff --git a/Editor/CodeGen/com.unity.netcode.editor.codegen.asmdef.meta b/Editor/CodeGen/com.unity.netcode.editor.codegen.asmdef.meta new file mode 100644 index 0000000..8a5c9cd --- /dev/null +++ b/Editor/CodeGen/com.unity.netcode.editor.codegen.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fe4fa159f4a96442ba22af67ddf20c65 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/DontShowInTransportDropdownAttribute.cs b/Editor/DontShowInTransportDropdownAttribute.cs new file mode 100644 index 0000000..465ea27 --- /dev/null +++ b/Editor/DontShowInTransportDropdownAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Unity.Netcode.Editor +{ + public class DontShowInTransportDropdownAttribute : Attribute + { + } +} diff --git a/Editor/DontShowInTransportDropdownAttribute.cs.meta b/Editor/DontShowInTransportDropdownAttribute.cs.meta new file mode 100644 index 0000000..da7b0cf --- /dev/null +++ b/Editor/DontShowInTransportDropdownAttribute.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5f097067d4254dc7ad018d7ad90df7c3 +timeCreated: 1620386886 \ No newline at end of file diff --git a/Editor/NetworkAnimatorEditor.cs b/Editor/NetworkAnimatorEditor.cs new file mode 100644 index 0000000..faae024 --- /dev/null +++ b/Editor/NetworkAnimatorEditor.cs @@ -0,0 +1,103 @@ +using System; +using Unity.Netcode.Components; +using UnityEditor; +using UnityEditor.Animations; +using UnityEngine; + +namespace Unity.Netcode.Editor +{ + public static class TextUtility + { + public static GUIContent TextContent(string name, string tooltip) + { + var newContent = new GUIContent(name); + newContent.tooltip = tooltip; + return newContent; + } + + public static GUIContent TextContent(string name) + { + return new GUIContent(name); + } + } + + [CustomEditor(typeof(NetworkAnimator), true)] + [CanEditMultipleObjects] + public class NetworkAnimatorEditor : UnityEditor.Editor + { + private NetworkAnimator m_AnimSync; + [NonSerialized] private bool m_Initialized; + private SerializedProperty m_AnimatorProperty; + private GUIContent m_AnimatorLabel; + + private void Init() + { + if (m_Initialized) + { + return; + } + + m_Initialized = true; + m_AnimSync = target as NetworkAnimator; + + m_AnimatorProperty = serializedObject.FindProperty("m_Animator"); + m_AnimatorLabel = TextUtility.TextContent("Animator", "The Animator component to synchronize."); + } + + public override void OnInspectorGUI() + { + Init(); + serializedObject.Update(); + DrawControls(); + serializedObject.ApplyModifiedProperties(); + } + + private void DrawControls() + { + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(m_AnimatorProperty, m_AnimatorLabel); + if (EditorGUI.EndChangeCheck()) + { + m_AnimSync.ResetParameterOptions(); + } + + if (m_AnimSync.Animator == null) + { + return; + } + + var controller = m_AnimSync.Animator.runtimeAnimatorController as AnimatorController; + if (controller != null) + { + var showWarning = false; + EditorGUI.indentLevel += 1; + int i = 0; + + foreach (var p in controller.parameters) + { + if (i >= NetworkAnimator.K_MaxAnimationParams) + { + showWarning = true; + break; + } + + bool oldSend = m_AnimSync.GetParameterAutoSend(i); + bool send = EditorGUILayout.Toggle(p.name, oldSend); + if (send != oldSend) + { + m_AnimSync.SetParameterAutoSend(i, send); + EditorUtility.SetDirty(target); + } + i += 1; + } + + if (showWarning) + { + EditorGUILayout.HelpBox($"NetworkAnimator can only select between the first {NetworkAnimator.K_MaxAnimationParams} parameters in a mecanim controller", MessageType.Warning); + } + + EditorGUI.indentLevel -= 1; + } + } + } +} diff --git a/Editor/NetworkAnimatorEditor.cs.meta b/Editor/NetworkAnimatorEditor.cs.meta new file mode 100644 index 0000000..a675853 --- /dev/null +++ b/Editor/NetworkAnimatorEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a32aeecf69a2542469927066f5b88005 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/NetworkBehaviourEditor.cs b/Editor/NetworkBehaviourEditor.cs new file mode 100644 index 0000000..d565c69 --- /dev/null +++ b/Editor/NetworkBehaviourEditor.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using UnityEditor; + +namespace Unity.Netcode.Editor +{ + [CustomEditor(typeof(NetworkBehaviour), true)] + [CanEditMultipleObjects] + public class NetworkBehaviourEditor : UnityEditor.Editor + { + private bool m_Initialized; + private readonly List m_NetworkVariableNames = new List(); + private readonly Dictionary m_NetworkVariableFields = new Dictionary(); + private readonly Dictionary m_NetworkVariableObjects = new Dictionary(); + + private GUIContent m_NetworkVariableLabelGuiContent; + + private void Init(MonoScript script) + { + m_Initialized = true; + + m_NetworkVariableNames.Clear(); + m_NetworkVariableFields.Clear(); + m_NetworkVariableObjects.Clear(); + + m_NetworkVariableLabelGuiContent = new GUIContent("NetworkVariable", "This variable is a NetworkVariable. It can not be serialized and can only be changed during runtime."); + + var fields = script.GetClass().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + for (int i = 0; i < fields.Length; i++) + { + var ft = fields[i].FieldType; + if (ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(NetworkVariable<>) && !fields[i].IsDefined(typeof(HideInInspector), true)) + { + m_NetworkVariableNames.Add(fields[i].Name); + m_NetworkVariableFields.Add(fields[i].Name, fields[i]); + } + } + } + + private void RenderNetworkVariable(int index) + { + if (!m_NetworkVariableFields.ContainsKey(m_NetworkVariableNames[index])) + { + serializedObject.Update(); + var scriptProperty = serializedObject.FindProperty("m_Script"); + if (scriptProperty == null) + { + return; + } + + var targetScript = scriptProperty.objectReferenceValue as MonoScript; + Init(targetScript); + } + + object value = m_NetworkVariableFields[m_NetworkVariableNames[index]].GetValue(target); + if (value == null) + { + var fieldType = m_NetworkVariableFields[m_NetworkVariableNames[index]].FieldType; + var networkVariable = (NetworkVariableBase)Activator.CreateInstance(fieldType, true); + m_NetworkVariableFields[m_NetworkVariableNames[index]].SetValue(target, networkVariable); + } + + var type = m_NetworkVariableFields[m_NetworkVariableNames[index]].GetValue(target).GetType(); + var genericType = type.GetGenericArguments()[0]; + + EditorGUILayout.BeginHorizontal(); + if (genericType.IsValueType) + { + var method = typeof(NetworkBehaviourEditor).GetMethod("RenderNetworkVariableValueType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic); + var genericMethod = method.MakeGenericMethod(genericType); + genericMethod.Invoke(this, new[] { (object)index }); + } + else + { + EditorGUILayout.LabelField("Type not renderable"); + } + + GUILayout.Label(m_NetworkVariableLabelGuiContent, EditorStyles.miniLabel, GUILayout.Width(EditorStyles.miniLabel.CalcSize(m_NetworkVariableLabelGuiContent).x)); + EditorGUILayout.EndHorizontal(); + } + + private void RenderNetworkVariableValueType(int index) where T : unmanaged + { + var networkVariable = (NetworkVariable)m_NetworkVariableFields[m_NetworkVariableNames[index]].GetValue(target); + var type = typeof(T); + object val = networkVariable.Value; + string name = m_NetworkVariableNames[index]; + + var behaviour = (NetworkBehaviour)target; + + // Only server can MODIFY. So allow modification if network is either not running or we are server + if (behaviour.IsBehaviourEditable()) + { + if (type == typeof(int)) + { + val = EditorGUILayout.IntField(name, (int)val); + } + else if (type == typeof(uint)) + { + val = (uint)EditorGUILayout.LongField(name, (long)((uint)val)); + } + else if (type == typeof(short)) + { + val = (short)EditorGUILayout.IntField(name, (int)((short)val)); + } + else if (type == typeof(ushort)) + { + val = (ushort)EditorGUILayout.IntField(name, (int)((ushort)val)); + } + else if (type == typeof(sbyte)) + { + val = (sbyte)EditorGUILayout.IntField(name, (int)((sbyte)val)); + } + else if (type == typeof(byte)) + { + val = (byte)EditorGUILayout.IntField(name, (int)((byte)val)); + } + else if (type == typeof(long)) + { + val = EditorGUILayout.LongField(name, (long)val); + } + else if (type == typeof(ulong)) + { + val = (ulong)EditorGUILayout.LongField(name, (long)((ulong)val)); + } + else if (type == typeof(bool)) + { + val = EditorGUILayout.Toggle(name, (bool)val); + } + else if (type == typeof(string)) + { + val = EditorGUILayout.TextField(name, (string)val); + } + else if (type.IsEnum) + { + val = EditorGUILayout.EnumPopup(name, (Enum)val); + } + else + { + EditorGUILayout.LabelField("Type not renderable"); + } + + networkVariable.Value = (T)val; + } + else + { + EditorGUILayout.LabelField(name, EditorStyles.wordWrappedLabel); + EditorGUILayout.SelectableLabel(val.ToString(), EditorStyles.wordWrappedLabel); + } + } + + public override void OnInspectorGUI() + { + if (!m_Initialized) + { + serializedObject.Update(); + var scriptProperty = serializedObject.FindProperty("m_Script"); + if (scriptProperty == null) + { + return; + } + + var targetScript = scriptProperty.objectReferenceValue as MonoScript; + Init(targetScript); + } + + EditorGUI.BeginChangeCheck(); + serializedObject.Update(); + + for (int i = 0; i < m_NetworkVariableNames.Count; i++) + { + RenderNetworkVariable(i); + } + + var property = serializedObject.GetIterator(); + bool expanded = true; + while (property.NextVisible(expanded)) + { + if (m_NetworkVariableNames.Contains(property.name)) + { + // Skip rendering of NetworkVars, they have special rendering + continue; + } + + if (property.propertyType == SerializedPropertyType.ObjectReference) + { + if (property.name == "m_Script") + { + EditorGUI.BeginDisabledGroup(true); + } + + EditorGUILayout.PropertyField(property, true); + + if (property.name == "m_Script") + { + EditorGUI.EndDisabledGroup(); + } + } + else + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.PropertyField(property, true); + EditorGUILayout.EndHorizontal(); + } + + expanded = false; + } + + serializedObject.ApplyModifiedProperties(); + EditorGUI.EndChangeCheck(); + } + } +} diff --git a/Editor/NetworkBehaviourEditor.cs.meta b/Editor/NetworkBehaviourEditor.cs.meta new file mode 100644 index 0000000..60ac9b8 --- /dev/null +++ b/Editor/NetworkBehaviourEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4a5c9c08a4038e449fd259764bc663f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs new file mode 100644 index 0000000..63342c1 --- /dev/null +++ b/Editor/NetworkManagerEditor.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEditorInternal; + +namespace Unity.Netcode.Editor +{ + [CustomEditor(typeof(NetworkManager), true)] + [CanEditMultipleObjects] + public class NetworkManagerEditor : UnityEditor.Editor + { + internal const string InstallMultiplayerToolsTipDismissedPlayerPrefKey = "Netcode_Tip_InstallMPTools_Dismissed"; + private static GUIStyle s_CenteredWordWrappedLabelStyle; + private static GUIStyle s_HelpBoxStyle; + + // Properties + private SerializedProperty m_DontDestroyOnLoadProperty; + private SerializedProperty m_RunInBackgroundProperty; + private SerializedProperty m_LogLevelProperty; + + // NetworkConfig + private SerializedProperty m_NetworkConfigProperty; + + // NetworkConfig fields + private SerializedProperty m_PlayerPrefabProperty; + private SerializedProperty m_ProtocolVersionProperty; + private SerializedProperty m_NetworkTransportProperty; + private SerializedProperty m_TickRateProperty; + private SerializedProperty m_MaxObjectUpdatesPerTickProperty; + private SerializedProperty m_ClientConnectionBufferTimeoutProperty; + private SerializedProperty m_ConnectionApprovalProperty; + private SerializedProperty m_EnsureNetworkVariableLengthSafetyProperty; + private SerializedProperty m_ForceSamePrefabsProperty; + private SerializedProperty m_EnableSceneManagementProperty; + private SerializedProperty m_RecycleNetworkIdsProperty; + private SerializedProperty m_NetworkIdRecycleDelayProperty; + private SerializedProperty m_RpcHashSizeProperty; + private SerializedProperty m_LoadSceneTimeOutProperty; + + private ReorderableList m_NetworkPrefabsList; + + private NetworkManager m_NetworkManager; + private bool m_Initialized; + + private readonly List m_TransportTypes = new List(); + private string[] m_TransportNames = { "Select transport..." }; + + private void ReloadTransports() + { + m_TransportTypes.Clear(); + + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + foreach (var assembly in assemblies) + { + var types = assembly.GetTypes(); + + foreach (var type in types) + { + if (type.IsSubclassOf(typeof(NetworkTransport)) && type.GetCustomAttributes(typeof(DontShowInTransportDropdownAttribute), true).Length == 0) + { + m_TransportTypes.Add(type); + } + } + } + + m_TransportNames = new string[m_TransportTypes.Count + 1]; + m_TransportNames[0] = "Select transport..."; + + for (int i = 0; i < m_TransportTypes.Count; i++) + { + m_TransportNames[i + 1] = m_TransportTypes[i].Name; + } + } + + private void Initialize() + { + if (m_Initialized) + { + return; + } + + m_Initialized = true; + m_NetworkManager = (NetworkManager)target; + + // Base properties + m_DontDestroyOnLoadProperty = serializedObject.FindProperty(nameof(NetworkManager.DontDestroy)); + m_RunInBackgroundProperty = serializedObject.FindProperty(nameof(NetworkManager.RunInBackground)); + m_LogLevelProperty = serializedObject.FindProperty(nameof(NetworkManager.LogLevel)); + m_NetworkConfigProperty = serializedObject.FindProperty(nameof(NetworkManager.NetworkConfig)); + + // NetworkConfig properties + m_PlayerPrefabProperty = m_NetworkConfigProperty.FindPropertyRelative(nameof(NetworkConfig.PlayerPrefab)); + m_ProtocolVersionProperty = m_NetworkConfigProperty.FindPropertyRelative("ProtocolVersion"); + m_NetworkTransportProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkTransport"); + m_TickRateProperty = m_NetworkConfigProperty.FindPropertyRelative("TickRate"); + m_ClientConnectionBufferTimeoutProperty = m_NetworkConfigProperty.FindPropertyRelative("ClientConnectionBufferTimeout"); + m_ConnectionApprovalProperty = m_NetworkConfigProperty.FindPropertyRelative("ConnectionApproval"); + m_EnsureNetworkVariableLengthSafetyProperty = m_NetworkConfigProperty.FindPropertyRelative("EnsureNetworkVariableLengthSafety"); + m_ForceSamePrefabsProperty = m_NetworkConfigProperty.FindPropertyRelative("ForceSamePrefabs"); + m_EnableSceneManagementProperty = m_NetworkConfigProperty.FindPropertyRelative("EnableSceneManagement"); + m_RecycleNetworkIdsProperty = m_NetworkConfigProperty.FindPropertyRelative("RecycleNetworkIds"); + m_NetworkIdRecycleDelayProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkIdRecycleDelay"); + m_RpcHashSizeProperty = m_NetworkConfigProperty.FindPropertyRelative("RpcHashSize"); + m_LoadSceneTimeOutProperty = m_NetworkConfigProperty.FindPropertyRelative("LoadSceneTimeOut"); + + + ReloadTransports(); + } + + private void CheckNullProperties() + { + // Base properties + m_DontDestroyOnLoadProperty = serializedObject.FindProperty(nameof(NetworkManager.DontDestroy)); + m_RunInBackgroundProperty = serializedObject.FindProperty(nameof(NetworkManager.RunInBackground)); + m_LogLevelProperty = serializedObject.FindProperty(nameof(NetworkManager.LogLevel)); + m_NetworkConfigProperty = serializedObject.FindProperty(nameof(NetworkManager.NetworkConfig)); + + // NetworkConfig properties + m_PlayerPrefabProperty = m_NetworkConfigProperty.FindPropertyRelative(nameof(NetworkConfig.PlayerPrefab)); + m_ProtocolVersionProperty = m_NetworkConfigProperty.FindPropertyRelative("ProtocolVersion"); + m_NetworkTransportProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkTransport"); + m_TickRateProperty = m_NetworkConfigProperty.FindPropertyRelative("TickRate"); + m_ClientConnectionBufferTimeoutProperty = m_NetworkConfigProperty.FindPropertyRelative("ClientConnectionBufferTimeout"); + m_ConnectionApprovalProperty = m_NetworkConfigProperty.FindPropertyRelative("ConnectionApproval"); + m_EnsureNetworkVariableLengthSafetyProperty = m_NetworkConfigProperty.FindPropertyRelative("EnsureNetworkVariableLengthSafety"); + m_ForceSamePrefabsProperty = m_NetworkConfigProperty.FindPropertyRelative("ForceSamePrefabs"); + m_EnableSceneManagementProperty = m_NetworkConfigProperty.FindPropertyRelative("EnableSceneManagement"); + m_RecycleNetworkIdsProperty = m_NetworkConfigProperty.FindPropertyRelative("RecycleNetworkIds"); + m_NetworkIdRecycleDelayProperty = m_NetworkConfigProperty.FindPropertyRelative("NetworkIdRecycleDelay"); + m_RpcHashSizeProperty = m_NetworkConfigProperty.FindPropertyRelative("RpcHashSize"); + m_LoadSceneTimeOutProperty = m_NetworkConfigProperty.FindPropertyRelative("LoadSceneTimeOut"); + } + + 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; + + return 8 + (networkOverrideInt == 0 ? EditorGUIUtility.singleLineHeight : (EditorGUIUtility.singleLineHeight * 2) + 5); + }; + m_NetworkPrefabsList.drawElementCallback = (rect, index, isActive, isFocused) => + { + rect.y += 5; + + var networkPrefab = m_NetworkPrefabsList.serializedProperty.GetArrayElementAtIndex(index); + var networkPrefabProp = networkPrefab.FindPropertyRelative(nameof(NetworkPrefab.Prefab)); + var networkSourceHashProp = networkPrefab.FindPropertyRelative(nameof(NetworkPrefab.SourceHashToOverride)); + var networkSourcePrefabProp = networkPrefab.FindPropertyRelative(nameof(NetworkPrefab.SourcePrefabToOverride)); + var networkTargetPrefabProp = networkPrefab.FindPropertyRelative(nameof(NetworkPrefab.OverridingTargetPrefab)); + var networkOverrideProp = networkPrefab.FindPropertyRelative(nameof(NetworkPrefab.Override)); + var networkOverrideInt = networkOverrideProp.enumValueIndex; + var networkOverrideEnum = (NetworkPrefabOverride)networkOverrideInt; + EditorGUI.LabelField(new Rect(rect.x + rect.width - 70, rect.y, 60, EditorGUIUtility.singleLineHeight), "Override"); + if (networkOverrideEnum == NetworkPrefabOverride.None) + { + if (EditorGUI.Toggle(new Rect(rect.x + rect.width - 15, rect.y, 10, EditorGUIUtility.singleLineHeight), false)) + { + networkOverrideProp.enumValueIndex = (int)NetworkPrefabOverride.Prefab; + } + } + else + { + if (!EditorGUI.Toggle(new Rect(rect.x + rect.width - 15, rect.y, 10, EditorGUIUtility.singleLineHeight), true)) + { + networkOverrideProp.enumValueIndex = 0; + networkOverrideEnum = NetworkPrefabOverride.None; + } + } + + if (networkOverrideEnum == NetworkPrefabOverride.None) + { + EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width - 80, EditorGUIUtility.singleLineHeight), networkPrefabProp, GUIContent.none); + } + else + { + networkOverrideProp.enumValueIndex = GUI.Toolbar(new Rect(rect.x, rect.y, 100, EditorGUIUtility.singleLineHeight), networkOverrideInt - 1, new[] { "Prefab", "Hash" }) + 1; + + if (networkOverrideEnum == NetworkPrefabOverride.Prefab) + { + EditorGUI.PropertyField(new Rect(rect.x + 110, rect.y, rect.width - 190, EditorGUIUtility.singleLineHeight), networkSourcePrefabProp, GUIContent.none); + } + else + { + EditorGUI.PropertyField(new Rect(rect.x + 110, rect.y, rect.width - 190, EditorGUIUtility.singleLineHeight), networkSourceHashProp, GUIContent.none); + } + + rect.y += EditorGUIUtility.singleLineHeight + 5; + + EditorGUI.LabelField(new Rect(rect.x, rect.y, 100, EditorGUIUtility.singleLineHeight), "Overriding Prefab"); + EditorGUI.PropertyField(new Rect(rect.x + 110, rect.y, rect.width - 110, EditorGUIUtility.singleLineHeight), networkTargetPrefabProp, GUIContent.none); + } + }; + m_NetworkPrefabsList.drawHeaderCallback = rect => EditorGUI.LabelField(rect, "NetworkPrefabs"); + } + + public override void OnInspectorGUI() + { + Initialize(); + CheckNullProperties(); + +#if !MULTIPLAYER_TOOLS + DrawInstallMultiplayerToolsTip(); +#endif + + { + var iterator = serializedObject.GetIterator(); + + for (bool enterChildren = true; iterator.NextVisible(enterChildren); enterChildren = false) + { + using (new EditorGUI.DisabledScope("m_Script" == iterator.propertyPath)) + { + EditorGUILayout.PropertyField(iterator, false); + } + } + } + + if (!m_NetworkManager.IsServer && !m_NetworkManager.IsClient) + { + serializedObject.Update(); + EditorGUILayout.PropertyField(m_DontDestroyOnLoadProperty); + EditorGUILayout.PropertyField(m_RunInBackgroundProperty); + EditorGUILayout.PropertyField(m_LogLevelProperty); + EditorGUILayout.Space(); + + EditorGUILayout.PropertyField(m_PlayerPrefabProperty); + EditorGUILayout.Space(); + + m_NetworkPrefabsList.DoLayoutList(); + EditorGUILayout.Space(); + + EditorGUILayout.LabelField("General", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_ProtocolVersionProperty); + + EditorGUILayout.PropertyField(m_NetworkTransportProperty); + + if (m_NetworkTransportProperty.objectReferenceValue == null) + { + EditorGUILayout.HelpBox("You have no transport selected. A transport is required for netcode to work. Which one do you want?", MessageType.Warning); + + int selection = EditorGUILayout.Popup(0, m_TransportNames); + + if (selection > 0) + { + ReloadTransports(); + + var transportComponent = m_NetworkManager.gameObject.GetComponent(m_TransportTypes[selection - 1]); + + if (transportComponent == null) + { + transportComponent = m_NetworkManager.gameObject.AddComponent(m_TransportTypes[selection - 1]); + } + + m_NetworkTransportProperty.objectReferenceValue = transportComponent; + + Repaint(); + } + } + + EditorGUILayout.PropertyField(m_TickRateProperty); + + EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel); + + EditorGUILayout.PropertyField(m_EnsureNetworkVariableLengthSafetyProperty); + + EditorGUILayout.LabelField("Connection", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_ConnectionApprovalProperty); + + using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.ConnectionApproval)) + { + EditorGUILayout.PropertyField(m_ClientConnectionBufferTimeoutProperty); + } + + EditorGUILayout.LabelField("Spawning", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_ForceSamePrefabsProperty); + + + EditorGUILayout.PropertyField(m_RecycleNetworkIdsProperty); + + using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.RecycleNetworkIds)) + { + EditorGUILayout.PropertyField(m_NetworkIdRecycleDelayProperty); + } + + EditorGUILayout.LabelField("Bandwidth", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_RpcHashSizeProperty); + + EditorGUILayout.LabelField("Scene Management", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_EnableSceneManagementProperty); + + using (new EditorGUI.DisabledScope(!m_NetworkManager.NetworkConfig.EnableSceneManagement)) + { + EditorGUILayout.PropertyField(m_LoadSceneTimeOutProperty); + } + + serializedObject.ApplyModifiedProperties(); + + + // Start buttons below + { + string buttonDisabledReasonSuffix = ""; + + if (!EditorApplication.isPlaying) + { + buttonDisabledReasonSuffix = ". This can only be done in play mode"; + GUI.enabled = false; + } + + if (GUILayout.Button(new GUIContent("Start Host", "Starts a host instance" + buttonDisabledReasonSuffix))) + { + m_NetworkManager.StartHost(); + } + + if (GUILayout.Button(new GUIContent("Start Server", "Starts a server instance" + buttonDisabledReasonSuffix))) + { + m_NetworkManager.StartServer(); + } + + if (GUILayout.Button(new GUIContent("Start Client", "Starts a client instance" + buttonDisabledReasonSuffix))) + { + m_NetworkManager.StartClient(); + } + + if (!EditorApplication.isPlaying) + { + GUI.enabled = true; + } + } + } + else + { + string instanceType = string.Empty; + + if (m_NetworkManager.IsHost) + { + instanceType = "Host"; + } + else if (m_NetworkManager.IsServer) + { + instanceType = "Server"; + } + else if (m_NetworkManager.IsClient) + { + instanceType = "Client"; + } + + EditorGUILayout.HelpBox("You cannot edit the NetworkConfig when a " + instanceType + " is running.", MessageType.Info); + + if (GUILayout.Button(new GUIContent("Stop " + instanceType, "Stops the " + instanceType + " instance."))) + { + m_NetworkManager.Shutdown(); + } + } + } + + private static void DrawInstallMultiplayerToolsTip() + { + const string getToolsText = "Access additional tools for multiplayer development by installing the Multiplayer Tools package in the Package Manager."; + const string openDocsButtonText = "Open Docs"; + const string dismissButtonText = "Dismiss"; + const string targetUrl = "https://docs-multiplayer.unity3d.com/docs/tutorials/goldenpath_series/goldenpath_foundation_module"; + const string infoIconName = "console.infoicon"; + + if (PlayerPrefs.GetInt(InstallMultiplayerToolsTipDismissedPlayerPrefKey, 0) != 0) + { + return; + } + + if (s_CenteredWordWrappedLabelStyle == null) + { + s_CenteredWordWrappedLabelStyle = new GUIStyle(GUI.skin.label); + s_CenteredWordWrappedLabelStyle.wordWrap = true; + s_CenteredWordWrappedLabelStyle.alignment = TextAnchor.MiddleLeft; + } + + if (s_HelpBoxStyle == null) + { + s_HelpBoxStyle = new GUIStyle(EditorStyles.helpBox); + s_HelpBoxStyle.padding = new RectOffset(10, 10, 10, 10); + } + + var openDocsButtonStyle = GUI.skin.button; + var dismissButtonStyle = EditorStyles.linkLabel; + + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.BeginHorizontal(s_HelpBoxStyle, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(false), GUILayout.MaxWidth(800)); + { + GUILayout.Label(new GUIContent(EditorGUIUtility.IconContent(infoIconName)), GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)); + GUILayout.Space(4); + GUILayout.Label(getToolsText, s_CenteredWordWrappedLabelStyle, GUILayout.ExpandHeight(true)); + + GUILayout.Space(4); + + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button(openDocsButtonText, openDocsButtonStyle, GUILayout.Width(90), GUILayout.Height(30))) + { + Application.OpenURL(targetUrl); + } + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + + GUILayout.Space(4); + + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button(dismissButtonText, dismissButtonStyle, GUILayout.ExpandWidth(false))) + { + PlayerPrefs.SetInt(InstallMultiplayerToolsTipDismissedPlayerPrefKey, 1); + } + EditorGUIUtility.AddCursorRect(GUILayoutUtility.GetLastRect(), MouseCursor.Link); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + } + GUILayout.EndHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(10); + } + } +} diff --git a/Editor/NetworkManagerEditor.cs.meta b/Editor/NetworkManagerEditor.cs.meta new file mode 100644 index 0000000..3a190ff --- /dev/null +++ b/Editor/NetworkManagerEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 74a8f011a324b7642b69098fe57bf635 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/NetworkObjectEditor.cs b/Editor/NetworkObjectEditor.cs new file mode 100644 index 0000000..d9e9d70 --- /dev/null +++ b/Editor/NetworkObjectEditor.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; + +namespace Unity.Netcode.Editor +{ + [CustomEditor(typeof(NetworkObject), true)] + [CanEditMultipleObjects] + public class NetworkObjectEditor : UnityEditor.Editor + { + private bool m_Initialized; + private NetworkObject m_NetworkObject; + private bool m_ShowObservers; + + private void Initialize() + { + if (m_Initialized) + { + return; + } + + m_Initialized = true; + m_NetworkObject = (NetworkObject)target; + } + + public override void OnInspectorGUI() + { + Initialize(); + + if (EditorApplication.isPlaying && !m_NetworkObject.IsSpawned && m_NetworkObject.NetworkManager != null && m_NetworkObject.NetworkManager.IsServer) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Spawn", "Spawns the object across the network")); + if (GUILayout.Toggle(false, "Spawn", EditorStyles.miniButtonLeft)) + { + m_NetworkObject.Spawn(); + EditorUtility.SetDirty(target); + } + + EditorGUILayout.EndHorizontal(); + } + else if (EditorApplication.isPlaying && m_NetworkObject.IsSpawned) + { + var guiEnabled = GUI.enabled; + GUI.enabled = false; + EditorGUILayout.TextField(nameof(NetworkObject.GlobalObjectIdHash), m_NetworkObject.GlobalObjectIdHash.ToString()); + EditorGUILayout.TextField(nameof(NetworkObject.NetworkObjectId), m_NetworkObject.NetworkObjectId.ToString()); + EditorGUILayout.TextField(nameof(NetworkObject.OwnerClientId), m_NetworkObject.OwnerClientId.ToString()); + EditorGUILayout.Toggle(nameof(NetworkObject.IsSpawned), m_NetworkObject.IsSpawned); + EditorGUILayout.Toggle(nameof(NetworkObject.IsLocalPlayer), m_NetworkObject.IsLocalPlayer); + EditorGUILayout.Toggle(nameof(NetworkObject.IsOwner), m_NetworkObject.IsOwner); + EditorGUILayout.Toggle(nameof(NetworkObject.IsOwnedByServer), m_NetworkObject.IsOwnedByServer); + EditorGUILayout.Toggle(nameof(NetworkObject.IsPlayerObject), m_NetworkObject.IsPlayerObject); + if (m_NetworkObject.IsSceneObject.HasValue) + { + EditorGUILayout.Toggle(nameof(NetworkObject.IsSceneObject), m_NetworkObject.IsSceneObject.Value); + } + else + { + EditorGUILayout.TextField(nameof(NetworkObject.IsSceneObject), "null"); + } + EditorGUILayout.Toggle(nameof(NetworkObject.DestroyWithScene), m_NetworkObject.DestroyWithScene); + EditorGUILayout.TextField(nameof(NetworkObject.NetworkManager), m_NetworkObject.NetworkManager == null ? "null" : m_NetworkObject.NetworkManager.gameObject.name); + GUI.enabled = guiEnabled; + + if (m_NetworkObject.NetworkManager != null && m_NetworkObject.NetworkManager.IsServer) + { + m_ShowObservers = EditorGUILayout.Foldout(m_ShowObservers, "Observers"); + + if (m_ShowObservers) + { + HashSet.Enumerator observerClientIds = m_NetworkObject.GetObservers(); + + EditorGUI.indentLevel += 1; + + while (observerClientIds.MoveNext()) + { + if (m_NetworkObject.NetworkManager.ConnectedClients[observerClientIds.Current].PlayerObject != null) + { + EditorGUILayout.ObjectField($"ClientId: {observerClientIds.Current}", m_NetworkObject.NetworkManager.ConnectedClients[observerClientIds.Current].PlayerObject, typeof(GameObject), false); + } + else + { + EditorGUILayout.TextField($"ClientId: {observerClientIds.Current}", EditorStyles.label); + } + } + + EditorGUI.indentLevel -= 1; + } + } + } + else + { + base.OnInspectorGUI(); + + var guiEnabled = GUI.enabled; + GUI.enabled = false; + EditorGUILayout.TextField(nameof(NetworkObject.GlobalObjectIdHash), m_NetworkObject.GlobalObjectIdHash.ToString()); + EditorGUILayout.TextField(nameof(NetworkObject.NetworkManager), m_NetworkObject.NetworkManager == null ? "null" : m_NetworkObject.NetworkManager.gameObject.name); + GUI.enabled = guiEnabled; + } + } + } +} diff --git a/Editor/NetworkObjectEditor.cs.meta b/Editor/NetworkObjectEditor.cs.meta new file mode 100644 index 0000000..89ad3f4 --- /dev/null +++ b/Editor/NetworkObjectEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 36e4b519d287d0f4e8bfb7d088a9275f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/NetworkTransformEditor.cs b/Editor/NetworkTransformEditor.cs new file mode 100644 index 0000000..bc8eca3 --- /dev/null +++ b/Editor/NetworkTransformEditor.cs @@ -0,0 +1,132 @@ +using UnityEditor; +using UnityEngine; +using Unity.Netcode.Components; + +namespace Unity.Netcode.Editor +{ + [CustomEditor(typeof(NetworkTransform))] + public class NetworkTransformEditor : UnityEditor.Editor + { + private SerializedProperty m_SyncPositionXProperty; + private SerializedProperty m_SyncPositionYProperty; + private SerializedProperty m_SyncPositionZProperty; + private SerializedProperty m_SyncRotationXProperty; + private SerializedProperty m_SyncRotationYProperty; + private SerializedProperty m_SyncRotationZProperty; + private SerializedProperty m_SyncScaleXProperty; + private SerializedProperty m_SyncScaleYProperty; + private SerializedProperty m_SyncScaleZProperty; + private SerializedProperty m_PositionThresholdProperty; + private SerializedProperty m_RotAngleThresholdProperty; + private SerializedProperty m_ScaleThresholdProperty; + private SerializedProperty m_InLocalSpaceProperty; + private SerializedProperty m_InterpolateProperty; + + private static int s_ToggleOffset = 45; + private static float s_MaxRowWidth = EditorGUIUtility.labelWidth + EditorGUIUtility.fieldWidth + 5; + private static GUIContent s_PositionLabel = EditorGUIUtility.TrTextContent("Position"); + private static GUIContent s_RotationLabel = EditorGUIUtility.TrTextContent("Rotation"); + private static GUIContent s_ScaleLabel = EditorGUIUtility.TrTextContent("Scale"); + + public void OnEnable() + { + m_SyncPositionXProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionX)); + m_SyncPositionYProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionY)); + m_SyncPositionZProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionZ)); + m_SyncRotationXProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncRotAngleX)); + m_SyncRotationYProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncRotAngleY)); + m_SyncRotationZProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncRotAngleZ)); + m_SyncScaleXProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncScaleX)); + m_SyncScaleYProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncScaleY)); + m_SyncScaleZProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncScaleZ)); + m_PositionThresholdProperty = serializedObject.FindProperty(nameof(NetworkTransform.PositionThreshold)); + m_RotAngleThresholdProperty = serializedObject.FindProperty(nameof(NetworkTransform.RotAngleThreshold)); + m_ScaleThresholdProperty = serializedObject.FindProperty(nameof(NetworkTransform.ScaleThreshold)); + m_InLocalSpaceProperty = serializedObject.FindProperty(nameof(NetworkTransform.InLocalSpace)); + m_InterpolateProperty = serializedObject.FindProperty(nameof(NetworkTransform.Interpolate)); + } + + public override void OnInspectorGUI() + { + EditorGUILayout.LabelField("Syncing", EditorStyles.boldLabel); + { + GUILayout.BeginHorizontal(); + + var rect = GUILayoutUtility.GetRect(EditorGUIUtility.fieldWidth, s_MaxRowWidth, EditorGUIUtility.singleLineHeight, EditorGUIUtility.singleLineHeight, EditorStyles.numberField); + var ctid = GUIUtility.GetControlID(7231, FocusType.Keyboard, rect); + + rect = EditorGUI.PrefixLabel(rect, ctid, s_PositionLabel); + rect.width = s_ToggleOffset; + + m_SyncPositionXProperty.boolValue = EditorGUI.ToggleLeft(rect, "X", m_SyncPositionXProperty.boolValue); + rect.x += s_ToggleOffset; + m_SyncPositionYProperty.boolValue = EditorGUI.ToggleLeft(rect, "Y", m_SyncPositionYProperty.boolValue); + rect.x += s_ToggleOffset; + m_SyncPositionZProperty.boolValue = EditorGUI.ToggleLeft(rect, "Z", m_SyncPositionZProperty.boolValue); + + GUILayout.EndHorizontal(); + } + { + GUILayout.BeginHorizontal(); + + var rect = GUILayoutUtility.GetRect(EditorGUIUtility.fieldWidth, s_MaxRowWidth, EditorGUIUtility.singleLineHeight, EditorGUIUtility.singleLineHeight, EditorStyles.numberField); + var ctid = GUIUtility.GetControlID(7231, FocusType.Keyboard, rect); + + rect = EditorGUI.PrefixLabel(rect, ctid, s_RotationLabel); + rect.width = s_ToggleOffset; + + m_SyncRotationXProperty.boolValue = EditorGUI.ToggleLeft(rect, "X", m_SyncRotationXProperty.boolValue); + rect.x += s_ToggleOffset; + m_SyncRotationYProperty.boolValue = EditorGUI.ToggleLeft(rect, "Y", m_SyncRotationYProperty.boolValue); + rect.x += s_ToggleOffset; + m_SyncRotationZProperty.boolValue = EditorGUI.ToggleLeft(rect, "Z", m_SyncRotationZProperty.boolValue); + + GUILayout.EndHorizontal(); + } + { + GUILayout.BeginHorizontal(); + + var rect = GUILayoutUtility.GetRect(EditorGUIUtility.fieldWidth, s_MaxRowWidth, EditorGUIUtility.singleLineHeight, EditorGUIUtility.singleLineHeight, EditorStyles.numberField); + var ctid = GUIUtility.GetControlID(7231, FocusType.Keyboard, rect); + + rect = EditorGUI.PrefixLabel(rect, ctid, s_ScaleLabel); + rect.width = s_ToggleOffset; + + m_SyncScaleXProperty.boolValue = EditorGUI.ToggleLeft(rect, "X", m_SyncScaleXProperty.boolValue); + rect.x += s_ToggleOffset; + m_SyncScaleYProperty.boolValue = EditorGUI.ToggleLeft(rect, "Y", m_SyncScaleYProperty.boolValue); + rect.x += s_ToggleOffset; + m_SyncScaleZProperty.boolValue = EditorGUI.ToggleLeft(rect, "Z", m_SyncScaleZProperty.boolValue); + + GUILayout.EndHorizontal(); + } + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Thresholds", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_PositionThresholdProperty); + EditorGUILayout.PropertyField(m_RotAngleThresholdProperty); + EditorGUILayout.PropertyField(m_ScaleThresholdProperty); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_InLocalSpaceProperty); + EditorGUILayout.PropertyField(m_InterpolateProperty); + + // if rigidbody is present but network rigidbody is not present + var go = ((NetworkTransform)target).gameObject; + if (go.TryGetComponent(out _) && go.TryGetComponent(out _) == false) + { + EditorGUILayout.HelpBox("This GameObject contains a Rigidbody but no NetworkRigidbody.\n" + + "Add a NetworkRigidbody component to improve Rigidbody synchronization.", MessageType.Warning); + } + + 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); + } + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/NetworkTransformEditor.cs.meta b/Editor/NetworkTransformEditor.cs.meta new file mode 100644 index 0000000..544457f --- /dev/null +++ b/Editor/NetworkTransformEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17891488cb32d4243b0710884463d70f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/com.unity.netcode.editor.asmdef b/Editor/com.unity.netcode.editor.asmdef new file mode 100644 index 0000000..5a9e666 --- /dev/null +++ b/Editor/com.unity.netcode.editor.asmdef @@ -0,0 +1,25 @@ +{ + "name": "Unity.Netcode.Editor", + "rootNamespace": "Unity.Netcode.Editor", + "references": [ + "Unity.Netcode.Runtime", + "Unity.Netcode.Components" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.unity.multiplayer.tools", + "expression": "", + "define": "MULTIPLAYER_TOOLS" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/com.unity.netcode.editor.asmdef.meta b/Editor/com.unity.netcode.editor.asmdef.meta new file mode 100644 index 0000000..f41a6d2 --- /dev/null +++ b/Editor/com.unity.netcode.editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9f4f5bf029cebb64f983b7bdc29f62a1 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a5eb171 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2021 Unity Technologies + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.md.meta b/LICENSE.md.meta new file mode 100644 index 0000000..245a130 --- /dev/null +++ b/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a850895ba88aa114f804fefa31faf4d2 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..51e6bb0 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +[![Forums](https://img.shields.io/badge/unity--forums-multiplayer-blue)](https://forum.unity.com/forums/multiplayer.26/) [![Discord](https://img.shields.io/discord/449263083769036810.svg?label=discord&logo=discord&color=informational)](https://discord.gg/FM8SE9E) +[![Website](https://img.shields.io/badge/docs-website-informational.svg)](https://docs-multiplayer.unity3d.com/) [![Api](https://img.shields.io/badge/docs-api-informational.svg)](https://docs-multiplayer.unity3d.com/docs/mlapi-api/introduction) + +Netcode for GameObjects provides networking capabilities to GameObject & MonoBehaviour Unity workflows. The framework is interoperable with many low-level transports, including the official [Unity Transport Package](https://docs.unity3d.com/Packages/com.unity.transport@1.0/manual/index.html). + +### Getting Started +Visit the [Multiplayer Docs Site](https://docs-multiplayer.unity3d.com/) for package & API documentation, as well as information about several samples which leverage the Netcode for GameObjects package. + +### Community and Feedback +For general questions, networking advice or discussions about Netcode for GameObjects, please join our [Discord Community](https://discord.gg/FM8SE9E) or create a post in the [Unity Multiplayer Forum](https://forum.unity.com/forums/multiplayer.26/). diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..2439891 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7a8193086d1cd7d4dadc6a324430350b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..7dc2790 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c83905999e4e71246b5a3ebf2565faf7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/AssemblyInfo.cs b/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..fc40c7e --- /dev/null +++ b/Runtime/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +#if UNITY_EDITOR +[assembly: InternalsVisibleTo("Unity.Netcode.EditorTests")] +[assembly: InternalsVisibleTo("Unity.Netcode.Editor.CodeGen")] +[assembly: InternalsVisibleTo("Unity.Netcode.Editor")] +[assembly: InternalsVisibleTo("TestProject.EditorTests")] +[assembly: InternalsVisibleTo("TestProject.RuntimeTests")] +[assembly: InternalsVisibleTo("TestProject.ToolsIntegration.RuntimeTests")] +#endif +[assembly: InternalsVisibleTo("Unity.Netcode.RuntimeTests")] + diff --git a/Runtime/AssemblyInfo.cs.meta b/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..89d9086 --- /dev/null +++ b/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c9fd74adf4a0f6e479be3978543dc6a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Collections.meta b/Runtime/Collections.meta new file mode 100644 index 0000000..6f36eb7 --- /dev/null +++ b/Runtime/Collections.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f2ef964afcae91248b2298b479ed1b53 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Collections/FixedQueue.cs b/Runtime/Collections/FixedQueue.cs new file mode 100644 index 0000000..1cbb4f1 --- /dev/null +++ b/Runtime/Collections/FixedQueue.cs @@ -0,0 +1,77 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Queue with a fixed size + /// + /// The type of the queue + public sealed class FixedQueue + { + private readonly T[] m_Queue; + private int m_QueueCount = 0; + private int m_QueueStart; + + /// + /// The amount of enqueued objects + /// + public int Count => m_QueueCount; + + /// + /// Gets the element at a given virtual index + /// + /// The virtual index to get the item from + /// The element at the virtual index + public T this[int index] => m_Queue[(m_QueueStart + index) % m_Queue.Length]; + + /// + /// Creates a new FixedQueue with a given size + /// + /// The size of the queue + public FixedQueue(int maxSize) + { + m_Queue = new T[maxSize]; + m_QueueStart = 0; + } + + /// + /// Enqueues an object + /// + /// + /// + public bool Enqueue(T t) + { + m_Queue[(m_QueueStart + m_QueueCount) % m_Queue.Length] = t; + if (++m_QueueCount > m_Queue.Length) + { + --m_QueueCount; + return true; + } + + return false; + } + + /// + /// Dequeues an object + /// + /// + public T Dequeue() + { + if (--m_QueueCount == -1) + { + throw new IndexOutOfRangeException("Cannot dequeue empty queue!"); + } + + T res = m_Queue[m_QueueStart]; + m_QueueStart = (m_QueueStart + 1) % m_Queue.Length; + return res; + } + + /// + /// Gets the element at a given virtual index + /// + /// The virtual index to get the item from + /// The element at the virtual index + public T ElementAt(int index) => m_Queue[(m_QueueStart + index) % m_Queue.Length]; + } +} diff --git a/Runtime/Collections/FixedQueue.cs.meta b/Runtime/Collections/FixedQueue.cs.meta new file mode 100644 index 0000000..bc855dd --- /dev/null +++ b/Runtime/Collections/FixedQueue.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8514b4eca0c7044d9b92faf9407ec93 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Configuration.meta b/Runtime/Configuration.meta new file mode 100644 index 0000000..69e1003 --- /dev/null +++ b/Runtime/Configuration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d3cc7700dfd03ee4397858710461d179 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Configuration/HashSize.cs b/Runtime/Configuration/HashSize.cs new file mode 100644 index 0000000..c5519f1 --- /dev/null +++ b/Runtime/Configuration/HashSize.cs @@ -0,0 +1,20 @@ +namespace Unity.Netcode +{ + /// + /// Represents the length of a var int encoded hash + /// Note that the HashSize does not say anything about the actual final output due to the var int encoding + /// It just says how many bytes the maximum will be + /// + public enum HashSize : byte + { + /// + /// Four byte hash + /// + VarIntFourBytes, + + /// + /// Eight byte hash + /// + VarIntEightBytes + } +} diff --git a/Runtime/Configuration/HashSize.cs.meta b/Runtime/Configuration/HashSize.cs.meta new file mode 100644 index 0000000..829ac77 --- /dev/null +++ b/Runtime/Configuration/HashSize.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5ae94548754e0a0409da85c0e3235bb4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Configuration/NetworkConfig.cs b/Runtime/Configuration/NetworkConfig.cs new file mode 100644 index 0000000..d938294 --- /dev/null +++ b/Runtime/Configuration/NetworkConfig.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using System.Linq; +using Unity.Collections; + +namespace Unity.Netcode +{ + /// + /// The configuration object used to start server, client and hosts + /// + [Serializable] + public class NetworkConfig + { + /// + /// The protocol version. Different versions doesn't talk to each other. + /// + [Tooltip("Use this to make two builds incompatible with each other")] + public ushort ProtocolVersion = 0; + + /// + /// The transport hosts the sever uses + /// + [Tooltip("The NetworkTransport to use")] + public NetworkTransport NetworkTransport = null; + + /// + /// The default player prefab + /// + [Tooltip("When set, NetworkManager will automatically create and spawn the assigned player prefab. This can be overridden by adding it to the NetworkPrefabs list and selecting override.")] + public GameObject PlayerPrefab; + + /// + /// A list of prefabs that can be dynamically spawned. + /// + [SerializeField] + [Tooltip("The prefabs that can be spawned across the network")] + internal List NetworkPrefabs = new List(); + + /// + /// This dictionary provides a quick way to check and see if a NetworkPrefab has a NetworkPrefab override. + /// Generated at runtime and OnValidate + /// + internal Dictionary NetworkPrefabOverrideLinks = new Dictionary(); + + internal Dictionary OverrideToNetworkPrefab = new Dictionary(); + + + /// + /// The tickrate of network ticks. This value controls how often netcode runs user code and sends out data. + /// + [Tooltip("The tickrate. This value controls how often netcode runs user code and sends out data. The value is in 'ticks per seconds' which means a value of 50 will result in 50 ticks being executed per second or a fixed delta time of 0.02.")] + public uint TickRate = 30; + + /// + /// The amount of seconds to wait for handshake to complete before timing out a client + /// + [Tooltip("The amount of seconds to wait for the handshake to complete before the client times out")] + public int ClientConnectionBufferTimeout = 10; + + /// + /// Whether or not to use connection approval + /// + [Tooltip("Whether or not to force clients to be approved before they connect")] + public bool ConnectionApproval = false; + + /// + /// The data to send during connection which can be used to decide on if a client should get accepted + /// + [Tooltip("The connection data sent along with connection requests")] + public byte[] ConnectionData = new byte[0]; + + /// + /// If your logic uses the NetworkTime, this should probably be turned off. If however it's needed to maximize accuracy, this is recommended to be turned on + /// + [Tooltip("Enable this to re-sync the NetworkTime after the initial sync")] + public bool EnableTimeResync = false; + + /// + /// If time re-sync is turned on, this specifies the interval between syncs in seconds. + /// + [Tooltip("The amount of seconds between re-syncs of NetworkTime, if enabled")] + public int TimeResyncInterval = 30; + + /// + /// Whether or not to ensure that NetworkVariables can be read even if a client accidentally writes where its not allowed to. This costs some CPU and bandwidth. + /// + [Tooltip("Ensures that NetworkVariables can be read even if a client accidental writes where its not allowed to. This will cost some CPU time and bandwidth")] + public bool EnsureNetworkVariableLengthSafety = false; + + /// + /// Enables scene management. This will allow network scene switches and automatic scene difference corrections upon connect. + /// SoftSynced scene objects wont work with this disabled. That means that disabling SceneManagement also enables PrefabSync. + /// + [Tooltip("Enables scene management. This will allow network scene switches and automatic scene difference corrections upon connect.\n" + + "SoftSynced scene objects wont work with this disabled. That means that disabling SceneManagement also enables PrefabSync.")] + public bool EnableSceneManagement = true; + + /// + /// Whether or not the netcode should check for differences in the prefabs at connection. + /// If you dynamically add prefabs at runtime, turn this OFF + /// + [Tooltip("Whether or not the netcode should check for differences in the prefab lists at connection")] + public bool ForceSamePrefabs = true; + + /// + /// If true, NetworkIds will be reused after the NetworkIdRecycleDelay. + /// + [Tooltip("If true, NetworkIds will be reused after the NetworkIdRecycleDelay")] + public bool RecycleNetworkIds = true; + + /// + /// The amount of seconds a NetworkId has to be unused in order for it to be reused. + /// + [Tooltip("The amount of seconds a NetworkId has to unused in order for it to be reused")] + public float NetworkIdRecycleDelay = 120f; + + /// + /// Decides how many bytes to use for Rpc messaging. Leave this to 2 bytes unless you are facing hash collisions + /// + [Tooltip("The maximum amount of bytes to use for RPC messages.")] + public HashSize RpcHashSize = HashSize.VarIntFourBytes; + + /// + /// The amount of seconds to wait for all clients to load or unload a requested scene + /// + [Tooltip("The amount of seconds to wait for all clients to load or unload a requested scene (only when EnableSceneManagement is enabled)")] + public int LoadSceneTimeOut = 120; + + /// + /// The amount of time a message should be buffered for without being consumed. If it is not consumed within this time, it will be dropped. + /// + [Tooltip("The amount of time a message should be buffered for without being consumed. If it is not consumed within this time, it will be dropped")] + public float MessageBufferTimeout = 20f; + + /// + /// Whether or not to enable network logs. + /// + public bool EnableNetworkLogs = true; + + /// + /// Whether or not to enable Snapshot System for variable updates. Not supported in this version. + /// + public bool UseSnapshotDelta { get; } = false; + /// + /// Whether or not to enable Snapshot System for spawn and despawn commands. Not supported in this version. + /// + public bool UseSnapshotSpawn { get; } = false; + /// + /// When Snapshot System spawn is enabled: max size of Snapshot Messages. Meant to fit MTU. + /// + public int SnapshotMaxSpawnUsage { get; } = 1200; + + 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) + /// + /// Returns a base64 encoded version of the configuration + /// + /// + public string ToBase64() + { + NetworkConfig config = this; + var writer = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(config.ProtocolVersion); + writer.WriteValueSafe(config.TickRate); + writer.WriteValueSafe(config.ClientConnectionBufferTimeout); + writer.WriteValueSafe(config.ConnectionApproval); + writer.WriteValueSafe(config.LoadSceneTimeOut); + writer.WriteValueSafe(config.EnableTimeResync); + writer.WriteValueSafe(config.EnsureNetworkVariableLengthSafety); + writer.WriteValueSafe(config.RpcHashSize); + writer.WriteValueSafe(ForceSamePrefabs); + writer.WriteValueSafe(EnableSceneManagement); + writer.WriteValueSafe(RecycleNetworkIds); + writer.WriteValueSafe(NetworkIdRecycleDelay); + writer.WriteValueSafe(EnableNetworkLogs); + + // Allocates + return Convert.ToBase64String(writer.ToArray()); + } + } + + /// + /// Sets the NetworkConfig data with that from a base64 encoded version + /// + /// The base64 encoded version + public void FromBase64(string base64) + { + NetworkConfig config = this; + byte[] binary = Convert.FromBase64String(base64); + using var reader = new FastBufferReader(binary, Allocator.Temp); + using (reader) + { + reader.ReadValueSafe(out config.ProtocolVersion); + reader.ReadValueSafe(out config.TickRate); + reader.ReadValueSafe(out config.ClientConnectionBufferTimeout); + reader.ReadValueSafe(out config.ConnectionApproval); + reader.ReadValueSafe(out config.LoadSceneTimeOut); + reader.ReadValueSafe(out config.EnableTimeResync); + reader.ReadValueSafe(out config.EnsureNetworkVariableLengthSafety); + reader.ReadValueSafe(out config.RpcHashSize); + reader.ReadValueSafe(out config.ForceSamePrefabs); + reader.ReadValueSafe(out config.EnableSceneManagement); + reader.ReadValueSafe(out config.RecycleNetworkIds); + reader.ReadValueSafe(out config.NetworkIdRecycleDelay); + reader.ReadValueSafe(out config.EnableNetworkLogs); + } + } + + + private ulong? m_ConfigHash = null; + + /// + /// Gets a SHA256 hash of parts of the NetworkConfig instance + /// + /// + /// + public ulong GetConfig(bool cache = true) + { + if (m_ConfigHash != null && cache) + { + return m_ConfigHash.Value; + } + + var writer = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(ProtocolVersion); + writer.WriteValueSafe(NetworkConstants.PROTOCOL_VERSION); + + if (ForceSamePrefabs) + { + var sortedDictionary = NetworkPrefabOverrideLinks.OrderBy(x => x.Key); + foreach (var sortedEntry in sortedDictionary) + + { + writer.WriteValueSafe(sortedEntry.Key); + } + } + writer.WriteValueSafe(ConnectionApproval); + writer.WriteValueSafe(ForceSamePrefabs); + writer.WriteValueSafe(EnableSceneManagement); + writer.WriteValueSafe(EnsureNetworkVariableLengthSafety); + writer.WriteValueSafe(RpcHashSize); + + if (cache) + { + m_ConfigHash = XXHash.Hash64(writer.ToArray()); + return m_ConfigHash.Value; + } + + return XXHash.Hash64(writer.ToArray()); + } + } + + /// + /// Compares a SHA256 hash with the current NetworkConfig instances hash + /// + /// + /// + public bool CompareConfig(ulong hash) + { + return hash == GetConfig(); + } + } +} + diff --git a/Runtime/Configuration/NetworkConfig.cs.meta b/Runtime/Configuration/NetworkConfig.cs.meta new file mode 100644 index 0000000..01a76bb --- /dev/null +++ b/Runtime/Configuration/NetworkConfig.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b056e7faa4d1cb4aac7bc304c765c3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Configuration/NetworkConstants.cs b/Runtime/Configuration/NetworkConstants.cs new file mode 100644 index 0000000..55facaf --- /dev/null +++ b/Runtime/Configuration/NetworkConstants.cs @@ -0,0 +1,10 @@ +namespace Unity.Netcode +{ + /// + /// A static class containing network constants + /// + internal static class NetworkConstants + { + internal const string PROTOCOL_VERSION = "14.0.0"; + } +} diff --git a/Runtime/Configuration/NetworkConstants.cs.meta b/Runtime/Configuration/NetworkConstants.cs.meta new file mode 100644 index 0000000..16fb2cc --- /dev/null +++ b/Runtime/Configuration/NetworkConstants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dae82900f88f6bb4a90c29d431f2b45a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Configuration/NetworkPrefab.cs b/Runtime/Configuration/NetworkPrefab.cs new file mode 100644 index 0000000..cc22258 --- /dev/null +++ b/Runtime/Configuration/NetworkPrefab.cs @@ -0,0 +1,45 @@ +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + internal enum NetworkPrefabOverride + { + None, + Prefab, + Hash + } + + /// + /// Class that represents a NetworkPrefab + /// + [Serializable] + internal class NetworkPrefab + { + /// + /// The override setttings for this NetworkPrefab + /// + public NetworkPrefabOverride Override; + + /// + /// Asset reference of the network prefab + /// + public GameObject Prefab; + + /// + /// Used when prefab is selected for the source prefab to override value (i.e. direct reference, the prefab is within the same project) + /// We keep a separate value as the user might want to have something different than the default Prefab for the SourcePrefabToOverride + /// + public GameObject SourcePrefabToOverride; + + /// + /// Used when hash is selected for the source prefab to override value (i.e. a direct reference is not possible such as in a multi-project pattern) + /// + public uint SourceHashToOverride; + + /// + /// The prefab to replace (override) the source prefab with + /// + public GameObject OverridingTargetPrefab; + } +} diff --git a/Runtime/Configuration/NetworkPrefab.cs.meta b/Runtime/Configuration/NetworkPrefab.cs.meta new file mode 100644 index 0000000..7890134 --- /dev/null +++ b/Runtime/Configuration/NetworkPrefab.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 10e0511afda4e7743b2cd7c9cf95e0ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Connection.meta b/Runtime/Connection.meta new file mode 100644 index 0000000..ac77c62 --- /dev/null +++ b/Runtime/Connection.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7155f1167eb96846aab2132b37c2815 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Connection/NetworkClient.cs b/Runtime/Connection/NetworkClient.cs new file mode 100644 index 0000000..38ae544 --- /dev/null +++ b/Runtime/Connection/NetworkClient.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Unity.Netcode +{ + /// + /// A NetworkClient + /// + public class NetworkClient + { + /// + /// The ClientId of the NetworkClient + /// + public ulong ClientId; + + /// + /// The PlayerObject of the Client + /// + public NetworkObject PlayerObject; + + /// + /// The NetworkObject's owned by this Client + /// + public readonly List OwnedObjects = new List(); + } +} diff --git a/Runtime/Connection/NetworkClient.cs.meta b/Runtime/Connection/NetworkClient.cs.meta new file mode 100644 index 0000000..cbe3914 --- /dev/null +++ b/Runtime/Connection/NetworkClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23ca4d14911834b41a761c62fb23773e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Connection/PendingClient.cs b/Runtime/Connection/PendingClient.cs new file mode 100644 index 0000000..5bbf392 --- /dev/null +++ b/Runtime/Connection/PendingClient.cs @@ -0,0 +1,33 @@ +namespace Unity.Netcode +{ + /// + /// A class representing a client that is currently in the process of connecting + /// + public class PendingClient + { + /// + /// The ClientId of the client + /// + public ulong ClientId { get; internal set; } + + /// + /// The state of the connection process for the client + /// + public State ConnectionState { get; internal set; } + + /// + /// The states of a connection + /// + public enum State + { + /// + /// Waiting for client to send it's initial connection request + /// + PendingConnection, + /// + /// Waiting for client connection request to be approved + /// + PendingApproval + } + } +} diff --git a/Runtime/Connection/PendingClient.cs.meta b/Runtime/Connection/PendingClient.cs.meta new file mode 100644 index 0000000..0a83ab2 --- /dev/null +++ b/Runtime/Connection/PendingClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ffbc5b303de84a4196e24f503752218 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core.meta b/Runtime/Core.meta new file mode 100644 index 0000000..a95ca56 --- /dev/null +++ b/Runtime/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb92f4010a5924b408aca753b55bd5ae +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/IndexAllocator.cs b/Runtime/Core/IndexAllocator.cs new file mode 100644 index 0000000..892fd6d --- /dev/null +++ b/Runtime/Core/IndexAllocator.cs @@ -0,0 +1,388 @@ +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/IndexAllocator.cs.meta b/Runtime/Core/IndexAllocator.cs.meta new file mode 100644 index 0000000..b7c7632 --- /dev/null +++ b/Runtime/Core/IndexAllocator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd9e1475e8c8e4a6d935fe2409e3bd26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs new file mode 100644 index 0000000..0c13955 --- /dev/null +++ b/Runtime/Core/NetworkBehaviour.cs @@ -0,0 +1,676 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using System.Reflection; +using Unity.Collections; + +namespace Unity.Netcode +{ + /// + /// The base class to override to write network code. Inherits MonoBehaviour + /// + public abstract class NetworkBehaviour : MonoBehaviour + { +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `protected` + internal enum __RpcExecStage +#pragma warning restore IDE1006 // restore naming rule violation check + { + None = 0, + Server = 1, + Client = 2 + } + +#pragma warning disable IDE1006 // disable naming rule violation check + // NetworkBehaviourILPP will override this in derived classes to return the name of the concrete type + internal virtual string __getTypeName() => nameof(NetworkBehaviour); +#pragma warning restore IDE1006 // restore naming rule violation check + +#pragma warning disable 414 // disable assigned but its value is never used +#pragma warning disable IDE1006 // disable naming rule violation check + [NonSerialized] + // RuntimeAccessModifiersILPP will make this `protected` + internal __RpcExecStage __rpc_exec_stage = __RpcExecStage.None; +#pragma warning restore 414 // restore assigned but its value is never used +#pragma warning restore IDE1006 // restore naming rule violation check + +#pragma warning disable 414 // disable assigned but its value is never used +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `protected` + internal void __sendServerRpc(FastBufferWriter writer, uint rpcMethodId, ServerRpcParams rpcParams, RpcDelivery delivery) +#pragma warning restore 414 // restore assigned but its value is never used +#pragma warning restore IDE1006 // restore naming rule violation check + { + NetworkDelivery networkDelivery = NetworkDelivery.Reliable; + switch (delivery) + { + case RpcDelivery.Reliable: + networkDelivery = NetworkDelivery.ReliableFragmentedSequenced; + break; + case RpcDelivery.Unreliable: + if (writer.Length > MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE - sizeof(RpcMessage.RpcType) - sizeof(ulong) - sizeof(uint) - sizeof(ushort)) + { + throw new OverflowException("RPC parameters are too large for unreliable delivery."); + } + networkDelivery = NetworkDelivery.Unreliable; + break; + } + + var message = new RpcMessage + { + Header = new RpcMessage.HeaderData + { + Type = RpcMessage.RpcType.Server, + NetworkObjectId = NetworkObjectId, + NetworkBehaviourId = NetworkBehaviourId, + NetworkMethodId = rpcMethodId + }, + RpcData = writer + }; + + var rpcMessageSize = 0; + + // If we are a server/host then we just no op and send to ourself + if (IsHost || IsServer) + { + using var tempBuffer = new FastBufferReader(writer, Allocator.Temp); + var context = new NetworkContext + { + SenderId = NetworkManager.ServerClientId, + Timestamp = Time.realtimeSinceStartup, + SystemOwner = NetworkManager, + // header information isn't valid since it's not a real message. + // Passing false to canDefer prevents it being accessed. + Header = new MessageHeader() + }; + message.Handle(tempBuffer, context, NetworkManager, NetworkManager.ServerClientId, false); + rpcMessageSize = tempBuffer.Length; + } + else + { + rpcMessageSize = NetworkManager.SendMessage(message, networkDelivery, NetworkManager.ServerClientId); + } + + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (NetworkManager.__rpc_name_table.TryGetValue(rpcMethodId, out var rpcMethodName)) + { + NetworkManager.NetworkMetrics.TrackRpcSent( + NetworkManager.ServerClientId, + NetworkObject, + rpcMethodName, + __getTypeName(), + rpcMessageSize); + } +#endif + } + +#pragma warning disable 414 // disable assigned but its value is never used +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `protected` + internal unsafe void __sendClientRpc(FastBufferWriter writer, uint rpcMethodId, ClientRpcParams rpcParams, RpcDelivery delivery) +#pragma warning disable 414 // disable assigned but its value is never used +#pragma warning disable IDE1006 // disable naming rule violation check + { + NetworkDelivery networkDelivery = NetworkDelivery.Reliable; + switch (delivery) + { + case RpcDelivery.Reliable: + networkDelivery = NetworkDelivery.ReliableFragmentedSequenced; + break; + case RpcDelivery.Unreliable: + if (writer.Length > MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE - sizeof(RpcMessage.RpcType) - sizeof(ulong) - sizeof(uint) - sizeof(ushort)) + { + throw new OverflowException("RPC parameters are too large for unreliable delivery."); + } + networkDelivery = NetworkDelivery.Unreliable; + break; + } + + var message = new RpcMessage + { + Header = new RpcMessage.HeaderData + { + Type = RpcMessage.RpcType.Client, + NetworkObjectId = NetworkObjectId, + NetworkBehaviourId = NetworkBehaviourId, + NetworkMethodId = rpcMethodId + }, + RpcData = writer + }; + int messageSize; + + // 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 (rpcParams.Send.TargetClientIds != null) + { + foreach (var clientId in rpcParams.Send.TargetClientIds) + { + if (clientId == NetworkManager.ServerClientId) + { + shouldSendToHost = true; + break; + } + } + + messageSize = NetworkManager.SendMessage(message, networkDelivery, in rpcParams.Send.TargetClientIds); + } + else if (rpcParams.Send.TargetClientIdsNativeArray != null) + { + foreach (var clientId in rpcParams.Send.TargetClientIdsNativeArray) + { + if (clientId == NetworkManager.ServerClientId) + { + shouldSendToHost = true; + break; + } + } + + messageSize = NetworkManager.SendMessage(message, networkDelivery, rpcParams.Send.TargetClientIdsNativeArray.Value); + } + else + { + shouldSendToHost = IsHost; + messageSize = NetworkManager.SendMessage(message, networkDelivery, NetworkManager.ConnectedClientsIds); + } + + // If we are a server/host then we just no op and send to ourself + if (shouldSendToHost) + { + using var tempBuffer = new FastBufferReader(writer, Allocator.Temp); + var context = new NetworkContext + { + SenderId = NetworkManager.ServerClientId, + Timestamp = Time.realtimeSinceStartup, + SystemOwner = NetworkManager, + // header information isn't valid since it's not a real message. + // Passing false to canDefer prevents it being accessed. + Header = new MessageHeader() + }; + message.Handle(tempBuffer, context, NetworkManager, NetworkManager.ServerClientId, false); + messageSize = tempBuffer.Length; + } + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (NetworkManager.__rpc_name_table.TryGetValue(rpcMethodId, out var rpcMethodName)) + { + foreach (var client in NetworkManager.ConnectedClients) + { + NetworkManager.NetworkMetrics.TrackRpcSent( + client.Key, + NetworkObject, + rpcMethodName, + __getTypeName(), + messageSize); + } + } +#endif + } + + /// + /// 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 + /// + public NetworkManager NetworkManager => NetworkObject.NetworkManager; + + /// + /// Gets if the object is the the personal clients player object + /// + public bool IsLocalPlayer => NetworkObject.IsLocalPlayer; + + /// + /// Gets if the object is owned by the local player or if the object is the local player object + /// + public bool IsOwner => NetworkObject.IsOwner; + + /// + /// Gets if we are executing as server + /// + protected bool IsServer => IsRunning && NetworkManager.IsServer; + + /// + /// Gets if we are executing as client + /// + protected bool IsClient => IsRunning && NetworkManager.IsClient; + + /// + /// Gets if we are executing as Host, I.E Server and Client + /// + protected bool IsHost => IsRunning && NetworkManager.IsHost; + + private bool IsRunning => NetworkManager && NetworkManager.IsListening; + + /// + /// Gets Whether or not the object has a owner + /// + public bool IsOwnedByServer => NetworkObject.IsOwnedByServer; + + /// + /// 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; + + internal bool IsBehaviourEditable() + { + // Only server can MODIFY. So allow modification if network is either not running or we are server + return !m_NetworkObject || + (m_NetworkObject.NetworkManager == null || + !m_NetworkObject.NetworkManager.IsListening || + m_NetworkObject.NetworkManager.IsServer); + } + + /// + /// Gets the NetworkObject that owns this NetworkBehaviour instance + /// TODO: this needs an overhaul. It's expensive, it's ja little naive in how it looks for networkObject in + /// its parent and worst, it creates a puzzle if you are a NetworkBehaviour wanting to see if you're live or not + /// (e.g. editor code). All you want to do is find out if NetworkManager is null, but to do that you + /// need NetworkObject, but if you try and grab NetworkObject and NetworkManager isn't up you'll get + /// the warning below. This is why IsBehaviourEditable had to be created. Matt was going to re-do + /// how NetworkObject works but it was close to the release and too risky to change + /// + /// + public NetworkObject NetworkObject + { + get + { + if (m_NetworkObject == null) + { + m_NetworkObject = GetComponentInParent(); + } + + if (m_NetworkObject == null && NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Could not get {nameof(NetworkObject)} for the {nameof(NetworkBehaviour)}. Are you missing a {nameof(NetworkObject)} component?"); + } + + return m_NetworkObject; + } + } + + /// + /// Gets whether or not this NetworkBehaviour instance has a NetworkObject owner. + /// + public bool HasNetworkObject => NetworkObject != null; + + private NetworkObject m_NetworkObject = null; + + /// + /// Gets the NetworkId of the NetworkObject that owns this NetworkBehaviour + /// + public ulong NetworkObjectId => NetworkObject.NetworkObjectId; + + /// + /// Gets NetworkId for this NetworkBehaviour from the owner NetworkObject + /// + public ushort NetworkBehaviourId => NetworkObject.GetNetworkBehaviourOrderIndex(this); + + /// + /// Internally caches the Id of this behaviour in a NetworkObject. Makes look-up faster + /// + internal ushort NetworkBehaviourIdCache = 0; + + /// + /// Returns a the NetworkBehaviour with a given BehaviourId for the current NetworkObject + /// + /// The behaviourId to return + /// Returns NetworkBehaviour with given behaviourId + protected NetworkBehaviour GetNetworkBehaviour(ushort behaviourId) + { + return NetworkObject.GetNetworkBehaviourAtOrderIndex(behaviourId); + } + + /// + /// Gets the ClientId that owns the NetworkObject + /// + public ulong OwnerClientId => NetworkObject.OwnerClientId; + + /// + /// Gets called when the gets spawned, message handlers are ready to be registered and the network is setup. + /// + public virtual void OnNetworkSpawn() { } + + /// + /// Gets called when the gets despawned. Is called both on the server and clients. + /// + public virtual void OnNetworkDespawn() { } + + internal void InternalOnNetworkSpawn() + { + InitializeVariables(); + } + + internal void InternalOnNetworkDespawn() + { + + } + + /// + /// Gets called when the local client gains ownership of this object + /// + public virtual void OnGainedOwnership() { } + + /// + /// Gets called when we loose ownership of this object + /// + public virtual void OnLostOwnership() { } + + /// + /// Gets called when the parent NetworkObject of this NetworkBehaviour's NetworkObject has changed + /// + public virtual void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { } + + private bool m_VarInit = false; + + private readonly List> m_DeliveryMappedNetworkVariableIndices = new List>(); + private readonly List m_DeliveryTypesForNetworkVariableGroups = new List(); + internal readonly List NetworkVariableFields = new List(); + + private static Dictionary s_FieldTypes = new Dictionary(); + + private static FieldInfo[] GetFieldInfoForType(Type type) + { + if (!s_FieldTypes.ContainsKey(type)) + { + s_FieldTypes.Add(type, GetFieldInfoForTypeRecursive(type)); + } + + return s_FieldTypes[type]; + } + + private static FieldInfo[] GetFieldInfoForTypeRecursive(Type type, List list = null) + { + if (list == null) + { + list = new List(); + list.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + } + else + { + list.AddRange(type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)); + } + + if (type.BaseType != null && type.BaseType != typeof(NetworkBehaviour)) + { + return GetFieldInfoForTypeRecursive(type.BaseType, list); + } + + return list.OrderBy(x => x.Name, StringComparer.Ordinal).ToArray(); + } + + internal void InitializeVariables() + { + if (m_VarInit) + { + return; + } + + m_VarInit = true; + + FieldInfo[] sortedFields = GetFieldInfoForType(GetType()); + + for (int i = 0; i < sortedFields.Length; i++) + { + Type fieldType = sortedFields[i].FieldType; + + if (fieldType.IsSubclassOf(typeof(NetworkVariableBase))) + { + var instance = (NetworkVariableBase)sortedFields[i].GetValue(this); + + if (instance == null) + { + instance = (NetworkVariableBase)Activator.CreateInstance(fieldType, true); + sortedFields[i].SetValue(this, instance); + } + + instance.Initialize(this); + + var instanceNameProperty = fieldType.GetProperty(nameof(NetworkVariableBase.Name)); + var sanitizedVariableName = sortedFields[i].Name.Replace("<", string.Empty).Replace(">k__BackingField", string.Empty); + instanceNameProperty?.SetValue(instance, sanitizedVariableName); + + NetworkVariableFields.Add(instance); + } + } + + { + // Create index map for delivery types + var firstLevelIndex = new Dictionary(); + int secondLevelCounter = 0; + + for (int i = 0; i < NetworkVariableFields.Count; i++) + { + var networkDelivery = NetworkVariableBase.Delivery; + if (!firstLevelIndex.ContainsKey(networkDelivery)) + { + firstLevelIndex.Add(networkDelivery, secondLevelCounter); + m_DeliveryTypesForNetworkVariableGroups.Add(networkDelivery); + secondLevelCounter++; + } + + if (firstLevelIndex[networkDelivery] >= m_DeliveryMappedNetworkVariableIndices.Count) + { + m_DeliveryMappedNetworkVariableIndices.Add(new HashSet()); + } + + m_DeliveryMappedNetworkVariableIndices[firstLevelIndex[networkDelivery]].Add(i); + } + } + } + + internal void PreNetworkVariableWrite() + { + // reset our "which variables got written" data + NetworkVariableIndexesToReset.Clear(); + NetworkVariableIndexesToResetSet.Clear(); + } + + internal void PostNetworkVariableWrite() + { + // mark any variables we wrote as no longer dirty + for (int i = 0; i < NetworkVariableIndexesToReset.Count; i++) + { + NetworkVariableFields[NetworkVariableIndexesToReset[i]].ResetDirty(); + } + } + + internal void VariableUpdate(ulong clientId) + { + if (!m_VarInit) + { + InitializeVariables(); + } + + PreNetworkVariableWrite(); + NetworkVariableUpdate(clientId, NetworkBehaviourId); + } + + internal readonly List NetworkVariableIndexesToReset = new List(); + internal readonly HashSet NetworkVariableIndexesToResetSet = new HashSet(); + + private void NetworkVariableUpdate(ulong clientId, int behaviourIndex) + { + if (!CouldHaveDirtyNetworkVariables()) + { + return; + } + + if (NetworkManager.NetworkConfig.UseSnapshotDelta) + { + for (int k = 0; k < NetworkVariableFields.Count; k++) + { + NetworkManager.SnapshotSystem.Store(NetworkObjectId, behaviourIndex, k, NetworkVariableFields[k]); + } + } + + if (!NetworkManager.NetworkConfig.UseSnapshotDelta) + { + for (int j = 0; j < m_DeliveryMappedNetworkVariableIndices.Count; j++) + { + var shouldSend = false; + for (int k = 0; k < NetworkVariableFields.Count; k++) + { + if (NetworkVariableFields[k].ShouldWrite(clientId, IsServer)) + { + shouldSend = true; + } + } + + if (shouldSend) + { + 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); + using (tmpWriter) + { + message.Serialize(tmpWriter); + } + } + else + { + NetworkManager.SendMessage(message, m_DeliveryTypesForNetworkVariableGroups[j], clientId); + } + } + } + } + } + + private bool CouldHaveDirtyNetworkVariables() + { + // TODO: There should be a better way by reading one dirty variable vs. 'n' + for (int i = 0; i < NetworkVariableFields.Count; i++) + { + if (NetworkVariableFields[i].IsDirty()) + { + return true; + } + } + + return false; + } + + internal void MarkVariablesDirty() + { + for (int j = 0; j < NetworkVariableFields.Count; j++) + { + NetworkVariableFields[j].SetDirty(true); + } + } + + internal void WriteNetworkVariableData(FastBufferWriter writer, ulong clientId) + { + if (NetworkVariableFields.Count == 0) + { + return; + } + + for (int j = 0; j < NetworkVariableFields.Count; j++) + { + bool canClientRead = NetworkVariableFields[j].CanClientRead(clientId); + + if (canClientRead) + { + var writePos = writer.Position; + writer.WriteValueSafe((ushort)0); + var startPos = writer.Position; + NetworkVariableFields[j].WriteField(writer); + var size = writer.Position - startPos; + writer.Seek(writePos); + writer.WriteValueSafe((ushort)size); + writer.Seek(startPos + size); + } + else + { + writer.WriteValueSafe((ushort)0); + } + } + } + + internal void SetNetworkVariableData(FastBufferReader reader) + { + if (NetworkVariableFields.Count == 0) + { + return; + } + + for (int j = 0; j < NetworkVariableFields.Count; j++) + { + reader.ReadValueSafe(out ushort varSize); + if (varSize == 0) + { + continue; + } + + var readStartPos = reader.Position; + NetworkVariableFields[j].ReadField(reader); + + if (NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + { + if (reader.Position > (readStartPos + varSize)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Var data read too far. {reader.Position - (readStartPos + varSize)} bytes."); + } + + reader.Seek(readStartPos + varSize); + } + else if (reader.Position < (readStartPos + varSize)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Var data read too little. {(readStartPos + varSize) - reader.Position} bytes."); + } + + reader.Seek(readStartPos + varSize); + } + } + } + } + + /// + /// Gets the local instance of a object with a given NetworkId + /// + /// + /// + protected NetworkObject GetNetworkObject(ulong networkId) + { + return NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(networkId, out NetworkObject networkObject) ? networkObject : null; + } + + public virtual void OnDestroy() + { + // this seems odd to do here, but in fact especially in tests we can find ourselves + // here without having called InitializedVariables, which causes problems if any + // of those variables use native containers (e.g. NetworkList) as they won't be + // registered here and therefore won't be cleaned up. + // + // we should study to understand the initialization patterns + if (!m_VarInit) + { + InitializeVariables(); + } + + for (int i = 0; i < NetworkVariableFields.Count; i++) + { + NetworkVariableFields[i].Dispose(); + } + } + } +} diff --git a/Runtime/Core/NetworkBehaviour.cs.meta b/Runtime/Core/NetworkBehaviour.cs.meta new file mode 100644 index 0000000..c8ead95 --- /dev/null +++ b/Runtime/Core/NetworkBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c8ea6ec00590bd44a983c228bcaee727 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs b/Runtime/Core/NetworkBehaviourUpdater.cs new file mode 100644 index 0000000..aace5b8 --- /dev/null +++ b/Runtime/Core/NetworkBehaviourUpdater.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using Unity.Profiling; + +namespace Unity.Netcode +{ + public class NetworkBehaviourUpdater + { + private HashSet m_Touched = new HashSet(); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + private ProfilerMarker m_NetworkBehaviourUpdate = new ProfilerMarker($"{nameof(NetworkBehaviour)}.{nameof(NetworkBehaviourUpdate)}"); +#endif + + internal void NetworkBehaviourUpdate(NetworkManager networkManager) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_NetworkBehaviourUpdate.Begin(); +#endif + try + { + if (networkManager.IsServer) + { + m_Touched.Clear(); + for (int i = 0; i < networkManager.ConnectedClientsList.Count; i++) + { + var client = networkManager.ConnectedClientsList[i]; + var spawnedObjs = networkManager.SpawnManager.SpawnedObjectsList; + m_Touched.UnionWith(spawnedObjs); + foreach (var sobj in spawnedObjs) + { + if (sobj.IsNetworkVisibleTo(client.ClientId)) + { + // Sync just the variables for just the objects this client sees + for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) + { + sobj.ChildNetworkBehaviours[k].VariableUpdate(client.ClientId); + } + } + } + } + + // Now, reset all the no-longer-dirty variables + foreach (var sobj in m_Touched) + { + for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) + { + sobj.ChildNetworkBehaviours[k].PostNetworkVariableWrite(); + } + } + } + else + { + // when client updates the server, it tells it about all its objects + foreach (var sobj in networkManager.SpawnManager.SpawnedObjectsList) + { + if (sobj.IsOwner) + { + for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) + { + sobj.ChildNetworkBehaviours[k].VariableUpdate(networkManager.ServerClientId); + } + } + } + + // Now, reset all the no-longer-dirty variables + foreach (var sobj in networkManager.SpawnManager.SpawnedObjectsList) + { + for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) + { + sobj.ChildNetworkBehaviours[k].PostNetworkVariableWrite(); + } + } + } + } + finally + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_NetworkBehaviourUpdate.End(); +#endif + } + } + + } +} diff --git a/Runtime/Core/NetworkBehaviourUpdater.cs.meta b/Runtime/Core/NetworkBehaviourUpdater.cs.meta new file mode 100644 index 0000000..df9f980 --- /dev/null +++ b/Runtime/Core/NetworkBehaviourUpdater.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d084c01093b446878bcb76e5d7f3221e +timeCreated: 1622225163 \ No newline at end of file diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs new file mode 100644 index 0000000..249d3b4 --- /dev/null +++ b/Runtime/Core/NetworkManager.cs @@ -0,0 +1,1618 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; +#if UNITY_EDITOR +using UnityEditor; +#endif +#if MULTIPLAYER_TOOLS +using Unity.Multiplayer.Tools; +#endif +using Unity.Profiling; +using UnityEngine.SceneManagement; +using Debug = UnityEngine.Debug; + +namespace Unity.Netcode +{ + /// + /// The main component of the library + /// + [AddComponentMenu("Netcode/" + nameof(NetworkManager), -100)] + public class NetworkManager : MonoBehaviour, INetworkUpdateSystem + { +#pragma warning disable IDE1006 // disable naming rule violation check + + // RuntimeAccessModifiersILPP will make this `public` + internal delegate void RpcReceiveHandler(NetworkBehaviour behaviour, FastBufferReader reader, __RpcParams parameters); + + // RuntimeAccessModifiersILPP will make this `public` + internal static readonly Dictionary __rpc_func_table = new Dictionary(); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + // RuntimeAccessModifiersILPP will make this `public` + internal static readonly Dictionary __rpc_name_table = new Dictionary(); +#endif + +#pragma warning restore IDE1006 // restore naming rule violation check + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + private static ProfilerMarker s_SyncTime = new ProfilerMarker($"{nameof(NetworkManager)}.SyncTime"); + private static ProfilerMarker s_TransportPoll = new ProfilerMarker($"{nameof(NetworkManager)}.TransportPoll"); + private static ProfilerMarker s_TransportConnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportConnect"); + private static ProfilerMarker s_HandleIncomingData = new ProfilerMarker($"{nameof(NetworkManager)}.{nameof(HandleIncomingData)}"); + 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 float k_DefaultBufferSizeSec = 0.05f; // todo talk with UX/Product, find good default value for this + + 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; } + + private NetworkPrefabHandler m_PrefabHandler; + + public NetworkPrefabHandler PrefabHandler + { + get + { + if (m_PrefabHandler == null) + { + m_PrefabHandler = new NetworkPrefabHandler(); + } + + return m_PrefabHandler; + } + } + + private class NetworkManagerHooks : INetworkHooks + { + private NetworkManager m_NetworkManager; + + internal NetworkManagerHooks(NetworkManager manager) + { + m_NetworkManager = manager; + } + + public void OnBeforeSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery) + { + } + + public void OnAfterSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery, int messageSizeBytes) + { + } + + public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) + { + return true; + } + + public bool OnVerifyCanReceive(ulong senderId, Type messageType) + { + if (m_NetworkManager.PendingClients.TryGetValue(senderId, out PendingClient client) && + (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"); + } + + return false; + } + + return true; + } + } + + private class NetworkManagerMessageSender : IMessageSender + { + private NetworkManager m_NetworkManager; + + public NetworkManagerMessageSender(NetworkManager manager) + { + m_NetworkManager = manager; + } + + public void Send(ulong clientId, NetworkDelivery delivery, FastBufferWriter batchData) + { + + var length = batchData.Length; + //TODO: Transport needs to have a way to send it data without copying and allocating here. + var bytes = batchData.ToArray(); + var sendBuffer = new ArraySegment(bytes, 0, length); + + m_NetworkManager.NetworkConfig.NetworkTransport.Send(clientId, sendBuffer, delivery); + + } + } + + /// + /// Returns the to use as the override as could be defined within the NetworkPrefab list + /// Note: This should be used to create pools (with components) + /// under the scenario where you are using the Host model as it spawns everything locally. As such, the override + /// will not be applied when spawning locally on a Host. + /// Related Classes and Interfaces: + /// + /// + /// + /// the to be checked for a defined NetworkPrefab override + /// a that is either the override or if no overrides exist it returns the same as the one passed in as a parameter + public GameObject GetNetworkPrefabOverride(GameObject gameObject) + { + var networkObject = gameObject.GetComponent(); + if (networkObject != null) + { + if (NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(networkObject.GlobalObjectIdHash)) + { + switch (NetworkConfig.NetworkPrefabOverrideLinks[networkObject.GlobalObjectIdHash].Override) + { + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + { + return NetworkConfig.NetworkPrefabOverrideLinks[networkObject.GlobalObjectIdHash].OverridingTargetPrefab; + } + } + } + } + return gameObject; + } + + public NetworkTimeSystem NetworkTimeSystem { get; private set; } + + public NetworkTickSystem NetworkTickSystem { get; private set; } + + public NetworkTime LocalTime => NetworkTickSystem?.LocalTime ?? default; + + public NetworkTime ServerTime => NetworkTickSystem?.ServerTime ?? default; + + /// + /// Gets or sets if the NetworkManager should be marked as DontDestroyOnLoad + /// + [HideInInspector] public bool DontDestroy = true; + + /// + /// Gets or sets if the application should be set to run in background + /// + [HideInInspector] public bool RunInBackground = true; + + /// + /// The log level to use + /// + [HideInInspector] public LogLevel LogLevel = LogLevel.Normal; + + /// + /// The singleton instance of the NetworkManager + /// + public static NetworkManager Singleton { get; private set; } + + /// + /// Gets the SpawnManager for this NetworkManager + /// + public NetworkSpawnManager SpawnManager { get; private set; } + + public CustomMessagingManager CustomMessagingManager { get; private set; } + + public NetworkSceneManager SceneManager { get; private set; } + + /// + /// Gets the networkId of the server + /// + public ulong ServerClientId => NetworkConfig.NetworkTransport?.ServerClientId ?? + throw new NullReferenceException( + $"The transport in the active {nameof(NetworkConfig)} is null"); + + /// + /// Returns ServerClientId if IsServer or LocalClientId if not + /// + public ulong LocalClientId + { + get => IsServer ? NetworkConfig.NetworkTransport.ServerClientId : m_LocalClientId; + internal set => m_LocalClientId = value; + } + + private ulong m_LocalClientId; + + private Dictionary m_ConnectedClients = new Dictionary(); + + private List m_ConnectedClientsList = new List(); + + private List m_ConnectedClientIds = new List(); + + /// + /// Gets a dictionary of connected clients and their clientId keys. This is only accessible on the server. + /// + public IReadOnlyDictionary ConnectedClients + { + get + { + if (IsServer == false) + { + throw new NotServerException($"{nameof(ConnectedClients)} should only be accessed on server."); + } + return m_ConnectedClients; + } + } + + /// + /// Gets a list of connected clients. This is only accessible on the server. + /// + public IReadOnlyList ConnectedClientsList + { + get + { + if (IsServer == false) + { + throw new NotServerException($"{nameof(ConnectedClientsList)} should only be accessed on server."); + } + return m_ConnectedClientsList; + } + } + + /// + /// Gets a list of just the IDs of all connected clients. This is only accessible on the server. + /// + public IReadOnlyList ConnectedClientsIds + { + get + { + if (IsServer == false) + { + throw new NotServerException($"{nameof(m_ConnectedClientIds)} should only be accessed on server."); + } + return m_ConnectedClientIds; + } + } + + /// + /// Gets the local for this client. + /// + public NetworkClient LocalClient { get; internal set; } + + /// + /// Gets a dictionary of the clients that have been accepted by the transport but are still pending by the Netcode. This is only populated on the server. + /// + public readonly Dictionary PendingClients = new Dictionary(); + + /// + /// Gets Whether or not a server is running + /// + public bool IsServer { get; internal set; } + + /// + /// Gets Whether or not a client is running + /// + public bool IsClient { get; internal set; } + + /// + /// Gets if we are running as host + /// + public bool IsHost => IsServer && IsClient; + + /// + /// Gets Whether or not we are listening for connections + /// + public bool IsListening { get; internal set; } + + /// + /// Gets if we are connected as a client + /// + public bool IsConnectedClient { get; internal set; } + + /// + /// The callback to invoke once a client connects. This callback is only ran on the server and on the local client that connects. + /// + public event Action OnClientConnectedCallback = null; + + internal void InvokeOnClientConnectedCallback(ulong clientId) => OnClientConnectedCallback?.Invoke(clientId); + + /// + /// The callback to invoke when a client disconnects. This callback is only ran on the server and on the local client that disconnects. + /// + public event Action OnClientDisconnectCallback = null; + + internal void InvokeOnClientDisconnectCallback(ulong clientId) => OnClientDisconnectCallback?.Invoke(clientId); + + /// + /// The callback to invoke once the server is ready + /// + public event Action OnServerStarted = null; + + /// + /// Delegate type called when connection has been approved. This only has to be set on the server. + /// + /// If true, a player object will be created. Otherwise the client will have no object. + /// The prefabHash to use for the client. If createPlayerObject is false, this is ignored. If playerPrefabHash is null, the default player prefab is used. + /// 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); + + /// + /// 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); + + /// + /// The current NetworkConfig + /// + [HideInInspector] public NetworkConfig NetworkConfig; + + /// + /// The current host name we are connected to, used to validate certificate + /// + public string ConnectedHostname { get; private set; } + + internal INetworkMetrics NetworkMetrics { get; private set; } + + internal static event Action OnSingletonReady; + +#if UNITY_EDITOR + private void OnValidate() + { + if (NetworkConfig == null) + { + return; // May occur when the component is added + } + + if (GetComponentInChildren() != null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(NetworkManager)} cannot be a {nameof(NetworkObject)}."); + } + } + + var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene(); + + // If the scene is not dirty or the asset database is currently updating then we can skip updating the NetworkPrefab information + if (!activeScene.isDirty || EditorApplication.isUpdating) + { + return; + } + + // During OnValidate we will always clear out NetworkPrefabOverrideLinks and rebuild it + NetworkConfig.NetworkPrefabOverrideLinks.Clear(); + + // Check network prefabs and assign to dictionary for quick look up + for (int i = 0; i < NetworkConfig.NetworkPrefabs.Count; i++) + { + var networkPrefab = NetworkConfig.NetworkPrefabs[i]; + var networkPrefabGo = networkPrefab?.Prefab; + if (networkPrefabGo != null) + { + var networkObject = networkPrefabGo.GetComponent(); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogError($"Cannot register {PrefabDebugHelper(networkPrefab)}, it does not have a {nameof(NetworkObject)} component at its root"); + } + } + else + { + { + var childNetworkObjects = new List(); + networkPrefabGo.GetComponentsInChildren(true, childNetworkObjects); + if (childNetworkObjects.Count > 1) // total count = 1 root NetworkObject + n child NetworkObjects + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} has child {nameof(NetworkObject)}(s) but they will not be spawned across the network (unsupported {nameof(NetworkPrefab)} setup)"); + } + } + } + + // Default to the standard NetworkPrefab.Prefab's NetworkObject first + var globalObjectIdHash = networkObject.GlobalObjectIdHash; + + // Now check to see if it has an override + switch (networkPrefab.Override) + { + case NetworkPrefabOverride.Prefab: + { + if (NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride == null && + NetworkConfig.NetworkPrefabs[i].Prefab != null) + { + if (networkPrefab.SourcePrefabToOverride == null) + { + networkPrefab.SourcePrefabToOverride = networkPrefabGo; + } + + globalObjectIdHash = networkPrefab.SourcePrefabToOverride.GetComponent().GlobalObjectIdHash; + } + + break; + } + case NetworkPrefabOverride.Hash: + globalObjectIdHash = networkPrefab.SourceHashToOverride; + break; + } + + // Add to the NetworkPrefabOverrideLinks or handle a new (blank) entries + if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) + { + NetworkConfig.NetworkPrefabOverrideLinks.Add(globalObjectIdHash, networkPrefab); + } + else + { + // Duplicate entries can happen when adding a new entry into a list of existing entries + // Either this is user error or a new entry, either case we replace it with a new, blank, NetworkPrefab under this condition + NetworkConfig.NetworkPrefabs[i] = new NetworkPrefab(); + } + } + } + } + } +#endif + + private void Initialize(bool server) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo(nameof(Initialize)); + } + + this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); + this.RegisterNetworkUpdate(NetworkUpdateStage.PostLateUpdate); + + MessagingSystem = new MessagingSystem(new NetworkManagerMessageSender(this), this); + + MessagingSystem.Hook(new NetworkManagerHooks(this)); +#if DEVELOPMENT_BUILD || UNITY_EDITOR + MessagingSystem.Hook(new ProfilingHooks()); +#endif + +#if MULTIPLAYER_TOOLS + MessagingSystem.Hook(new MetricHooks(this)); +#endif + LocalClientId = ulong.MaxValue; + + PendingClients.Clear(); + m_ConnectedClients.Clear(); + m_ConnectedClientsList.Clear(); + m_ConnectedClientIds.Clear(); + LocalClient = null; + NetworkObject.OrphanChildren.Clear(); + + // Create spawn manager instance + SpawnManager = new NetworkSpawnManager(this); + + CustomMessagingManager = new CustomMessagingManager(this); + + SceneManager = new NetworkSceneManager(this); + + BehaviourUpdater = new NetworkBehaviourUpdater(); + + + if (NetworkMetrics == null) + { +#if MULTIPLAYER_TOOLS + NetworkMetrics = new NetworkMetrics(); +#else + NetworkMetrics = new NullNetworkMetrics(); +#endif + } + +#if MULTIPLAYER_TOOLS + NetworkSolutionInterface.SetInterface(new NetworkSolutionInterfaceParameters + { + NetworkObjectProvider = new NetworkObjectProvider(this), + }); +#endif + + if (NetworkConfig.NetworkTransport == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError("No transport has been selected!"); + } + + return; + } + + //This 'if' should never enter + if (SnapshotSystem != null) + { + SnapshotSystem.Dispose(); + SnapshotSystem = null; + } + + SnapshotSystem = new SnapshotSystem(this); + + if (server) + { + NetworkTimeSystem = NetworkTimeSystem.ServerTimeSystem(); + } + else + { + NetworkTimeSystem = new NetworkTimeSystem(1.0 / NetworkConfig.TickRate, k_DefaultBufferSizeSec, 0.2); + } + + NetworkTickSystem = new NetworkTickSystem(NetworkConfig.TickRate, 0, 0); + NetworkTickSystem.Tick += OnNetworkManagerTick; + + this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); + + // This is used to remove entries not needed or invalid + var removeEmptyPrefabs = new List(); + + // Always clear our prefab override links before building + NetworkConfig.NetworkPrefabOverrideLinks.Clear(); + + // Build the NetworkPrefabOverrideLinks dictionary + for (int i = 0; i < NetworkConfig.NetworkPrefabs.Count; i++) + { + var sourcePrefabGlobalObjectIdHash = (uint)0; + var targetPrefabGlobalObjectIdHash = (uint)0; + var networkObject = (NetworkObject)null; + if (NetworkConfig.NetworkPrefabs[i] == null || (NetworkConfig.NetworkPrefabs[i].Prefab == null && NetworkConfig.NetworkPrefabs[i].Override == NetworkPrefabOverride.None)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning( + $"{nameof(NetworkPrefab)} cannot be null ({nameof(NetworkPrefab)} at index: {i})"); + } + + removeEmptyPrefabs.Add(i); + continue; + } + else if (NetworkConfig.NetworkPrefabs[i].Override == NetworkPrefabOverride.None) + { + var networkPrefab = NetworkConfig.NetworkPrefabs[i]; + networkObject = networkPrefab.Prefab.GetComponent(); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{PrefabDebugHelper(networkPrefab)} is missing " + + $"a {nameof(NetworkObject)} component (entry will be ignored)."); + } + removeEmptyPrefabs.Add(i); + continue; + } + + // Otherwise get the GlobalObjectIdHash value + sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; + } + else // Validate Overrides + { + // Validate source prefab override values first + switch (NetworkConfig.NetworkPrefabs[i].Override) + { + case NetworkPrefabOverride.Hash: + { + if (NetworkConfig.NetworkPrefabs[i].SourceHashToOverride == 0) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourceHashToOverride)} is zero " + + "(entry will be ignored)."); + } + removeEmptyPrefabs.Add(i); + continue; + } + sourcePrefabGlobalObjectIdHash = NetworkConfig.NetworkPrefabs[i].SourceHashToOverride; + break; + } + case NetworkPrefabOverride.Prefab: + { + if (NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.SourcePrefabToOverride)} is null (entry will be ignored)."); + } + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {NetworkConfig.NetworkPrefabs[i].SourceHashToOverride} will be removed and ignored."); + removeEmptyPrefabs.Add(i); + continue; + } + else + { + networkObject = NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.GetComponent(); + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} ({NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.name}) " + + $"is missing a {nameof(NetworkObject)} component (entry will be ignored)."); + } + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry (\"{NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.name}\") will be removed and ignored."); + removeEmptyPrefabs.Add(i); + continue; + } + sourcePrefabGlobalObjectIdHash = networkObject.GlobalObjectIdHash; + } + break; + } + } + + // Validate target prefab override values next + if (NetworkConfig.NetworkPrefabs[i].OverridingTargetPrefab == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"{nameof(NetworkPrefab)} {nameof(NetworkPrefab.OverridingTargetPrefab)} is null!"); + } + removeEmptyPrefabs.Add(i); + switch (NetworkConfig.NetworkPrefabs[i].Override) + { + case NetworkPrefabOverride.Hash: + { + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry {NetworkConfig.NetworkPrefabs[i].SourceHashToOverride} will be removed and ignored."); + break; + } + case NetworkPrefabOverride.Prefab: + { + Debug.LogWarning($"{nameof(NetworkPrefab)} override entry ({NetworkConfig.NetworkPrefabs[i].SourcePrefabToOverride.name}) will be removed and ignored."); + break; + } + } + continue; + } + else + { + targetPrefabGlobalObjectIdHash = NetworkConfig.NetworkPrefabs[i].OverridingTargetPrefab.GetComponent().GlobalObjectIdHash; + } + } + + // Assign the appropriate GlobalObjectIdHash to the appropriate NetworkPrefab + if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(sourcePrefabGlobalObjectIdHash)) + { + if (NetworkConfig.NetworkPrefabs[i].Override == NetworkPrefabOverride.None) + { + NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, NetworkConfig.NetworkPrefabs[i]); + } + else + { + if (!NetworkConfig.OverrideToNetworkPrefab.ContainsKey(targetPrefabGlobalObjectIdHash)) + { + switch (NetworkConfig.NetworkPrefabs[i].Override) + { + case NetworkPrefabOverride.Prefab: + { + NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, NetworkConfig.NetworkPrefabs[i]); + NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); + } + break; + case NetworkPrefabOverride.Hash: + { + NetworkConfig.NetworkPrefabOverrideLinks.Add(sourcePrefabGlobalObjectIdHash, NetworkConfig.NetworkPrefabs[i]); + NetworkConfig.OverrideToNetworkPrefab.Add(targetPrefabGlobalObjectIdHash, sourcePrefabGlobalObjectIdHash); + } + break; + } + } + else + { + // This can happen if a user tries to make several GlobalObjectIdHash values point to the same target + Debug.LogError($"{nameof(NetworkPrefab)} (\"{networkObject.name}\") has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} target entry value of: {targetPrefabGlobalObjectIdHash}! Removing entry from list!"); + removeEmptyPrefabs.Add(i); + } + } + } + else + { + // This should never happen, but in the case it somehow does log an error and remove the duplicate entry + Debug.LogError($"{nameof(NetworkPrefab)} ({networkObject.name}) has a duplicate {nameof(NetworkObject.GlobalObjectIdHash)} source entry value of: {sourcePrefabGlobalObjectIdHash}! Removing entry from list!"); + removeEmptyPrefabs.Add(i); + } + } + + // If we have a player prefab, then we need to verify it is in the list of NetworkPrefabOverrideLinks for client side spawning. + if (NetworkConfig.PlayerPrefab != null) + { + var playerPrefabNetworkObject = NetworkConfig.PlayerPrefab.GetComponent(); + if (playerPrefabNetworkObject != null) + { + //In the event there is no NetworkPrefab entry (i.e. no override for default player prefab) + if (!NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(playerPrefabNetworkObject + .GlobalObjectIdHash)) + { + //Then add a new entry for the player prefab + var playerNetworkPrefab = new NetworkPrefab(); + playerNetworkPrefab.Prefab = NetworkConfig.PlayerPrefab; + NetworkConfig.NetworkPrefabs.Insert(0, playerNetworkPrefab); + NetworkConfig.NetworkPrefabOverrideLinks.Add(playerPrefabNetworkObject.GlobalObjectIdHash, + playerNetworkPrefab); + } + } + else + { + // Provide the name of the prefab with issues so the user can more easily find the prefab and fix it + Debug.LogError($"{nameof(NetworkConfig.PlayerPrefab)} (\"{NetworkConfig.PlayerPrefab.name}\") has no NetworkObject assigned to it!."); + } + } + + // Clear out anything that is invalid or not used (for invalid entries we already logged warnings to the user earlier) + // Iterate backwards so indices don't shift as we remove + for (int i = removeEmptyPrefabs.Count - 1; i >= 0; i--) + { + NetworkConfig.NetworkPrefabs.RemoveAt(removeEmptyPrefabs[i]); + } + + removeEmptyPrefabs.Clear(); + + NetworkConfig.NetworkTransport.OnTransportEvent += HandleRawTransportPoll; + + NetworkConfig.NetworkTransport.Initialize(); + } + + /// + /// Starts a server + /// + public bool StartServer() + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo("StartServer()"); + } + + if (IsServer || IsClient) + { + 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; + + SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); + + OnServerStarted?.Invoke(); + + return result; + } + + /// + /// Starts a client + /// + public bool StartClient() + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo(nameof(StartClient)); + } + + if (IsServer || IsClient) + { + 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(); + + IsServer = false; + IsClient = true; + IsListening = true; + + return result; + } + + /// + /// Starts a Host + /// + public bool StartHost() + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo(nameof(StartHost)); + } + + if (IsServer || IsClient) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning("Cannot start host 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(); + MessagingSystem.ClientConnected(ServerClientId); + LocalClientId = ServerClientId; + NetworkMetrics.SetConnectionId(LocalClientId); + + IsServer = true; + IsClient = true; + IsListening = true; + + if (NetworkConfig.ConnectionApproval) + { + InvokeConnectionApproval(NetworkConfig.ConnectionData, ServerClientId, + (createPlayerObject, playerPrefabHash, approved, position, rotation) => + { + // You cannot decline the local server. Force approved to true + if (!approved) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning( + "You cannot decline the host connection. The connection was automatically approved."); + } + } + + HandleApproval(ServerClientId, createPlayerObject, playerPrefabHash, true, position, rotation); + }); + } + else + { + HandleApproval(ServerClientId, NetworkConfig.PlayerPrefab != null, null, true, null, null); + } + + SpawnManager.ServerSpawnSceneObjectsOnStartSweep(); + + OnServerStarted?.Invoke(); + + return result; + } + + public void SetSingleton() + { + Singleton = this; + + OnSingletonReady?.Invoke(); + } + + private void OnEnable() + { + if (DontDestroy) + { + DontDestroyOnLoad(gameObject); + } + + if (RunInBackground) + { + Application.runInBackground = true; + } + + if (Singleton == null) + { + SetSingleton(); + } + } + + private void Awake() + { + UnityEngine.SceneManagement.SceneManager.sceneUnloaded += OnSceneUnloaded; + } + + // Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when unloading a scene with a NetworkManager + private void OnSceneUnloaded(Scene scene) + { + if (scene == gameObject.scene) + { + OnDestroy(); + } + } + + // Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when quitting the application. + private void OnApplicationQuit() + { + OnDestroy(); + } + + // Note that this gets also called manually by OnSceneUnloaded and OnApplicationQuit + private void OnDestroy() + { + Shutdown(); + + UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded; + + if (Singleton == this) + { + Singleton = null; + } + } + + /// + /// Globally shuts down the library. + /// Disconnects clients if connected and stops server if running. + /// + public void Shutdown() + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo(nameof(Shutdown)); + } + + if (IsServer) + { + // make sure all messages are flushed before transport disconnect clients + if (MessagingSystem != null) + { + MessagingSystem.ProcessSendQueues(); + } + + var disconnectedIds = new HashSet(); + + //Don't know if I have to disconnect the clients. I'm assuming the NetworkTransport does all the cleaning on shutdown. But this way the clients get a disconnect message from server (so long it does't get lost) + + foreach (KeyValuePair pair in ConnectedClients) + { + if (!disconnectedIds.Contains(pair.Key)) + { + disconnectedIds.Add(pair.Key); + + if (pair.Key == NetworkConfig.NetworkTransport.ServerClientId) + { + continue; + } + + NetworkConfig.NetworkTransport.DisconnectRemoteClient(pair.Key); + } + } + + foreach (KeyValuePair pair in PendingClients) + { + if (!disconnectedIds.Contains(pair.Key)) + { + disconnectedIds.Add(pair.Key); + if (pair.Key == NetworkConfig.NetworkTransport.ServerClientId) + { + continue; + } + + NetworkConfig.NetworkTransport.DisconnectRemoteClient(pair.Key); + } + } + } + + if (IsClient && IsConnectedClient) + { + // Client only, send disconnect to server + NetworkConfig.NetworkTransport.DisconnectLocalClient(); + } + + IsConnectedClient = false; + IsServer = false; + IsClient = false; + + this.UnregisterAllNetworkUpdates(); + + if (SnapshotSystem != null) + { + SnapshotSystem.Dispose(); + SnapshotSystem = null; + } + + if (NetworkTickSystem != null) + { + NetworkTickSystem.Tick -= OnNetworkManagerTick; + NetworkTickSystem = null; + } + + if (MessagingSystem != null) + { + MessagingSystem.Dispose(); + MessagingSystem = null; + } + + NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll; + + if (SpawnManager != null) + { + SpawnManager.DestroyNonSceneObjects(); + SpawnManager.ServerResetShudownStateForSceneObjects(); + + SpawnManager = null; + } + + if (SceneManager != null) + { + // Let the NetworkSceneManager clean up its two SceneEvenData instances + SceneManager.Dispose(); + SceneManager = null; + } + + if (CustomMessagingManager != null) + { + CustomMessagingManager = null; + } + + if (BehaviourUpdater != null) + { + BehaviourUpdater = null; + } + + // This is required for handling the potential scenario where multiple NetworkManager instances are created. + // See MTT-860 for more information + if (IsListening) + { + //The Transport is set during initialization, thus it is possible for the Transport to be null + NetworkConfig?.NetworkTransport?.Shutdown(); + } + + IsListening = false; + } + + // INetworkUpdateSystem + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + switch (updateStage) + { + case NetworkUpdateStage.EarlyUpdate: + OnNetworkEarlyUpdate(); + break; + case NetworkUpdateStage.PreUpdate: + OnNetworkPreUpdate(); + break; + case NetworkUpdateStage.PostLateUpdate: + OnNetworkPostLateUpdate(); + break; + } + } + + private void OnNetworkEarlyUpdate() + { + if (!IsListening) + { + return; + } + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportPoll.Begin(); +#endif + NetworkEvent networkEvent; + do + { + networkEvent = NetworkConfig.NetworkTransport.PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime); + HandleRawTransportPoll(networkEvent, clientId, payload, receiveTime); + // Only do another iteration if: there are no more messages AND (there is no limit to max events or we have processed less than the maximum) + } while (IsListening && networkEvent != NetworkEvent.Nothing); + + MessagingSystem.ProcessIncomingMessageQueue(); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportPoll.End(); +#endif + } + + // TODO Once we have a way to subscribe to NetworkUpdateLoop with order we can move this out of NetworkManager but for now this needs to be here because we need strict ordering. + private void OnNetworkPreUpdate() + { + if (IsServer == false && IsConnectedClient == false) + { + // As a client wait to run the time system until we are connected. + return; + } + + // Only update RTT here, server time is updated by time sync messages + var reset = NetworkTimeSystem.Advance(Time.deltaTime); + if (reset) + { + NetworkTickSystem.Reset(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime); + } + NetworkTickSystem.UpdateTick(NetworkTimeSystem.LocalTime, NetworkTimeSystem.ServerTime); + + if (IsServer == false) + { + NetworkTimeSystem.Sync(NetworkTimeSystem.LastSyncedServerTimeSec + Time.deltaTime, NetworkConfig.NetworkTransport.GetCurrentRtt(ServerClientId) / 1000d); + } + } + + private void OnNetworkPostLateUpdate() + { + MessagingSystem.ProcessSendQueues(); + NetworkMetrics.DispatchFrame(); + SpawnManager.CleanupStaleTriggers(); + } + + /// + /// 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() + { + // Do NetworkVariable updates + BehaviourUpdater.NetworkBehaviourUpdate(this); + + int timeSyncFrequencyTicks = (int)(k_TimeSyncFrequency * NetworkConfig.TickRate); + if (IsServer && NetworkTickSystem.ServerTime.Tick % timeSyncFrequencyTicks == 0) + { + SyncTime(); + } + } + + private void SendConnectionRequest() + { + var message = new ConnectionRequestMessage + { + ConfigHash = NetworkConfig.GetConfig(), + ShouldSendConnectionData = NetworkConfig.ConnectionApproval, + ConnectionData = NetworkConfig.ConnectionData + }; + SendMessage(message, NetworkDelivery.ReliableSequenced, ServerClientId); + } + + private IEnumerator ApprovalTimeout(ulong clientId) + { + NetworkTime timeStarted = LocalTime; + + //We yield every frame incase a pending client disconnects and someone else gets its connection id + while ((LocalTime - timeStarted).Time < NetworkConfig.ClientConnectionBufferTimeout && PendingClients.ContainsKey(clientId)) + { + yield return null; + } + + if (PendingClients.ContainsKey(clientId) && !ConnectedClients.ContainsKey(clientId)) + { + // Timeout + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo($"Client {clientId} Handshake Timed Out"); + } + + DisconnectClient(clientId); + } + } + + private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, ArraySegment payload, float receiveTime) + { + switch (networkEvent) + { + case NetworkEvent.Connect: +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportConnect.Begin(); +#endif + MessagingSystem.ClientConnected(clientId); + if (IsServer) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo("Client Connected"); + } + + PendingClients.Add(clientId, new PendingClient() + { + ClientId = clientId, + ConnectionState = PendingClient.State.PendingConnection + }); + + StartCoroutine(ApprovalTimeout(clientId)); + } + else + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo("Connected"); + } + + SendConnectionRequest(); + StartCoroutine(ApprovalTimeout(clientId)); + } + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportConnect.End(); +#endif + break; + case NetworkEvent.Data: + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo($"Incoming Data From {clientId}: {payload.Count} bytes"); + } + + HandleIncomingData(clientId, payload, receiveTime); + break; + } + case NetworkEvent.Disconnect: +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportDisconnect.Begin(); +#endif + + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo($"Disconnect Event From {clientId}"); + } + + if (IsServer) + { + OnClientDisconnectFromServer(clientId); + } + else + { + Shutdown(); + } + + OnClientDisconnectCallback?.Invoke(clientId); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportDisconnect.End(); +#endif + break; + } + } + + internal unsafe int SendMessage(in TMessageType message, NetworkDelivery delivery, in TClientIdListType clientIds) + where TMessageType : INetworkMessage + where TClientIdListType : IReadOnlyList + { + // Prevent server sending to itself + if (IsServer) + { + ulong* nonServerIds = stackalloc ulong[clientIds.Count]; + int newIdx = 0; + for (int idx = 0; idx < clientIds.Count; ++idx) + { + if (clientIds[idx] == ServerClientId) + { + continue; + } + + nonServerIds[newIdx++] = clientIds[idx]; + } + + if (newIdx == 0) + { + return 0; + } + return MessagingSystem.SendMessage(message, delivery, nonServerIds, newIdx); + } + return MessagingSystem.SendMessage(message, delivery, clientIds); + } + + internal unsafe int SendMessage(in T message, NetworkDelivery delivery, + ulong* clientIds, int numClientIds) + where T : INetworkMessage + { + // Prevent server sending to itself + if (IsServer) + { + ulong* nonServerIds = stackalloc ulong[numClientIds]; + int newIdx = 0; + for (int idx = 0; idx < numClientIds; ++idx) + { + if (clientIds[idx] == ServerClientId) + { + continue; + } + + nonServerIds[newIdx++] = clientIds[idx]; + } + + if (newIdx == 0) + { + return 0; + } + return MessagingSystem.SendMessage(message, delivery, nonServerIds, newIdx); + } + + return MessagingSystem.SendMessage(message, delivery, clientIds, numClientIds); + } + + internal unsafe int SendMessage(in T message, NetworkDelivery delivery, in NativeArray clientIds) + where T : INetworkMessage + { + return SendMessage(message, delivery, (ulong*)clientIds.GetUnsafePtr(), clientIds.Length); + } + + internal int SendMessage(in T message, NetworkDelivery delivery, ulong clientId) + where T : INetworkMessage + { + // Prevent server sending to itself + if (IsServer && clientId == ServerClientId) + { + return 0; + } + return MessagingSystem.SendMessage(message, delivery, clientId); + } + + internal void HandleIncomingData(ulong clientId, ArraySegment payload, float receiveTime) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_HandleIncomingData.Begin(); +#endif + + MessagingSystem.HandleIncomingData(clientId, payload, receiveTime); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_HandleIncomingData.End(); +#endif + } + + /// + /// Disconnects the remote client. + /// + /// The ClientId to disconnect + public void DisconnectClient(ulong clientId) + { + if (!IsServer) + { + throw new NotServerException("Only server can disconnect remote clients. Use StopClient instead."); + } + + OnClientDisconnectFromServer(clientId); + + NetworkConfig.NetworkTransport.DisconnectRemoteClient(clientId); + } + + private void OnClientDisconnectFromServer(ulong clientId) + { + PendingClients.Remove(clientId); + + if (ConnectedClients.TryGetValue(clientId, out NetworkClient networkClient)) + { + if (IsServer) + { + var playerObject = networkClient.PlayerObject; + if (playerObject != null) + { + if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].PlayerObject.GlobalObjectIdHash)) + { + PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].PlayerObject); + } + else + { + Destroy(playerObject.gameObject); + } + } + + for (int i = networkClient.OwnedObjects.Count - 1; i >= 0; i--) + { + var ownedObject = networkClient.OwnedObjects[i]; + if (ownedObject != null) + { + if (!ownedObject.DontDestroyWithOwner) + { + if (PrefabHandler.ContainsHandler(ConnectedClients[clientId].OwnedObjects[i] + .GlobalObjectIdHash)) + { + PrefabHandler.HandleNetworkPrefabDestroy(ConnectedClients[clientId].OwnedObjects[i]); + } + else + { + Destroy(ownedObject.gameObject); + } + } + 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); + } + } + + for (int i = 0; i < ConnectedClientsList.Count; i++) + { + if (ConnectedClientsList[i].ClientId == clientId) + { + m_ConnectedClientsList.RemoveAt(i); + break; + } + } + + for (int i = 0; i < ConnectedClientsIds.Count; i++) + { + if (ConnectedClientsIds[i] == clientId) + { + m_ConnectedClientIds.RemoveAt(i); + break; + } + } + + m_ConnectedClients.Remove(clientId); + } + MessagingSystem.ClientDisconnected(clientId); + } + + private void SyncTime() + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_SyncTime.Begin(); +#endif + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo("Syncing Time To Clients"); + } + + var message = new TimeSyncMessage + { + Tick = NetworkTickSystem.ServerTime.Tick + }; + SendMessage(message, NetworkDelivery.Unreliable, ConnectedClientsIds); +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_SyncTime.End(); +#endif + } + + /// + /// Server Side: Handles the approval of a client + /// + /// client being approved + /// whether we want to create a player or not + /// the GlobalObjectIdHash value for the Network Prefab to create as the player + /// Is the player approved or not? + /// Used when createPlayerObject is true, position of the player when spawned + /// Used when createPlayerObject is true, rotation of the player when spawned + internal void HandleApproval(ulong ownerClientId, bool createPlayerObject, uint? playerPrefabHash, bool approved, Vector3? position, Quaternion? rotation) + { + if (approved) + { + // Inform new client it got approved + PendingClients.Remove(ownerClientId); + + var client = new NetworkClient { ClientId = ownerClientId, }; + m_ConnectedClients.Add(ownerClientId, client); + m_ConnectedClientsList.Add(client); + m_ConnectedClientIds.Add(client.ClientId); + + if (createPlayerObject) + { + var networkObject = SpawnManager.CreateLocalNetworkObject(false, playerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, null, position, rotation); + SpawnManager.SpawnNetworkObjectLocally(networkObject, SpawnManager.GetNetworkObjectId(), false, true, ownerClientId, false); + + ConnectedClients[ownerClientId].PlayerObject = networkObject; + } + + // Server doesn't send itself the connection approved message + if (ownerClientId != ServerClientId) + { + var message = new ConnectionApprovedMessage + { + OwnerClientId = ownerClientId, + NetworkTick = LocalTime.Tick + }; + if (!NetworkConfig.EnableSceneManagement) + { + if (SpawnManager.SpawnedObjectsList.Count != 0) + { + message.SceneObjectCount = SpawnManager.SpawnedObjectsList.Count; + message.SpawnedObjectsList = SpawnManager.SpawnedObjectsList; + } + } + + SendMessage(message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId); + + // If scene management is enabled, then let NetworkSceneManager handle the initial scene and NetworkObject synchronization + if (!NetworkConfig.EnableSceneManagement) + { + InvokeOnClientConnectedCallback(ownerClientId); + } + else + { + SceneManager.SynchronizeNetworkObjects(ownerClientId); + } + } + else // Server just adds itself as an observer to all spawned NetworkObjects + { + SpawnManager.UpdateObservedNetworkObjects(ownerClientId); + InvokeOnClientConnectedCallback(ownerClientId); + } + + if (!createPlayerObject || (playerPrefabHash == null && NetworkConfig.PlayerPrefab == null)) + { + return; + } + + // Separating this into a contained function call for potential further future separation of when this notification is sent. + ApprovedPlayerSpawn(ownerClientId, playerPrefabHash ?? NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash); + } + else + { + PendingClients.Remove(ownerClientId); + NetworkConfig.NetworkTransport.DisconnectRemoteClient(ownerClientId); + } + } + + /// + /// Spawns the newly approved player + /// + /// new player client identifier + /// the prefab GlobalObjectIdHash value for this player + internal void ApprovedPlayerSpawn(ulong clientId, uint playerPrefabHash) + { + foreach (var clientPair in ConnectedClients) + { + if (clientPair.Key == clientId || + clientPair.Key == ServerClientId || // Server already spawned it + ConnectedClients[clientId].PlayerObject == null || + !ConnectedClients[clientId].PlayerObject.Observers.Contains(clientPair.Key)) + { + continue; //The new client. + } + + var message = new CreateObjectMessage + { + ObjectInfo = ConnectedClients[clientId].PlayerObject.GetMessageSceneObject(clientPair.Key, false) + }; + message.ObjectInfo.Header.Hash = playerPrefabHash; + message.ObjectInfo.Header.IsSceneObject = false; + message.ObjectInfo.Header.HasParent = false; + message.ObjectInfo.Header.IsPlayerObject = true; + message.ObjectInfo.Header.OwnerClientId = clientId; + var size = SendMessage(message, NetworkDelivery.ReliableFragmentedSequenced, clientPair.Key); + NetworkMetrics.TrackObjectSpawnSent(clientPair.Key, ConnectedClients[clientId].PlayerObject, size); + } + } + } +} diff --git a/Runtime/Core/NetworkManager.cs.meta b/Runtime/Core/NetworkManager.cs.meta new file mode 100644 index 0000000..7998b48 --- /dev/null +++ b/Runtime/Core/NetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 593a2fe42fa9d37498c96f9a383b6521 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/NetworkObject.cs b/Runtime/Core/NetworkObject.cs new file mode 100644 index 0000000..ea7e99a --- /dev/null +++ b/Runtime/Core/NetworkObject.cs @@ -0,0 +1,1155 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// A component used to identify that a GameObject in the network + /// + [AddComponentMenu("Netcode/" + nameof(NetworkObject), -99)] + [DisallowMultipleComponent] + public sealed class NetworkObject : MonoBehaviour + { + [HideInInspector] + [SerializeField] + internal uint GlobalObjectIdHash; + +#if UNITY_EDITOR + private void OnValidate() + { + GenerateGlobalObjectIdHash(); + } + + internal void GenerateGlobalObjectIdHash() + { + // do NOT regenerate GlobalObjectIdHash for NetworkPrefabs while Editor is in PlayMode + if (UnityEditor.EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name)) + { + return; + } + + // do NOT regenerate GlobalObjectIdHash if Editor is transitioning into or out of PlayMode + if (!UnityEditor.EditorApplication.isPlaying && UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) + { + return; + } + + var globalObjectIdString = UnityEditor.GlobalObjectId.GetGlobalObjectIdSlow(this).ToString(); + GlobalObjectIdHash = XXHash.Hash32(globalObjectIdString); + } +#endif + + /// + /// Gets the NetworkManager that owns this NetworkObject instance + /// + public NetworkManager NetworkManager => NetworkManagerOwner ?? NetworkManager.Singleton; + + /// + /// The NetworkManager that owns this NetworkObject. + /// This property controls where this NetworkObject belongs. + /// This property is null by default currently, which means that the above NetworkManager getter will return the Singleton. + /// In the future this is the path where alternative NetworkManagers should be injected for running multi NetworkManagers + /// + internal NetworkManager NetworkManagerOwner; + + private ulong m_NetworkObjectId; + + /// + /// Gets the unique Id of this object that is synced across the network + /// + public ulong NetworkObjectId { get; internal set; } + + /// + /// 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; + + /// + /// If true, the object will always be replicated as root on clients and the parent will be ignored. + /// + public bool AlwaysReplicateAsRoot; + + /// + /// Gets if this object is a player object + /// + public bool IsPlayerObject { get; internal set; } + + /// + /// Gets if the object is the the personal clients player object + /// + public bool IsLocalPlayer => NetworkManager != null && IsPlayerObject && OwnerClientId == NetworkManager.LocalClientId; + + /// + /// Gets if the object is owned by the local player or if the object is the local player object + /// + public bool IsOwner => NetworkManager != null && OwnerClientId == NetworkManager.LocalClientId; + + /// + /// Gets Whether or not the object is owned by anyone + /// + public bool IsOwnedByServer => NetworkManager != null && OwnerClientId == NetworkManager.ServerClientId; + + /// + /// Gets if the object has yet been spawned across the network + /// + public bool IsSpawned { get; internal set; } + + /// + /// Gets if the object is a SceneObject, null if it's not yet spawned but is a scene object. + /// + public bool? IsSceneObject { get; internal set; } + + /// + /// Gets whether or not the object should be automatically removed when the scene is unloaded. + /// + public bool DestroyWithScene { get; set; } + + /// + /// Delegate type for checking visibility + /// + /// The clientId to check visibility for + public delegate bool VisibilityDelegate(ulong clientId); + + /// + /// Delegate invoked when the netcode needs to know if the object should be visible to a client, if null it will assume true + /// + public VisibilityDelegate CheckObjectVisibility = null; + + /// + /// Delegate type for checking spawn options + /// + /// The clientId to check spawn options for + public delegate bool SpawnDelegate(ulong clientId); + + /// + /// Delegate invoked when the netcode needs to know if it should include the transform when spawning the object, if null it will assume true + /// + public SpawnDelegate IncludeTransformWhenSpawning = null; + + /// + /// Whether or not to destroy this object if it's owner is destroyed. + /// If false, the objects ownership will be given to the server. + /// + public bool DontDestroyWithOwner; + + /// + /// Whether or not to enable automatic NetworkObject parent synchronization. + /// + public bool AutoObjectParentSync = true; + + internal readonly HashSet Observers = new HashSet(); + +#if MULTIPLAYER_TOOLS + private string m_CachedNameForMetrics; +#endif + internal string GetNameForMetrics() + { +#if MULTIPLAYER_TOOLS + return m_CachedNameForMetrics ??= name; +#else + return null; +#endif + } + + /// + /// Returns Observers enumerator + /// + /// Observers enumerator + public HashSet.Enumerator GetObservers() + { + if (!IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + return Observers.GetEnumerator(); + } + + /// + /// Whether or not this object is visible to a specific client + /// + /// The clientId of the client + /// True if the client knows about the object + public bool IsNetworkVisibleTo(ulong clientId) + { + if (!IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + return Observers.Contains(clientId); + } + + private void Awake() + { + SetCachedParent(transform.parent); + } + + /// + /// Shows a previously hidden to a client + /// + /// The client to show the to + public void NetworkShow(ulong clientId) + { + if (!IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + if (!NetworkManager.IsServer) + { + throw new NotServerException("Only server can change visibility"); + } + + if (Observers.Contains(clientId)) + { + throw new VisibilityChangeException("The object is already visible"); + } + + if (NetworkManager.NetworkConfig.UseSnapshotSpawn) + { + SnapshotSpawn(clientId); + } + + Observers.Add(clientId); + + NetworkManager.SpawnManager.SendSpawnCallForObject(clientId, this); + } + + /// + /// Shows a list of previously hidden s to a client + /// + /// The s to show + /// The client to show the objects to + public static void NetworkShow(List networkObjects, ulong clientId) + { + if (networkObjects == null || networkObjects.Count == 0) + { + throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided"); + } + + NetworkManager networkManager = networkObjects[0].NetworkManager; + + if (!networkManager.IsServer) + { + throw new NotServerException("Only server can change visibility"); + } + + // Do the safety loop first to prevent putting the netcode in an invalid state. + for (int i = 0; i < networkObjects.Count; i++) + { + if (!networkObjects[i].IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + if (networkObjects[i].Observers.Contains(clientId)) + { + throw new VisibilityChangeException($"{nameof(NetworkObject)} with NetworkId: {networkObjects[i].NetworkObjectId} is already visible"); + } + + if (networkObjects[i].NetworkManager != networkManager) + { + throw new ArgumentNullException("All " + nameof(NetworkObject) + "s must belong to the same " + nameof(NetworkManager)); + } + } + + foreach (var networkObject in networkObjects) + { + networkObject.NetworkShow(clientId); + } + } + + /// + /// Hides a object from a specific client + /// + /// The client to hide the object for + public void NetworkHide(ulong clientId) + { + if (!IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + if (!NetworkManager.IsServer) + { + throw new NotServerException("Only server can change visibility"); + } + + if (!Observers.Contains(clientId)) + { + throw new VisibilityChangeException("The object is already hidden"); + } + + if (clientId == NetworkManager.ServerClientId) + { + throw new VisibilityChangeException("Cannot hide an object from the server"); + } + + + Observers.Remove(clientId); + + if (NetworkManager.NetworkConfig.UseSnapshotSpawn) + { + SnapshotDespawn(clientId); + } + else + { + var message = new DestroyObjectMessage + { + NetworkObjectId = NetworkObjectId + }; + // Send destroy call + var size = NetworkManager.SendMessage(message, NetworkDelivery.ReliableSequenced, clientId); + NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size); + } + } + + /// + /// Hides a list of objects from a client + /// + /// The objects to hide + /// The client to hide the objects from + public static void NetworkHide(List networkObjects, ulong clientId) + { + if (networkObjects == null || networkObjects.Count == 0) + { + throw new ArgumentNullException("At least one " + nameof(NetworkObject) + " has to be provided"); + } + + NetworkManager networkManager = networkObjects[0].NetworkManager; + + if (!networkManager.IsServer) + { + throw new NotServerException("Only server can change visibility"); + } + + if (clientId == networkManager.ServerClientId) + { + throw new VisibilityChangeException("Cannot hide an object from the server"); + } + + // Do the safety loop first to prevent putting the netcode in an invalid state. + for (int i = 0; i < networkObjects.Count; i++) + { + if (!networkObjects[i].IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + if (!networkObjects[i].Observers.Contains(clientId)) + { + throw new VisibilityChangeException($"{nameof(NetworkObject)} with {nameof(NetworkObjectId)}: {networkObjects[i].NetworkObjectId} is already hidden"); + } + + if (networkObjects[i].NetworkManager != networkManager) + { + throw new ArgumentNullException("All " + nameof(NetworkObject) + "s must belong to the same " + nameof(NetworkManager)); + } + } + + foreach (var networkObject in networkObjects) + { + networkObject.NetworkHide(clientId); + } + } + + private void OnDestroy() + { + 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)) + { + 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) + { + if (!NetworkManager.IsListening) + { + throw new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before spawning objects"); + } + + if (!NetworkManager.IsServer) + { + throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); + } + + 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)) + { + NetworkManager.SpawnManager.SendSpawnCallForObject(NetworkManager.ConnectedClientsList[i].ClientId, this); + } + } + } + + /// + /// Spawns this across the network. Can only be called from the Server + /// + /// Should the object be destroyed when the scene is changed + public void Spawn(bool destroyWithScene = false) + { + SpawnInternal(destroyWithScene, null, false); + } + + /// + /// Spawns a across the network with a given owner. Can only be called from server + /// + /// The clientId to own the object + /// Should the object be destroyed when the scene is changed + public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false) + { + SpawnInternal(destroyWithScene, clientId, false); + } + + /// + /// Spawns a across the network and makes it the player object for the given client + /// + /// The clientId whos player object this is + /// Should the object be destroyd when the scene is changed + public void SpawnAsPlayerObject(ulong clientId, bool destroyWithScene = false) + { + SpawnInternal(destroyWithScene, clientId, true); + } + + /// + /// Despawns the of this and sends a destroy message for it to all connected clients. + /// + /// (true) the will be destroyed (false) the will persist after being despawned + public void Despawn(bool destroy = true) + { + NetworkManager.SpawnManager.DespawnObject(this, destroy); + } + + /// + /// Removes all ownership of an object from any client. Can only be called from server + /// + public void RemoveOwnership() + { + NetworkManager.SpawnManager.RemoveOwnership(this); + } + + /// + /// Changes the owner of the object. Can only be called from server + /// + /// The new owner clientId + public void ChangeOwnership(ulong newOwnerClientId) + { + NetworkManager.SpawnManager.ChangeOwnership(this, newOwnerClientId); + } + + internal void InvokeBehaviourOnLostOwnership() + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + ChildNetworkBehaviours[i].OnLostOwnership(); + } + } + + internal void InvokeBehaviourOnGainedOwnership() + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + ChildNetworkBehaviours[i].OnGainedOwnership(); + } + } + + internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNetworkObject) + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + ChildNetworkBehaviours[i].OnNetworkObjectParentChanged(parentNetworkObject); + } + } + + private bool m_IsReparented; // Did initial parent (came from the scene hierarchy) change at runtime? + private ulong? m_LatestParent; // What is our last set parent NetworkObject's ID? + private Transform m_CachedParent; // What is our last set parent Transform reference? + + internal void SetCachedParent(Transform parentTransform) + { + m_CachedParent = parentTransform; + } + + internal (bool IsReparented, ulong? LatestParent) GetNetworkParenting() => (m_IsReparented, m_LatestParent); + + internal void SetNetworkParenting(bool isReparented, ulong? latestParent) + { + m_IsReparented = isReparented; + m_LatestParent = latestParent; + } + + public bool TrySetParent(Transform parent, bool worldPositionStays = true) + { + return TrySetParent(parent.GetComponent(), worldPositionStays); + } + + public bool TrySetParent(GameObject parent, bool worldPositionStays = true) + { + return TrySetParent(parent.GetComponent(), worldPositionStays); + } + + public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) + { + if (!AutoObjectParentSync) + { + return false; + } + + if (NetworkManager == null || !NetworkManager.IsListening) + { + return false; + } + + if (!NetworkManager.IsServer) + { + return false; + } + + if (!IsSpawned) + { + return false; + } + + if (parent == null) + { + return false; + } + + if (!parent.IsSpawned) + { + return false; + } + + transform.SetParent(parent.transform, worldPositionStays); + return true; + } + + private void OnTransformParentChanged() + { + if (!AutoObjectParentSync) + { + return; + } + + if (transform.parent == m_CachedParent) + { + return; + } + + if (NetworkManager == null || !NetworkManager.IsListening) + { + transform.parent = m_CachedParent; + Debug.LogException(new NotListeningException($"{nameof(NetworkManager)} is not listening, start a server or host before reparenting")); + return; + } + + if (!NetworkManager.IsServer) + { + transform.parent = m_CachedParent; + Debug.LogException(new NotServerException($"Only the server can reparent {nameof(NetworkObject)}s")); + return; + } + + if (!IsSpawned) + { + transform.parent = m_CachedParent; + Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented after being spawned")); + return; + } + + var parentTransform = transform.parent; + if (parentTransform != null) + { + var parentObject = transform.parent.GetComponent(); + if (parentObject == null) + { + transform.parent = m_CachedParent; + Debug.LogException(new InvalidParentException($"Invalid parenting, {nameof(NetworkObject)} moved under a non-{nameof(NetworkObject)} parent")); + return; + } + + if (!parentObject.IsSpawned) + { + transform.parent = m_CachedParent; + Debug.LogException(new SpawnStateException($"{nameof(NetworkObject)} can only be reparented under another spawned {nameof(NetworkObject)}")); + return; + } + + m_LatestParent = parentObject.NetworkObjectId; + } + else + { + m_LatestParent = null; + } + + m_IsReparented = true; + ApplyNetworkParenting(); + + var message = new ParentSyncMessage + { + NetworkObjectId = NetworkObjectId, + IsReparented = m_IsReparented, + IsLatestParentSet = m_LatestParent != null && m_LatestParent.HasValue, + LatestParent = m_LatestParent + }; + + unsafe + { + var maxCount = NetworkManager.ConnectedClientsIds.Count; + ulong* clientIds = stackalloc ulong[maxCount]; + int idx = 0; + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (Observers.Contains(clientId)) + { + clientIds[idx++] = clientId; + } + } + + NetworkManager.SendMessage(message, NetworkDelivery.ReliableSequenced, clientIds, idx); + } + } + + // We're keeping this set called OrphanChildren which contains NetworkObjects + // because at the time we initialize/spawn NetworkObject locally, we might not have its parent replicated from the other side + // + // For instance, if we're spawning NetworkObject 5 and its parent is 10, what should happen if we do not have 10 yet? + // let's say 10 is on the way to be replicated in a few frames and we could fix that parent-child relationship later. + // + // If you couldn't find your parent, we put you into OrphanChildren set and everytime we spawn another NetworkObject locally due to replication, + // we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one. + internal static HashSet OrphanChildren = new HashSet(); + + internal bool ApplyNetworkParenting() + { + if (!AutoObjectParentSync) + { + return false; + } + + if (!IsSpawned) + { + return false; + } + + if (!m_IsReparented) + { + return true; + } + + if (m_LatestParent == null || !m_LatestParent.HasValue) + { + m_CachedParent = null; + transform.parent = null; + + InvokeBehaviourOnNetworkObjectParentChanged(null); + return true; + } + + 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"); + } + } + return false; + } + + var parentObject = NetworkManager.SpawnManager.SpawnedObjects[m_LatestParent.Value]; + + m_CachedParent = parentObject.transform; + transform.parent = parentObject.transform; + + InvokeBehaviourOnNetworkObjectParentChanged(parentObject); + return true; + } + + internal static void CheckOrphanChildren() + { + var objectsToRemove = new List(); + foreach (var orphanObject in OrphanChildren) + { + if (orphanObject.ApplyNetworkParenting()) + { + objectsToRemove.Add(orphanObject); + } + } + foreach (var networkObject in objectsToRemove) + { + OrphanChildren.Remove(networkObject); + } + } + + internal void InvokeBehaviourNetworkSpawn() + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + ChildNetworkBehaviours[i].InternalOnNetworkSpawn(); + ChildNetworkBehaviours[i].OnNetworkSpawn(); + } + } + + internal void InvokeBehaviourNetworkDespawn() + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + ChildNetworkBehaviours[i].InternalOnNetworkDespawn(); + ChildNetworkBehaviours[i].OnNetworkDespawn(); + } + } + + private List m_ChildNetworkBehaviours; + + internal List ChildNetworkBehaviours + { + get + { + if (m_ChildNetworkBehaviours != null) + { + return m_ChildNetworkBehaviours; + } + + m_ChildNetworkBehaviours = new List(); + var networkBehaviours = GetComponentsInChildren(true); + for (int i = 0; i < networkBehaviours.Length; i++) + { + if (networkBehaviours[i].NetworkObject == this) + { + m_ChildNetworkBehaviours.Add(networkBehaviours[i]); + } + } + + return m_ChildNetworkBehaviours; + } + } + + internal void WriteNetworkVariableData(FastBufferWriter writer, ulong clientId) + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + var behavior = ChildNetworkBehaviours[i]; + behavior.InitializeVariables(); + behavior.WriteNetworkVariableData(writer, clientId); + } + } + + internal void MarkVariablesDirty() + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + var behavior = ChildNetworkBehaviours[i]; + behavior.MarkVariablesDirty(); + } + } + + internal void SetNetworkVariableData(FastBufferReader reader) + { + for (int i = 0; i < ChildNetworkBehaviours.Count; i++) + { + var behaviour = ChildNetworkBehaviours[i]; + behaviour.InitializeVariables(); + behaviour.SetNetworkVariableData(reader); + } + } + + internal ushort GetNetworkBehaviourOrderIndex(NetworkBehaviour instance) + { + // read the cached index, and verify it first + if (instance.NetworkBehaviourIdCache < ChildNetworkBehaviours.Count) + { + if (ChildNetworkBehaviours[instance.NetworkBehaviourIdCache] == instance) + { + return instance.NetworkBehaviourIdCache; + } + + // invalid cached id reset + instance.NetworkBehaviourIdCache = default; + } + + for (ushort i = 0; i < ChildNetworkBehaviours.Count; i++) + { + if (ChildNetworkBehaviours[i] == instance) + { + // cache the id, for next query + instance.NetworkBehaviourIdCache = i; + return i; + } + } + + return 0; + } + + internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index) + { + if (index >= ChildNetworkBehaviours.Count) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Behaviour index was out of bounds. Did you mess up the order of your {nameof(NetworkBehaviour)}s?"); + } + + return null; + } + + return ChildNetworkBehaviours[index]; + } + + internal struct SceneObject + { + public struct HeaderData + { + public ulong NetworkObjectId; + public ulong OwnerClientId; + public uint Hash; + + public bool IsPlayerObject; + public bool HasParent; + public bool IsSceneObject; + public bool HasTransform; + public bool IsReparented; + public bool HasNetworkVariables; + } + + public HeaderData Header; + + //If(Metadata.HasParent) + public ulong ParentObjectId; + + //If(Metadata.HasTransform) + public struct TransformData + { + public Vector3 Position; + public Quaternion Rotation; + } + + public TransformData Transform; + + //If(Metadata.IsReparented) + public bool IsLatestParentSet; + + //If(IsLatestParentSet) + public ulong? LatestParent; + + public NetworkObject OwnerObject; + public ulong TargetClientId; + + public unsafe void Serialize(FastBufferWriter writer) + { + if (!writer.TryBeginWrite( + sizeof(HeaderData) + + (Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0) + + (Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0) + + (Header.IsReparented + ? FastBufferWriter.GetWriteSize(IsLatestParentSet) + + (IsLatestParentSet ? FastBufferWriter.GetWriteSize() : 0) + : 0))) + { + throw new OverflowException("Could not serialize SceneObject: Out of buffer space."); + } + + writer.WriteValue(Header); + + if (Header.HasParent) + { + writer.WriteValue(ParentObjectId); + } + + if (Header.HasTransform) + { + writer.WriteValue(Transform); + } + + if (Header.IsReparented) + { + writer.WriteValue(IsLatestParentSet); + if (IsLatestParentSet) + { + writer.WriteValue((ulong)LatestParent); + } + } + + if (Header.HasNetworkVariables) + { + OwnerObject.WriteNetworkVariableData(writer, TargetClientId); + } + } + + public unsafe void Deserialize(FastBufferReader reader) + { + if (!reader.TryBeginRead(sizeof(HeaderData))) + { + throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); + } + reader.ReadValue(out Header); + if (!reader.TryBeginRead( + (Header.HasParent ? FastBufferWriter.GetWriteSize(ParentObjectId) : 0) + + (Header.HasTransform ? FastBufferWriter.GetWriteSize(Transform) : 0) + + (Header.IsReparented ? FastBufferWriter.GetWriteSize(IsLatestParentSet) : 0))) + { + throw new OverflowException("Could not deserialize SceneObject: Out of buffer space."); + } + + if (Header.HasParent) + { + reader.ReadValue(out ParentObjectId); + } + + if (Header.HasTransform) + { + reader.ReadValue(out Transform); + } + + if (Header.IsReparented) + { + reader.ReadValue(out IsLatestParentSet); + if (IsLatestParentSet) + { + reader.ReadValueSafe(out ulong latestParent); + LatestParent = latestParent; + } + } + } + } + + internal SceneObject GetMessageSceneObject(ulong targetClientId, bool includeNetworkVariableData = true) + { + var obj = new SceneObject + { + Header = new SceneObject.HeaderData + { + IsPlayerObject = IsPlayerObject, + NetworkObjectId = NetworkObjectId, + OwnerClientId = OwnerClientId, + IsSceneObject = IsSceneObject ?? true, + Hash = HostCheckForGlobalObjectIdHashOverride(), + HasNetworkVariables = includeNetworkVariableData + }, + OwnerObject = this, + TargetClientId = targetClientId + }; + + NetworkObject parentNetworkObject = null; + + if (!AlwaysReplicateAsRoot && transform.parent != null) + { + parentNetworkObject = transform.parent.GetComponent(); + } + + if (parentNetworkObject) + { + obj.Header.HasParent = true; + obj.ParentObjectId = parentNetworkObject.NetworkObjectId; + } + if (IncludeTransformWhenSpawning == null || IncludeTransformWhenSpawning(OwnerClientId)) + { + obj.Header.HasTransform = true; + obj.Transform = new SceneObject.TransformData + { + Position = transform.position, + Rotation = transform.rotation + }; + } + + var (isReparented, latestParent) = GetNetworkParenting(); + obj.Header.IsReparented = isReparented; + if (isReparented) + { + var isLatestParentSet = latestParent != null && latestParent.HasValue; + obj.IsLatestParentSet = isLatestParentSet; + if (isLatestParentSet) + { + obj.LatestParent = latestParent.Value; + } + } + + return obj; + } + + /// + /// Used to deserialize a serialized scene object which occurs + /// when the client is approved or during a scene transition + /// + /// Deserialized scene object data + /// reader for the NetworkVariable data + /// NetworkManager instance + /// optional to use NetworkObject deserialized + internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBufferReader variableData, NetworkManager networkManager) + { + Vector3? position = null; + Quaternion? rotation = null; + ulong? parentNetworkId = null; + + if (sceneObject.Header.HasTransform) + { + position = sceneObject.Transform.Position; + rotation = sceneObject.Transform.Rotation; + } + + if (sceneObject.Header.HasParent) + { + parentNetworkId = sceneObject.ParentObjectId; + } + + //Attempt to create a local NetworkObject + var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject( + sceneObject.Header.IsSceneObject, sceneObject.Header.Hash, + sceneObject.Header.OwnerClientId, parentNetworkId, position, rotation, sceneObject.Header.IsReparented); + + networkObject?.SetNetworkParenting(sceneObject.Header.IsReparented, sceneObject.LatestParent); + + if (networkObject == null) + { + // Log the error that the NetworkObject failed to construct + Debug.LogError($"Failed to spawn {nameof(NetworkObject)} for Hash {sceneObject.Header.Hash}."); + + // If we failed to load this NetworkObject, then skip past the network variable data + variableData.ReadValueSafe(out ushort varSize); + variableData.Seek(variableData.Position + varSize); + + // We have nothing left to do here. + return null; + } + + // Spawn the NetworkObject( + networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, variableData, false); + + return networkObject; + } + + /// + /// Only applies to Host mode. + /// Will return the registered source NetworkPrefab's GlobalObjectIdHash if one exists. + /// Server and Clients will always return the NetworkObject's GlobalObjectIdHash. + /// + /// + internal uint HostCheckForGlobalObjectIdHashOverride() + { + if (NetworkManager.IsHost) + { + if (NetworkManager.PrefabHandler.ContainsHandler(this)) + { + var globalObjectIdHash = NetworkManager.PrefabHandler.GetSourceGlobalObjectIdHash(GlobalObjectIdHash); + return globalObjectIdHash == 0 ? GlobalObjectIdHash : globalObjectIdHash; + } + else + if (NetworkManager.NetworkConfig.OverrideToNetworkPrefab.ContainsKey(GlobalObjectIdHash)) + { + return NetworkManager.NetworkConfig.OverrideToNetworkPrefab[GlobalObjectIdHash]; + } + } + + return GlobalObjectIdHash; + } + } +} diff --git a/Runtime/Core/NetworkObject.cs.meta b/Runtime/Core/NetworkObject.cs.meta new file mode 100644 index 0000000..d865988 --- /dev/null +++ b/Runtime/Core/NetworkObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d5a57f767e5e46a458fc5d3c628d0cbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/NetworkUpdateLoop.cs b/Runtime/Core/NetworkUpdateLoop.cs new file mode 100644 index 0000000..b9c52c1 --- /dev/null +++ b/Runtime/Core/NetworkUpdateLoop.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.LowLevel; +using UnityEngine.PlayerLoop; + +namespace Unity.Netcode +{ + /// + /// Defines the required interface of a network update system being executed by the network update loop. + /// + public interface INetworkUpdateSystem + { + void NetworkUpdate(NetworkUpdateStage updateStage); + } + + /// + /// Defines network update stages being executed by the network update loop. + /// + public enum NetworkUpdateStage : byte + { + Unset = 0, // Default + Initialization = 1, + EarlyUpdate = 2, + FixedUpdate = 3, + PreUpdate = 4, + Update = 5, + PreLateUpdate = 6, + PostLateUpdate = 7 + } + + /// + /// Represents the network update loop injected into low-level player loop in Unity. + /// + public static class NetworkUpdateLoop + { + private static Dictionary> s_UpdateSystem_Sets; + private static Dictionary s_UpdateSystem_Arrays; + private const int k_UpdateSystem_InitialArrayCapacity = 1024; + + static NetworkUpdateLoop() + { + s_UpdateSystem_Sets = new Dictionary>(); + s_UpdateSystem_Arrays = new Dictionary(); + + foreach (NetworkUpdateStage updateStage in Enum.GetValues(typeof(NetworkUpdateStage))) + { + s_UpdateSystem_Sets.Add(updateStage, new HashSet()); + s_UpdateSystem_Arrays.Add(updateStage, new INetworkUpdateSystem[k_UpdateSystem_InitialArrayCapacity]); + } + } + + /// + /// Registers a network update system to be executed in all network update stages. + /// + public static void RegisterAllNetworkUpdates(this INetworkUpdateSystem updateSystem) + { + foreach (NetworkUpdateStage updateStage in Enum.GetValues(typeof(NetworkUpdateStage))) + { + RegisterNetworkUpdate(updateSystem, updateStage); + } + } + + /// + /// Registers a network update system to be executed in a specific network update stage. + /// + public static void RegisterNetworkUpdate(this INetworkUpdateSystem updateSystem, NetworkUpdateStage updateStage = NetworkUpdateStage.Update) + { + var sysSet = s_UpdateSystem_Sets[updateStage]; + if (!sysSet.Contains(updateSystem)) + { + sysSet.Add(updateSystem); + + int setLen = sysSet.Count; + var sysArr = s_UpdateSystem_Arrays[updateStage]; + int arrLen = sysArr.Length; + + if (setLen > arrLen) + { + // double capacity + sysArr = s_UpdateSystem_Arrays[updateStage] = new INetworkUpdateSystem[arrLen *= 2]; + } + + sysSet.CopyTo(sysArr); + + if (setLen < arrLen) + { + // null terminator + sysArr[setLen] = null; + } + } + } + + /// + /// Unregisters a network update system from all network update stages. + /// + public static void UnregisterAllNetworkUpdates(this INetworkUpdateSystem updateSystem) + { + foreach (NetworkUpdateStage updateStage in Enum.GetValues(typeof(NetworkUpdateStage))) + { + UnregisterNetworkUpdate(updateSystem, updateStage); + } + } + + /// + /// Unregisters a network update system from a specific network update stage. + /// + public static void UnregisterNetworkUpdate(this INetworkUpdateSystem updateSystem, NetworkUpdateStage updateStage = NetworkUpdateStage.Update) + { + var sysSet = s_UpdateSystem_Sets[updateStage]; + if (sysSet.Contains(updateSystem)) + { + sysSet.Remove(updateSystem); + + int setLen = sysSet.Count; + var sysArr = s_UpdateSystem_Arrays[updateStage]; + int arrLen = sysArr.Length; + + sysSet.CopyTo(sysArr); + + if (setLen < arrLen) + { + // null terminator + sysArr[setLen] = null; + } + } + } + + /// + /// The current network update stage being executed. + /// + public static NetworkUpdateStage UpdateStage; + + private static void RunNetworkUpdateStage(NetworkUpdateStage updateStage) + { + UpdateStage = updateStage; + + var sysArr = s_UpdateSystem_Arrays[updateStage]; + int arrLen = sysArr.Length; + for (int curIdx = 0; curIdx < arrLen; curIdx++) + { + var curSys = sysArr[curIdx]; + if (curSys == null) + { + // null terminator + break; + } + + curSys.NetworkUpdate(updateStage); + } + } + + internal struct NetworkInitialization + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkInitialization), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.Initialization) + }; + } + } + + internal struct NetworkEarlyUpdate + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkEarlyUpdate), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.EarlyUpdate) + }; + } + } + + internal struct NetworkFixedUpdate + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkFixedUpdate), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.FixedUpdate) + }; + } + } + + internal struct NetworkPreUpdate + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkPreUpdate), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.PreUpdate) + }; + } + } + + internal struct NetworkUpdate + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkUpdate), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.Update) + }; + } + } + + internal struct NetworkPreLateUpdate + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkPreLateUpdate), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.PreLateUpdate) + }; + } + } + + internal struct NetworkPostLateUpdate + { + public static PlayerLoopSystem CreateLoopSystem() + { + return new PlayerLoopSystem + { + type = typeof(NetworkPostLateUpdate), + updateDelegate = () => RunNetworkUpdateStage(NetworkUpdateStage.PostLateUpdate) + }; + } + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Initialize() + { + UnregisterLoopSystems(); + RegisterLoopSystems(); + } + + private enum LoopSystemPosition + { + After, + Before + } + + private static bool TryAddLoopSystem(ref PlayerLoopSystem parentLoopSystem, PlayerLoopSystem childLoopSystem, Type anchorSystemType, LoopSystemPosition loopSystemPosition) + { + int systemPosition = -1; + if (anchorSystemType != null) + { + for (int i = 0; i < parentLoopSystem.subSystemList.Length; i++) + { + var subsystem = parentLoopSystem.subSystemList[i]; + if (subsystem.type == anchorSystemType) + { + systemPosition = loopSystemPosition == LoopSystemPosition.After ? i + 1 : i; + break; + } + } + } + else + { + systemPosition = loopSystemPosition == LoopSystemPosition.After ? parentLoopSystem.subSystemList.Length : 0; + } + + if (systemPosition == -1) + { + return false; + } + + var newSubsystemList = new PlayerLoopSystem[parentLoopSystem.subSystemList.Length + 1]; + + // begin = systemsBefore + systemsAfter + // + systemsBefore + if (systemPosition > 0) + { + Array.Copy(parentLoopSystem.subSystemList, newSubsystemList, systemPosition); + } + // + childSystem + newSubsystemList[systemPosition] = childLoopSystem; + // + systemsAfter + if (systemPosition < parentLoopSystem.subSystemList.Length) + { + Array.Copy(parentLoopSystem.subSystemList, systemPosition, newSubsystemList, systemPosition + 1, parentLoopSystem.subSystemList.Length - systemPosition); + } + // end = systemsBefore + childSystem + systemsAfter + + parentLoopSystem.subSystemList = newSubsystemList; + + return true; + } + + private static bool TryRemoveLoopSystem(ref PlayerLoopSystem parentLoopSystem, Type childSystemType) + { + int systemPosition = -1; + for (int i = 0; i < parentLoopSystem.subSystemList.Length; i++) + { + var subsystem = parentLoopSystem.subSystemList[i]; + if (subsystem.type == childSystemType) + { + systemPosition = i; + break; + } + } + + if (systemPosition == -1) + { + return false; + } + + var newSubsystemList = new PlayerLoopSystem[parentLoopSystem.subSystemList.Length - 1]; + + // begin = systemsBefore + childSystem + systemsAfter + // + systemsBefore + if (systemPosition > 0) + { + Array.Copy(parentLoopSystem.subSystemList, newSubsystemList, systemPosition); + } + // + systemsAfter + if (systemPosition < parentLoopSystem.subSystemList.Length - 1) + { + Array.Copy(parentLoopSystem.subSystemList, systemPosition + 1, newSubsystemList, systemPosition, parentLoopSystem.subSystemList.Length - systemPosition - 1); + } + // end = systemsBefore + systemsAfter + + parentLoopSystem.subSystemList = newSubsystemList; + + return true; + } + + internal static void RegisterLoopSystems() + { + var rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + for (int i = 0; i < rootPlayerLoop.subSystemList.Length; i++) + { + ref var currentSystem = ref rootPlayerLoop.subSystemList[i]; + + if (currentSystem.type == typeof(Initialization)) + { + TryAddLoopSystem(ref currentSystem, NetworkInitialization.CreateLoopSystem(), null, LoopSystemPosition.After); + } + else if (currentSystem.type == typeof(EarlyUpdate)) + { + TryAddLoopSystem(ref currentSystem, NetworkEarlyUpdate.CreateLoopSystem(), typeof(EarlyUpdate.ScriptRunDelayedStartupFrame), LoopSystemPosition.Before); + } + else if (currentSystem.type == typeof(FixedUpdate)) + { + TryAddLoopSystem(ref currentSystem, NetworkFixedUpdate.CreateLoopSystem(), typeof(FixedUpdate.ScriptRunBehaviourFixedUpdate), LoopSystemPosition.Before); + } + else if (currentSystem.type == typeof(PreUpdate)) + { + TryAddLoopSystem(ref currentSystem, NetworkPreUpdate.CreateLoopSystem(), typeof(PreUpdate.PhysicsUpdate), LoopSystemPosition.Before); + } + else if (currentSystem.type == typeof(Update)) + { + TryAddLoopSystem(ref currentSystem, NetworkUpdate.CreateLoopSystem(), typeof(Update.ScriptRunBehaviourUpdate), LoopSystemPosition.Before); + } + else if (currentSystem.type == typeof(PreLateUpdate)) + { + TryAddLoopSystem(ref currentSystem, NetworkPreLateUpdate.CreateLoopSystem(), typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate), LoopSystemPosition.Before); + } + else if (currentSystem.type == typeof(PostLateUpdate)) + { + TryAddLoopSystem(ref currentSystem, NetworkPostLateUpdate.CreateLoopSystem(), typeof(PostLateUpdate.PlayerSendFrameComplete), LoopSystemPosition.After); + } + } + + PlayerLoop.SetPlayerLoop(rootPlayerLoop); + } + + internal static void UnregisterLoopSystems() + { + var rootPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + for (int i = 0; i < rootPlayerLoop.subSystemList.Length; i++) + { + ref var currentSystem = ref rootPlayerLoop.subSystemList[i]; + + if (currentSystem.type == typeof(Initialization)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkInitialization)); + } + else if (currentSystem.type == typeof(EarlyUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkEarlyUpdate)); + } + else if (currentSystem.type == typeof(FixedUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkFixedUpdate)); + } + else if (currentSystem.type == typeof(PreUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPreUpdate)); + } + else if (currentSystem.type == typeof(Update)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkUpdate)); + } + else if (currentSystem.type == typeof(PreLateUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPreLateUpdate)); + } + else if (currentSystem.type == typeof(PostLateUpdate)) + { + TryRemoveLoopSystem(ref currentSystem, typeof(NetworkPostLateUpdate)); + } + } + + PlayerLoop.SetPlayerLoop(rootPlayerLoop); + } + } +} diff --git a/Runtime/Core/NetworkUpdateLoop.cs.meta b/Runtime/Core/NetworkUpdateLoop.cs.meta new file mode 100644 index 0000000..403ee6e --- /dev/null +++ b/Runtime/Core/NetworkUpdateLoop.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0cd9c24b9acfd4e82a71c795f37235c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/SnapshotRTT.cs b/Runtime/Core/SnapshotRTT.cs new file mode 100644 index 0000000..207160f --- /dev/null +++ b/Runtime/Core/SnapshotRTT.cs @@ -0,0 +1,93 @@ +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/SnapshotRTT.cs.meta b/Runtime/Core/SnapshotRTT.cs.meta new file mode 100644 index 0000000..92cc411 --- /dev/null +++ b/Runtime/Core/SnapshotRTT.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 69c3c1c5a885d4aed99ee2e1fa40f763 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/SnapshotSystem.cs b/Runtime/Core/SnapshotSystem.cs new file mode 100644 index 0000000..b8fe48e --- /dev/null +++ b/Runtime/Core/SnapshotSystem.cs @@ -0,0 +1,1062 @@ +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; + } + + // 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 Snapshot + { + // todo --M1-- functionality to grow these will be needed in a later milestone + private const int k_MaxVariables = 2000; + private int m_MaxSpawns = 100; + private int m_MaxDespawns = 100; + + private const int k_BufferSize = 30000; + + 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; + + internal NetworkManager NetworkManager; + + // indexed by ObjectId + internal Dictionary TickAppliedSpawn = new Dictionary(); + internal Dictionary TickAppliedDespawn = new Dictionary(); + + /// + /// Constructor + /// Allocated a MemoryStream to be reused for this Snapshot + /// + internal Snapshot() + { + // 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[m_MaxSpawns]; + Despawns = new SnapshotDespawnCommand[m_MaxDespawns]; + } + + 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 (!NetworkManager.IsServer) + { + clientList.Add(NetworkManager.ServerClientId); + } + else + { + foreach (var clientId in NetworkManager.ConnectedClientsIds) + { + if (clientId != NetworkManager.ServerClientId) + { + clientList.Add(clientId); + } + } + } + + return clientList; + } + + internal void AddSpawn(SnapshotSpawnCommand command) + { + if (NumSpawns >= m_MaxSpawns) + { + Array.Resize(ref Spawns, 2 * m_MaxSpawns); + m_MaxSpawns = m_MaxSpawns * 2; + // Debug.Log($"[JEFF] spawn size is now {m_MaxSpawns}"); + } + + if (NumSpawns < m_MaxSpawns) + { + 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 >= m_MaxDespawns) + { + Array.Resize(ref Despawns, 2 * m_MaxDespawns); + m_MaxDespawns = m_MaxDespawns * 2; + // Debug.Log($"[JEFF] despawn size is now {m_MaxDespawns}"); + } + + if (NumDespawns < m_MaxDespawns) + { + if (command.TargetClientIds == default) + { + command.TargetClientIds = GetClientList(); + } + if (command.TargetClientIds.Count > 0) + { + Despawns[NumDespawns] = command; + NumDespawns++; + } + } + } + + 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 ReadSpawns(in SnapshotDataMessage message) + { + 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}"); + + if (spawnCommand.ParentNetworkId == spawnCommand.NetworkObjectId) + { + var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(false, spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, null, spawnCommand.ObjectPosition, spawnCommand.ObjectRotation); + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, false); + } + else + { + var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(false, spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, spawnCommand.ParentNetworkId, spawnCommand.ObjectPosition, spawnCommand.ObjectRotation); + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, false); + } + } + 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}"); + + NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(despawnCommand.NetworkObjectId, + out NetworkObject networkObject); + + NetworkManager.SpawnManager.OnDespawnObject(networkObject, true); + } + } + + 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 = NetworkManager.SpawnManager.SpawnedObjects; + + if (spawnedObjects.ContainsKey(key.NetworkObjectId)) + { + var behaviour = spawnedObjects[key.NetworkObjectId] + .GetNetworkBehaviourAtOrderIndex(key.BehaviourIndex); + return behaviour.NetworkVariableFields[key.VariableIndex]; + } + + return null; + } + } + + + 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 class SnapshotSystem : INetworkUpdateSystem, IDisposable + { + // temporary, debugging sentinels + internal const ushort SentinelBefore = 0x4246; + internal const ushort SentinelAfter = 0x89CE; + + private NetworkManager m_NetworkManager = default; + private Snapshot m_Snapshot = default; + + // by clientId + private Dictionary m_ClientData = new Dictionary(); + private Dictionary m_ConnectionRtts = new Dictionary(); + + private int m_CurrentTick = NetworkTickSystem.NoTick; + + /// + /// Constructor + /// + /// Registers the snapshot system for early updates, keeps reference to the NetworkManager + internal SnapshotSystem(NetworkManager networkManager) + { + m_Snapshot = new Snapshot(); + + m_NetworkManager = networkManager; + m_Snapshot.NetworkManager = networkManager; + + this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); + } + + 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_NetworkManager.NetworkConfig.UseSnapshotDelta && !m_NetworkManager.NetworkConfig.UseSnapshotSpawn) + { + return; + } + + if (updateStage == NetworkUpdateStage.EarlyUpdate) + { + var tick = m_NetworkManager.NetworkTickSystem.LocalTime.Tick; + + if (tick != m_CurrentTick) + { + m_CurrentTick = tick; + if (m_NetworkManager.IsServer) + { + for (int i = 0; i < m_NetworkManager.ConnectedClientsList.Count; i++) + { + var clientId = m_NetworkManager.ConnectedClientsList[i].ClientId; + + // don't send to ourselves + if (clientId != m_NetworkManager.ServerClientId) + { + SendSnapshot(clientId); + } + } + } + else if (m_NetworkManager.IsConnectedClient) + { + SendSnapshot(m_NetworkManager.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)m_Snapshot.Allocator.Range, + + // todo --M1-- + // this sends the whole buffer + // we'll need to build a per-client list + SendMainBuffer = m_Snapshot.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); + + m_NetworkManager.SendMessage(message, NetworkDelivery.Unreliable, clientId); + + 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 (m_Snapshot.NumSpawns > 0) + { + clientData.NextSpawnIndex %= m_Snapshot.NumSpawns; + } + else + { + clientData.NextSpawnIndex = 0; + } + + if (m_Snapshot.NumDespawns > 0) + { + clientData.NextDespawnIndex %= m_Snapshot.NumDespawns; + } + else + { + clientData.NextDespawnIndex = 0; + } + + message.Spawns = new NativeList(m_Snapshot.NumSpawns, Allocator.TempJob); + message.Despawns = new NativeList(m_Snapshot.NumDespawns, Allocator.TempJob); + var spawnUsage = 0; + + for (var j = 0; j < m_Snapshot.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 (m_Snapshot.Spawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteSpawn(m_Snapshot.Spawns[index])*/) + { + spawnUsage += FastBufferWriter.GetWriteSize(); + + // limit spawn sizes, compare current pos to very first position we wrote to + if (spawnUsage > m_NetworkManager.NetworkConfig.SnapshotMaxSpawnUsage) + { + overSize = true; + break; + } + var sentSpawn = m_Snapshot.GetSpawnData(clientData, in m_Snapshot.Spawns[index], out var spawn); + message.Spawns.Add(spawn); + + m_Snapshot.Spawns[index].TimesWritten++; + clientData.SentSpawns.Add(sentSpawn); + spawnWritten++; + } + clientData.NextSpawnIndex = (clientData.NextSpawnIndex + 1) % m_Snapshot.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 < m_Snapshot.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 (m_Snapshot.Despawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteDespawn(m_Snapshot.Despawns[index])*/) + { + spawnUsage += FastBufferWriter.GetWriteSize(); + + // limit spawn sizes, compare current pos to very first position we wrote to + if (spawnUsage > m_NetworkManager.NetworkConfig.SnapshotMaxSpawnUsage) + { + overSize = true; + break; + } + var sentDespawn = m_Snapshot.GetDespawnData(clientData, in m_Snapshot.Despawns[index], out var despawn); + message.Despawns.Add(despawn); + m_Snapshot.Despawns[index].TimesWritten++; + clientData.SentSpawns.Add(sentDespawn); + despawnWritten++; + } + clientData.NextDespawnIndex = (clientData.NextDespawnIndex + 1) % m_Snapshot.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(m_Snapshot.LastEntry, Allocator.TempJob); + for (var i = 0; i < m_Snapshot.LastEntry; i++) + { + var entryMeta = m_Snapshot.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; + m_Snapshot.AddSpawn(command); + + // Debug.Log($"[Spawn] {command.NetworkObjectId} {command.TickWritten}"); + } + + internal void Despawn(SnapshotDespawnCommand command) + { + command.TickWritten = m_CurrentTick; + m_Snapshot.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_NetworkManager.NetworkTickSystem.LocalTime.Tick; + + int pos = m_Snapshot.Find(k); + if (pos == Entry.NotFound) + { + pos = m_Snapshot.AddEntry(k); + } + + m_Snapshot.Entries[pos].Key.TickWritten = k.TickWritten; + + WriteVariableToSnapshot(m_Snapshot, networkVariable, pos); + } + + private unsafe void WriteVariableToSnapshot(Snapshot snapshot, 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, Allocator.Temp); + using (varBuffer) + { + networkVariable.WriteDelta(varBuffer); + if (varBuffer.Length > snapshot.Entries[index].Length) + { + // allocate this Entry's buffer + snapshot.AllocateEntry(ref snapshot.Entries[index], index, (int)varBuffer.Length); + } + + fixed (byte* buffer = snapshot.MainBuffer) + { + UnsafeUtility.MemCpy(buffer + snapshot.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 + } + + m_Snapshot.ReadBuffer(message); + m_Snapshot.ReadIndex(message); + m_Snapshot.ReadAcks(clientId, m_ClientData[clientId], message, GetConnectionRtt(clientId)); + m_Snapshot.ReadSpawns(message); + } + + // 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 < m_Snapshot.LastEntry; i++) + { + table += string.Format("NetworkVariable {0}:{1}:{2} written {5}, range [{3}, {4}] ", m_Snapshot.Entries[i].Key.NetworkObjectId, m_Snapshot.Entries[i].Key.BehaviourIndex, + m_Snapshot.Entries[i].Key.VariableIndex, m_Snapshot.Entries[i].Position, m_Snapshot.Entries[i].Position + m_Snapshot.Entries[i].Length, m_Snapshot.Entries[i].Key.TickWritten); + + for (int j = 0; j < m_Snapshot.Entries[i].Length && j < 4; j++) + { + table += m_Snapshot.MainBuffer[m_Snapshot.Entries[i].Position + j].ToString("X2") + " "; + } + + table += "\n"; + } + + table += "=== Spawns ===\n"; + + for (int i = 0; i < m_Snapshot.NumSpawns; i++) + { + string targets = ""; + foreach (var target in m_Snapshot.Spawns[i].TargetClientIds) + { + targets += target.ToString() + ", "; + } + table += $"Spawn Object Id {m_Snapshot.Spawns[i].NetworkObjectId}, Tick {m_Snapshot.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/Core/SnapshotSystem.cs.meta b/Runtime/Core/SnapshotSystem.cs.meta new file mode 100644 index 0000000..27ffd35 --- /dev/null +++ b/Runtime/Core/SnapshotSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c275febadb27c4d18b41218e3353b84b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Exceptions.meta b/Runtime/Exceptions.meta new file mode 100644 index 0000000..d8caa1b --- /dev/null +++ b/Runtime/Exceptions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2643fec0e8509fd409e5aceeba931323 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Exceptions/InvalidParentException.cs b/Runtime/Exceptions/InvalidParentException.cs new file mode 100644 index 0000000..66a3679 --- /dev/null +++ b/Runtime/Exceptions/InvalidParentException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Exception thrown when the new parent candidate of the NetworkObject is not valid + /// + public class InvalidParentException : Exception + { + public InvalidParentException() { } + public InvalidParentException(string message) : base(message) { } + public InvalidParentException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/Runtime/Exceptions/InvalidParentException.cs.meta b/Runtime/Exceptions/InvalidParentException.cs.meta new file mode 100644 index 0000000..12b58cc --- /dev/null +++ b/Runtime/Exceptions/InvalidParentException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abe1766318991412cb1aba96c4dbcc4e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Exceptions/NetworkConfigurationException.cs b/Runtime/Exceptions/NetworkConfigurationException.cs new file mode 100644 index 0000000..3e1efd2 --- /dev/null +++ b/Runtime/Exceptions/NetworkConfigurationException.cs @@ -0,0 +1,28 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Exception thrown when a change to a configuration is wrong + /// + public class NetworkConfigurationException : Exception + { + /// + /// Constructs a NetworkConfigurationException + /// + public NetworkConfigurationException() { } + + /// + /// Constructs a NetworkConfigurationException with a message + /// + /// The exception message + public NetworkConfigurationException(string message) : base(message) { } + + /// + /// Constructs a NetworkConfigurationException with a message and a inner exception + /// + /// The exception message + /// The inner exception + public NetworkConfigurationException(string message, Exception inner) : base(message, inner) { } + } +} diff --git a/Runtime/Exceptions/NetworkConfigurationException.cs.meta b/Runtime/Exceptions/NetworkConfigurationException.cs.meta new file mode 100644 index 0000000..e28f918 --- /dev/null +++ b/Runtime/Exceptions/NetworkConfigurationException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f50fae9dd20afbd438766cc330b188a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Exceptions/NotListeningException.cs b/Runtime/Exceptions/NotListeningException.cs new file mode 100644 index 0000000..f345432 --- /dev/null +++ b/Runtime/Exceptions/NotListeningException.cs @@ -0,0 +1,28 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Exception thrown when the operation require NetworkManager to be listening. + /// + public class NotListeningException : Exception + { + /// + /// Constructs a NotListeningException + /// + public NotListeningException() { } + + /// + /// Constructs a NotListeningException with a message + /// + /// The exception message + public NotListeningException(string message) : base(message) { } + + /// + /// Constructs a NotListeningException with a message and a inner exception + /// + /// The exception message + /// The inner exception + public NotListeningException(string message, Exception inner) : base(message, inner) { } + } +} diff --git a/Runtime/Exceptions/NotListeningException.cs.meta b/Runtime/Exceptions/NotListeningException.cs.meta new file mode 100644 index 0000000..1e465d7 --- /dev/null +++ b/Runtime/Exceptions/NotListeningException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc58b328162929140aeb42e6a4a9354b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Exceptions/NotServerException.cs b/Runtime/Exceptions/NotServerException.cs new file mode 100644 index 0000000..036b6b3 --- /dev/null +++ b/Runtime/Exceptions/NotServerException.cs @@ -0,0 +1,28 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Exception thrown when the operation can only be done on the server + /// + public class NotServerException : Exception + { + /// + /// Constructs a NotServerException + /// + public NotServerException() { } + + /// + /// Constructs a NotServerException with a message + /// + /// The exception message + public NotServerException(string message) : base(message) { } + + /// + /// Constructs a NotServerException with a message and a inner exception + /// + /// The exception message + /// The inner exception + public NotServerException(string message, Exception inner) : base(message, inner) { } + } +} diff --git a/Runtime/Exceptions/NotServerException.cs.meta b/Runtime/Exceptions/NotServerException.cs.meta new file mode 100644 index 0000000..f168972 --- /dev/null +++ b/Runtime/Exceptions/NotServerException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 751e55ce1418ecd4e9b08fef9d8e9591 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Exceptions/SpawnStateException.cs b/Runtime/Exceptions/SpawnStateException.cs new file mode 100644 index 0000000..712ddef --- /dev/null +++ b/Runtime/Exceptions/SpawnStateException.cs @@ -0,0 +1,33 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Exception thrown when an object is not yet spawned + /// + public class SpawnStateException : Exception + { + /// + /// Constructs a SpawnStateException + /// + public SpawnStateException() { } + + /// + /// Constructs a SpawnStateException with a message + /// + /// The exception message + public SpawnStateException(string message) : base(message) { } + + /// + /// Constructs a SpawnStateException with a message and a inner exception + /// + /// The exception message + /// The inner exception + public SpawnStateException(string message, Exception inner) : base(message, inner) { } + } + + public class InvalidChannelException : Exception + { + public InvalidChannelException(string message) : base(message) { } + } +} diff --git a/Runtime/Exceptions/SpawnStateException.cs.meta b/Runtime/Exceptions/SpawnStateException.cs.meta new file mode 100644 index 0000000..e5c4b2c --- /dev/null +++ b/Runtime/Exceptions/SpawnStateException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 70c2a851ae78c6e49b34c7e4ca46305f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Exceptions/VisibilityChangeException.cs b/Runtime/Exceptions/VisibilityChangeException.cs new file mode 100644 index 0000000..2a89cb3 --- /dev/null +++ b/Runtime/Exceptions/VisibilityChangeException.cs @@ -0,0 +1,28 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Exception thrown when a visibility change fails + /// + public class VisibilityChangeException : Exception + { + /// + /// Constructs a VisibilityChangeException + /// + public VisibilityChangeException() { } + + /// + /// Constructs a VisibilityChangeException with a message + /// + /// The exception message + public VisibilityChangeException(string message) : base(message) { } + + /// + /// Constructs a VisibilityChangeException with a message and a inner exception + /// + /// The exception message + /// The inner exception + public VisibilityChangeException(string message, Exception inner) : base(message, inner) { } + } +} diff --git a/Runtime/Exceptions/VisibilityChangeException.cs.meta b/Runtime/Exceptions/VisibilityChangeException.cs.meta new file mode 100644 index 0000000..6a600a8 --- /dev/null +++ b/Runtime/Exceptions/VisibilityChangeException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85d754bbea7d74a48918ebe05833e7ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Hashing.meta b/Runtime/Hashing.meta new file mode 100644 index 0000000..a1ee329 --- /dev/null +++ b/Runtime/Hashing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e0b5218305f20f9419d00c4ab2adabef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Hashing/XXHash.meta b/Runtime/Hashing/XXHash.meta new file mode 100644 index 0000000..f1fa98a --- /dev/null +++ b/Runtime/Hashing/XXHash.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2c61e8fe9a68a486fbbc3128d233ded2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Hashing/XXHash/LICENSE b/Runtime/Hashing/XXHash/LICENSE new file mode 100644 index 0000000..6b55f78 --- /dev/null +++ b/Runtime/Hashing/XXHash/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015, 2016 Sedat Kapanoglu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Runtime/Hashing/XXHash/LICENSE.meta b/Runtime/Hashing/XXHash/LICENSE.meta new file mode 100644 index 0000000..c6b28aa --- /dev/null +++ b/Runtime/Hashing/XXHash/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cf89ecbf6f9954c8ea6d0848b1e79d87 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Hashing/XXHash/XXHash.cs b/Runtime/Hashing/XXHash/XXHash.cs new file mode 100644 index 0000000..cfcb9d6 --- /dev/null +++ b/Runtime/Hashing/XXHash/XXHash.cs @@ -0,0 +1,318 @@ +// +// Copyright (c) 2015-2019 Sedat Kapanoglu +// MIT License (see LICENSE file for details) +// + +// @mfatihmar (Unity): Modified for Unity support + +using System.Text; +using System.Runtime.CompilerServices; + +namespace Unity.Netcode +{ + /// + /// XXHash implementation. + /// + internal static class XXHash + { + private const ulong k_Prime64v1 = 11400714785074694791ul; + private const ulong k_Prime64v2 = 14029467366897019727ul; + private const ulong k_Prime64v3 = 1609587929392839161ul; + private const ulong k_Prime64v4 = 9650029242287828579ul; + private const ulong k_Prime64v5 = 2870177450012600261ul; + + private const uint k_Prime32v1 = 2654435761u; + private const uint k_Prime32v2 = 2246822519u; + private const uint k_Prime32v3 = 3266489917u; + private const uint k_Prime32v4 = 668265263u; + private const uint k_Prime32v5 = 374761393u; + + public static uint Hash32(string text) => Hash32(text, Encoding.UTF8); + public static uint Hash32(string text, Encoding encoding) => Hash32(encoding.GetBytes(text)); + public static uint Hash32(byte[] buffer) + { + unsafe + { + fixed (byte* ptr = buffer) + { + return Hash32(ptr, buffer.Length); + } + } + } + + /// + /// Generate a 32-bit xxHash value. + /// + /// Input buffer. + /// Input buffer length. + /// Optional seed. + /// 32-bit hash value. + public static unsafe uint Hash32(byte* buffer, int bufferLength, uint seed = 0) + { + const int stripeLength = 16; + + int len = bufferLength; + int remainingLen = len; + uint acc; + + byte* pInput = buffer; + if (len >= stripeLength) + { + uint acc1 = seed + k_Prime32v1 + k_Prime32v2; + uint acc2 = seed + k_Prime32v2; + uint acc3 = seed; + uint acc4 = seed - k_Prime32v1; + + do + { + acc = processStripe32(ref pInput, ref acc1, ref acc2, ref acc3, ref acc4); + remainingLen -= stripeLength; + } while (remainingLen >= stripeLength); + } + else + { + acc = seed + k_Prime32v5; + } + + acc += (uint)len; + acc = processRemaining32(pInput, acc, remainingLen); + + return avalanche32(acc); + } + + public static ulong Hash64(string text) => Hash64(text, Encoding.UTF8); + public static ulong Hash64(string text, Encoding encoding) => Hash64(encoding.GetBytes(text)); + public static ulong Hash64(byte[] buffer) + { + unsafe + { + fixed (byte* ptr = buffer) + { + return Hash64(ptr, buffer.Length); + } + } + } + + /// + /// Generate a 64-bit xxHash value. + /// + /// Input buffer. + /// Input buffer length. + /// Optional seed. + /// Computed 64-bit hash value. + public static unsafe ulong Hash64(byte* buffer, int bufferLength, ulong seed = 0) + { + const int stripeLength = 32; + + int len = bufferLength; + int remainingLen = len; + ulong acc; + + byte* pInput = buffer; + if (len >= stripeLength) + { + ulong acc1 = seed + k_Prime64v1 + k_Prime64v2; + ulong acc2 = seed + k_Prime64v2; + ulong acc3 = seed; + ulong acc4 = seed - k_Prime64v1; + + do + { + acc = processStripe64(ref pInput, ref acc1, ref acc2, ref acc3, ref acc4); + remainingLen -= stripeLength; + } while (remainingLen >= stripeLength); + } + else + { + acc = seed + k_Prime64v5; + } + + acc += (ulong)len; + acc = processRemaining64(pInput, acc, remainingLen); + + + return avalanche64(acc); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong processStripe64( + ref byte* pInput, + ref ulong acc1, + ref ulong acc2, + ref ulong acc3, + ref ulong acc4) + { + processLane64(ref acc1, ref pInput); + processLane64(ref acc2, ref pInput); + processLane64(ref acc3, ref pInput); + processLane64(ref acc4, ref pInput); + + ulong acc = Bits.RotateLeft(acc1, 1) + + Bits.RotateLeft(acc2, 7) + + Bits.RotateLeft(acc3, 12) + + Bits.RotateLeft(acc4, 18); + + mergeAccumulator64(ref acc, acc1); + mergeAccumulator64(ref acc, acc2); + mergeAccumulator64(ref acc, acc3); + mergeAccumulator64(ref acc, acc4); + return acc; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void processLane64(ref ulong accn, ref byte* pInput) + { + ulong lane = *(ulong*)pInput; + accn = round64(accn, lane); + pInput += 8; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong processRemaining64( + byte* pInput, + ulong acc, + int remainingLen) + { + for (ulong lane; remainingLen >= 8; remainingLen -= 8, pInput += 8) + { + lane = *(ulong*)pInput; + + acc ^= round64(0, lane); + acc = Bits.RotateLeft(acc, 27) * k_Prime64v1; + acc += k_Prime64v4; + } + + for (uint lane32; remainingLen >= 4; remainingLen -= 4, pInput += 4) + { + lane32 = *(uint*)pInput; + + acc ^= lane32 * k_Prime64v1; + acc = Bits.RotateLeft(acc, 23) * k_Prime64v2; + acc += k_Prime64v3; + } + + for (byte lane8; remainingLen >= 1; remainingLen--, pInput++) + { + lane8 = *pInput; + acc ^= lane8 * k_Prime64v5; + acc = Bits.RotateLeft(acc, 11) * k_Prime64v1; + } + + return acc; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong avalanche64(ulong acc) + { + acc ^= acc >> 33; + acc *= k_Prime64v2; + acc ^= acc >> 29; + acc *= k_Prime64v3; + acc ^= acc >> 32; + return acc; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong round64(ulong accn, ulong lane) + { + accn += lane * k_Prime64v2; + return Bits.RotateLeft(accn, 31) * k_Prime64v1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void mergeAccumulator64(ref ulong acc, ulong accn) + { + acc ^= round64(0, accn); + acc *= k_Prime64v1; + acc += k_Prime64v4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe uint processStripe32( + ref byte* pInput, + ref uint acc1, + ref uint acc2, + ref uint acc3, + ref uint acc4) + { + processLane32(ref pInput, ref acc1); + processLane32(ref pInput, ref acc2); + processLane32(ref pInput, ref acc3); + processLane32(ref pInput, ref acc4); + + return Bits.RotateLeft(acc1, 1) + + Bits.RotateLeft(acc2, 7) + + Bits.RotateLeft(acc3, 12) + + Bits.RotateLeft(acc4, 18); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe void processLane32(ref byte* pInput, ref uint accn) + { + uint lane = *(uint*)pInput; + accn = round32(accn, lane); + pInput += 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe uint processRemaining32( + byte* pInput, + uint acc, + int remainingLen) + { + for (uint lane; remainingLen >= 4; remainingLen -= 4, pInput += 4) + { + lane = *(uint*)pInput; + acc += lane * k_Prime32v3; + acc = Bits.RotateLeft(acc, 17) * k_Prime32v4; + } + + for (byte lane; remainingLen >= 1; remainingLen--, pInput++) + { + lane = *pInput; + acc += lane * k_Prime32v5; + acc = Bits.RotateLeft(acc, 11) * k_Prime32v1; + } + + return acc; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint round32(uint accn, uint lane) + { + accn += lane * k_Prime32v2; + accn = Bits.RotateLeft(accn, 13); + accn *= k_Prime32v1; + return accn; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint avalanche32(uint acc) + { + acc ^= acc >> 15; + acc *= k_Prime32v2; + acc ^= acc >> 13; + acc *= k_Prime32v3; + acc ^= acc >> 16; + return acc; + } + + /// + /// Bit operations. + /// + private static class Bits + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong RotateLeft(ulong value, int bits) + { + return (value << bits) | (value >> (64 - bits)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static uint RotateLeft(uint value, int bits) + { + return (value << bits) | (value >> (32 - bits)); + } + } + } +} diff --git a/Runtime/Hashing/XXHash/XXHash.cs.meta b/Runtime/Hashing/XXHash/XXHash.cs.meta new file mode 100644 index 0000000..5c090bb --- /dev/null +++ b/Runtime/Hashing/XXHash/XXHash.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b5aa7a49e9e694f148d810d34577546b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Logging.meta b/Runtime/Logging.meta new file mode 100644 index 0000000..3624765 --- /dev/null +++ b/Runtime/Logging.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb2e0b0239cebdd44b534f09be21f1e6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Logging/LogLevel.cs b/Runtime/Logging/LogLevel.cs new file mode 100644 index 0000000..7cfc107 --- /dev/null +++ b/Runtime/Logging/LogLevel.cs @@ -0,0 +1,28 @@ +namespace Unity.Netcode +{ + /// + /// Log level + /// + public enum LogLevel + { + /// + /// Developer logging level, most verbose + /// + Developer, + + /// + /// Normal logging level, medium verbose + /// + Normal, + + /// + /// Error logging level, very quiet + /// + Error, + + /// + /// Nothing logging level, no logging will be done + /// + Nothing + } +} diff --git a/Runtime/Logging/LogLevel.cs.meta b/Runtime/Logging/LogLevel.cs.meta new file mode 100644 index 0000000..9037533 --- /dev/null +++ b/Runtime/Logging/LogLevel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b97845a27cb50d44da22aae51800abfe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Logging/NetworkLog.cs b/Runtime/Logging/NetworkLog.cs new file mode 100644 index 0000000..78de043 --- /dev/null +++ b/Runtime/Logging/NetworkLog.cs @@ -0,0 +1,84 @@ +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Helper class for logging + /// + public static class NetworkLog + { + /// + /// Gets the current log level. + /// + /// The current log level. + 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}"); + + /// + /// Logs an info log locally and on the server if possible. + /// + /// The message to log + public static void LogInfoServer(string message) => LogServer(message, LogType.Info); + + /// + /// Logs a warning log locally and on the server if possible. + /// + /// The message to log + public static void LogWarningServer(string message) => LogServer(message, LogType.Warning); + + /// + /// Logs an error log locally and on the server if possible. + /// + /// The message to log + public static void LogErrorServer(string message) => LogServer(message, LogType.Error); + + private static void LogServer(string message, LogType logType) + { + // Get the sender of the local log + ulong localId = NetworkManager.Singleton != null ? NetworkManager.Singleton.LocalClientId : 0; + + switch (logType) + { + case LogType.Info: + LogInfoServerLocal(message, localId); + break; + case LogType.Warning: + LogWarningServerLocal(message, localId); + break; + case LogType.Error: + LogErrorServerLocal(message, localId); + break; + } + + if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer && NetworkManager.Singleton.NetworkConfig.EnableNetworkLogs) + { + + var networkMessage = new ServerLogMessage + { + LogType = logType, + Message = message + }; + var size = NetworkManager.Singleton.SendMessage(networkMessage, NetworkDelivery.ReliableFragmentedSequenced, + NetworkManager.Singleton.ServerClientId); + + NetworkManager.Singleton.NetworkMetrics.TrackServerLogSent(NetworkManager.Singleton.ServerClientId, (uint)logType, size); + } + } + + internal static void LogInfoServerLocal(string message, ulong sender) => Debug.Log($"[Netcode-Server Sender={sender}] {message}"); + internal static void LogWarningServerLocal(string message, ulong sender) => Debug.LogWarning($"[Netcode-Server Sender={sender}] {message}"); + internal static void LogErrorServerLocal(string message, ulong sender) => Debug.LogError($"[Netcode-Server Sender={sender}] {message}"); + + internal enum LogType : byte + { + Info, + Warning, + Error, + None + } + } +} diff --git a/Runtime/Logging/NetworkLog.cs.meta b/Runtime/Logging/NetworkLog.cs.meta new file mode 100644 index 0000000..fcd80da --- /dev/null +++ b/Runtime/Logging/NetworkLog.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b33d7f8ddec3b140a4ff09659174e8d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging.meta b/Runtime/Messaging.meta new file mode 100644 index 0000000..cba5d38 --- /dev/null +++ b/Runtime/Messaging.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 563c514253a555b48a68707decc5c088 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/BatchHeader.cs b/Runtime/Messaging/BatchHeader.cs new file mode 100644 index 0000000..a71fd6a --- /dev/null +++ b/Runtime/Messaging/BatchHeader.cs @@ -0,0 +1,13 @@ +namespace Unity.Netcode +{ + /// + /// Header placed at the start of each message batch + /// + internal struct BatchHeader + { + /// + /// Total number of messages in the batch. + /// + public ushort BatchSize; + } +} diff --git a/Runtime/Messaging/BatchHeader.cs.meta b/Runtime/Messaging/BatchHeader.cs.meta new file mode 100644 index 0000000..b96a0b8 --- /dev/null +++ b/Runtime/Messaging/BatchHeader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 941fcfe2222f8734ab5bfb9bc4787717 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/CustomMessageManager.cs b/Runtime/Messaging/CustomMessageManager.cs new file mode 100644 index 0000000..f84a379 --- /dev/null +++ b/Runtime/Messaging/CustomMessageManager.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; + +namespace Unity.Netcode +{ + /// + /// The manager class to manage custom messages, note that this is different from the NetworkManager custom messages. + /// These are named and are much easier to use. + /// + public class CustomMessagingManager + { + private readonly NetworkManager m_NetworkManager; + + internal CustomMessagingManager(NetworkManager networkManager) + { + m_NetworkManager = networkManager; + } + + /// + /// Delegate used for incoming unnamed messages + /// + /// The clientId that sent the message + /// The stream containing the message data + public delegate void UnnamedMessageDelegate(ulong clientId, FastBufferReader reader); + + /// + /// Event invoked when unnamed messages arrive + /// + public event UnnamedMessageDelegate OnUnnamedMessage; + + internal void InvokeUnnamedMessage(ulong clientId, FastBufferReader reader) + { + if (OnUnnamedMessage != null) + { + var pos = reader.Position; + var delegates = OnUnnamedMessage.GetInvocationList(); + foreach (var handler in delegates) + { + reader.Seek(pos); + ((UnnamedMessageDelegate)handler).Invoke(clientId, reader); + } + } + m_NetworkManager.NetworkMetrics.TrackUnnamedMessageReceived(clientId, reader.Length); + } + + /// + /// Sends unnamed message to all clients + /// + /// The message stream containing the data + /// The delivery type (QoS) to send data with + public void SendUnnamedMessageToAll(FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) + { + SendUnnamedMessage(m_NetworkManager.ConnectedClientsIds, messageBuffer, networkDelivery); + } + + + /// + /// Sends unnamed message to a list of clients + /// + /// The clients to send to, sends to everyone if null + /// The message stream containing the data + /// The delivery type (QoS) to send data with + public void SendUnnamedMessage(IReadOnlyList clientIds, FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) + { + if (!m_NetworkManager.IsServer) + { + throw new InvalidOperationException("Can not send unnamed messages to multiple users as a client"); + } + + if (clientIds == null) + { + throw new ArgumentNullException("You must pass in a valid clientId List"); + } + + var message = new UnnamedMessage + { + Data = messageBuffer + }; + var size = m_NetworkManager.SendMessage(message, networkDelivery, clientIds); + + // Size is zero if we were only sending the message to ourself in which case it isn't sent. + if (size != 0) + { + m_NetworkManager.NetworkMetrics.TrackUnnamedMessageSent(clientIds, size); + } + } + + /// + /// Sends a unnamed message to a specific client + /// + /// The client to send the message to + /// The message stream containing the data + /// The delivery type (QoS) to send data with + public void SendUnnamedMessage(ulong clientId, FastBufferWriter messageBuffer, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) + { + var message = new UnnamedMessage + { + Data = messageBuffer + }; + var size = m_NetworkManager.SendMessage(message, networkDelivery, clientId); + // Size is zero if we were only sending the message to ourself in which case it isn't sent. + if (size != 0) + { + m_NetworkManager.NetworkMetrics.TrackUnnamedMessageSent(clientId, size); + } + } + + /// + /// Delegate used to handle named messages + /// + public delegate void HandleNamedMessageDelegate(ulong senderClientId, FastBufferReader messagePayload); + + private Dictionary m_NamedMessageHandlers32 = new Dictionary(); + private Dictionary m_NamedMessageHandlers64 = new Dictionary(); + + private Dictionary m_MessageHandlerNameLookup32 = new Dictionary(); + private Dictionary m_MessageHandlerNameLookup64 = new Dictionary(); + + internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader reader) + { + var bytesCount = reader.Length; + + if (m_NetworkManager == null) + { + // We dont know what size to use. Try every (more collision prone) + if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32)) + { + messageHandler32(sender, reader); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount); + } + + if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64)) + { + messageHandler64(sender, reader); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount); + } + } + else + { + // Only check the right size. + switch (m_NetworkManager.NetworkConfig.RpcHashSize) + { + case HashSize.VarIntFourBytes: + if (m_NamedMessageHandlers32.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler32)) + { + messageHandler32(sender, reader); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup32[hash], bytesCount); + } + break; + case HashSize.VarIntEightBytes: + if (m_NamedMessageHandlers64.TryGetValue(hash, out HandleNamedMessageDelegate messageHandler64)) + { + messageHandler64(sender, reader); + m_NetworkManager.NetworkMetrics.TrackNamedMessageReceived(sender, m_MessageHandlerNameLookup64[hash], bytesCount); + } + break; + } + } + } + + /// + /// Registers a named message handler delegate. + /// + /// Name of the message. + /// The callback to run when a named message is received. + public void RegisterNamedMessageHandler(string name, HandleNamedMessageDelegate callback) + { + var hash32 = XXHash.Hash32(name); + var hash64 = XXHash.Hash64(name); + + m_NamedMessageHandlers32[hash32] = callback; + m_NamedMessageHandlers64[hash64] = callback; + + m_MessageHandlerNameLookup32[hash32] = name; + m_MessageHandlerNameLookup64[hash64] = name; + } + + /// + /// Unregisters a named message handler. + /// + /// The name of the message. + public void UnregisterNamedMessageHandler(string name) + { + var hash32 = XXHash.Hash32(name); + var hash64 = XXHash.Hash64(name); + + m_NamedMessageHandlers32.Remove(hash32); + m_NamedMessageHandlers64.Remove(hash64); + + m_MessageHandlerNameLookup32.Remove(hash32); + m_MessageHandlerNameLookup64.Remove(hash64); + } + + /// + /// Sends a named message to all clients + /// + /// The message stream containing the data + /// The delivery type (QoS) to send data with + public void SendNamedMessageToAll(string messageName, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) + { + SendNamedMessage(messageName, m_NetworkManager.ConnectedClientsIds, messageStream, networkDelivery); + } + + /// + /// Sends a named message + /// + /// The message name to send + /// The client to send the message to + /// The message stream containing the data + /// The delivery type (QoS) to send data with + public void SendNamedMessage(string messageName, ulong clientId, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) + { + ulong hash = 0; + switch (m_NetworkManager.NetworkConfig.RpcHashSize) + { + case HashSize.VarIntFourBytes: + hash = XXHash.Hash32(messageName); + break; + case HashSize.VarIntEightBytes: + hash = XXHash.Hash64(messageName); + break; + } + + var message = new NamedMessage + { + Hash = hash, + Data = messageStream + }; + var size = m_NetworkManager.SendMessage(message, networkDelivery, clientId); + + // Size is zero if we were only sending the message to ourself in which case it isn't sent. + if (size != 0) + { + m_NetworkManager.NetworkMetrics.TrackNamedMessageSent(clientId, messageName, size); + } + } + + /// + /// Sends the named message + /// + /// The message name to send + /// The clients to send to, sends to everyone if null + /// The message stream containing the data + /// The delivery type (QoS) to send data with + public void SendNamedMessage(string messageName, IReadOnlyList clientIds, FastBufferWriter messageStream, NetworkDelivery networkDelivery = NetworkDelivery.ReliableSequenced) + { + if (!m_NetworkManager.IsServer) + { + throw new InvalidOperationException("Can not send unnamed messages to multiple users as a client"); + } + + if (clientIds == null) + { + throw new ArgumentNullException("You must pass in a valid clientId List"); + } + + ulong hash = 0; + switch (m_NetworkManager.NetworkConfig.RpcHashSize) + { + case HashSize.VarIntFourBytes: + hash = XXHash.Hash32(messageName); + break; + case HashSize.VarIntEightBytes: + hash = XXHash.Hash64(messageName); + break; + } + var message = new NamedMessage + { + Hash = hash, + Data = messageStream + }; + var size = m_NetworkManager.SendMessage(message, networkDelivery, clientIds); + + // Size is zero if we were only sending the message to ourself in which case it isn't sent. + if (size != 0) + { + m_NetworkManager.NetworkMetrics.TrackNamedMessageSent(clientIds, messageName, size); + } + } + } +} diff --git a/Runtime/Messaging/CustomMessageManager.cs.meta b/Runtime/Messaging/CustomMessageManager.cs.meta new file mode 100644 index 0000000..0799b32 --- /dev/null +++ b/Runtime/Messaging/CustomMessageManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 15c647dad40a44d46886dca112cfd524 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/ILPPMessageProvider.cs b/Runtime/Messaging/ILPPMessageProvider.cs new file mode 100644 index 0000000..b228b20 --- /dev/null +++ b/Runtime/Messaging/ILPPMessageProvider.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Unity.Netcode +{ + internal struct ILPPMessageProvider : IMessageProvider + { +#pragma warning disable IDE1006 // disable naming rule violation check + // This is NOT modified by RuntimeAccessModifiersILPP right now, but is populated by ILPP. + internal static readonly List __network_message_types = new List(); +#pragma warning restore IDE1006 // restore naming rule violation check + + public List GetMessages() + { + return __network_message_types; + } + } +} diff --git a/Runtime/Messaging/ILPPMessageProvider.cs.meta b/Runtime/Messaging/ILPPMessageProvider.cs.meta new file mode 100644 index 0000000..06309ca --- /dev/null +++ b/Runtime/Messaging/ILPPMessageProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5bfc2b13811eb5429a0eef59dd88b71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/IMessageProvider.cs b/Runtime/Messaging/IMessageProvider.cs new file mode 100644 index 0000000..d974094 --- /dev/null +++ b/Runtime/Messaging/IMessageProvider.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Unity.Netcode +{ + internal interface IMessageProvider + { + List GetMessages(); + } +} diff --git a/Runtime/Messaging/IMessageProvider.cs.meta b/Runtime/Messaging/IMessageProvider.cs.meta new file mode 100644 index 0000000..1d8a89c --- /dev/null +++ b/Runtime/Messaging/IMessageProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ed41def88d712c4b9c115310e6b0127 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/IMessageSender.cs b/Runtime/Messaging/IMessageSender.cs new file mode 100644 index 0000000..5248503 --- /dev/null +++ b/Runtime/Messaging/IMessageSender.cs @@ -0,0 +1,7 @@ +namespace Unity.Netcode +{ + internal interface IMessageSender + { + void Send(ulong clientId, NetworkDelivery delivery, FastBufferWriter batchData); + } +} diff --git a/Runtime/Messaging/IMessageSender.cs.meta b/Runtime/Messaging/IMessageSender.cs.meta new file mode 100644 index 0000000..56d9cc9 --- /dev/null +++ b/Runtime/Messaging/IMessageSender.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 15b54cd88eba22648ade4240523b8c65 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/INetworkHooks.cs b/Runtime/Messaging/INetworkHooks.cs new file mode 100644 index 0000000..ce64fc5 --- /dev/null +++ b/Runtime/Messaging/INetworkHooks.cs @@ -0,0 +1,97 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Used to react to different events in the messaging system. Primary use case is for + /// collecting profiling data and metrics data. Additionally, it provides OnVerifyCanSend and OnVerifyCanReceive + /// to allow for networking implementations to put limits on when certain messages can or can't be sent or received. + /// + internal interface INetworkHooks + { + /// + /// Called before an individual message is sent. + /// + /// The destination clientId + /// The type of the message being sent + /// + void OnBeforeSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery); + + /// + /// Called after an individual message is sent. + /// + /// The destination clientId + /// The type of the message being sent + /// + /// Number of bytes in the message, not including the message header + void OnAfterSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery, int messageSizeBytes); + + /// + /// Called before an individual message is received. + /// + /// The source clientId + /// The type of the message being sent + /// Number of bytes in the message, not including the message header + void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes); + + /// + /// Called after an individual message is received. + /// + /// The source clientId + /// The type of the message being sent + /// Number of bytes in the message, not including the message header + void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes); + + /// + /// Called before a batch of messages is sent + /// + /// The destination clientId + /// Number of messages in the batch + /// Number of bytes in the batch, including the batch header + /// + void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery); + + /// + /// Called after a batch of messages is sent + /// + /// The destination clientId + /// Number of messages in the batch + /// Number of bytes in the batch, including the batch header + /// + void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery); + + /// + /// Called before a batch of messages is received + /// + /// The source clientId + /// Number of messages in the batch + /// Number of bytes in the batch, including the batch header + void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes); + + /// + /// Called after a batch of messages is received + /// + /// The source clientId + /// Number of messages in the batch + /// Number of bytes in the batch, including the batch header + void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes); + + + /// + /// Called before a message is sent. If this returns false, the message will be discarded. + /// + /// The destination clientId + /// The type of the message + /// + /// + bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery); + + /// + /// Called before a message is received. If this returns false, the message will be discarded. + /// + /// The source clientId + /// The type of the message + /// + bool OnVerifyCanReceive(ulong senderId, Type messageType); + } +} diff --git a/Runtime/Messaging/INetworkHooks.cs.meta b/Runtime/Messaging/INetworkHooks.cs.meta new file mode 100644 index 0000000..cc2de4a --- /dev/null +++ b/Runtime/Messaging/INetworkHooks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b199c5a160beabb47bd6b0e4f06cfcd2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/INetworkMessage.cs b/Runtime/Messaging/INetworkMessage.cs new file mode 100644 index 0000000..2f7a1f9 --- /dev/null +++ b/Runtime/Messaging/INetworkMessage.cs @@ -0,0 +1,49 @@ +using Unity.Collections; + +namespace Unity.Netcode +{ + /// + /// Base building block for creating a message. Any struct (or class) that implements INetworkMessage + /// will automatically be found by the system and all the proper mechanisms for sending and receiving + /// that message will be hooked up automatically. + /// + /// It's generally recommended to implement INetworkMessage types as structs, and define your messages + /// as close as you can to the network transport format. For messages with no dynamic-length or optional + /// data, FastBufferWriter allows for serializing the entire struct at once via writer.WriteValue(this) + /// + /// In addition to the specified Serialize method, all INetworkMessage types must also have a + /// static message handler for receiving messages of the following name and signature: + /// + /// + /// public static void Receive(FastBufferReader reader, in NetworkContext context) + /// + /// + /// It is the responsibility of the Serialize and Receive methods to ensure there is enough buffer space + /// to perform the serialization/deserialization, either via and + /// , or via and + /// . The former is more efficient when it can be used + /// for bounds checking for multiple values at once. + /// + /// When bandwidth is a bigger concern than CPU usage, values can be packed with + /// and . + /// + /// Note that for messages sent using non-fragmenting delivery modes (anything other than + /// ), there is a hard limit of 1300 bytes per message. + /// With the fragmenting delivery mode, the limit is 64000 bytes per message. If your messages exceed that limit, + /// you will have to split them into multiple smaller messages. + /// + /// Messages are sent with: + /// + /// + /// + /// + /// + internal interface INetworkMessage + { + /// + /// Used to serialize the message. + /// + /// + void Serialize(FastBufferWriter writer); + } +} diff --git a/Runtime/Messaging/INetworkMessage.cs.meta b/Runtime/Messaging/INetworkMessage.cs.meta new file mode 100644 index 0000000..d6c9a32 --- /dev/null +++ b/Runtime/Messaging/INetworkMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 16079de8d8821a24c91db930bc892b5d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/MessageHeader.cs b/Runtime/Messaging/MessageHeader.cs new file mode 100644 index 0000000..a3a9543 --- /dev/null +++ b/Runtime/Messaging/MessageHeader.cs @@ -0,0 +1,21 @@ +namespace Unity.Netcode +{ + /// + /// This is the header data that's serialized to the network when sending an + /// + internal struct MessageHeader + { + /// + /// The byte representation of the message type. This is automatically assigned to each message + /// by the MessagingSystem. This value is deterministic only so long as the list of messages remains + /// unchanged - if new messages are added or messages are removed, MessageType assignments may be + /// calculated differently. + /// + public byte MessageType; + + /// + /// The total size of the message, NOT including the header. + /// + public ushort MessageSize; + } +} diff --git a/Runtime/Messaging/MessageHeader.cs.meta b/Runtime/Messaging/MessageHeader.cs.meta new file mode 100644 index 0000000..1931b10 --- /dev/null +++ b/Runtime/Messaging/MessageHeader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 74fa727ddec342c48ab49156a32b977b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages.meta b/Runtime/Messaging/Messages.meta new file mode 100644 index 0000000..6b3c359 --- /dev/null +++ b/Runtime/Messaging/Messages.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fd834639d7f09614fa4f3296921871d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs new file mode 100644 index 0000000..c4d3aef --- /dev/null +++ b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs @@ -0,0 +1,49 @@ +namespace Unity.Netcode +{ + internal struct ChangeOwnershipMessage : INetworkMessage + { + public ulong NetworkObjectId; + public ulong OwnerClientId; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValueSafe(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return; + } + reader.ReadValueSafe(out ChangeOwnershipMessage message); + message.Handle(reader, context, context.SenderId, networkManager, reader.Length); + } + + public void Handle(FastBufferReader reader, in NetworkContext context, ulong senderId, NetworkManager networkManager, int messageSize) + { + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject)) + { + networkManager.SpawnManager.TriggerOnSpawn(NetworkObjectId, reader, context); + return; + } + + if (networkObject.OwnerClientId == networkManager.LocalClientId) + { + //We are current owner. + networkObject.InvokeBehaviourOnLostOwnership(); + } + + networkObject.OwnerClientId = OwnerClientId; + + if (OwnerClientId == networkManager.LocalClientId) + { + //We are new owner. + networkObject.InvokeBehaviourOnGainedOwnership(); + } + + networkManager.NetworkMetrics.TrackOwnershipChangeReceived(senderId, networkObject, messageSize); + } + } +} diff --git a/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs.meta b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs.meta new file mode 100644 index 0000000..a995996 --- /dev/null +++ b/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 841becdc46b20d5408a81bc30ac950f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs new file mode 100644 index 0000000..bf3ecac --- /dev/null +++ b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; + +namespace Unity.Netcode +{ + internal struct ConnectionApprovedMessage : INetworkMessage + { + public ulong OwnerClientId; + public int NetworkTick; + public int SceneObjectCount; + + // Not serialized, held as references to serialize NetworkVariable data + public HashSet SpawnedObjectsList; + + public void Serialize(FastBufferWriter writer) + { + if (!writer.TryBeginWrite(sizeof(ulong) + sizeof(int) + sizeof(int))) + { + throw new OverflowException( + $"Not enough space in the write buffer to serialize {nameof(ConnectionApprovedMessage)}"); + } + writer.WriteValue(OwnerClientId); + writer.WriteValue(NetworkTick); + writer.WriteValue(SceneObjectCount); + + if (SceneObjectCount != 0) + { + // Serialize NetworkVariable data + foreach (var sobj in SpawnedObjectsList) + { + if (sobj.CheckObjectVisibility == null || sobj.CheckObjectVisibility(OwnerClientId)) + { + sobj.Observers.Add(OwnerClientId); + var sceneObject = sobj.GetMessageSceneObject(OwnerClientId); + sceneObject.Serialize(writer); + } + } + } + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return; + } + + if (!reader.TryBeginRead(sizeof(ulong) + sizeof(int) + sizeof(int))) + { + throw new OverflowException( + $"Not enough space in the buffer to read {nameof(ConnectionApprovedMessage)}"); + } + + var message = new ConnectionApprovedMessage(); + reader.ReadValue(out message.OwnerClientId); + reader.ReadValue(out message.NetworkTick); + reader.ReadValue(out message.SceneObjectCount); + message.Handle(reader, context.SenderId, networkManager); + } + + public void Handle(FastBufferReader reader, ulong clientId, NetworkManager networkManager) + { + networkManager.LocalClientId = OwnerClientId; + networkManager.NetworkMetrics.SetConnectionId(networkManager.LocalClientId); + + var time = new NetworkTime(networkManager.NetworkTickSystem.TickRate, NetworkTick); + networkManager.NetworkTimeSystem.Reset(time.Time, 0.15f); // Start with a constant RTT of 150 until we receive values from the transport. + networkManager.NetworkTickSystem.Reset(networkManager.NetworkTimeSystem.LocalTime, networkManager.NetworkTimeSystem.ServerTime); + + networkManager.LocalClient = new NetworkClient() { ClientId = networkManager.LocalClientId }; + + // Only if scene management is disabled do we handle NetworkObject synchronization at this point + if (!networkManager.NetworkConfig.EnableSceneManagement) + { + networkManager.SpawnManager.DestroySceneObjects(); + + // Deserializing NetworkVariable data is deferred from Receive() to Handle to avoid needing + // to create a list to hold the data. This is a breach of convention for performance reasons. + for (ushort i = 0; i < SceneObjectCount; i++) + { + var sceneObject = new NetworkObject.SceneObject(); + sceneObject.Deserialize(reader); + NetworkObject.AddSceneObject(sceneObject, reader, networkManager); + } + + // Mark the client being connected + networkManager.IsConnectedClient = true; + // When scene management is disabled we notify after everything is synchronized + networkManager.InvokeOnClientConnectedCallback(clientId); + } + } + } +} diff --git a/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs.meta b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs.meta new file mode 100644 index 0000000..31cdaad --- /dev/null +++ b/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ebbc74ce01b073340aa445f3bd59ff62 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/ConnectionRequestMessage.cs b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs new file mode 100644 index 0000000..874a9c6 --- /dev/null +++ b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs @@ -0,0 +1,115 @@ +namespace Unity.Netcode +{ + internal struct ConnectionRequestMessage : INetworkMessage + { + public ulong ConfigHash; + + public byte[] ConnectionData; + + public bool ShouldSendConnectionData; + + public void Serialize(FastBufferWriter writer) + { + if (ShouldSendConnectionData) + { + writer.WriteValueSafe(ConfigHash); + writer.WriteValueSafe(ConnectionData); + } + else + { + writer.WriteValueSafe(ConfigHash); + } + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsServer) + { + return; + } + + var message = new ConnectionRequestMessage(); + if (networkManager.NetworkConfig.ConnectionApproval) + { + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(message.ConfigHash) + + FastBufferWriter.GetWriteSize())) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Incomplete connection request message given config - possible {nameof(NetworkConfig)} mismatch."); + } + + networkManager.DisconnectClient(context.SenderId); + return; + } + reader.ReadValue(out message.ConfigHash); + + if (!networkManager.NetworkConfig.CompareConfig(message.ConfigHash)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(NetworkConfig)} mismatch. The configuration between the server and client does not match"); + } + + networkManager.DisconnectClient(context.SenderId); + return; + } + + reader.ReadValueSafe(out message.ConnectionData); + } + else + { + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(message.ConfigHash))) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Incomplete connection request message."); + } + + networkManager.DisconnectClient(context.SenderId); + return; + } + reader.ReadValue(out message.ConfigHash); + + if (!networkManager.NetworkConfig.CompareConfig(message.ConfigHash)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(NetworkConfig)} mismatch. The configuration between the server and client does not match"); + } + + networkManager.DisconnectClient(context.SenderId); + return; + } + } + message.Handle(networkManager, context.SenderId); + } + + public void Handle(NetworkManager networkManager, ulong senderId) + { + if (networkManager.PendingClients.TryGetValue(senderId, out PendingClient client)) + { + // Set to pending approval to prevent future connection requests from being approved + client.ConnectionState = PendingClient.State.PendingApproval; + } + + if (networkManager.NetworkConfig.ConnectionApproval) + { + // Note: Delegate creation allocates. + // Note: ToArray() also allocates. :( + networkManager.InvokeConnectionApproval(ConnectionData, senderId, + (createPlayerObject, playerPrefabHash, approved, position, rotation) => + { + var localCreatePlayerObject = createPlayerObject; + networkManager.HandleApproval(senderId, localCreatePlayerObject, playerPrefabHash, approved, + position, rotation); + }); + } + else + { + networkManager.HandleApproval(senderId, networkManager.NetworkConfig.PlayerPrefab != null, null, true, null, null); + } + } + } +} diff --git a/Runtime/Messaging/Messages/ConnectionRequestMessage.cs.meta b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs.meta new file mode 100644 index 0000000..6b6ae75 --- /dev/null +++ b/Runtime/Messaging/Messages/ConnectionRequestMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fd160468676e06049ad81dcfb22c3dc2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/CreateObjectMessage.cs b/Runtime/Messaging/Messages/CreateObjectMessage.cs new file mode 100644 index 0000000..3cea33d --- /dev/null +++ b/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -0,0 +1,30 @@ +namespace Unity.Netcode +{ + internal struct CreateObjectMessage : INetworkMessage + { + public NetworkObject.SceneObject ObjectInfo; + + public void Serialize(FastBufferWriter writer) + { + ObjectInfo.Serialize(writer); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return; + } + var message = new CreateObjectMessage(); + message.ObjectInfo.Deserialize(reader); + message.Handle(context.SenderId, reader, networkManager); + } + + public void Handle(ulong senderId, FastBufferReader reader, NetworkManager networkManager) + { + var networkObject = NetworkObject.AddSceneObject(ObjectInfo, reader, networkManager); + networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, reader.Length); + } + } +} diff --git a/Runtime/Messaging/Messages/CreateObjectMessage.cs.meta b/Runtime/Messaging/Messages/CreateObjectMessage.cs.meta new file mode 100644 index 0000000..1fa4fb6 --- /dev/null +++ b/Runtime/Messaging/Messages/CreateObjectMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ff075de988adf5a4294620aa2d85fcd0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/DestroyObjectMessage.cs b/Runtime/Messaging/Messages/DestroyObjectMessage.cs new file mode 100644 index 0000000..d19904b --- /dev/null +++ b/Runtime/Messaging/Messages/DestroyObjectMessage.cs @@ -0,0 +1,41 @@ +namespace Unity.Netcode +{ + internal struct DestroyObjectMessage : INetworkMessage + { + public ulong NetworkObjectId; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValueSafe(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return; + } + reader.ReadValueSafe(out DestroyObjectMessage message); + message.Handle(context.SenderId, networkManager, reader.Length); + } + + public void Handle(ulong senderId, NetworkManager networkManager, int messageSize) + { + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out var networkObject)) + { + // This is the same check and log message that happens inside OnDespawnObject, but we have to do it here + // while we still have access to the network ID, otherwise the log message will be less useful. + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Trying to destroy {nameof(NetworkObject)} #{NetworkObjectId} but it does not exist in {nameof(NetworkSpawnManager.SpawnedObjects)} anymore!"); + } + + return; + } + + networkManager.NetworkMetrics.TrackObjectDestroyReceived(senderId, networkObject, messageSize); + networkManager.SpawnManager.OnDespawnObject(networkObject, true); + } + } +} diff --git a/Runtime/Messaging/Messages/DestroyObjectMessage.cs.meta b/Runtime/Messaging/Messages/DestroyObjectMessage.cs.meta new file mode 100644 index 0000000..35e4b04 --- /dev/null +++ b/Runtime/Messaging/Messages/DestroyObjectMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18473ed11c97e7241aecb0c72d4bffb2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/NamedMessage.cs b/Runtime/Messaging/Messages/NamedMessage.cs new file mode 100644 index 0000000..a6bab60 --- /dev/null +++ b/Runtime/Messaging/Messages/NamedMessage.cs @@ -0,0 +1,22 @@ +namespace Unity.Netcode +{ + internal struct NamedMessage : INetworkMessage + { + public ulong Hash; + public FastBufferWriter Data; + + public unsafe void Serialize(FastBufferWriter writer) + { + writer.WriteValueSafe(Hash); + writer.WriteBytesSafe(Data.GetUnsafePtr(), Data.Length); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var message = new NamedMessage(); + reader.ReadValueSafe(out message.Hash); + + ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(message.Hash, context.SenderId, reader); + } + } +} diff --git a/Runtime/Messaging/Messages/NamedMessage.cs.meta b/Runtime/Messaging/Messages/NamedMessage.cs.meta new file mode 100644 index 0000000..f3977c4 --- /dev/null +++ b/Runtime/Messaging/Messages/NamedMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b6bcd41dcce65743ba387fc4e2c6fa8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs new file mode 100644 index 0000000..f791a6b --- /dev/null +++ b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; + +namespace Unity.Netcode +{ + /// + /// This particular struct is a little weird because it doesn't actually contain the data + /// it's serializing. Instead, it contains references to the data it needs to do the + /// serialization. This is due to the generally amorphous nature of network variable + /// deltas, since they're all driven by custom virtual method overloads. + /// + internal struct NetworkVariableDeltaMessage : INetworkMessage + { + public ulong NetworkObjectId; + public ushort NetworkBehaviourIndex; + + public HashSet DeliveryMappedNetworkVariableIndex; + public ulong ClientId; + public NetworkBehaviour NetworkBehaviour; + + public void Serialize(FastBufferWriter writer) + { + if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize(NetworkObjectId) + + FastBufferWriter.GetWriteSize(NetworkBehaviourIndex))) + { + throw new OverflowException( + $"Not enough space in the buffer to write {nameof(NetworkVariableDeltaMessage)}"); + } + writer.WriteValue(NetworkObjectId); + writer.WriteValue(NetworkBehaviourIndex); + for (int k = 0; k < NetworkBehaviour.NetworkVariableFields.Count; k++) + { + if (!DeliveryMappedNetworkVariableIndex.Contains(k)) + { + // This var does not belong to the currently iterating delivery group. + if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + { + writer.WriteValueSafe((short)0); + } + else + { + writer.WriteValueSafe(false); + } + + 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); + + if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + { + if (!shouldWrite) + { + writer.WriteValueSafe((ushort)0); + } + } + else + { + writer.WriteValueSafe(shouldWrite); + } + + if (shouldWrite) + { + if (NetworkBehaviour.NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + { + var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, short.MaxValue); + NetworkBehaviour.NetworkVariableFields[k].WriteDelta(tmpWriter); + + writer.WriteValueSafe((ushort)tmpWriter.Length); + tmpWriter.CopyTo(writer); + } + else + { + NetworkBehaviour.NetworkVariableFields[k].WriteDelta(writer); + } + + if (!NetworkBehaviour.NetworkVariableIndexesToResetSet.Contains(k)) + { + NetworkBehaviour.NetworkVariableIndexesToResetSet.Add(k); + NetworkBehaviour.NetworkVariableIndexesToReset.Add(k); + } + + NetworkBehaviour.NetworkManager.NetworkMetrics.TrackNetworkVariableDeltaSent( + ClientId, + NetworkBehaviour.NetworkObject, + NetworkBehaviour.NetworkVariableFields[k].Name, + NetworkBehaviour.__getTypeName(), + writer.Length); + } + } + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + + var message = new NetworkVariableDeltaMessage(); + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(message.NetworkObjectId) + + FastBufferWriter.GetWriteSize(message.NetworkBehaviourIndex))) + { + throw new OverflowException( + $"Not enough data in the buffer to read {nameof(NetworkVariableDeltaMessage)}"); + } + reader.ReadValue(out message.NetworkObjectId); + reader.ReadValue(out message.NetworkBehaviourIndex); + message.Handle(context.SenderId, reader, context, networkManager); + } + + public void Handle(ulong senderId, FastBufferReader reader, in NetworkContext context, NetworkManager networkManager) + { + if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(NetworkObjectId, out NetworkObject networkObject)) + { + NetworkBehaviour behaviour = networkObject.GetNetworkBehaviourAtOrderIndex(NetworkBehaviourIndex); + + if (behaviour == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Network variable delta message received for a non-existent behaviour. {nameof(NetworkObjectId)}: {NetworkObjectId}, {nameof(NetworkBehaviourIndex)}: {NetworkBehaviourIndex}"); + } + } + else + { + for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++) + { + ushort varSize = 0; + + if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + { + reader.ReadValueSafe(out varSize); + + if (varSize == 0) + { + continue; + } + } + else + { + reader.ReadValueSafe(out bool deltaExists); + if (!deltaExists) + { + continue; + } + } + + if (networkManager.IsServer) + { + // 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) + { + 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}]"); + } + + reader.Seek(reader.Position + varSize); + continue; + } + + //This client wrote somewhere they are not allowed. This is critical + //We can't just skip this field. Because we don't actually know how to dummy read + //That is, we don't know how many bytes to skip. Because the interface doesn't have a + //Read that gives us the value. Only a Read that applies the value straight away + //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) + { + 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}]"); + } + + return; + } + int readStartPos = reader.Position; + + behaviour.NetworkVariableFields[i].ReadDelta(reader, networkManager.IsServer); + + networkManager.NetworkMetrics.TrackNetworkVariableDeltaReceived( + senderId, + networkObject, + behaviour.NetworkVariableFields[i].Name, + behaviour.__getTypeName(), + reader.Length); + + + if (networkManager.NetworkConfig.EnsureNetworkVariableLengthSafety) + { + if (reader.Position > (readStartPos + varSize)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning( + $"Var delta read too far. {reader.Position - (readStartPos + varSize)} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}"); + } + + reader.Seek(readStartPos + varSize); + } + else if (reader.Position < (readStartPos + varSize)) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning( + $"Var delta read too little. {(readStartPos + varSize) - reader.Position} bytes. => {nameof(NetworkObjectId)}: {NetworkObjectId} - {nameof(NetworkObject.GetNetworkBehaviourOrderIndex)}(): {networkObject.GetNetworkBehaviourOrderIndex(behaviour)} - VariableIndex: {i}"); + } + + reader.Seek(readStartPos + varSize); + } + } + } + } + } + else + { + networkManager.SpawnManager.TriggerOnSpawn(NetworkObjectId, reader, context); + } + } + } +} diff --git a/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs.meta b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs.meta new file mode 100644 index 0000000..3593f1a --- /dev/null +++ b/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dfa1a454cc9fdb647ba89479fa6b8299 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/ParentSyncMessage.cs b/Runtime/Messaging/Messages/ParentSyncMessage.cs new file mode 100644 index 0000000..20ba96f --- /dev/null +++ b/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -0,0 +1,67 @@ +namespace Unity.Netcode +{ + internal struct ParentSyncMessage : INetworkMessage + { + public ulong NetworkObjectId; + + public bool IsReparented; + + //If(Metadata.IsReparented) + public bool IsLatestParentSet; + + //If(IsLatestParentSet) + public ulong? LatestParent; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValueSafe(NetworkObjectId); + writer.WriteValueSafe(IsReparented); + if (IsReparented) + { + writer.WriteValueSafe(IsLatestParentSet); + if (IsLatestParentSet) + { + writer.WriteValueSafe((ulong)LatestParent); + } + } + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return; + } + + var message = new ParentSyncMessage(); + reader.ReadValueSafe(out message.NetworkObjectId); + reader.ReadValueSafe(out message.IsReparented); + if (message.IsReparented) + { + reader.ReadValueSafe(out message.IsLatestParentSet); + if (message.IsLatestParentSet) + { + reader.ReadValueSafe(out ulong latestParent); + message.LatestParent = latestParent; + } + } + + message.Handle(reader, context, networkManager); + } + + public void Handle(FastBufferReader reader, in NetworkContext context, NetworkManager networkManager) + { + if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId)) + { + var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId]; + networkObject.SetNetworkParenting(IsReparented, LatestParent); + networkObject.ApplyNetworkParenting(); + } + else + { + networkManager.SpawnManager.TriggerOnSpawn(NetworkObjectId, reader, context); + } + } + } +} diff --git a/Runtime/Messaging/Messages/ParentSyncMessage.cs.meta b/Runtime/Messaging/Messages/ParentSyncMessage.cs.meta new file mode 100644 index 0000000..2b01978 --- /dev/null +++ b/Runtime/Messaging/Messages/ParentSyncMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 237bfa46868f1ff48863f3a6df2f5506 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/RpcMessage.cs b/Runtime/Messaging/Messages/RpcMessage.cs new file mode 100644 index 0000000..723fe20 --- /dev/null +++ b/Runtime/Messaging/Messages/RpcMessage.cs @@ -0,0 +1,109 @@ +using System; + +namespace Unity.Netcode +{ + internal struct RpcMessage : INetworkMessage + { + public enum RpcType : byte + { + Server, + Client + } + + public struct HeaderData + { + public RpcType Type; + public ulong NetworkObjectId; + public ushort NetworkBehaviourId; + public uint NetworkMethodId; + } + + public HeaderData Header; + public FastBufferWriter RpcData; + + + public unsafe void Serialize(FastBufferWriter writer) + { + if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize(Header) + RpcData.Length)) + { + throw new OverflowException("Not enough space in the buffer to store RPC data."); + } + writer.WriteValue(Header); + writer.WriteBytes(RpcData.GetUnsafePtr(), RpcData.Length); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var message = new RpcMessage(); + if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(message.Header))) + { + throw new OverflowException("Not enough space in the buffer to read RPC data."); + } + reader.ReadValue(out message.Header); + message.Handle(reader, context, (NetworkManager)context.SystemOwner, context.SenderId, true); + } + + public void Handle(FastBufferReader reader, in NetworkContext context, NetworkManager networkManager, ulong senderId, bool canDefer) + { + if (NetworkManager.__rpc_func_table.ContainsKey(Header.NetworkMethodId)) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(Header.NetworkObjectId)) + { + if (canDefer) + { + networkManager.SpawnManager.TriggerOnSpawn(Header.NetworkObjectId, reader, context); + } + else + { + NetworkLog.LogError($"Tried to invoke an RPC on a non-existent {nameof(NetworkObject)} with {nameof(canDefer)}=false"); + } + return; + } + + var networkObject = networkManager.SpawnManager.SpawnedObjects[Header.NetworkObjectId]; + + var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(Header.NetworkBehaviourId); + if (networkBehaviour == null) + { + return; + } + + var rpcParams = new __RpcParams(); + switch (Header.Type) + { + case RpcType.Server: + rpcParams.Server = new ServerRpcParams + { + Receive = new ServerRpcReceiveParams + { + SenderClientId = senderId + } + }; + break; + case RpcType.Client: + rpcParams.Client = new ClientRpcParams + { + Receive = new ClientRpcReceiveParams + { + } + }; + break; + } + + NetworkManager.__rpc_func_table[Header.NetworkMethodId](networkBehaviour, reader, rpcParams); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (NetworkManager.__rpc_name_table.TryGetValue(Header.NetworkMethodId, out var rpcMethodName)) + { + networkManager.NetworkMetrics.TrackRpcReceived( + senderId, + networkObject, + rpcMethodName, + networkBehaviour.__getTypeName(), + reader.Length); + } +#endif + } + } + } +} diff --git a/Runtime/Messaging/Messages/RpcMessage.cs.meta b/Runtime/Messaging/Messages/RpcMessage.cs.meta new file mode 100644 index 0000000..e0a661e --- /dev/null +++ b/Runtime/Messaging/Messages/RpcMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8fc9f8cca6a18b428460b62bce2d8f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/SceneEventMessage.cs b/Runtime/Messaging/Messages/SceneEventMessage.cs new file mode 100644 index 0000000..2104211 --- /dev/null +++ b/Runtime/Messaging/Messages/SceneEventMessage.cs @@ -0,0 +1,19 @@ +namespace Unity.Netcode +{ + // Todo: Would be lovely to get this one nicely formatted with all the data it sends in the struct + // like most of the other messages when we have some more time and can come back and refactor this. + internal struct SceneEventMessage : INetworkMessage + { + public SceneEventData EventData; + + public void Serialize(FastBufferWriter writer) + { + EventData.Serialize(writer); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + ((NetworkManager)context.SystemOwner).SceneManager.HandleSceneEvent(context.SenderId, reader); + } + } +} diff --git a/Runtime/Messaging/Messages/SceneEventMessage.cs.meta b/Runtime/Messaging/Messages/SceneEventMessage.cs.meta new file mode 100644 index 0000000..9e4e619 --- /dev/null +++ b/Runtime/Messaging/Messages/SceneEventMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 152f6ccef0320cf4abcf19099c1997e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/ServerLogMessage.cs b/Runtime/Messaging/Messages/ServerLogMessage.cs new file mode 100644 index 0000000..41e17bf --- /dev/null +++ b/Runtime/Messaging/Messages/ServerLogMessage.cs @@ -0,0 +1,50 @@ +namespace Unity.Netcode +{ + internal struct ServerLogMessage : INetworkMessage + { + public NetworkLog.LogType LogType; + // It'd be lovely to be able to replace this with FixedString or NativeArray... + // But it's not really practical. On the sending side, the user is likely to want + // to work with strings and would need to convert, and on the receiving side, + // we'd have to convert it to a string to be able to pass it to the log system. + // So an allocation is unavoidable here on both sides. + public string Message; + + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValueSafe(LogType); + BytePacker.WriteValuePacked(writer, Message); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (networkManager.IsServer && networkManager.NetworkConfig.EnableNetworkLogs) + { + var message = new ServerLogMessage(); + reader.ReadValueSafe(out message.LogType); + ByteUnpacker.ReadValuePacked(reader, out message.Message); + message.Handle(context.SenderId, networkManager, reader.Length); + } + } + + public void Handle(ulong senderId, NetworkManager networkManager, int messageSize) + { + networkManager.NetworkMetrics.TrackServerLogReceived(senderId, (uint)LogType, messageSize); + + switch (LogType) + { + case NetworkLog.LogType.Info: + NetworkLog.LogInfoServerLocal(Message, senderId); + break; + case NetworkLog.LogType.Warning: + NetworkLog.LogWarningServerLocal(Message, senderId); + break; + case NetworkLog.LogType.Error: + NetworkLog.LogErrorServerLocal(Message, senderId); + break; + } + } + } +} diff --git a/Runtime/Messaging/Messages/ServerLogMessage.cs.meta b/Runtime/Messaging/Messages/ServerLogMessage.cs.meta new file mode 100644 index 0000000..ebc461d --- /dev/null +++ b/Runtime/Messaging/Messages/ServerLogMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d1c8aae1f7b7194eb3ab1cab260f34f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/SnapshotDataMessage.cs b/Runtime/Messaging/Messages/SnapshotDataMessage.cs new file mode 100644 index 0000000..1d1ec8b --- /dev/null +++ b/Runtime/Messaging/Messages/SnapshotDataMessage.cs @@ -0,0 +1,161 @@ +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) + )) + { + Entries.Dispose(); + Spawns.Dispose(); + Despawns.Dispose(); + 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)); + + Entries.Dispose(); + Spawns.Dispose(); + Despawns.Dispose(); + } + + public static unsafe void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + var message = new SnapshotDataMessage(); + if (!reader.TryBeginRead( + FastBufferWriter.GetWriteSize(message.CurrentTick) + + FastBufferWriter.GetWriteSize(message.Sequence) + + FastBufferWriter.GetWriteSize(message.Range) + )) + { + throw new OverflowException($"Not enough space to deserialize {nameof(SnapshotDataMessage)}"); + } + reader.ReadValue(out message.CurrentTick); + reader.ReadValue(out message.Sequence); + + reader.ReadValue(out message.Range); + message.ReceiveMainBuffer = new NativeArray(message.Range, Allocator.Temp); + reader.ReadBytesSafe((byte*)message.ReceiveMainBuffer.GetUnsafePtr(), message.Range); + reader.ReadValueSafe(out message.Ack); + + reader.ReadValueSafe(out ushort length); + message.Entries = new NativeList(length, Allocator.Temp); + message.Entries.Length = length; + reader.ReadBytesSafe((byte*)message.Entries.GetUnsafePtr(), message.Entries.Length * sizeof(EntryData)); + + reader.ReadValueSafe(out length); + message.Spawns = new NativeList(length, Allocator.Temp); + message.Spawns.Length = length; + reader.ReadBytesSafe((byte*)message.Spawns.GetUnsafePtr(), message.Spawns.Length * sizeof(SpawnData)); + + reader.ReadValueSafe(out length); + message.Despawns = new NativeList(length, Allocator.Temp); + message.Despawns.Length = length; + reader.ReadBytesSafe((byte*)message.Despawns.GetUnsafePtr(), message.Despawns.Length * sizeof(DespawnData)); + + using (message.ReceiveMainBuffer) + using (message.Entries) + using (message.Spawns) + using (message.Despawns) + { + message.Handle(context.SenderId, networkManager); + } + } + + public void Handle(ulong senderId, NetworkManager networkManager) + { + // todo: temporary hack around bug + if (!networkManager.IsServer) + { + senderId = networkManager.ServerClientId; + } + + var snapshotSystem = networkManager.SnapshotSystem; + snapshotSystem.HandleSnapshot(senderId, this); + } + } +} diff --git a/Runtime/Messaging/Messages/SnapshotDataMessage.cs.meta b/Runtime/Messaging/Messages/SnapshotDataMessage.cs.meta new file mode 100644 index 0000000..a549a7c --- /dev/null +++ b/Runtime/Messaging/Messages/SnapshotDataMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5cf75026c2ab86646aac16b39d7259ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/TimeSyncMessage.cs b/Runtime/Messaging/Messages/TimeSyncMessage.cs new file mode 100644 index 0000000..6cbdfdc --- /dev/null +++ b/Runtime/Messaging/Messages/TimeSyncMessage.cs @@ -0,0 +1,29 @@ +namespace Unity.Netcode +{ + internal struct TimeSyncMessage : INetworkMessage + { + public int Tick; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValueSafe(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.IsClient) + { + return; + } + reader.ReadValueSafe(out TimeSyncMessage message); + message.Handle(context.SenderId, networkManager); + } + + public void Handle(ulong senderId, NetworkManager networkManager) + { + var time = new NetworkTime(networkManager.NetworkTickSystem.TickRate, Tick); + networkManager.NetworkTimeSystem.Sync(time.Time, networkManager.NetworkConfig.NetworkTransport.GetCurrentRtt(senderId) / 1000d); + } + } +} diff --git a/Runtime/Messaging/Messages/TimeSyncMessage.cs.meta b/Runtime/Messaging/Messages/TimeSyncMessage.cs.meta new file mode 100644 index 0000000..8c64b06 --- /dev/null +++ b/Runtime/Messaging/Messages/TimeSyncMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94afa081c8f5e0a4fb05e0643a968c46 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/Messages/UnnamedMessage.cs b/Runtime/Messaging/Messages/UnnamedMessage.cs new file mode 100644 index 0000000..9f349de --- /dev/null +++ b/Runtime/Messaging/Messages/UnnamedMessage.cs @@ -0,0 +1,17 @@ +namespace Unity.Netcode +{ + internal struct UnnamedMessage : INetworkMessage + { + public FastBufferWriter Data; + + public unsafe void Serialize(FastBufferWriter writer) + { + writer.WriteBytesSafe(Data.GetUnsafePtr(), Data.Length); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeUnnamedMessage(context.SenderId, reader); + } + } +} diff --git a/Runtime/Messaging/Messages/UnnamedMessage.cs.meta b/Runtime/Messaging/Messages/UnnamedMessage.cs.meta new file mode 100644 index 0000000..838f2b4 --- /dev/null +++ b/Runtime/Messaging/Messages/UnnamedMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7cece0a7c7653648a7bc8fa920843be +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 new file mode 100644 index 0000000..ab6b438 --- /dev/null +++ b/Runtime/Messaging/MessagingSystem.cs @@ -0,0 +1,474 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; + +namespace Unity.Netcode +{ + + internal class InvalidMessageStructureException : SystemException + { + public InvalidMessageStructureException() { } + public InvalidMessageStructureException(string issue) : base(issue) { } + } + + internal class MessagingSystem : IDisposable + { + private struct ReceiveQueueItem + { + public FastBufferReader Reader; + public MessageHeader Header; + public ulong SenderId; + public float Timestamp; + } + + private struct SendQueueItem + { + public BatchHeader BatchHeader; + public FastBufferWriter Writer; + public readonly NetworkDelivery NetworkDelivery; + + public SendQueueItem(NetworkDelivery delivery, int writerSize, Allocator writerAllocator, int maxWriterSize = -1) + { + Writer = new FastBufferWriter(writerSize, writerAllocator, maxWriterSize); + NetworkDelivery = delivery; + BatchHeader = default; + } + } + + internal delegate void MessageHandler(FastBufferReader reader, in NetworkContext context); + + private NativeList m_IncomingMessageQueue = new NativeList(16, Allocator.Persistent); + + private MessageHandler[] m_MessageHandlers = new MessageHandler[255]; + private Type[] m_ReverseTypeMap = new Type[255]; + + private Dictionary m_MessageTypes = new Dictionary(); + private Dictionary> m_SendQueues = new Dictionary>(); + + private List m_Hooks = new List(); + + private byte m_HighMessageType; + private object m_Owner; + private IMessageSender m_MessageSender; + private bool m_Disposed; + + internal Type[] MessageTypes => m_ReverseTypeMap; + internal MessageHandler[] MessageHandlers => m_MessageHandlers; + internal int MessageHandlerCount => m_HighMessageType; + + internal byte GetMessageType(Type t) + { + return m_MessageTypes[t]; + } + + public const int NON_FRAGMENTED_MESSAGE_MAX_SIZE = 1300; + public const int FRAGMENTED_MESSAGE_MAX_SIZE = 64000; + + internal struct MessageWithHandler + { + public Type MessageType; + public MessageHandler Handler; + } + + public MessagingSystem(IMessageSender messageSender, object owner, IMessageProvider provider = null) + { + try + { + m_MessageSender = messageSender; + m_Owner = owner; + + if (provider == null) + { + provider = new ILPPMessageProvider(); + } + var allowedTypes = provider.GetMessages(); + + allowedTypes.Sort((a, b) => string.CompareOrdinal(a.MessageType.FullName, b.MessageType.FullName)); + foreach (var type in allowedTypes) + { + RegisterMessageType(type); + } + } + catch (Exception) + { + Dispose(); + throw; + } + } + + public void Dispose() + { + if (m_Disposed) + { + return; + } + + // Can't just iterate SendQueues or SendQueues.Keys because ClientDisconnected removes + // from the queue. + foreach (var kvp in m_SendQueues) + { + CleanupDisconnectedClient(kvp.Key); + } + m_IncomingMessageQueue.Dispose(); + m_Disposed = true; + } + + ~MessagingSystem() + { + Dispose(); + } + + public void Hook(INetworkHooks hooks) + { + m_Hooks.Add(hooks); + } + + private void RegisterMessageType(MessageWithHandler messageWithHandler) + { + m_MessageHandlers[m_HighMessageType] = messageWithHandler.Handler; + m_ReverseTypeMap[m_HighMessageType] = messageWithHandler.MessageType; + m_MessageTypes[messageWithHandler.MessageType] = m_HighMessageType++; + } + + internal void HandleIncomingData(ulong clientId, ArraySegment data, float receiveTime) + { + unsafe + { + fixed (byte* nativeData = data.Array) + { + var batchReader = + new FastBufferReader(nativeData, Allocator.None, data.Count, data.Offset); + if (!batchReader.TryBeginRead(sizeof(BatchHeader))) + { + NetworkLog.LogWarning("Received a packet too small to contain a BatchHeader. Ignoring it."); + return; + } + + batchReader.ReadValue(out BatchHeader batchHeader); + + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnBeforeReceiveBatch(clientId, batchHeader.BatchSize, batchReader.Length); + } + + for (var messageIdx = 0; messageIdx < batchHeader.BatchSize; ++messageIdx) + { + if (!batchReader.TryBeginRead(sizeof(MessageHeader))) + { + NetworkLog.LogWarning("Received a batch that didn't have enough data for all of its batches, ending early!"); + return; + } + batchReader.ReadValue(out MessageHeader messageHeader); + + if (!batchReader.TryBeginRead(messageHeader.MessageSize)) + { + NetworkLog.LogWarning("Received a message that claimed a size larger than the packet, ending early!"); + return; + } + m_IncomingMessageQueue.Add(new ReceiveQueueItem + { + Header = messageHeader, + SenderId = clientId, + Timestamp = receiveTime, + // Copy the data for this message into a new FastBufferReader that owns that memory. + // We can't guarantee the memory in the ArraySegment stays valid because we don't own it, + // so we must move it to memory we do own. + Reader = new FastBufferReader(batchReader.GetUnsafePtrAtCurrentPosition(), Allocator.TempJob, messageHeader.MessageSize) + }); + batchReader.Seek(batchReader.Position + messageHeader.MessageSize); + } + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnAfterReceiveBatch(clientId, batchHeader.BatchSize, batchReader.Length); + } + } + } + } + + private bool CanReceive(ulong clientId, Type messageType) + { + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + if (!m_Hooks[hookIdx].OnVerifyCanReceive(clientId, messageType)) + { + return false; + } + } + + return true; + } + + public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp) + { + if (header.MessageType >= m_HighMessageType) + { + Debug.LogWarning($"Received a message with invalid message type value {header.MessageType}"); + reader.Dispose(); + return; + } + var context = new NetworkContext + { + SystemOwner = m_Owner, + SenderId = senderId, + Timestamp = timestamp, + Header = header + }; + var type = m_ReverseTypeMap[header.MessageType]; + if (!CanReceive(senderId, type)) + { + reader.Dispose(); + return; + } + + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnBeforeReceiveMessage(senderId, type, reader.Length); + } + var handler = m_MessageHandlers[header.MessageType]; + using (reader) + { + // No user-land message handler exceptions should escape the receive loop. + // If an exception is throw, the message is ignored. + // Example use case: A bad message is received that can't be deserialized and throws + // an OverflowException because it specifies a length greater than the number of bytes in it + // for some dynamic-length value. + try + { + handler.Invoke(reader, context); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnAfterReceiveMessage(senderId, type, reader.Length); + } + } + + internal unsafe void ProcessIncomingMessageQueue() + { + for (var i = 0; i < m_IncomingMessageQueue.Length; ++i) + { + // Avoid copies... + ref var item = ref m_IncomingMessageQueue.GetUnsafeList()->ElementAt(i); + HandleMessage(item.Header, item.Reader, item.SenderId, item.Timestamp); + } + + m_IncomingMessageQueue.Clear(); + } + + internal void ClientConnected(ulong clientId) + { + if (m_SendQueues.ContainsKey(clientId)) + { + return; + } + m_SendQueues[clientId] = new NativeList(16, Allocator.Persistent); + } + + internal void ClientDisconnected(ulong clientId) + { + if (!m_SendQueues.ContainsKey(clientId)) + { + return; + } + CleanupDisconnectedClient(clientId); + m_SendQueues.Remove(clientId); + } + + private unsafe void CleanupDisconnectedClient(ulong clientId) + { + var queue = m_SendQueues[clientId]; + for (var i = 0; i < queue.Length; ++i) + { + queue.GetUnsafeList()->ElementAt(i).Writer.Dispose(); + } + + queue.Dispose(); + } + + private bool CanSend(ulong clientId, Type messageType, NetworkDelivery delivery) + { + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + if (!m_Hooks[hookIdx].OnVerifyCanSend(clientId, messageType, delivery)) + { + return false; + } + } + + return true; + } + + internal unsafe int SendMessage(in TMessageType message, NetworkDelivery delivery, in TClientIdListType clientIds) + where TMessageType : INetworkMessage + where TClientIdListType : IReadOnlyList + { + var maxSize = delivery == NetworkDelivery.ReliableFragmentedSequenced ? FRAGMENTED_MESSAGE_MAX_SIZE : NON_FRAGMENTED_MESSAGE_MAX_SIZE; + var tmpSerializer = new FastBufferWriter(NON_FRAGMENTED_MESSAGE_MAX_SIZE - sizeof(MessageHeader), Allocator.Temp, maxSize - sizeof(MessageHeader)); + using (tmpSerializer) + { + message.Serialize(tmpSerializer); + + for (var i = 0; i < clientIds.Count; ++i) + { + var clientId = clientIds[i]; + + if (!CanSend(clientId, typeof(TMessageType), delivery)) + { + continue; + } + + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnBeforeSendMessage(clientId, typeof(TMessageType), delivery); + } + + var sendQueueItem = m_SendQueues[clientId]; + if (sendQueueItem.Length == 0) + { + sendQueueItem.Add(new SendQueueItem(delivery, NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.TempJob, + maxSize)); + sendQueueItem.GetUnsafeList()->ElementAt(0).Writer.Seek(sizeof(BatchHeader)); + } + else + { + ref var lastQueueItem = ref sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1); + if (lastQueueItem.NetworkDelivery != delivery || + lastQueueItem.Writer.MaxCapacity - lastQueueItem.Writer.Position + < tmpSerializer.Length + sizeof(MessageHeader)) + { + sendQueueItem.Add(new SendQueueItem(delivery, NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.TempJob, + maxSize)); + sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1).Writer.Seek(sizeof(BatchHeader)); + } + } + + ref var writeQueueItem = ref sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1); + writeQueueItem.Writer.TryBeginWrite(sizeof(MessageHeader) + tmpSerializer.Length); + var header = new MessageHeader + { + MessageSize = (ushort)tmpSerializer.Length, + MessageType = m_MessageTypes[typeof(TMessageType)], + }; + + writeQueueItem.Writer.WriteValue(header); + writeQueueItem.Writer.WriteBytes(tmpSerializer.GetUnsafePtr(), tmpSerializer.Length); + writeQueueItem.BatchHeader.BatchSize++; + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnAfterSendMessage(clientId, typeof(TMessageType), delivery, tmpSerializer.Length + sizeof(MessageHeader)); + } + } + + return tmpSerializer.Length; + } + } + + private struct PointerListWrapper : IReadOnlyList + where T : unmanaged + { + private unsafe T* m_Value; + private int m_Length; + + internal unsafe PointerListWrapper(T* ptr, int length) + { + m_Value = ptr; + m_Length = length; + } + + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => m_Length; + } + + public unsafe T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => m_Value[index]; + } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + internal unsafe int SendMessage(in T message, NetworkDelivery delivery, + ulong* clientIds, int numClientIds) + where T : INetworkMessage + { + return SendMessage(message, delivery, new PointerListWrapper(clientIds, numClientIds)); + } + + internal unsafe int SendMessage(in T message, NetworkDelivery delivery, ulong clientId) + where T : INetworkMessage + { + ulong* clientIds = stackalloc ulong[] { clientId }; + return SendMessage(message, delivery, new PointerListWrapper(clientIds, 1)); + } + + internal unsafe int SendMessage(in T message, NetworkDelivery delivery, in NativeArray clientIds) + where T : INetworkMessage + { + return SendMessage(message, delivery, new PointerListWrapper((ulong*)clientIds.GetUnsafePtr(), clientIds.Length)); + } + + internal unsafe void ProcessSendQueues() + { + foreach (var kvp in m_SendQueues) + { + var clientId = kvp.Key; + var sendQueueItem = kvp.Value; + for (var i = 0; i < sendQueueItem.Length; ++i) + { + ref var queueItem = ref sendQueueItem.GetUnsafeList()->ElementAt(i); + if (queueItem.BatchHeader.BatchSize == 0) + { + queueItem.Writer.Dispose(); + continue; + } + + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnBeforeSendBatch(clientId, queueItem.BatchHeader.BatchSize, queueItem.Writer.Length, queueItem.NetworkDelivery); + } + + queueItem.Writer.Seek(0); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // Skipping the Verify and sneaking the write mark in because we know it's fine. + queueItem.Writer.Handle->AllowedWriteMark = 2; +#endif + queueItem.Writer.WriteValue(queueItem.BatchHeader); + + try + { + m_MessageSender.Send(clientId, queueItem.NetworkDelivery, queueItem.Writer); + } + finally + { + queueItem.Writer.Dispose(); + } + + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnAfterSendBatch(clientId, queueItem.BatchHeader.BatchSize, queueItem.Writer.Length, queueItem.NetworkDelivery); + } + } + sendQueueItem.Clear(); + } + } + } +} diff --git a/Runtime/Messaging/MessagingSystem.cs.meta b/Runtime/Messaging/MessagingSystem.cs.meta new file mode 100644 index 0000000..2aa51a8 --- /dev/null +++ b/Runtime/Messaging/MessagingSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a6de3c592caa3a41bdfe9b1e818bcf4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/NetworkContext.cs b/Runtime/Messaging/NetworkContext.cs new file mode 100644 index 0000000..8adaffa --- /dev/null +++ b/Runtime/Messaging/NetworkContext.cs @@ -0,0 +1,29 @@ +namespace Unity.Netcode +{ + /// + /// Metadata passed into the Receive handler for . + /// + internal ref struct NetworkContext + { + /// + /// An opaque object used to represent the owner of the MessagingSystem that's receiving the message. + /// Outside of testing environments, the type of this variable will be + /// + public object SystemOwner; + + /// + /// The originator of the message + /// + public ulong SenderId; + + /// + /// The timestamp at which the message was received + /// + public float Timestamp; + + /// + /// The header data that was sent with the message + /// + public MessageHeader Header; + } +} diff --git a/Runtime/Messaging/NetworkContext.cs.meta b/Runtime/Messaging/NetworkContext.cs.meta new file mode 100644 index 0000000..523510d --- /dev/null +++ b/Runtime/Messaging/NetworkContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9a4d028b61e2b140927b5ebee04d384 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/RpcAttributes.cs b/Runtime/Messaging/RpcAttributes.cs new file mode 100644 index 0000000..5d0a501 --- /dev/null +++ b/Runtime/Messaging/RpcAttributes.cs @@ -0,0 +1,51 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// RPC delivery types + /// + public enum RpcDelivery + { + /// + /// Reliable delivery + /// + Reliable = 0, + + /// + /// Unreliable delivery + /// + Unreliable + } + + /// + /// Represents the common base class for Rpc attributes. + /// + public abstract class RpcAttribute : Attribute + { + /// + /// Type of RPC delivery method + /// + public RpcDelivery Delivery = RpcDelivery.Reliable; + } + + /// + /// Marks a method as ServerRpc. + /// A ServerRpc marked method will be fired by a client but executed on the server. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ServerRpcAttribute : RpcAttribute + { + /// + /// Whether or not the ServerRpc should only be run if executed by the owner of the object + /// + public bool RequireOwnership = true; + } + + /// + /// Marks a method as ClientRpc. + /// A ClientRpc marked method will be fired by the server but executed on clients. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ClientRpcAttribute : RpcAttribute { } +} diff --git a/Runtime/Messaging/RpcAttributes.cs.meta b/Runtime/Messaging/RpcAttributes.cs.meta new file mode 100644 index 0000000..5f9dcea --- /dev/null +++ b/Runtime/Messaging/RpcAttributes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac745c8131b811e4887a83fa9e65b4af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Messaging/RpcParams.cs b/Runtime/Messaging/RpcParams.cs new file mode 100644 index 0000000..3ea839b --- /dev/null +++ b/Runtime/Messaging/RpcParams.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Unity.Collections; + +namespace Unity.Netcode +{ + public struct ServerRpcSendParams + { + } + + public struct ServerRpcReceiveParams + { + public ulong SenderClientId; + } + + public struct ServerRpcParams + { + public ServerRpcSendParams Send; + public ServerRpcReceiveParams Receive; + } + + public struct ClientRpcSendParams + { + /// + /// IEnumerable version of target id list - use either this OR TargetClientIdsNativeArray + /// Note: Even if you provide a value type such as NativeArray, enumerating it will cause boxing. + /// If you want to avoid boxing, use TargetClientIdsNativeArray + /// + public IReadOnlyList TargetClientIds; + + /// + /// NativeArray version of target id list - use either this OR TargetClientIds + /// This option avoids any GC allocations but is a bit trickier to use. + /// + public NativeArray? TargetClientIdsNativeArray; + } + + public struct ClientRpcReceiveParams + { + } + + public struct ClientRpcParams + { + public ClientRpcSendParams Send; + public ClientRpcReceiveParams Receive; + } + +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `public` + internal struct __RpcParams +#pragma warning restore IDE1006 // restore naming rule violation check + { + public ServerRpcParams Server; + public ClientRpcParams Client; + } +} diff --git a/Runtime/Messaging/RpcParams.cs.meta b/Runtime/Messaging/RpcParams.cs.meta new file mode 100644 index 0000000..a22c768 --- /dev/null +++ b/Runtime/Messaging/RpcParams.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: accabc5a65e8a45dda9207fac37d1b24 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Metrics.meta b/Runtime/Metrics.meta new file mode 100644 index 0000000..38b97fa --- /dev/null +++ b/Runtime/Metrics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8b953b7e03110e1478bcbd44fc4abe07 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Metrics/INetworkMetrics.cs b/Runtime/Metrics/INetworkMetrics.cs new file mode 100644 index 0000000..f187deb --- /dev/null +++ b/Runtime/Metrics/INetworkMetrics.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; + +namespace Unity.Netcode +{ + internal interface INetworkMetrics + { + void SetConnectionId(ulong connectionId); + + void TrackTransportBytesSent(long bytesCount); + + void TrackTransportBytesReceived(long bytesCount); + + void TrackNetworkMessageSent(ulong receivedClientId, string messageType, long bytesCount); + + void TrackNetworkMessageReceived(ulong senderClientId, string messageType, long bytesCount); + + void TrackNamedMessageSent(ulong receiverClientId, string messageName, long bytesCount); + + void TrackNamedMessageSent(IReadOnlyCollection receiverClientIds, string messageName, long bytesCount); + + void TrackNamedMessageReceived(ulong senderClientId, string messageName, long bytesCount); + + void TrackUnnamedMessageSent(ulong receiverClientId, long bytesCount); + + void TrackUnnamedMessageSent(IReadOnlyCollection receiverClientIds, long bytesCount); + + void TrackUnnamedMessageReceived(ulong senderClientId, long bytesCount); + + void TrackNetworkVariableDeltaSent( + ulong receiverClientId, + NetworkObject networkObject, + string variableName, + string networkBehaviourName, + long bytesCount); + + void TrackNetworkVariableDeltaReceived( + ulong senderClientId, + NetworkObject networkObject, + string variableName, + string networkBehaviourName, + long bytesCount); + + void TrackOwnershipChangeSent(ulong receiverClientId, NetworkObject networkObject, long bytesCount); + + void TrackOwnershipChangeReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount); + + void TrackObjectSpawnSent(ulong receiverClientId, NetworkObject networkObject, long bytesCount); + + void TrackObjectSpawnReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount); + + void TrackObjectDestroySent(ulong receiverClientId, NetworkObject networkObject, long bytesCount); + + void TrackObjectDestroyReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount); + + void TrackRpcSent( + ulong receiverClientId, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount); + + void TrackRpcSent( + ulong[] receiverClientIds, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount); + + void TrackRpcReceived( + ulong senderClientId, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount); + + void TrackServerLogSent(ulong receiverClientId, uint logType, long bytesCount); + + void TrackServerLogReceived(ulong senderClientId, uint logType, long bytesCount); + + void TrackSceneEventSent(IReadOnlyList receiverClientIds, uint sceneEventType, string sceneName, long bytesCount); + + void TrackSceneEventSent(ulong receiverClientId, uint sceneEventType, string sceneName, long bytesCount); + + void TrackSceneEventReceived(ulong senderClientId, uint sceneEventType, string sceneName, long bytesCount); + + void DispatchFrame(); + } +} diff --git a/Runtime/Metrics/INetworkMetrics.cs.meta b/Runtime/Metrics/INetworkMetrics.cs.meta new file mode 100644 index 0000000..34d2295 --- /dev/null +++ b/Runtime/Metrics/INetworkMetrics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01a54e8729c7e3a4bb566d9274c44cb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Metrics/MetricHooks.cs b/Runtime/Metrics/MetricHooks.cs new file mode 100644 index 0000000..741aac5 --- /dev/null +++ b/Runtime/Metrics/MetricHooks.cs @@ -0,0 +1,61 @@ +using System; + +namespace Unity.Netcode +{ + internal class MetricHooks : INetworkHooks + { + private readonly NetworkManager m_NetworkManager; + + public MetricHooks(NetworkManager networkManager) + { + m_NetworkManager = networkManager; + } + + + public void OnBeforeSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery) + { + } + + public void OnAfterSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery, int messageSizeBytes) + { + m_NetworkManager.NetworkMetrics.TrackNetworkMessageSent(clientId, messageType.Name, messageSizeBytes); + } + + public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + m_NetworkManager.NetworkMetrics.TrackNetworkMessageReceived(senderId, messageType.Name, messageSizeBytes); + } + + public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + } + + public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + } + + public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + m_NetworkManager.NetworkMetrics.TrackTransportBytesSent(batchSizeInBytes); + } + + public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + m_NetworkManager.NetworkMetrics.TrackTransportBytesReceived(batchSizeInBytes); + } + + public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + } + + public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) + { + return true; + } + + public bool OnVerifyCanReceive(ulong senderId, Type messageType) + { + return true; + } + } +} diff --git a/Runtime/Metrics/MetricHooks.cs.meta b/Runtime/Metrics/MetricHooks.cs.meta new file mode 100644 index 0000000..2ad0052 --- /dev/null +++ b/Runtime/Metrics/MetricHooks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 07e749f617714f248823af11048bfe46 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Metrics/NetworkMetrics.cs b/Runtime/Metrics/NetworkMetrics.cs new file mode 100644 index 0000000..4c00b1e --- /dev/null +++ b/Runtime/Metrics/NetworkMetrics.cs @@ -0,0 +1,429 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections.Generic; +using Unity.Multiplayer.Tools; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Multiplayer.Tools.NetStats; +using UnityEngine; + +namespace Unity.Netcode +{ + internal class NetworkMetrics : INetworkMetrics + { + const ulong k_MaxMetricsPerFrame = 1000L; + + static Dictionary s_SceneEventTypeNames; + + static NetworkMetrics() + { + s_SceneEventTypeNames = new Dictionary(); + foreach (SceneEventType type in Enum.GetValues(typeof(SceneEventType))) + { + s_SceneEventTypeNames[(uint)type] = type.ToString(); + } + } + + private static string GetSceneEventTypeName(uint typeCode) + { + if (!s_SceneEventTypeNames.TryGetValue(typeCode, out string name)) + { + name = "Unknown"; + } + + return name; + } + + private readonly Counter m_TransportBytesSent = new Counter(NetworkMetricTypes.TotalBytesSent.Id) + { + ShouldResetOnDispatch = true, + }; + private readonly Counter m_TransportBytesReceived = new Counter(NetworkMetricTypes.TotalBytesReceived.Id) + { + ShouldResetOnDispatch = true, + }; + + private readonly EventMetric m_NetworkMessageSentEvent = new EventMetric(NetworkMetricTypes.NetworkMessageSent.Id); + private readonly EventMetric m_NetworkMessageReceivedEvent = new EventMetric(NetworkMetricTypes.NetworkMessageReceived.Id); + private readonly EventMetric m_NamedMessageSentEvent = new EventMetric(NetworkMetricTypes.NamedMessageSent.Id); + private readonly EventMetric m_NamedMessageReceivedEvent = new EventMetric(NetworkMetricTypes.NamedMessageReceived.Id); + private readonly EventMetric m_UnnamedMessageSentEvent = new EventMetric(NetworkMetricTypes.UnnamedMessageSent.Id); + private readonly EventMetric m_UnnamedMessageReceivedEvent = new EventMetric(NetworkMetricTypes.UnnamedMessageReceived.Id); + private readonly EventMetric m_NetworkVariableDeltaSentEvent = new EventMetric(NetworkMetricTypes.NetworkVariableDeltaSent.Id); + private readonly EventMetric m_NetworkVariableDeltaReceivedEvent = new EventMetric(NetworkMetricTypes.NetworkVariableDeltaReceived.Id); + private readonly EventMetric m_OwnershipChangeSentEvent = new EventMetric(NetworkMetricTypes.OwnershipChangeSent.Id); + private readonly EventMetric m_OwnershipChangeReceivedEvent = new EventMetric(NetworkMetricTypes.OwnershipChangeReceived.Id); + private readonly EventMetric m_ObjectSpawnSentEvent = new EventMetric(NetworkMetricTypes.ObjectSpawnedSent.Id); + private readonly EventMetric m_ObjectSpawnReceivedEvent = new EventMetric(NetworkMetricTypes.ObjectSpawnedReceived.Id); + private readonly EventMetric m_ObjectDestroySentEvent = new EventMetric(NetworkMetricTypes.ObjectDestroyedSent.Id); + private readonly EventMetric m_ObjectDestroyReceivedEvent = new EventMetric(NetworkMetricTypes.ObjectDestroyedReceived.Id); + private readonly EventMetric m_RpcSentEvent = new EventMetric(NetworkMetricTypes.RpcSent.Id); + private readonly EventMetric m_RpcReceivedEvent = new EventMetric(NetworkMetricTypes.RpcReceived.Id); + private readonly EventMetric m_ServerLogSentEvent = new EventMetric(NetworkMetricTypes.ServerLogSent.Id); + private readonly EventMetric m_ServerLogReceivedEvent = new EventMetric(NetworkMetricTypes.ServerLogReceived.Id); + private readonly EventMetric m_SceneEventSentEvent = new EventMetric(NetworkMetricTypes.SceneEventSent.Id); + private readonly EventMetric m_SceneEventReceivedEvent = new EventMetric(NetworkMetricTypes.SceneEventReceived.Id); + + private ulong m_NumberOfMetricsThisFrame; + + public NetworkMetrics() + { + Dispatcher = new MetricDispatcherBuilder() + .WithCounters(m_TransportBytesSent, m_TransportBytesReceived) + .WithMetricEvents(m_NetworkMessageSentEvent, m_NetworkMessageReceivedEvent) + .WithMetricEvents(m_NamedMessageSentEvent, m_NamedMessageReceivedEvent) + .WithMetricEvents(m_UnnamedMessageSentEvent, m_UnnamedMessageReceivedEvent) + .WithMetricEvents(m_NetworkVariableDeltaSentEvent, m_NetworkVariableDeltaReceivedEvent) + .WithMetricEvents(m_OwnershipChangeSentEvent, m_OwnershipChangeReceivedEvent) + .WithMetricEvents(m_ObjectSpawnSentEvent, m_ObjectSpawnReceivedEvent) + .WithMetricEvents(m_ObjectDestroySentEvent, m_ObjectDestroyReceivedEvent) + .WithMetricEvents(m_RpcSentEvent, m_RpcReceivedEvent) + .WithMetricEvents(m_ServerLogSentEvent, m_ServerLogReceivedEvent) + .WithMetricEvents(m_SceneEventSentEvent, m_SceneEventReceivedEvent) + .Build(); + + Dispatcher.RegisterObserver(NetcodeObserver.Observer); + } + + internal IMetricDispatcher Dispatcher { get; } + + private bool CanSendMetrics => m_NumberOfMetricsThisFrame < k_MaxMetricsPerFrame; + + public void SetConnectionId(ulong connectionId) + { + Dispatcher.SetConnectionId(connectionId); + } + + public void TrackTransportBytesSent(long bytesCount) + { + m_TransportBytesSent.Increment(bytesCount); + } + + public void TrackTransportBytesReceived(long bytesCount) + { + m_TransportBytesReceived.Increment(bytesCount); + } + + public void TrackNetworkMessageSent(ulong receivedClientId, string messageType, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_NetworkMessageSentEvent.Mark(new NetworkMessageEvent(new ConnectionInfo(receivedClientId), messageType, bytesCount)); + IncrementMetricCount(); + } + + public void TrackNetworkMessageReceived(ulong senderClientId, string messageType, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_NetworkMessageReceivedEvent.Mark(new NetworkMessageEvent(new ConnectionInfo(senderClientId), messageType, bytesCount)); + IncrementMetricCount(); + } + + public void TrackNamedMessageSent(ulong receiverClientId, string messageName, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_NamedMessageSentEvent.Mark(new NamedMessageEvent(new ConnectionInfo(receiverClientId), messageName, bytesCount)); + IncrementMetricCount(); + } + + public void TrackNamedMessageSent(IReadOnlyCollection receiverClientIds, string messageName, long bytesCount) + { + foreach (var receiver in receiverClientIds) + { + TrackNamedMessageSent(receiver, messageName, bytesCount); + } + } + + public void TrackNamedMessageReceived(ulong senderClientId, string messageName, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_NamedMessageReceivedEvent.Mark(new NamedMessageEvent(new ConnectionInfo(senderClientId), messageName, bytesCount)); + IncrementMetricCount(); + } + + public void TrackUnnamedMessageSent(ulong receiverClientId, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_UnnamedMessageSentEvent.Mark(new UnnamedMessageEvent(new ConnectionInfo(receiverClientId), bytesCount)); + IncrementMetricCount(); + } + + public void TrackUnnamedMessageSent(IReadOnlyCollection receiverClientIds, long bytesCount) + { + foreach (var receiverClientId in receiverClientIds) + { + TrackUnnamedMessageSent(receiverClientId, bytesCount); + } + } + + public void TrackUnnamedMessageReceived(ulong senderClientId, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_UnnamedMessageReceivedEvent.Mark(new UnnamedMessageEvent(new ConnectionInfo(senderClientId), bytesCount)); + IncrementMetricCount(); + } + + public void TrackNetworkVariableDeltaSent( + ulong receiverClientId, + NetworkObject networkObject, + string variableName, + string networkBehaviourName, + long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_NetworkVariableDeltaSentEvent.Mark( + new NetworkVariableEvent( + new ConnectionInfo(receiverClientId), + GetObjectIdentifier(networkObject), + variableName, + networkBehaviourName, + bytesCount)); + IncrementMetricCount(); + } + + public void TrackNetworkVariableDeltaReceived( + ulong senderClientId, + NetworkObject networkObject, + string variableName, + string networkBehaviourName, + long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_NetworkVariableDeltaReceivedEvent.Mark( + new NetworkVariableEvent( + new ConnectionInfo(senderClientId), + GetObjectIdentifier(networkObject), + variableName, + networkBehaviourName, + bytesCount)); + IncrementMetricCount(); + } + + public void TrackOwnershipChangeSent(ulong receiverClientId, NetworkObject networkObject, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_OwnershipChangeSentEvent.Mark(new OwnershipChangeEvent(new ConnectionInfo(receiverClientId), GetObjectIdentifier(networkObject), bytesCount)); + IncrementMetricCount(); + } + + public void TrackOwnershipChangeReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_OwnershipChangeReceivedEvent.Mark(new OwnershipChangeEvent(new ConnectionInfo(senderClientId), + GetObjectIdentifier(networkObject), bytesCount)); + IncrementMetricCount(); + } + + public void TrackObjectSpawnSent(ulong receiverClientId, NetworkObject networkObject, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_ObjectSpawnSentEvent.Mark(new ObjectSpawnedEvent(new ConnectionInfo(receiverClientId), GetObjectIdentifier(networkObject), bytesCount)); + IncrementMetricCount(); + } + + public void TrackObjectSpawnReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_ObjectSpawnReceivedEvent.Mark(new ObjectSpawnedEvent(new ConnectionInfo(senderClientId), GetObjectIdentifier(networkObject), bytesCount)); + IncrementMetricCount(); + } + + public void TrackObjectDestroySent(ulong receiverClientId, NetworkObject networkObject, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_ObjectDestroySentEvent.Mark(new ObjectDestroyedEvent(new ConnectionInfo(receiverClientId), GetObjectIdentifier(networkObject), bytesCount)); + IncrementMetricCount(); + } + + public void TrackObjectDestroyReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_ObjectDestroyReceivedEvent.Mark(new ObjectDestroyedEvent(new ConnectionInfo(senderClientId), GetObjectIdentifier(networkObject), bytesCount)); + IncrementMetricCount(); + } + + public void TrackRpcSent( + ulong receiverClientId, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_RpcSentEvent.Mark( + new RpcEvent( + new ConnectionInfo(receiverClientId), + GetObjectIdentifier(networkObject), + rpcName, + networkBehaviourName, + bytesCount)); + IncrementMetricCount(); + } + + public void TrackRpcSent( + ulong[] receiverClientIds, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount) + { + foreach (var receiverClientId in receiverClientIds) + { + TrackRpcSent(receiverClientId, networkObject, rpcName, networkBehaviourName, bytesCount); + } + } + + public void TrackRpcReceived( + ulong senderClientId, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_RpcReceivedEvent.Mark( + new RpcEvent(new ConnectionInfo(senderClientId), + GetObjectIdentifier(networkObject), + rpcName, + networkBehaviourName, + bytesCount)); + IncrementMetricCount(); + } + + public void TrackServerLogSent(ulong receiverClientId, uint logType, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_ServerLogSentEvent.Mark(new ServerLogEvent(new ConnectionInfo(receiverClientId), (Multiplayer.Tools.MetricTypes.LogLevel)logType, bytesCount)); + IncrementMetricCount(); + } + + public void TrackServerLogReceived(ulong senderClientId, uint logType, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_ServerLogReceivedEvent.Mark(new ServerLogEvent(new ConnectionInfo(senderClientId), (Multiplayer.Tools.MetricTypes.LogLevel)logType, bytesCount)); + IncrementMetricCount(); + } + + public void TrackSceneEventSent(IReadOnlyList receiverClientIds, uint sceneEventType, string sceneName, long bytesCount) + { + foreach (var receiverClientId in receiverClientIds) + { + TrackSceneEventSent(receiverClientId, sceneEventType, sceneName, bytesCount); + } + } + + public void TrackSceneEventSent(ulong receiverClientId, uint sceneEventType, string sceneName, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_SceneEventSentEvent.Mark(new SceneEventMetric(new ConnectionInfo(receiverClientId), GetSceneEventTypeName(sceneEventType), sceneName, bytesCount)); + IncrementMetricCount(); + } + + public void TrackSceneEventReceived(ulong senderClientId, uint sceneEventType, string sceneName, long bytesCount) + { + if (!CanSendMetrics) + { + return; + } + + m_SceneEventReceivedEvent.Mark(new SceneEventMetric(new ConnectionInfo(senderClientId), GetSceneEventTypeName(sceneEventType), sceneName, bytesCount)); + IncrementMetricCount(); + } + + public void DispatchFrame() + { + Dispatcher.Dispatch(); + m_NumberOfMetricsThisFrame = 0; + } + + private void IncrementMetricCount() + { + m_NumberOfMetricsThisFrame++; + } + + private static NetworkObjectIdentifier GetObjectIdentifier(NetworkObject networkObject) + { + return new NetworkObjectIdentifier(networkObject.GetNameForMetrics(), networkObject.NetworkObjectId); + } + } + + internal class NetcodeObserver + { + public static IMetricObserver Observer { get; } = MetricObserverFactory.Construct(); + } +} +#endif diff --git a/Runtime/Metrics/NetworkMetrics.cs.meta b/Runtime/Metrics/NetworkMetrics.cs.meta new file mode 100644 index 0000000..ec92878 --- /dev/null +++ b/Runtime/Metrics/NetworkMetrics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef49007a4c206f44ea2c8fabf104a8f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Metrics/NetworkObjectProvider.cs b/Runtime/Metrics/NetworkObjectProvider.cs new file mode 100644 index 0000000..e1f6f01 --- /dev/null +++ b/Runtime/Metrics/NetworkObjectProvider.cs @@ -0,0 +1,27 @@ +#if MULTIPLAYER_TOOLS +using Unity.Multiplayer.Tools; +using UnityEngine; + +namespace Unity.Netcode +{ + class NetworkObjectProvider : INetworkObjectProvider + { + private readonly NetworkManager m_NetworkManager; + + public NetworkObjectProvider(NetworkManager networkManager) + { + m_NetworkManager = networkManager; + } + + public Object GetNetworkObject(ulong networkObjectId) + { + if(m_NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject value)) + { + return value; + } + + return null; + } + } +} +#endif diff --git a/Runtime/Metrics/NetworkObjectProvider.cs.meta b/Runtime/Metrics/NetworkObjectProvider.cs.meta new file mode 100644 index 0000000..38ebecf --- /dev/null +++ b/Runtime/Metrics/NetworkObjectProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 685ceb9b27db429dba04833cd6be49d8 +timeCreated: 1629857362 \ No newline at end of file diff --git a/Runtime/Metrics/NullNetworkMetrics.cs b/Runtime/Metrics/NullNetworkMetrics.cs new file mode 100644 index 0000000..514af8f --- /dev/null +++ b/Runtime/Metrics/NullNetworkMetrics.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; + +namespace Unity.Netcode +{ + internal class NullNetworkMetrics : INetworkMetrics + { + public void SetConnectionId(ulong connectionId) + { + } + + public void TrackTransportBytesSent(long bytesCount) + { + } + + public void TrackTransportBytesReceived(long bytesCount) + { + } + + public void TrackNetworkMessageSent(ulong receivedClientId, string messageType, long bytesCount) + { + } + + public void TrackNetworkMessageReceived(ulong senderClientId, string messageType, long bytesCount) + { + } + + public void TrackNamedMessageSent(ulong receiverClientId, string messageName, long bytesCount) + { + } + + public void TrackNamedMessageSent(IReadOnlyCollection receiverClientIds, string messageName, long bytesCount) + { + } + + public void TrackNamedMessageReceived(ulong senderClientId, string messageName, long bytesCount) + { + } + + public void TrackUnnamedMessageSent(ulong receiverClientId, long bytesCount) + { + } + + public void TrackUnnamedMessageSent(IReadOnlyCollection receiverClientIds, long bytesCount) + { + } + + public void TrackUnnamedMessageReceived(ulong senderClientId, long bytesCount) + { + } + + public void TrackNetworkVariableDeltaSent( + ulong receiverClientId, + NetworkObject networkObject, + string variableName, + string networkBehaviourName, + long bytesCount) + { + } + + public void TrackNetworkVariableDeltaReceived( + ulong senderClientId, + NetworkObject networkObject, + string variableName, + string networkBehaviourName, + long bytesCount) + { + } + + public void TrackOwnershipChangeSent(ulong receiverClientId, NetworkObject networkObject, long bytesCount) + { + } + + public void TrackOwnershipChangeReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount) + { + } + + public void TrackObjectSpawnSent(ulong receiverClientId, NetworkObject networkObject, long bytesCount) + { + } + + public void TrackObjectSpawnReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount) + { + } + + public void TrackObjectDestroySent(ulong senderClientId, NetworkObject networkObject, long bytesCount) + { + } + + public void TrackObjectDestroyReceived(ulong senderClientId, NetworkObject networkObject, long bytesCount) + { + } + + public void TrackRpcSent( + ulong receiverClientId, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount) + { + } + + public void TrackRpcSent( + ulong[] receiverClientIds, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount) + { + } + + public void TrackRpcReceived( + ulong senderClientId, + NetworkObject networkObject, + string rpcName, + string networkBehaviourName, + long bytesCount) + { + } + + public void TrackServerLogSent(ulong receiverClientId, uint logType, long bytesCount) + { + } + + public void TrackServerLogReceived(ulong senderClientId, uint logType, long bytesCount) + { + } + + public void TrackSceneEventSent(IReadOnlyList receiverClientIds, uint sceneEventType, string sceneName, long bytesCount) + { + } + + public void TrackSceneEventSent(ulong receiverClientId, uint sceneEventType, string sceneName, long bytesCount) + { + } + + public void TrackSceneEventReceived(ulong senderClientId, uint sceneEventType, string sceneName, long bytesCount) + { + } + + public void DispatchFrame() + { + } + } +} diff --git a/Runtime/Metrics/NullNetworkMetrics.cs.meta b/Runtime/Metrics/NullNetworkMetrics.cs.meta new file mode 100644 index 0000000..15d47eb --- /dev/null +++ b/Runtime/Metrics/NullNetworkMetrics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b858c4c3c7e08884bafdba745e688dbb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Metrics/StreamExtensions.cs b/Runtime/Metrics/StreamExtensions.cs new file mode 100644 index 0000000..615efb5 --- /dev/null +++ b/Runtime/Metrics/StreamExtensions.cs @@ -0,0 +1,12 @@ +using System.IO; + +namespace Unity.Netcode +{ + public static class StreamExtensions + { + public static long SafeGetLengthOrDefault(this Stream stream) + { + return stream.CanSeek ? stream.Length : 0; + } + } +} diff --git a/Runtime/Metrics/StreamExtensions.cs.meta b/Runtime/Metrics/StreamExtensions.cs.meta new file mode 100644 index 0000000..3eb9911 --- /dev/null +++ b/Runtime/Metrics/StreamExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 61dd9b1558f6d7c46ad323b2c2c03c29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkVariable.meta b/Runtime/NetworkVariable.meta new file mode 100644 index 0000000..6b78b53 --- /dev/null +++ b/Runtime/NetworkVariable.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 40a137fab0c0c6a46a2440d9a859755f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkVariable/Collections.meta b/Runtime/NetworkVariable/Collections.meta new file mode 100644 index 0000000..f463abb --- /dev/null +++ b/Runtime/NetworkVariable/Collections.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 614a7000210157648b54dd16914b97b5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs b/Runtime/NetworkVariable/Collections/NetworkList.cs new file mode 100644 index 0000000..8cae300 --- /dev/null +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -0,0 +1,536 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; + +namespace Unity.Netcode +{ + /// + /// Event based NetworkVariable container for syncing Lists + /// + /// The type for the list + public class NetworkList : NetworkVariableBase where T : unmanaged, IEquatable + { + private NativeList m_List = new NativeList(64, Allocator.Persistent); + private NativeList> m_DirtyEvents = new NativeList>(64, Allocator.Persistent); + + /// + /// Delegate type for list changed event + /// + /// Struct containing information about the change event + public delegate void OnListChangedDelegate(NetworkListEvent changeEvent); + + /// + /// The callback to be invoked when the list gets changed + /// + 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) + { + foreach (var value in values) + { + m_List.Add(value); + + } + } + + /// + public override void ResetDirty() + { + base.ResetDirty(); + m_DirtyEvents.Clear(); + } + + /// + public override bool IsDirty() + { + // we call the base class to allow the SetDirty() mechanism to work + return base.IsDirty() || m_DirtyEvents.Length > 0; + } + + /// + public override void WriteDelta(FastBufferWriter writer) + { + + if (base.IsDirty()) + { + writer.WriteValueSafe((ushort)1); + writer.WriteValueSafe(NetworkListEvent.EventType.Full); + WriteField(writer); + + return; + } + + writer.WriteValueSafe((ushort)m_DirtyEvents.Length); + for (int i = 0; i < m_DirtyEvents.Length; i++) + { + writer.WriteValueSafe(m_DirtyEvents[i].Type); + switch (m_DirtyEvents[i].Type) + { + case NetworkListEvent.EventType.Add: + { + writer.WriteValueSafe(m_DirtyEvents[i].Value); + } + break; + case NetworkListEvent.EventType.Insert: + { + writer.WriteValueSafe(m_DirtyEvents[i].Index); + writer.WriteValueSafe(m_DirtyEvents[i].Value); + } + break; + case NetworkListEvent.EventType.Remove: + { + writer.WriteValueSafe(m_DirtyEvents[i].Value); + } + break; + case NetworkListEvent.EventType.RemoveAt: + { + writer.WriteValueSafe(m_DirtyEvents[i].Index); + } + break; + case NetworkListEvent.EventType.Value: + { + writer.WriteValueSafe(m_DirtyEvents[i].Index); + writer.WriteValueSafe(m_DirtyEvents[i].Value); + } + break; + case NetworkListEvent.EventType.Clear: + { + //Nothing has to be written + } + break; + } + } + } + + /// + public override void WriteField(FastBufferWriter writer) + { + writer.WriteValueSafe((ushort)m_List.Length); + for (int i = 0; i < m_List.Length; i++) + { + writer.WriteValueSafe(m_List[i]); + } + } + + /// + public override void ReadField(FastBufferReader reader) + { + m_List.Clear(); + reader.ReadValueSafe(out ushort count); + for (int i = 0; i < count; i++) + { + reader.ReadValueSafe(out T value); + m_List.Add(value); + } + } + + /// + public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) + { + reader.ReadValueSafe(out ushort deltaCount); + for (int i = 0; i < deltaCount; i++) + { + reader.ReadValueSafe(out NetworkListEvent.EventType eventType); + switch (eventType) + { + case NetworkListEvent.EventType.Add: + { + reader.ReadValueSafe(out T value); + m_List.Add(value); + + if (OnListChanged != null) + { + OnListChanged(new NetworkListEvent + { + Type = eventType, + Index = m_List.Length - 1, + Value = m_List[m_List.Length - 1] + }); + } + + if (keepDirtyDelta) + { + m_DirtyEvents.Add(new NetworkListEvent() + { + Type = eventType, + Index = m_List.Length - 1, + Value = m_List[m_List.Length - 1] + }); + } + } + break; + case NetworkListEvent.EventType.Insert: + { + reader.ReadValueSafe(out int index); + reader.ReadValueSafe(out T value); + m_List.InsertRangeWithBeginEnd(index, index + 1); + m_List[index] = value; + + if (OnListChanged != null) + { + OnListChanged(new NetworkListEvent + { + Type = eventType, + Index = index, + Value = m_List[index] + }); + } + + if (keepDirtyDelta) + { + m_DirtyEvents.Add(new NetworkListEvent() + { + Type = eventType, + Index = index, + Value = m_List[index] + }); + } + } + break; + case NetworkListEvent.EventType.Remove: + { + reader.ReadValueSafe(out T value); + int index = m_List.IndexOf(value); + if (index == -1) + { + break; + } + + m_List.RemoveAt(index); + + if (OnListChanged != null) + { + OnListChanged(new NetworkListEvent + { + Type = eventType, + Index = index, + Value = value + }); + } + + if (keepDirtyDelta) + { + m_DirtyEvents.Add(new NetworkListEvent() + { + Type = eventType, + Index = index, + Value = value + }); + } + } + break; + case NetworkListEvent.EventType.RemoveAt: + { + reader.ReadValueSafe(out int index); + T value = m_List[index]; + m_List.RemoveAt(index); + + if (OnListChanged != null) + { + OnListChanged(new NetworkListEvent + { + Type = eventType, + Index = index, + Value = value + }); + } + + if (keepDirtyDelta) + { + m_DirtyEvents.Add(new NetworkListEvent() + { + Type = eventType, + Index = index, + Value = value + }); + } + } + break; + case NetworkListEvent.EventType.Value: + { + reader.ReadValueSafe(out int index); + reader.ReadValueSafe(out T value); + if (index < m_List.Length) + { + m_List[index] = value; + } + + if (OnListChanged != null) + { + OnListChanged(new NetworkListEvent + { + Type = eventType, + Index = index, + Value = value + }); + } + + if (keepDirtyDelta) + { + m_DirtyEvents.Add(new NetworkListEvent() + { + Type = eventType, + Index = index, + Value = value + }); + } + } + break; + case NetworkListEvent.EventType.Clear: + { + //Read nothing + m_List.Clear(); + + if (OnListChanged != null) + { + OnListChanged(new NetworkListEvent + { + Type = eventType, + }); + } + + if (keepDirtyDelta) + { + m_DirtyEvents.Add(new NetworkListEvent() + { + Type = eventType + }); + } + } + break; + case NetworkListEvent.EventType.Full: + { + ReadField(reader); + ResetDirty(); + } + break; + } + } + } + + /// + public IEnumerator GetEnumerator() + { + return m_List.GetEnumerator(); + } + + /// + public void Add(T item) + { + m_List.Add(item); + + var listEvent = new NetworkListEvent() + { + Type = NetworkListEvent.EventType.Add, + Value = item, + Index = m_List.Length - 1 + }; + + HandleAddListEvent(listEvent); + } + + /// + public void Clear() + { + m_List.Clear(); + + var listEvent = new NetworkListEvent() + { + Type = NetworkListEvent.EventType.Clear + }; + + HandleAddListEvent(listEvent); + } + + /// + public bool Contains(T item) + { + int index = NativeArrayExtensions.IndexOf(m_List, item); + return index == -1; + } + + /// + public bool Remove(T item) + { + int index = NativeArrayExtensions.IndexOf(m_List, item); + if (index == -1) + { + return false; + } + + m_List.RemoveAt(index); + var listEvent = new NetworkListEvent() + { + Type = NetworkListEvent.EventType.Remove, + Value = item + }; + + HandleAddListEvent(listEvent); + return true; + } + + /// + public int Count => m_List.Length; + + /// + public int IndexOf(T item) + { + return m_List.IndexOf(item); + } + + /// + public void Insert(int index, T item) + { + m_List.InsertRangeWithBeginEnd(index, index + 1); + m_List[index] = item; + + var listEvent = new NetworkListEvent() + { + Type = NetworkListEvent.EventType.Insert, + Index = index, + Value = item + }; + + HandleAddListEvent(listEvent); + } + + /// + public void RemoveAt(int index) + { + m_List.RemoveAt(index); + + var listEvent = new NetworkListEvent() + { + Type = NetworkListEvent.EventType.RemoveAt, + Index = index + }; + + HandleAddListEvent(listEvent); + } + + /// + public T this[int index] + { + get => m_List[index]; + set + { + m_List[index] = value; + + var listEvent = new NetworkListEvent() + { + Type = NetworkListEvent.EventType.Value, + Index = index, + Value = value + }; + + HandleAddListEvent(listEvent); + } + } + + private void HandleAddListEvent(NetworkListEvent listEvent) + { + m_DirtyEvents.Add(listEvent); + OnListChanged?.Invoke(listEvent); + } + + public int LastModifiedTick + { + get + { + // todo: implement proper network tick for NetworkList + return NetworkTickSystem.NoTick; + } + } + + public override void Dispose() + { + m_List.Dispose(); + m_DirtyEvents.Dispose(); + } + } + + /// + /// Struct containing event information about changes to a NetworkList. + /// + /// The type for the list that the event is about + public struct NetworkListEvent + { + /// + /// Enum representing the different operations available for triggering an event. + /// + public enum EventType : byte + { + /// + /// Add + /// + Add, + + /// + /// Insert + /// + Insert, + + /// + /// Remove + /// + Remove, + + /// + /// Remove at + /// + RemoveAt, + + /// + /// Value changed + /// + Value, + + /// + /// Clear + /// + Clear, + + /// + /// Full list refresh + /// + Full + } + + /// + /// Enum representing the operation made to the list. + /// + public EventType Type; + + /// + /// The value changed, added or removed if available. + /// + public T Value; + + /// + /// the index changed, added or removed if available + /// + public int Index; + } +} diff --git a/Runtime/NetworkVariable/Collections/NetworkList.cs.meta b/Runtime/NetworkVariable/Collections/NetworkList.cs.meta new file mode 100644 index 0000000..4844473 --- /dev/null +++ b/Runtime/NetworkVariable/Collections/NetworkList.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 42fb86f9389141347856337e3c62ad80 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs new file mode 100644 index 0000000..013877c --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -0,0 +1,131 @@ +using UnityEngine; +using System; + +namespace Unity.Netcode +{ + /// + /// A variable that can be synchronized over the network. + /// + [Serializable] + public class NetworkVariable : NetworkVariableBase where T : unmanaged + { + /// + /// Delegate type for value changed event + /// + /// The value before the change + /// The new value + public delegate void OnValueChangedDelegate(T previousValue, T newValue); + /// + /// The callback to be invoked when the value gets changed + /// + 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) + { + m_InternalValue = value; + } + + [SerializeField] + private protected T m_InternalValue; + + /// + /// The value of the NetworkVariable container + /// + 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 + + // 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)) + { + throw new InvalidOperationException("Client can't write to NetworkVariables"); + } + Set(value); + } + } + + private protected void Set(T value) + { + m_IsDirty = true; + T previousValue = m_InternalValue; + m_InternalValue = value; + OnValueChanged?.Invoke(previousValue, m_InternalValue); + } + + /// + /// Writes the variable to the writer + /// + /// The stream to write the value to + public override void WriteDelta(FastBufferWriter writer) + { + WriteField(writer); + } + + + /// + /// Reads value from the reader and applies it + /// + /// The stream to read the value from + /// Whether or not the container should keep the dirty delta, or mark the delta as consumed + public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) + { + T previousValue = m_InternalValue; + reader.ReadValueSafe(out m_InternalValue); + + if (keepDirtyDelta) + { + m_IsDirty = true; + } + + OnValueChanged?.Invoke(previousValue, m_InternalValue); + } + + /// + public override void ReadField(FastBufferReader reader) + { + reader.ReadValueSafe(out m_InternalValue); + } + + /// + public override void WriteField(FastBufferWriter writer) + { + writer.WriteValueSafe(m_InternalValue); + } + } +} diff --git a/Runtime/NetworkVariable/NetworkVariable.cs.meta b/Runtime/NetworkVariable/NetworkVariable.cs.meta new file mode 100644 index 0000000..79049f8 --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0f6bd4b05b09e604986db16876d2cd4f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkVariable/NetworkVariableBase.cs b/Runtime/NetworkVariable/NetworkVariableBase.cs new file mode 100644 index 0000000..60d61eb --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -0,0 +1,117 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// Interface for network value containers + /// + public abstract class NetworkVariableBase : IDisposable + { + /// + /// The delivery type (QoS) to send data with + /// + internal const NetworkDelivery Delivery = NetworkDelivery.ReliableSequenced; + + private protected NetworkBehaviour m_NetworkBehaviour; + + public void Initialize(NetworkBehaviour networkBehaviour) + { + m_NetworkBehaviour = networkBehaviour; + } + + protected NetworkVariableBase(NetworkVariableReadPermission readPermIn = NetworkVariableReadPermission.Everyone) + { + ReadPerm = readPermIn; + } + + private protected bool m_IsDirty; + + /// + /// Gets or sets the name of the network variable's instance + /// (MemberInfo) where it was declared. + /// + public string Name { get; internal set; } + + /// + /// The read permission for this var + /// + public readonly NetworkVariableReadPermission ReadPerm; + + /// + /// Sets whether or not the variable needs to be delta synced + /// + public virtual void SetDirty(bool isDirty) + { + m_IsDirty = isDirty; + } + + /// + /// Resets the dirty state and marks the variable as synced / clean + /// + public virtual void ResetDirty() + { + m_IsDirty = false; + } + + /// + /// Gets Whether or not the container is dirty + /// + /// Whether or not the container is dirty + 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) + { + case NetworkVariableReadPermission.Everyone: + return true; + case NetworkVariableReadPermission.OwnerOnly: + return m_NetworkBehaviour.OwnerClientId == clientId; + } + return true; + } + + /// + /// Writes the dirty changes, that is, the changes since the variable was last dirty, to the writer + /// + /// The stream to write the dirty changes to + public abstract void WriteDelta(FastBufferWriter writer); + + /// + /// Writes the complete state of the variable to the writer + /// + /// The stream to write the state to + public abstract void WriteField(FastBufferWriter writer); + + /// + /// Reads the complete state from the reader and applies it + /// + /// The stream to read the state from + public abstract void ReadField(FastBufferReader reader); + + /// + /// Reads delta from the reader and applies them to the internal value + /// + /// 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/NetworkVariableBase.cs.meta b/Runtime/NetworkVariable/NetworkVariableBase.cs.meta new file mode 100644 index 0000000..ef85502 --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariableBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f45dec3ed9b6942d889ef48036aba763 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkVariable/NetworkVariablePermission.cs b/Runtime/NetworkVariable/NetworkVariablePermission.cs new file mode 100644 index 0000000..df2456f --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariablePermission.cs @@ -0,0 +1,18 @@ +namespace Unity.Netcode +{ + /// + /// Permission type + /// + public enum NetworkVariableReadPermission + { + /// + /// Everyone + /// + Everyone, + + /// + /// Owner-ownly + /// + OwnerOnly, + } +} diff --git a/Runtime/NetworkVariable/NetworkVariablePermission.cs.meta b/Runtime/NetworkVariable/NetworkVariablePermission.cs.meta new file mode 100644 index 0000000..4a37866 --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariablePermission.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bc753a77be3d7e846b7bb2612a67ac62 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Profiling.meta b/Runtime/Profiling.meta new file mode 100644 index 0000000..16188a8 --- /dev/null +++ b/Runtime/Profiling.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0fb2af5a23f5eba4eba7151395239c81 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Profiling/ProfilingHooks.cs b/Runtime/Profiling/ProfilingHooks.cs new file mode 100644 index 0000000..a328be4 --- /dev/null +++ b/Runtime/Profiling/ProfilingHooks.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Unity.Profiling; + +namespace Unity.Netcode +{ + internal class ProfilingHooks : INetworkHooks + { + private Dictionary m_HandlerProfilerMarkers = new Dictionary(); + private Dictionary m_SenderProfilerMarkers = new Dictionary(); + private readonly ProfilerMarker m_SendBatch = new ProfilerMarker($"{nameof(MessagingSystem)}.SendBatch"); + private readonly ProfilerMarker m_ReceiveBatch = new ProfilerMarker($"{nameof(MessagingSystem)}.ReceiveBatchBatch"); + + private ProfilerMarker GetHandlerProfilerMarker(Type type) + { + var result = m_HandlerProfilerMarkers.TryGetValue(type, out var marker); + if (result) + { + return marker; + } + + marker = new ProfilerMarker($"{nameof(MessagingSystem)}.DeserializeAndHandle.{type.Name}"); + m_HandlerProfilerMarkers[type] = marker; + return marker; + } + + private ProfilerMarker GetSenderProfilerMarker(Type type) + { + var result = m_SenderProfilerMarkers.TryGetValue(type, out var marker); + if (result) + { + return marker; + } + + marker = new ProfilerMarker($"{nameof(MessagingSystem)}.SerializeAndEnqueue.{type.Name}"); + m_SenderProfilerMarkers[type] = marker; + return marker; + } + + public void OnBeforeSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery) + { + GetSenderProfilerMarker(messageType).Begin(); + } + + public void OnAfterSendMessage(ulong clientId, Type messageType, NetworkDelivery delivery, int messageSizeBytes) + { + GetSenderProfilerMarker(messageType).End(); + } + + public void OnBeforeReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + GetHandlerProfilerMarker(messageType).Begin(); + } + + public void OnAfterReceiveMessage(ulong senderId, Type messageType, int messageSizeBytes) + { + GetHandlerProfilerMarker(messageType).End(); + } + + public void OnBeforeSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + m_SendBatch.Begin(); + } + + public void OnAfterSendBatch(ulong clientId, int messageCount, int batchSizeInBytes, NetworkDelivery delivery) + { + m_SendBatch.End(); + } + + public void OnBeforeReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + m_ReceiveBatch.Begin(); + } + + public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeInBytes) + { + m_ReceiveBatch.End(); + } + + public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) + { + return true; + } + + public bool OnVerifyCanReceive(ulong senderId, Type messageType) + { + return true; + } + } +} diff --git a/Runtime/Profiling/ProfilingHooks.cs.meta b/Runtime/Profiling/ProfilingHooks.cs.meta new file mode 100644 index 0000000..b30d52e --- /dev/null +++ b/Runtime/Profiling/ProfilingHooks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: da46509a8ea6d1c4390e0102f6422785 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Reflection.meta b/Runtime/Reflection.meta new file mode 100644 index 0000000..da604a3 --- /dev/null +++ b/Runtime/Reflection.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: af81f9951b096ff4cb8e4f8a4106104a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Reflection/TypeExtensions.cs b/Runtime/Reflection/TypeExtensions.cs new file mode 100644 index 0000000..216e489 --- /dev/null +++ b/Runtime/Reflection/TypeExtensions.cs @@ -0,0 +1,31 @@ +using System; + +namespace Unity.Netcode +{ + internal static class TypeExtensions + { + internal static bool HasInterface(this Type type, Type interfaceType) + { + var ifaces = type.GetInterfaces(); + for (int i = 0; i < ifaces.Length; i++) + { + if (ifaces[i] == interfaceType) + { + return true; + } + } + + return false; + } + + internal static bool IsNullable(this Type type) + { + if (!type.IsValueType) + { + return true; // ref-type + } + + return Nullable.GetUnderlyingType(type) != null; + } + } +} diff --git a/Runtime/Reflection/TypeExtensions.cs.meta b/Runtime/Reflection/TypeExtensions.cs.meta new file mode 100644 index 0000000..80c06af --- /dev/null +++ b/Runtime/Reflection/TypeExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e168a2bc1a1e2642af0369780fb560c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SceneManagement.meta b/Runtime/SceneManagement.meta new file mode 100644 index 0000000..12020c4 --- /dev/null +++ b/Runtime/SceneManagement.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ef03104715c587a4d83b4eb3118412e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs new file mode 100644 index 0000000..5cb3fc2 --- /dev/null +++ b/Runtime/SceneManagement/NetworkSceneManager.cs @@ -0,0 +1,1900 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using UnityEngine; +using UnityEngine.SceneManagement; + + +namespace Unity.Netcode +{ + /// + /// Used for local notifications of various scene events. + /// The of delegate type uses this class to provide + /// scene event status/state. + /// + public class SceneEvent + { + /// + /// The returned by + /// This is set for the following s: + /// + /// + /// + public AsyncOperation AsyncOperation; + + /// + /// Will always be set to the current + /// + public SceneEventType SceneEventType; + + /// + /// If applicable, this reflects the type of scene loading or unloading that is occurring. + /// This is set for the following s: + /// + /// + /// + /// + /// + /// + /// + public LoadSceneMode LoadSceneMode; + + /// + /// This will be set to the scene name that the event pertains to. + /// This is set for the following s: + /// + /// + /// + /// + /// + /// + /// + public string SceneName; + + /// + /// When a scene is loaded, the Scene structure is returned. + /// This is set for the following s: + /// + /// + public Scene Scene; + + /// + /// Events that always set the to the local client identifier, + /// are initiated (and processed locally) by the server-host, and sent to all clients + /// to be processed: + /// + /// + /// + /// + /// + /// Events that always set the to the local client identifier, + /// are initiated (and processed locally) by a client or server-host, and if initiated + /// by a client will always be sent to and processed on the server-host: + /// + /// + /// + /// + /// Events that always set the to the ServerId: + /// + /// + /// + public ulong ClientId; + + /// + /// List of clients that completed a loading or unloading event + /// This is set for the following s: + /// + /// + /// + public List ClientsThatCompleted; + + /// + /// List of clients that timed out during a loading or unloading event + /// This is set for the following s: + /// + /// + /// + public List ClientsThatTimedOut; + } + + /// + /// Main class for managing network scenes when is enabled. + /// Uses the message to communicate between the server and client(s) + /// + 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. + internal static bool DisableReSynchronization; + + /// + /// Used to detect if a scene event is underway + /// Only 1 scene event can occur on the server at a time for now. + /// + private static bool s_IsSceneEventActive = false; + + // TODO: Remove `m_IsRunningUnitTest` entirely after we switch to multi-process testing + // In MultiInstance tests, we cannot allow clients to load additional scenes as they're sharing the same scene space / Unity instance. +#if UNITY_INCLUDE_TESTS + private readonly bool m_IsRunningUnitTest = SceneManager.GetActiveScene().name.StartsWith("InitTestScene"); +#endif + + /// + /// The delegate callback definition for scene event notifications + /// For more details review over and + /// + /// + public delegate void SceneEventDelegate(SceneEvent sceneEvent); + + /// + /// Event that will notify the local client or server of all scene events that take place + /// For more details review over , , and + /// Subscribe to this event to receive all notifications + /// + /// Alternate Single Event Type Notification Registration Options + /// To receive only a specific event type notification or a limited set of notifications you can alternately subscribe to + /// each notification type individually via the following events: + /// -- Invoked only when a event is being processed + /// -- Invoked only when an event is being processed + /// -- Invoked only when a event is being processed + /// -- Invoked only when a event is being processed + /// -- Invoked only when an event is being processed + /// -- Invoked only when a event is being processed + /// -- Invoked only when an event is being processed + /// -- Invoked only when a event is being processed + /// + public event SceneEventDelegate OnSceneEvent; + + /// + /// Delegate declaration for the OnLoad event + /// View for more information + /// + /// the client that is processing this event (the server will receive all of these events for every client and itself) + /// name of the scene being processed + /// the LoadSceneMode mode for the scene being loaded + /// the associated that can be used for scene loading progress + public delegate void OnLoadDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation); + + /// + /// Delegate declaration for the OnUnload event + /// View for more information + /// + /// the client that is processing this event (the server will receive all of these events for every client and itself) + /// name of the scene being processed + /// the associated that can be used for scene unloading progress + public delegate void OnUnloadDelegateHandler(ulong clientId, string sceneName, AsyncOperation asyncOperation); + + /// + /// Delegate declaration for the OnSynchronize event + /// View for more information + /// + /// the client that is processing this event (the server will receive all of these events for every client and itself) + public delegate void OnSynchronizeDelegateHandler(ulong clientId); + + /// + /// Delegate declaration for the OnLoadEventCompleted and OnUnloadEventCompleted events + /// View for more information + /// View for more information + /// + /// scene pertaining to this event + /// of the associated event completed + /// the clients that completed the loading event + /// the clients (if any) that timed out during the loading event + public delegate void OnEventCompletedDelegateHandler(string sceneName, LoadSceneMode loadSceneMode, List clientsCompleted, List clientsTimedOut); + + /// + /// Delegate declaration for the OnLoadComplete event + /// View for more information + /// + /// the client that is processing this event (the server will receive all of these events for every client and itself) + /// the scene name pertaining to this event + /// the mode the scene was loaded in + public delegate void OnLoadCompleteDelegateHandler(ulong clientId, string sceneName, LoadSceneMode loadSceneMode); + + /// + /// Delegate declaration for the OnUnloadComplete event + /// View for more information + /// + /// the client that is processing this event (the server will receive all of these events for every client and itself) + /// the scene name pertaining to this event + public delegate void OnUnloadCompleteDelegateHandler(ulong clientId, string sceneName); + + /// + /// Delegate declaration for the OnSynchronizeComplete event + /// View for more information + /// + /// the client that completed this event + public delegate void OnSynchronizeCompleteDelegateHandler(ulong clientId); + + /// + /// Invoked when a event is started by the server + /// The server and client(s) will receive this notification + /// + public event OnLoadDelegateHandler OnLoad; + + /// + /// Invoked when a event is started by the server + /// The server and client(s) will receive this notification + /// + public event OnUnloadDelegateHandler OnUnload; + + /// + /// Invoked when a event is started by the server + /// after a client is approved for connection in order to synchronize the client with the currently loaded + /// scenes and NetworkObjects. This event signifies the beginning of the synchronization event. + /// The server and client will receive this notification + /// Note: this event is generated on a per newly connected and approved client basis + /// + public event OnSynchronizeDelegateHandler OnSynchronize; + + /// + /// Invoked when a event is generated by the server. + /// This event signifies the end of an existing event as it pertains + /// to all clients connected when the event was started. This event signifies that all clients (and server) have + /// finished the event. + /// Note: this is useful to know when all clients have loaded the same scene (single or additive mode) + /// + public event OnEventCompletedDelegateHandler OnLoadEventCompleted; + + /// + /// Invoked when a event is generated by the server. + /// This event signifies the end of an existing event as it pertains + /// to all clients connected when the event was started. This event signifies that all clients (and server) have + /// finished the event. + /// Note: this is useful to know when all clients have unloaded a specific scene. The will + /// always be for this event + /// + public event OnEventCompletedDelegateHandler OnUnloadEventCompleted; + + /// + /// Invoked when a event is generated by a client or server. + /// The server receives this message from all clients (including itself). + /// Each client receives their own notification sent to the server. + /// + public event OnLoadCompleteDelegateHandler OnLoadComplete; + + /// + /// Invoked when a event is generated by a client or server. + /// The server receives this message from all clients (including itself). + /// Each client receives their own notification sent to the server. + /// + public event OnUnloadCompleteDelegateHandler OnUnloadComplete; + + /// + /// Invoked when a event is generated by a client. + /// The server receives this message from the client, but will never generate this event for itself. + /// Each client receives their own notification sent to the server. + /// Note: This is useful to know that a client has completed the entire connection sequence, loaded all scenes, and + /// synchronized all NetworkObjects. + /// + public event OnSynchronizeCompleteDelegateHandler OnSynchronizeComplete; + + /// + /// Delegate declaration for the handler that provides + /// an additional level of scene loading security and/or validation to assure the scene being loaded + /// is valid scene to be loaded in the LoadSceneMode specified. + /// + /// Build Settings Scenes in Build List index of the scene + /// Name of the scene + /// LoadSceneMode the scene is going to be loaded + /// true (valid) or false (not valid) + public delegate bool VerifySceneBeforeLoadingDelegateHandler(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode); + + /// + /// Delegate handler defined by that is invoked before the + /// server or client loads a scene during an active netcode game session. + /// Client Side: In order for clients to be notified of this condition you must assign the delegate handler. + /// Server Side: will return . + /// + public VerifySceneBeforeLoadingDelegateHandler VerifySceneBeforeLoading; + + internal readonly Dictionary SceneEventProgressTracking = new Dictionary(); + + /// + /// Used to track in-scene placed NetworkObjects + /// We store them by: + /// [GlobalObjectIdHash][Scene.Handle][NetworkObject] + /// The Scene.Handle aspect allows us to distinguish duplicated in-scene placed NetworkObjects created by the loading + /// of the same additive scene multiple times. + /// + internal readonly Dictionary> ScenePlacedObjects = new Dictionary>(); + + /// + /// This is used for the deserialization of in-scene placed NetworkObjects in order to distinguish duplicated in-scene + /// placed NetworkObjects created by the loading of the same additive scene multiple times. + /// + internal Scene SceneBeingSynchronized; + + /// + /// Used to track which scenes are currently loaded + /// We store the scenes as [SceneHandle][Scene] in order to handle the loading and unloading of the same scene additively + /// Scene handle is only unique locally. So, clients depend upon the in order + /// to be able to know which specific scene instance the server is instructing the client to unload. + /// The client links the server scene handle to the client local scene handle upon a scene being loaded + /// + /// + internal Dictionary ScenesLoaded = new Dictionary(); + + /// + /// Since Scene.handle is unique per client, we create a look-up table between the client and server to associate server unique scene + /// instances with client unique scene instances + /// + internal Dictionary ServerSceneHandleToClientSceneHandle = new Dictionary(); + + /// + /// Hash to build index lookup table + /// + internal Dictionary HashToBuildIndex = new Dictionary(); + + /// + /// Build index to hash lookup table + /// + internal Dictionary BuildIndexToHash = new Dictionary(); + + /// + /// The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned + /// they need to be moved into the do not destroy temporary scene + /// 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 + /// + internal static bool IsSpawnedObjectsPendingInDontDestroyOnLoad; + + /// + /// Client and Server: + /// Used for all scene event processing + /// + internal Dictionary SceneEventDataStore; + + private NetworkManager m_NetworkManager { get; } + + internal Scene DontDestroyOnLoadScene; + + /// + /// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and + /// the server's currently active scene will be loaded in single mode on the client + /// unless it was already loaded. + /// + /// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded + /// scenes will be loaded additively. Users need to determine which scenes are valid to load via the + /// method. + /// + public LoadSceneMode ClientSynchronizationMode { get; internal set; } + + /// + /// When true, the messages will be turned off + /// + private bool m_DisableValidationWarningMessages; + + /// + /// Handle NetworkSeneManager clean up + /// + public void Dispose() + { + foreach (var keypair in SceneEventDataStore) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogInfo($"{nameof(SceneEventDataStore)} is disposing {nameof(SceneEventData.SceneEventId)} '{keypair.Key}'."); + } + keypair.Value.Dispose(); + } + SceneEventDataStore.Clear(); + SceneEventDataStore = null; + } + + /// + /// Creates a new SceneEventData object for a new scene event + /// + /// SceneEventData instance + internal SceneEventData BeginSceneEvent() + { + var sceneEventData = new SceneEventData(m_NetworkManager); + SceneEventDataStore.Add(sceneEventData.SceneEventId, sceneEventData); + return sceneEventData; + } + + /// + /// Disposes and removes SceneEventData object for the scene event + /// + /// SceneEventId to end + internal void EndSceneEvent(uint sceneEventId) + { + if (SceneEventDataStore.ContainsKey(sceneEventId)) + { + SceneEventDataStore[sceneEventId].Dispose(); + SceneEventDataStore.Remove(sceneEventId); + } + else + { + Debug.LogWarning($"Trying to dispose and remove SceneEventData Id '{sceneEventId}' that no longer exists!"); + } + } + + /// + /// Gets the scene name from full path to the scene + /// + internal string GetSceneNameFromPath(string scenePath) + { + var begin = scenePath.LastIndexOf("/", StringComparison.Ordinal) + 1; + var end = scenePath.LastIndexOf(".", StringComparison.Ordinal); + return scenePath.Substring(begin, end - begin); + } + + /// + /// Generates the hash values and associated tables + /// for the scenes in build list + /// + internal void GenerateScenesInBuild() + { + HashToBuildIndex.Clear(); + BuildIndexToHash.Clear(); + for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++) + { + var scenePath = SceneUtility.GetScenePathByBuildIndex(i); + var hash = XXHash.Hash32(scenePath); + var buildIndex = SceneUtility.GetBuildIndexByScenePath(scenePath); + HashToBuildIndex.Add(hash, buildIndex); + BuildIndexToHash.Add(buildIndex, hash); + } + } + + /// + /// Gets the scene name from a hash value generated from the full scene path + /// + internal string SceneNameFromHash(uint sceneHash) + { + // In the event there is no scene associated with the scene event then just return "No Scene" + // This can happen during unit tests when clients first connect and the only scene loaded is the + // unit test scene (which is ignored by default) that results in a scene event that has no associated + // scene. Under this specific special case, we just return "No Scene". + if (sceneHash == 0) + { + return "No Scene"; + } + return GetSceneNameFromPath(ScenePathFromHash(sceneHash)); + } + + /// + /// Gets the full scene path from a hash value + /// + internal string ScenePathFromHash(uint sceneHash) + { + if (HashToBuildIndex.ContainsKey(sceneHash)) + { + return SceneUtility.GetScenePathByBuildIndex(HashToBuildIndex[sceneHash]); + } + else + { + throw new Exception($"Scene Hash {sceneHash} does not exist in the {nameof(HashToBuildIndex)} table!"); + } + } + + /// + /// Gets the associated hash value for the scene name or path + /// + internal uint SceneHashFromNameOrPath(string sceneNameOrPath) + { + var buildIndex = SceneUtility.GetBuildIndexByScenePath(sceneNameOrPath); + if (buildIndex >= 0) + { + if (BuildIndexToHash.ContainsKey(buildIndex)) + { + return BuildIndexToHash[buildIndex]; + } + else + { + throw new Exception($"Scene '{sceneNameOrPath}' has a build index of {buildIndex} that does not exist in the {nameof(BuildIndexToHash)} table!"); + } + } + else + { + throw new Exception($"Scene '{sceneNameOrPath}' couldn't be loaded because it has not been added to the build settings scenes in build list."); + } + } + + /// + /// When set to true, this will disable the console warnings about + /// a scene being invalidated. + /// + /// true/false + public void DisableValidationWarnings(bool disabled) + { + m_DisableValidationWarningMessages = disabled; + } + + /// + /// This will change how clients are initially synchronized. + /// LoadSceneMode.Single: All currently loaded scenes on the client will be unloaded and + /// the server's currently active scene will be loaded in single mode on the client + /// unless it was already loaded. + /// + /// LoadSceneMode.Additive: All currently loaded scenes are left as they are and any newly loaded + /// scenes will be loaded additively. Users need to determine which scenes are valid to load via the + /// method. + /// + /// for initial client synchronization + public void SetClientSynchronizationMode(LoadSceneMode mode) + { + ClientSynchronizationMode = mode; + } + + /// + /// Constructor + /// + /// one instance per instance + /// maximum pool size + internal NetworkSceneManager(NetworkManager networkManager) + { + m_NetworkManager = networkManager; + SceneEventDataStore = new Dictionary(); + + GenerateScenesInBuild(); + + // If NetworkManager has this set to true, then we can get the DDOL (DontDestroyOnLoad) from its GaemObject + if (networkManager.DontDestroy) + { + DontDestroyOnLoadScene = networkManager.gameObject.scene; + } + else + { + // Otherwise, we have to create a GameObject and move it into the DDOL in order to + // register the DDOL scene handle with NetworkSceneManager + var myDDOLObject = new GameObject("DDOL-NWSM"); + UnityEngine.Object.DontDestroyOnLoad(myDDOLObject); + DontDestroyOnLoadScene = myDDOLObject.scene; + UnityEngine.Object.Destroy(myDDOLObject); + } + + ServerSceneHandleToClientSceneHandle.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle); + ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene); + } + + /// + /// If the VerifySceneBeforeLoading delegate handler has been set by the user, this will provide + /// an additional level of security and/or validation that the scene being loaded in the specified + /// loading mode is "a valid scene to be loaded in the LoadSceneMode specified". + /// + /// index into ScenesInBuild + /// LoadSceneMode the scene is going to be loaded + /// true (Valid) or false (Invalid) + internal bool ValidateSceneBeforeLoading(uint sceneHash, LoadSceneMode loadSceneMode) + { + var validated = true; + var sceneName = SceneNameFromHash(sceneHash); + var sceneIndex = SceneUtility.GetBuildIndexByScenePath(sceneName); + if (VerifySceneBeforeLoading != null) + { + validated = VerifySceneBeforeLoading.Invoke((int)sceneIndex, sceneName, loadSceneMode); + } + if (!validated && !m_DisableValidationWarningMessages) + { + var serverHostorClient = "Client"; + if (m_NetworkManager.IsServer) + { + serverHostorClient = m_NetworkManager.IsHost ? "Host" : "Server"; + } + + Debug.LogWarning($"Scene {sceneName} of Scenes in Build Index {sceneIndex} being loaded in {loadSceneMode} mode failed validation on the {serverHostorClient}!"); + } + return validated; + } + + /// + /// Since SceneManager.GetSceneByName only returns the first scene that matches the name + /// we must "find" a newly added scene by looking through all loaded scenes and determining + /// which scene with the same name has not yet been loaded. + /// In order to support loading the same additive scene within in-scene placed NetworkObjects, + /// we must do this to be able to soft synchronize the "right version" of the NetworkObject. + /// + /// + /// + internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName) + { + for (int i = 0; i < SceneManager.sceneCount; i++) + { + var sceneLoaded = SceneManager.GetSceneAt(i); + if (sceneLoaded.name == sceneName) + { + if (!ScenesLoaded.ContainsKey(sceneLoaded.handle)) + { + ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded); + return sceneLoaded; + } + } + } + + throw new Exception($"Failed to find any loaded scene named {sceneName}!"); + } + + /// + /// Client Side Only: + /// This takes a server scene handle that is written by the server before the scene relative + /// NetworkObject is serialized and converts the server scene handle to a local client handle + /// so it can set the appropriate SceneBeingSynchronized. + /// Note: This is now part of the soft synchronization process and is needed for the scenario + /// where a user loads the same scene additively that has an in-scene placed NetworkObject + /// which means each scene relative in-scene placed NetworkObject will have the identical GlobalObjectIdHash + /// value. Scene handles are used to distinguish between in-scene placed NetworkObjects under this situation. + /// + /// + internal void SetTheSceneBeingSynchronized(int serverSceneHandle) + { + var clientSceneHandle = serverSceneHandle; + if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverSceneHandle)) + { + clientSceneHandle = ServerSceneHandleToClientSceneHandle[serverSceneHandle]; + // If we were already set, then ignore + if (SceneBeingSynchronized.IsValid() && SceneBeingSynchronized.isLoaded && SceneBeingSynchronized.handle == clientSceneHandle) + { + return; + } + + // Get the scene currently being synchronized + SceneBeingSynchronized = ScenesLoaded.ContainsKey(clientSceneHandle) ? ScenesLoaded[clientSceneHandle] : new Scene(); + + if (!SceneBeingSynchronized.IsValid() || !SceneBeingSynchronized.isLoaded) + { + // Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of + SceneBeingSynchronized = SceneManager.GetActiveScene(); + + // Keeping the warning here in the event we cannot find the scene being synchronized + Debug.LogWarning($"[{nameof(NetworkSceneManager)}- {nameof(ScenesLoaded)}] Could not find the appropriate scene to set as being synchronized! Using the currently active scene."); + } + } + else + { + // Most common scenario for DontDestroyOnLoad is when NetworkManager is set to not be destroyed + if (serverSceneHandle == DontDestroyOnLoadScene.handle) + { + SceneBeingSynchronized = m_NetworkManager.gameObject.scene; + return; + } + else + { + // Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of + // or the NetworkObject has yet to be moved to that specific scene (i.e. no DontDestroyOnLoad scene exists yet). + SceneBeingSynchronized = SceneManager.GetActiveScene(); + + // This could be the scenario where NetworkManager.DontDestroy is false and we are creating the first NetworkObject (client side) to be in the DontDestroyOnLoad scene + // Otherwise, this is some other specific scenario that we might not be handling currently. + Debug.LogWarning($"[{nameof(SceneEventData)}- Scene Handle Mismatch] {nameof(serverSceneHandle)} could not be found in {nameof(ServerSceneHandleToClientSceneHandle)}. Using the currently active scene."); + } + } + } + + /// + /// During soft synchronization of in-scene placed NetworkObjects, this is now used by NetworkSpawnManager.CreateLocalNetworkObject + /// + /// + /// + internal NetworkObject GetSceneRelativeInSceneNetworkObject(uint globalObjectIdHash) + { + if (ScenePlacedObjects.ContainsKey(globalObjectIdHash)) + { + if (ScenePlacedObjects[globalObjectIdHash].ContainsKey(SceneBeingSynchronized.handle)) + { + var inScenePlacedNetworkObject = ScenePlacedObjects[globalObjectIdHash][SceneBeingSynchronized.handle]; + + // We can only have 1 duplicated globalObjectIdHash per scene instance, so remove it once it has been returned + ScenePlacedObjects[globalObjectIdHash].Remove(SceneBeingSynchronized.handle); + + return inScenePlacedNetworkObject; + } + } + return null; + } + + /// + /// Generic sending of scene event data + /// + /// array of client identifiers to receive the scene event message + private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds) + { + if (targetClientIds.Length == 0) + { + // This would be the Host/Server with no clients connected + // Silently return as there is nothing to be done + return; + } + var message = new SceneEventMessage + { + EventData = SceneEventDataStore[sceneEventId] + }; + var size = m_NetworkManager.SendMessage(message, k_DeliveryType, targetClientIds); + + m_NetworkManager.NetworkMetrics.TrackSceneEventSent( + targetClientIds, (uint)SceneEventDataStore[sceneEventId].SceneEventType, SceneNameFromHash(SceneEventDataStore[sceneEventId].SceneHash), size); + } + + /// + /// Entry method for scene unloading validation + /// + /// the scene to be unloaded + /// + private SceneEventProgress ValidateSceneEventUnLoading(Scene scene) + { + if (!m_NetworkManager.IsServer) + { + throw new NotServerException("Only server can start a scene event!"); + } + + if (!m_NetworkManager.NetworkConfig.EnableSceneManagement) + { + //Log message about enabling SceneManagement + throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " + + $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " + + $"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}."); + } + + if (!scene.isLoaded) + { + Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!"); + return new SceneEventProgress(null, SceneEventProgressStatus.SceneNotLoaded); + } + + return ValidateSceneEvent(scene.name, true); + } + + /// + /// Entry method for scene loading validation + /// + /// scene name to load + /// + private SceneEventProgress ValidateSceneEventLoading(string sceneName) + { + if (!m_NetworkManager.IsServer) + { + throw new NotServerException("Only server can start a scene event!"); + } + if (!m_NetworkManager.NetworkConfig.EnableSceneManagement) + { + //Log message about enabling SceneManagement + throw new Exception($"{nameof(NetworkConfig.EnableSceneManagement)} flag is not enabled in the {nameof(NetworkManager)}'s {nameof(NetworkConfig)}. " + + $"Please set {nameof(NetworkConfig.EnableSceneManagement)} flag to true before calling " + + $"{nameof(NetworkSceneManager.LoadScene)} or {nameof(NetworkSceneManager.UnloadScene)}."); + } + + return ValidateSceneEvent(sceneName); + } + + /// + /// Validates the new scene event request by the server-side code. + /// This also initializes some commonly shared values as well as SceneEventProgress + /// + /// + /// that should have a of otherwise it failed. + private SceneEventProgress ValidateSceneEvent(string sceneName, bool isUnloading = false) + { + // Return scene event already in progress if one is already in progress + if (s_IsSceneEventActive) + { + return new SceneEventProgress(null, SceneEventProgressStatus.SceneEventInProgress); + } + + // Return invalid scene name status if the scene name is invalid + if (SceneUtility.GetBuildIndexByScenePath(sceneName) == InvalidSceneNameOrPath) + { + Debug.LogError($"Scene '{sceneName}' couldn't be loaded because it has not been added to the build settings scenes in build list."); + return new SceneEventProgress(null, SceneEventProgressStatus.InvalidSceneName); + } + + var sceneEventProgress = new SceneEventProgress(m_NetworkManager) + { + SceneHash = SceneHashFromNameOrPath(sceneName) + }; + + SceneEventProgressTracking.Add(sceneEventProgress.Guid, sceneEventProgress); + + if (!isUnloading) + { + // The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned + // they need to be moved into the do not destroy temporary scene + // 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 + IsSpawnedObjectsPendingInDontDestroyOnLoad = true; + } + + s_IsSceneEventActive = true; + + // Set our callback delegate handler for completion + sceneEventProgress.OnComplete = OnSceneEventProgressCompleted; + + return sceneEventProgress; + } + + /// + /// Callback for the handler + /// + /// + /// + private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress) + { + var sceneEventData = BeginSceneEvent(); + sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; + sceneEventData.SceneHash = sceneEventProgress.SceneHash; + sceneEventData.SceneEventType = sceneEventProgress.SceneEventType; + sceneEventData.ClientsCompleted = sceneEventProgress.DoneClients; + sceneEventData.LoadSceneMode = sceneEventProgress.LoadSceneMode; + sceneEventData.ClientsTimedOut = m_NetworkManager.ConnectedClients.Keys.Except(sceneEventProgress.DoneClients).ToList(); + + var message = new SceneEventMessage + { + EventData = sceneEventData + }; + var size = m_NetworkManager.SendMessage(message, k_DeliveryType, m_NetworkManager.ConnectedClientsIds); + + m_NetworkManager.NetworkMetrics.TrackSceneEventSent( + m_NetworkManager.ConnectedClientsIds, + (uint)sceneEventProgress.SceneEventType, + SceneNameFromHash(sceneEventProgress.SceneHash), + size); + + // Send a local notification to the server that all clients are done loading or unloading + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventProgress.SceneEventType, + SceneName = SceneNameFromHash(sceneEventProgress.SceneHash), + ClientId = m_NetworkManager.ServerClientId, + LoadSceneMode = sceneEventProgress.LoadSceneMode, + ClientsThatCompleted = sceneEventProgress.DoneClients, + ClientsThatTimedOut = m_NetworkManager.ConnectedClients.Keys.Except(sceneEventProgress.DoneClients).ToList(), + }); + + if (sceneEventData.SceneEventType == SceneEventType.LoadEventCompleted) + { + OnLoadEventCompleted?.Invoke(SceneNameFromHash(sceneEventProgress.SceneHash), sceneEventProgress.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); + } + else + { + OnUnloadEventCompleted?.Invoke(SceneNameFromHash(sceneEventProgress.SceneHash), sceneEventProgress.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); + } + + EndSceneEvent(sceneEventData.SceneEventId); + return true; + } + + /// + /// Server Side: + /// Unloads an additively loaded scene. If you want to unload a mode loaded scene load another scene. + /// When applicable, the is delivered within the via the + /// + /// scene name to unload + /// ( means it was successful) + public SceneEventProgressStatus UnloadScene(Scene scene) + { + var sceneName = scene.name; + var sceneHandle = scene.handle; + if (!scene.isLoaded) + { + Debug.LogWarning($"{nameof(UnloadScene)} was called, but the scene {scene.name} is not currently loaded!"); + return SceneEventProgressStatus.SceneNotLoaded; + } + + var sceneEventProgress = ValidateSceneEventUnLoading(scene); + if (sceneEventProgress.Status != SceneEventProgressStatus.Started) + { + return sceneEventProgress.Status; + } + + if (!ScenesLoaded.ContainsKey(sceneHandle)) + { + Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!"); + return SceneEventProgressStatus.InternalNetcodeError; + } + var sceneEventData = BeginSceneEvent(); + sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; + sceneEventData.SceneEventType = SceneEventType.Unload; + sceneEventData.SceneHash = SceneHashFromNameOrPath(sceneName); + sceneEventData.LoadSceneMode = LoadSceneMode.Additive; // The only scenes unloaded are scenes that were additively loaded + sceneEventData.SceneHandle = sceneHandle; + + // This will be the message we send to everyone when this scene event sceneEventProgress is complete + sceneEventProgress.SceneEventType = SceneEventType.UnloadEventCompleted; + + ScenesLoaded.Remove(scene.handle); + + AsyncOperation sceneUnload = SceneManager.UnloadSceneAsync(scene); + sceneUnload.completed += (AsyncOperation asyncOp2) => { OnSceneUnloaded(sceneEventData.SceneEventId); }; + sceneEventProgress.SetSceneLoadOperation(sceneUnload); + + // Notify local server that a scene is going to be unloaded + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = sceneUnload, + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = sceneName, + ClientId = m_NetworkManager.ServerClientId // Server can only invoke this + }); + + OnUnload?.Invoke(m_NetworkManager.ServerClientId, sceneName, sceneUnload); + + //Return the status + return sceneEventProgress.Status; + } + + /// + /// Client Side: + /// Handles scene events. + /// + private void OnClientUnloadScene(uint sceneEventId) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + var sceneName = SceneNameFromHash(sceneEventData.SceneHash); + + if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle)) + { + throw new Exception($"Client failed to unload scene {sceneName} " + + $"because we are missing the client scene handle due to the server scene handle {sceneEventData.SceneHandle} not being found!"); + } + + var sceneHandle = ServerSceneHandleToClientSceneHandle[sceneEventData.SceneHandle]; + + if (!ScenesLoaded.ContainsKey(sceneHandle)) + { + // Error scene handle not found! + throw new Exception($"Client failed to unload scene {sceneName} " + + $"because the client scene handle {sceneHandle} was not found in ScenesLoaded!"); + } + s_IsSceneEventActive = true; + var sceneUnload = (AsyncOperation)null; +#if UNITY_INCLUDE_TESTS + if (m_IsRunningUnitTest) + { + sceneUnload = new AsyncOperation(); + } + else + { + sceneUnload = SceneManager.UnloadSceneAsync(ScenesLoaded[sceneHandle]); + sceneUnload.completed += asyncOp2 => OnSceneUnloaded(sceneEventId); + } +#else + sceneUnload = SceneManager.UnloadSceneAsync(ScenesLoaded[sceneHandle]); + sceneUnload.completed += asyncOp2 => OnSceneUnloaded(sceneEventId); +#endif + ScenesLoaded.Remove(sceneHandle); + + // Remove our server to scene handle lookup + ServerSceneHandleToClientSceneHandle.Remove(sceneEventData.SceneHandle); + + // Notify the local client that a scene is going to be unloaded + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = sceneUnload, + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = LoadSceneMode.Additive, // The only scenes unloaded are scenes that were additively loaded + SceneName = sceneName, + ClientId = m_NetworkManager.LocalClientId // Server sent this message to the client, but client is executing it + }); + + OnUnload?.Invoke(m_NetworkManager.LocalClientId, sceneName, sceneUnload); + +#if UNITY_INCLUDE_TESTS + if (m_IsRunningUnitTest) + { + OnSceneUnloaded(sceneEventId); + } +#endif + } + + /// + /// Server and Client: + /// Invoked when an additively loaded scene is unloaded + /// + private void OnSceneUnloaded(uint sceneEventId) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + // First thing we do, if we are a server, is to send the unload scene event. + if (m_NetworkManager.IsServer) + { + // 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()); + + //Second, server sets itself as having finished unloading + if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) + { + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId); + } + } + + // Next we prepare to send local notifications for unload complete + sceneEventData.SceneEventType = SceneEventType.UnloadComplete; + + //Notify the client or server that a scene was unloaded + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = SceneNameFromHash(sceneEventData.SceneHash), + ClientId = m_NetworkManager.IsServer ? m_NetworkManager.ServerClientId : m_NetworkManager.LocalClientId + }); + + OnUnloadComplete?.Invoke(m_NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash)); + + // 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 }); + } + + EndSceneEvent(sceneEventId); + // This scene event is now considered "complete" + s_IsSceneEventActive = false; + } + + /// + /// Clears all scenes when loading in single mode + /// Since we assume a single mode loaded scene will be considered the "currently active scene", + /// we only unload any additively loaded scenes. + /// + internal void UnloadAdditivelyLoadedScenes() + { + // Unload all additive scenes while making sure we don't try to unload the base scene ( loaded in single mode ). + var currentActiveScene = SceneManager.GetActiveScene(); + foreach (var keyHandleEntry in ScenesLoaded) + { + if (currentActiveScene.name != keyHandleEntry.Value.name) + { + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = SceneManager.UnloadSceneAsync(keyHandleEntry.Value), + SceneEventType = SceneEventType.Unload, + SceneName = keyHandleEntry.Value.name, + LoadSceneMode = LoadSceneMode.Additive, // The only scenes unloaded are scenes that were additively loaded + ClientId = m_NetworkManager.ServerClientId + }); + } + } + // clear out our scenes loaded list + ScenesLoaded.Clear(); + } + + /// + /// Server side: + /// Loads the scene name in either additive or single loading mode. + /// When applicable, the is delivered within the via + /// + /// the name of the scene to be loaded + /// ( means it was successful) + public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSceneMode) + { + var sceneEventProgress = ValidateSceneEventLoading(sceneName); + if (sceneEventProgress.Status != SceneEventProgressStatus.Started) + { + return sceneEventProgress.Status; + } + + // This will be the message we send to everyone when this scene event sceneEventProgress is complete + sceneEventProgress.SceneEventType = SceneEventType.LoadEventCompleted; + sceneEventProgress.LoadSceneMode = loadSceneMode; + + var sceneEventData = BeginSceneEvent(); + + // Now set up the current scene event + sceneEventData.SceneEventProgressId = sceneEventProgress.Guid; + sceneEventData.SceneEventType = SceneEventType.Load; + sceneEventData.SceneHash = SceneHashFromNameOrPath(sceneName); + sceneEventData.LoadSceneMode = loadSceneMode; + + // This both checks to make sure the scene is valid and if not resets the active scene event + s_IsSceneEventActive = ValidateSceneBeforeLoading(sceneEventData.SceneHash, loadSceneMode); + if (!s_IsSceneEventActive) + { + EndSceneEvent(sceneEventData.SceneEventId); + return SceneEventProgressStatus.SceneFailedVerification; + } + + if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) + { + // Destroy current scene objects before switching. + m_NetworkManager.SpawnManager.ServerDestroySpawnedSceneObjects(); + + // Preserve the objects that should not be destroyed during the scene event + MoveObjectsToDontDestroyOnLoad(); + + // Now Unload all currently additively loaded scenes + UnloadAdditivelyLoadedScenes(); + } + + // Now start loading the scene + AsyncOperation sceneLoad = SceneManager.LoadSceneAsync(sceneName, loadSceneMode); + sceneLoad.completed += (AsyncOperation asyncOp2) => { OnSceneLoaded(sceneEventData.SceneEventId, sceneName); }; + sceneEventProgress.SetSceneLoadOperation(sceneLoad); + + // Notify the local server that a scene loading event has begun + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = sceneLoad, + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = sceneName, + ClientId = m_NetworkManager.ServerClientId + }); + + OnLoad?.Invoke(m_NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); + + //Return our scene progress instance + return sceneEventProgress.Status; + } + + /// + /// Client Side: + /// Handles both forms of scene loading + /// + /// Stream data associated with the event + private void OnClientSceneLoadingEvent(uint sceneEventId) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + var sceneName = SceneNameFromHash(sceneEventData.SceneHash); + + // Run scene validation before loading a scene + if (!ValidateSceneBeforeLoading(sceneEventData.SceneHash, sceneEventData.LoadSceneMode)) + { + EndSceneEvent(sceneEventId); + return; + } + +#if UNITY_INCLUDE_TESTS + if (m_IsRunningUnitTest) + { + // Send the loading message + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = new AsyncOperation(), + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = sceneName, + ClientId = m_NetworkManager.LocalClientId + }); + + // Only for testing + OnLoad?.Invoke(m_NetworkManager.ServerClientId, sceneName, sceneEventData.LoadSceneMode, new AsyncOperation()); + + // Unit tests must mirror the server's scenes loaded dictionary, otherwise this portion will fail + if (ScenesLoaded.ContainsKey(sceneEventData.SceneHandle)) + { + OnClientLoadedScene(sceneEventId, ScenesLoaded[sceneEventData.SceneHandle]); + } + else + { + EndSceneEvent(sceneEventId); + throw new Exception($"Could not find the scene handle {sceneEventData.SceneHandle} for scene {sceneName} " + + $"during unit test. Did you forget to register this in the unit test?"); + } + return; + } +#endif + + if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) + { + // Move ALL NetworkObjects to the temp scene + MoveObjectsToDontDestroyOnLoad(); + + // Now Unload all currently additively loaded scenes + UnloadAdditivelyLoadedScenes(); + } + + // The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned + // they need to be moved into the do not destroy temporary scene + // 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 + if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) + { + IsSpawnedObjectsPendingInDontDestroyOnLoad = true; + } + + var sceneLoad = SceneManager.LoadSceneAsync(sceneName, sceneEventData.LoadSceneMode); + sceneLoad.completed += asyncOp2 => OnSceneLoaded(sceneEventId, sceneName); + + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = sceneLoad, + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = sceneName, + ClientId = m_NetworkManager.LocalClientId + }); + + OnLoad?.Invoke(m_NetworkManager.LocalClientId, sceneName, sceneEventData.LoadSceneMode, sceneLoad); + } + + + /// + /// Client and Server: + /// Generic on scene loaded callback method to be called upon a scene loading + /// + private void OnSceneLoaded(uint sceneEventId, string sceneName) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + var nextScene = GetAndAddNewlyLoadedSceneByName(sceneName); + if (!nextScene.isLoaded || !nextScene.IsValid()) + { + throw new Exception($"Failed to find valid scene internal Unity.Netcode for {nameof(GameObject)}s error!"); + } + + if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) + { + SceneManager.SetActiveScene(nextScene); + } + + //Get all NetworkObjects loaded by the scene + PopulateScenePlacedObjects(nextScene); + + if (sceneEventData.LoadSceneMode == LoadSceneMode.Single) + { + // Move all objects to the new scene + MoveObjectsFromDontDestroyOnLoadToScene(nextScene); + } + + // The Condition: While a scene is asynchronously loaded in single loading scene mode, if any new NetworkObjects are spawned + // they need to be moved into the do not destroy temporary scene + // 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 + IsSpawnedObjectsPendingInDontDestroyOnLoad = false; + + if (m_NetworkManager.IsServer) + { + OnServerLoadedScene(sceneEventId, nextScene); + } + else + { + // For the client, we make a server scene handle to client scene handle look up table + if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.SceneHandle)) + { + ServerSceneHandleToClientSceneHandle.Add(sceneEventData.SceneHandle, nextScene.handle); + } + else + { + // If the exact same handle exists then there are problems with using handles + throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})"); + } + + OnClientLoadedScene(sceneEventId, nextScene); + } + } + + /// + /// Server side: + /// On scene loaded callback method invoked by OnSceneLoading only + /// + private void OnServerLoadedScene(uint sceneEventId, Scene scene) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + // Register in-scene placed NetworkObjects with spawn manager + foreach (var keyValuePairByGlobalObjectIdHash in ScenePlacedObjects) + { + foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value) + { + if (!keyValuePairBySceneHandle.Value.IsPlayerObject) + { + m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value, m_NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, null, true); + } + } + } + + // Set the server's scene's handle so the client can build a look up table + sceneEventData.SceneHandle = scene.handle; + + // Send all clients the scene load event + for (int j = 0; j < m_NetworkManager.ConnectedClientsList.Count; j++) + { + var clientId = m_NetworkManager.ConnectedClientsList[j].ClientId; + if (clientId != m_NetworkManager.ServerClientId) + { + sceneEventData.TargetClientId = clientId; + var message = new SceneEventMessage + { + EventData = sceneEventData + }; + var size = m_NetworkManager.SendMessage(message, k_DeliveryType, clientId); + m_NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEventData.SceneEventType, scene.name, size); + } + } + + s_IsSceneEventActive = false; + //First, notify local server that the scene was loaded + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = SceneEventType.LoadComplete, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = SceneNameFromHash(sceneEventData.SceneHash), + ClientId = m_NetworkManager.ServerClientId, + Scene = scene, + }); + + OnLoadComplete?.Invoke(m_NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); + + //Second, set the server as having loaded for the associated SceneEventProgress + if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) + { + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId); + } + EndSceneEvent(sceneEventId); + } + + /// + /// Client side: + /// On scene loaded callback method invoked by OnSceneLoading only + /// + private void OnClientLoadedScene(uint sceneEventId, Scene scene) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + sceneEventData.DeserializeScenePlacedObjects(); + + sceneEventData.SceneEventType = SceneEventType.LoadComplete; + SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId }); + s_IsSceneEventActive = false; + + // Notify local client that the scene was loaded + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = SceneEventType.LoadComplete, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = SceneNameFromHash(sceneEventData.SceneHash), + ClientId = m_NetworkManager.LocalClientId, + Scene = scene, + }); + + OnLoadComplete?.Invoke(m_NetworkManager.LocalClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); + + EndSceneEvent(sceneEventId); + } + + /// + /// Server Side: + /// This is used for players that have just had their connection approved and will assure they are synchronized + /// properly if they are late joining + /// Note: We write out all of the scenes to be loaded first and then all of the NetworkObjects that need to be + /// synchronized. + /// + /// newly joined client identifier + internal void SynchronizeNetworkObjects(ulong clientId) + { + // Update the clients + m_NetworkManager.SpawnManager.UpdateObservedNetworkObjects(clientId); + + var sceneEventData = BeginSceneEvent(); + + sceneEventData.InitializeForSynch(); + sceneEventData.TargetClientId = clientId; + sceneEventData.LoadSceneMode = ClientSynchronizationMode; + var activeScene = SceneManager.GetActiveScene(); + sceneEventData.SceneEventType = SceneEventType.Synchronize; + + // Organize how (and when) we serialize our NetworkObjects + for (int i = 0; i < SceneManager.sceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + + var sceneHash = SceneHashFromNameOrPath(scene.path); + + // This would depend upon whether we are additive or not + // If we are the base scene, then we set the root scene index; + if (activeScene == scene) + { + if (!ValidateSceneBeforeLoading(sceneHash, sceneEventData.LoadSceneMode)) + { + continue; + } + sceneEventData.SceneHash = sceneHash; + sceneEventData.SceneHandle = scene.handle; + } + else if (!ValidateSceneBeforeLoading(sceneHash, LoadSceneMode.Additive)) + { + continue; + } + + sceneEventData.AddSceneToSynchronize(sceneHash, scene.handle); + } + + sceneEventData.AddSpawnedNetworkObjects(); + + var message = new SceneEventMessage + { + EventData = sceneEventData + }; + var size = m_NetworkManager.SendMessage(message, k_DeliveryType, clientId); + m_NetworkManager.NetworkMetrics.TrackSceneEventSent( + clientId, (uint)sceneEventData.SceneEventType, "", size); + + // Notify the local server that the client has been sent the synchronize event + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + ClientId = clientId + }); + + OnSynchronize?.Invoke(clientId); + + EndSceneEvent(sceneEventData.SceneEventId); + } + + /// + /// This is called when the client receives the event + /// Note: This can recurse one additional time by the client if the current scene loaded by the client + /// is already loaded. + /// + private void OnClientBeginSync(uint sceneEventId) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + var sceneHash = sceneEventData.GetNextSceneSynchronizationHash(); + var sceneHandle = sceneEventData.GetNextSceneSynchronizationHandle(); + var sceneName = SceneNameFromHash(sceneHash); + var activeScene = SceneManager.GetActiveScene(); + + var loadSceneMode = sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive; + + // Always check to see if the scene needs to be validated + if (!ValidateSceneBeforeLoading(sceneHash, loadSceneMode)) + { + EndSceneEvent(sceneEventId); + return; + } + + // If this is the beginning of the synchronization event, then send client a notification that synchronization has begun + if (sceneHash == sceneEventData.SceneHash) + { + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = SceneEventType.Synchronize, + ClientId = m_NetworkManager.LocalClientId, + }); + + OnSynchronize?.Invoke(m_NetworkManager.LocalClientId); + + // Clear the in-scene placed NetworkObjects when we load the first scene in our synchronization process + ScenePlacedObjects.Clear(); + } + + var shouldPassThrough = false; + var sceneLoad = (AsyncOperation)null; + + // Check to see if the client already has loaded the scene to be loaded + if (sceneName == activeScene.name) + { + // If the client is already in the same scene, then pass through and + // don't try to reload it. + shouldPassThrough = true; + } + +#if UNITY_INCLUDE_TESTS + if (m_IsRunningUnitTest) + { + // In unit tests, we don't allow clients to load additional scenes since + // MultiInstance unit tests share the same scene space. + shouldPassThrough = true; + sceneLoad = new AsyncOperation(); + } +#endif + if (!shouldPassThrough) + { + // If not, then load the scene + sceneLoad = SceneManager.LoadSceneAsync(sceneName, loadSceneMode); + sceneLoad.completed += asyncOp2 => ClientLoadedSynchronization(sceneEventId, sceneHash, sceneHandle); + } + + // Notify local client that a scene load has begun + OnSceneEvent?.Invoke(new SceneEvent() + { + AsyncOperation = sceneLoad, + SceneEventType = SceneEventType.Load, + LoadSceneMode = loadSceneMode, + SceneName = sceneName, + ClientId = m_NetworkManager.LocalClientId, + }); + + OnLoad?.Invoke(m_NetworkManager.LocalClientId, sceneName, loadSceneMode, sceneLoad); + + if (shouldPassThrough) + { + // If so, then pass through + ClientLoadedSynchronization(sceneEventId, sceneHash, sceneHandle); + } + } + + /// + /// Once a scene is loaded ( or if it was already loaded) this gets called. + /// This handles all of the in-scene and dynamically spawned NetworkObject synchronization + /// + /// Netcode scene index that was loaded + private void ClientLoadedSynchronization(uint sceneEventId, uint sceneHash, int sceneHandle) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + var sceneName = SceneNameFromHash(sceneHash); + var nextScene = GetAndAddNewlyLoadedSceneByName(sceneName); + + if (!nextScene.isLoaded || !nextScene.IsValid()) + { + throw new Exception($"Failed to find valid scene internal Unity.Netcode for {nameof(GameObject)}s error!"); + } + + var loadSceneMode = (sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive); + + // For now, during a synchronization event, we will make the first scene the "base/master" scene that denotes a "complete scene switch" + if (loadSceneMode == LoadSceneMode.Single) + { + SceneManager.SetActiveScene(nextScene); + } + + if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneHandle)) + { + ServerSceneHandleToClientSceneHandle.Add(sceneHandle, nextScene.handle); + } + else + { + // If the exact same handle exists then there are problems with using handles + throw new Exception($"Server Scene Handle ({sceneEventData.SceneHandle}) already exist! Happened during scene load of {nextScene.name} with Client Handle ({nextScene.handle})"); + } + + // Apply all in-scene placed NetworkObjects loaded by the scene + PopulateScenePlacedObjects(nextScene, false); + + // Send notification back to server that we finished loading this scene + var responseSceneEventData = BeginSceneEvent(); + responseSceneEventData.LoadSceneMode = loadSceneMode; + responseSceneEventData.SceneEventType = SceneEventType.LoadComplete; + responseSceneEventData.SceneHash = sceneHash; + + + var message = new SceneEventMessage + { + EventData = responseSceneEventData + }; + var size = m_NetworkManager.SendMessage(message, k_DeliveryType, m_NetworkManager.ServerClientId); + + m_NetworkManager.NetworkMetrics.TrackSceneEventSent(m_NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size); + + EndSceneEvent(responseSceneEventData.SceneEventId); + + // Send notification to local client that the scene has finished loading + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = SceneEventType.LoadComplete, + LoadSceneMode = loadSceneMode, + SceneName = sceneName, + Scene = nextScene, + ClientId = m_NetworkManager.LocalClientId, + }); + + OnLoadComplete?.Invoke(m_NetworkManager.LocalClientId, sceneName, loadSceneMode); + + // Check to see if we still have scenes to load and synchronize with + HandleClientSceneEvent(sceneEventId); + } + + /// + /// Client Side: + /// Handles incoming Scene_Event messages for clients + /// + /// data associated with the event + private void HandleClientSceneEvent(uint sceneEventId) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + switch (sceneEventData.SceneEventType) + { + case SceneEventType.Load: + { + OnClientSceneLoadingEvent(sceneEventId); + break; + } + case SceneEventType.Unload: + { + OnClientUnloadScene(sceneEventId); + break; + } + case SceneEventType.Synchronize: + { + if (!sceneEventData.IsDoneWithSynchronization()) + { + OnClientBeginSync(sceneEventId); + } + else + { + // Include anything in the DDOL scene + PopulateScenePlacedObjects(DontDestroyOnLoadScene, false); + // Synchronize the NetworkObjects for this scene + sceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager); + + sceneEventData.SceneEventType = SceneEventType.SynchronizeComplete; + SendSceneEventData(sceneEventId, new ulong[] { m_NetworkManager.ServerClientId }); + + // All scenes are synchronized, let the server know we are done synchronizing + m_NetworkManager.IsConnectedClient = true; + + // Client is now synchronized and fully "connected". This also means the client can send "RPCs" at this time + m_NetworkManager.InvokeOnClientConnectedCallback(m_NetworkManager.LocalClientId); + + // Notify the client that they have finished synchronizing + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + ClientId = m_NetworkManager.LocalClientId, // Client sent this to the server + }); + + OnSynchronizeComplete?.Invoke(m_NetworkManager.LocalClientId); + + EndSceneEvent(sceneEventId); + } + break; + } + case SceneEventType.ReSynchronize: + { + // Notify the local client that they have been re-synchronized after being synchronized with an in progress game session + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + ClientId = m_NetworkManager.ServerClientId, // Server sent this to client + }); + + EndSceneEvent(sceneEventId); + break; + } + case SceneEventType.LoadEventCompleted: + case SceneEventType.UnloadEventCompleted: + { + // Notify the local client that all clients have finished loading or unloading + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = SceneNameFromHash(sceneEventData.SceneHash), + ClientId = m_NetworkManager.ServerClientId, + ClientsThatCompleted = sceneEventData.ClientsCompleted, + ClientsThatTimedOut = sceneEventData.ClientsTimedOut, + }); + + if (sceneEventData.SceneEventType == SceneEventType.LoadEventCompleted) + { + OnLoadEventCompleted?.Invoke(SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); + } + else + { + OnUnloadEventCompleted?.Invoke(SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode, sceneEventData.ClientsCompleted, sceneEventData.ClientsTimedOut); + } + + EndSceneEvent(sceneEventId); + + break; + } + default: + { + Debug.LogWarning($"{sceneEventData.SceneEventType} is not currently supported!"); + break; + } + } + } + + /// + /// Server Side: + /// Handles incoming Scene_Event messages for host or server + /// + private void HandleServerSceneEvent(uint sceneEventId, ulong clientId) + { + var sceneEventData = SceneEventDataStore[sceneEventId]; + switch (sceneEventData.SceneEventType) + { + case SceneEventType.LoadComplete: + { + // Notify the local server that the client has finished loading a scene + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = SceneNameFromHash(sceneEventData.SceneHash), + ClientId = clientId + }); + + OnLoadComplete?.Invoke(clientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); + + if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) + { + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(clientId); + } + EndSceneEvent(sceneEventId); + break; + } + case SceneEventType.UnloadComplete: + { + if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) + { + SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(clientId); + } + // Notify the local server that the client has finished unloading a scene + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + LoadSceneMode = sceneEventData.LoadSceneMode, + SceneName = SceneNameFromHash(sceneEventData.SceneHash), + ClientId = clientId + }); + + OnUnloadComplete?.Invoke(clientId, SceneNameFromHash(sceneEventData.SceneHash)); + + EndSceneEvent(sceneEventId); + break; + } + case SceneEventType.SynchronizeComplete: + { + // Notify the local server that a client has finished synchronizing + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + SceneName = string.Empty, + ClientId = clientId + }); + + OnSynchronizeComplete?.Invoke(clientId); + + // We now can call the client connected callback on the server at this time + // This assures the client is fully synchronized with all loaded scenes and + // 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; + SendSceneEventData(sceneEventId, new ulong[] { clientId }); + + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = sceneEventData.SceneEventType, + SceneName = string.Empty, + ClientId = clientId + }); + } + EndSceneEvent(sceneEventId); + break; + } + default: + { + Debug.LogWarning($"{sceneEventData.SceneEventType} is not currently supported!"); + break; + } + } + } + + /// + /// Both Client and Server: Incoming scene event entry point + /// + /// client who sent the scene event + /// data associated with the scene event + internal void HandleSceneEvent(ulong clientId, FastBufferReader reader) + { + if (m_NetworkManager != null) + { + var sceneEventData = BeginSceneEvent(); + + sceneEventData.Deserialize(reader); + + m_NetworkManager.NetworkMetrics.TrackSceneEventReceived( + clientId, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), reader.Length); + + if (sceneEventData.IsSceneEventClientSide()) + { + HandleClientSceneEvent(sceneEventData.SceneEventId); + } + else + { + HandleServerSceneEvent(sceneEventData.SceneEventId, clientId); + } + } + else + { + Debug.LogError($"{nameof(NetworkSceneManager.HandleSceneEvent)} was invoked but {nameof(NetworkManager)} reference was null!"); + } + } + + /// + /// Moves all NetworkObjects that don't have the set to + /// the "Do not destroy on load" scene. + /// + private void MoveObjectsToDontDestroyOnLoad() + { + // Move ALL NetworkObjects to the temp scene + var objectsToKeep = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList); + + foreach (var sobj in objectsToKeep) + { + if (!sobj.DestroyWithScene || (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.gameObject.scene == DontDestroyOnLoadScene)) + { + // Only move objects with no parent as child objects will follow + if (sobj.gameObject.transform.parent == null) + { + UnityEngine.Object.DontDestroyOnLoad(sobj.gameObject); + // Since we are doing a scene transition, disable the GameObject until the next scene is loaded + sobj.gameObject.SetActive(false); + } + } + else if (m_NetworkManager.IsServer) + { + sobj.Despawn(); + } + } + } + + /// + /// Should be invoked on both the client and server side after: + /// -- A new scene has been loaded + /// -- Before any "DontDestroyOnLoad" NetworkObjects have been added back into the scene. + /// Added the ability to choose not to clear the scene placed objects for additive scene loading. + /// We organize our ScenePlacedObjects by: + /// [GlobalObjectIdHash][SceneHandle][NetworkObject] + /// 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) + { + if (clearScenePlacedObjects) + { + ScenePlacedObjects.Clear(); + } + + var networkObjects = UnityEngine.Object.FindObjectsOfType(); + + // Just add every NetworkObject found that isn't already in the list + // With additive scenes, we can have multiple in-scene placed NetworkObjects with the same GlobalObjectIdHash value + // During Client Side Synchronization: We add them on a FIFO basis, for each scene loaded without clearing, and then + // 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 "MultiInstanceHelpers" 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) + { + if (!ScenePlacedObjects.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + { + ScenePlacedObjects.Add(networkObjectInstance.GlobalObjectIdHash, new Dictionary()); + } + + if (!ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash].ContainsKey(networkObjectInstance.gameObject.scene.handle)) + { + ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash].Add(networkObjectInstance.gameObject.scene.handle, networkObjectInstance); + } + else + { + var exitingEntryName = ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash][networkObjectInstance.gameObject.scene.handle] != null ? + ScenePlacedObjects[networkObjectInstance.GlobalObjectIdHash][networkObjectInstance.gameObject.scene.handle].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}!"); + } + } + } + } + + /// + /// Moves all spawned NetworkObjects (from do not destroy on load) to the scene specified + /// + /// scene to move the NetworkObjects to + private void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene) + { + // Move ALL NetworkObjects to the temp scene + var objectsToKeep = m_NetworkManager.SpawnManager.SpawnedObjectsList; + + foreach (var sobj in objectsToKeep) + { + if (sobj.gameObject.scene == DontDestroyOnLoadScene && (sobj.IsSceneObject == null || sobj.IsSceneObject.Value)) + { + continue; + } + + // Only move objects with no parent as child objects will follow + if (sobj.gameObject.transform.parent == null) + { + // set it back to active at this point + sobj.gameObject.SetActive(true); + SceneManager.MoveGameObjectToScene(sobj.gameObject, scene); + } + } + } + } +} diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs.meta b/Runtime/SceneManagement/NetworkSceneManager.cs.meta new file mode 100644 index 0000000..f076388 --- /dev/null +++ b/Runtime/SceneManagement/NetworkSceneManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38822fcfed96a2d4b87b165d44e5a612 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SceneManagement/SceneEventData.cs b/Runtime/SceneManagement/SceneEventData.cs new file mode 100644 index 0000000..02c3ba7 --- /dev/null +++ b/Runtime/SceneManagement/SceneEventData.cs @@ -0,0 +1,769 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using Unity.Collections; +using UnityEngine.SceneManagement; + + +namespace Unity.Netcode +{ + /// + /// The different types of scene events communicated between a server and client. + /// Used by for messages + /// Note: This is only when is enabled + /// See also: + /// + public enum SceneEventType : byte + { + /// + /// Load a scene + /// Invocation: Server Side + /// Message Flow: Server to client + /// Event Notification: Both server and client are notified a load scene event started + /// + Load, + /// + /// Unload a scene + /// Invocation: Server Side + /// Message Flow: Server to client + /// Event Notification: Both server and client are notified an unload scene event started + /// + Unload, + /// + /// Synchronize current game session state for approved clients + /// Invocation: Server Side + /// Message Flow: Server to client + /// Event Notification: Server and Client receives a local notification (server receives the ClientId being synchronized) + /// + Synchronize, + /// + /// Game session re-synchronization of NetworkObjects that were destroyed during a event + /// 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, + /// + /// All clients have finished loading a scene + /// Invocation: Server Side + /// Message Flow: Server to Client + /// Event Notification: Both server and client receive a local notification containing the clients that finished + /// as well as the clients that timed out (if any). + /// + LoadEventCompleted, + /// + /// All clients have unloaded a scene + /// Invocation: Server Side + /// Message Flow: Server to Client + /// Event Notification: Both server and client receive a local notification containing the clients that finished + /// as well as the clients that timed out (if any). + /// + UnloadEventCompleted, + /// + /// A client has finished loading a scene + /// Invocation: Client Side + /// Message Flow: Client to Server + /// Event Notification: Both server and client receive a local notification + /// + LoadComplete, + /// + /// A client has finished unloading a scene + /// Invocation: Client Side + /// Message Flow: Client to Server + /// Event Notification: Both server and client receive a local notification + /// + UnloadComplete, + /// + /// A client has finished synchronizing from a event + /// Invocation: Client Side + /// Message Flow: Client to Server + /// Event Notification: Both server and client receive a local notification + /// + SynchronizeComplete, + } + + /// + /// Used by for messages + /// Note: This is only when is enabled + /// + internal class SceneEventData : IDisposable + { + internal SceneEventType SceneEventType; + internal LoadSceneMode LoadSceneMode; + internal Guid SceneEventProgressId; + internal uint SceneEventId; + + + internal uint SceneHash; + internal int SceneHandle; + + /// Only used for scene events, this assures permissions when writing + /// NetworkVariable information. If that process changes, then we need to update this + internal ulong TargetClientId; + + private Dictionary> m_SceneNetworkObjects; + private Dictionary m_SceneNetworkObjectDataOffsets; + + /// + /// Client or Server Side: + /// Client side: Generates a list of all NetworkObjects by their NetworkObjectId that was spawned during th synchronization process + /// Server side: Compares list from client to make sure client didn't drop a message about a NetworkObject being despawned while it + /// was synchronizing (if so server will send another message back to the client informing the client of NetworkObjects to remove) + /// spawned during an initial synchronization. + /// + private List m_NetworkObjectsSync = new List(); + + /// + /// Server Side Re-Synchronization: + /// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned, + /// the server will compile a list and send back an Event_ReSync message to the client. + /// + private List m_NetworkObjectsToBeRemoved = new List(); + + private bool m_HasInternalBuffer; + internal FastBufferReader InternalBuffer; + + private NetworkManager m_NetworkManager; + + internal List ClientsCompleted; + internal List ClientsTimedOut; + + internal Queue ScenesToSynchronize; + internal Queue SceneHandlesToSynchronize; + + + /// + /// Server Side: + /// Add a scene and its handle to the list of scenes the client should load before synchronizing + /// Since scene handles are not the same per instance, the client builds a server scene handle to + /// client scene handle lookup table. + /// Why include the scene handle? In order to support loading of the same additive scene more than once + /// we must distinguish which scene we are talking about when the server tells the client to unload a scene. + /// The server will always communicate its local relative scene's handle and the client will determine its + /// local relative handle from the table being built. + /// Look for usage to see where + /// entries are being added to or removed from the table + /// + /// + /// + internal void AddSceneToSynchronize(uint sceneHash, int sceneHandle) + { + ScenesToSynchronize.Enqueue(sceneHash); + SceneHandlesToSynchronize.Enqueue((uint)sceneHandle); + } + + /// + /// Client Side: + /// Gets the next scene hash to be loaded for approval and/or late joining + /// + /// + internal uint GetNextSceneSynchronizationHash() + { + return ScenesToSynchronize.Dequeue(); + } + + /// + /// Client Side: + /// Gets the next scene handle to be loaded for approval and/or late joining + /// + /// + internal int GetNextSceneSynchronizationHandle() + { + return (int)SceneHandlesToSynchronize.Dequeue(); + } + + /// + /// Client Side: + /// Determines if all scenes have been processed during the synchronization process + /// + /// true/false + internal bool IsDoneWithSynchronization() + { + if (ScenesToSynchronize.Count == 0 && SceneHandlesToSynchronize.Count == 0) + { + return true; + } + else if (ScenesToSynchronize.Count != SceneHandlesToSynchronize.Count) + { + // This should never happen, but in the event it does... + throw new Exception($"[{nameof(SceneEventData)}-Internal Mismatch Error] {nameof(ScenesToSynchronize)} count != {nameof(SceneHandlesToSynchronize)} count!"); + } + return false; + } + + /// + /// Server Side: + /// Called just before the synchronization process + /// + internal void InitializeForSynch() + { + if (m_SceneNetworkObjects == null) + { + m_SceneNetworkObjects = new Dictionary>(); + } + else + { + m_SceneNetworkObjects.Clear(); + } + + if (ScenesToSynchronize == null) + { + ScenesToSynchronize = new Queue(); + } + else + { + ScenesToSynchronize.Clear(); + } + + if (SceneHandlesToSynchronize == null) + { + SceneHandlesToSynchronize = new Queue(); + } + else + { + SceneHandlesToSynchronize.Clear(); + } + } + + internal void AddSpawnedNetworkObjects() + { + m_NetworkObjectsSync = m_NetworkManager.SpawnManager.SpawnedObjectsList.ToList(); + m_NetworkObjectsSync.Sort(SortNetworkObjects); + } + + /// + /// Server Side: + /// Used during the synchronization process to associate NetworkObjects with scenes + /// + /// + /// + internal void AddNetworkObjectForSynch(uint sceneIndex, NetworkObject networkObject) + { + if (!m_SceneNetworkObjects.ContainsKey(sceneIndex)) + { + m_SceneNetworkObjects.Add(sceneIndex, new List()); + } + + m_SceneNetworkObjects[sceneIndex].Add(networkObject); + } + + /// + /// Client and Server: + /// Determines if the scene event type was intended for the client ( or server ) + /// + /// true (client should handle this message) false (server should handle this message) + internal bool IsSceneEventClientSide() + { + switch (SceneEventType) + { + case SceneEventType.Load: + case SceneEventType.Unload: + case SceneEventType.Synchronize: + case SceneEventType.ReSynchronize: + case SceneEventType.LoadEventCompleted: + case SceneEventType.UnloadEventCompleted: + { + return true; + } + } + return false; + } + + /// + /// Server Side: + /// Sorts the NetworkObjects to assure proper instantiation order of operations for + /// registered INetworkPrefabInstanceHandler implementations + /// + /// + /// + /// + private int SortNetworkObjects(NetworkObject first, NetworkObject second) + { + var doesFirstHaveHandler = m_NetworkManager.PrefabHandler.ContainsHandler(first); + var doesSecondHaveHandler = m_NetworkManager.PrefabHandler.ContainsHandler(second); + if (doesFirstHaveHandler != doesSecondHaveHandler) + { + if (doesFirstHaveHandler) + { + return 1; + } + else + { + return -1; + } + } + return 0; + } + + /// + /// Client and Server Side: + /// Serializes data based on the SceneEvent type () + /// + /// to write the scene event data + internal void Serialize(FastBufferWriter writer) + { + // Write the scene event type + writer.WriteValueSafe(SceneEventType); + + // Write the scene loading mode + writer.WriteValueSafe(LoadSceneMode); + + // Write the scene event progress Guid + if (SceneEventType != SceneEventType.Synchronize) + { + writer.WriteValueSafe(SceneEventProgressId); + } + + // Write the scene index and handle + writer.WriteValueSafe(SceneHash); + writer.WriteValueSafe(SceneHandle); + + switch (SceneEventType) + { + case SceneEventType.Synchronize: + { + WriteSceneSynchronizationData(writer); + break; + } + case SceneEventType.Load: + { + SerializeScenePlacedObjects(writer); + break; + } + case SceneEventType.SynchronizeComplete: + { + WriteClientSynchronizationResults(writer); + break; + } + case SceneEventType.ReSynchronize: + { + WriteClientReSynchronizationData(writer); + break; + } + case SceneEventType.LoadEventCompleted: + case SceneEventType.UnloadEventCompleted: + { + WriteSceneEventProgressDone(writer); + break; + } + } + } + + /// + /// Server Side: + /// Called at the end of a event once the scene is loaded and scene placed NetworkObjects + /// have been locally spawned + /// + internal void WriteSceneSynchronizationData(FastBufferWriter writer) + { + // Write the scenes we want to load, in the order we want to load them + writer.WriteValueSafe(ScenesToSynchronize.ToArray()); + writer.WriteValueSafe(SceneHandlesToSynchronize.ToArray()); + + + // Store our current position in the stream to come back and say how much data we have written + var positionStart = writer.Position; + + // Size Place Holder -- Start + // !!NOTE!!: Since this is a placeholder to be set after we know how much we have written, + // for stream offset purposes this MUST not be a packed value! + writer.WriteValueSafe((int)0); + int totalBytes = 0; + + // Write the number of NetworkObjects we are serializing + writer.WriteValueSafe(m_NetworkObjectsSync.Count()); + for (var i = 0; i < m_NetworkObjectsSync.Count(); ++i) + { + var noStart = writer.Position; + var sceneObject = m_NetworkObjectsSync[i].GetMessageSceneObject(TargetClientId); + writer.WriteValueSafe(m_NetworkObjectsSync[i].gameObject.scene.handle); + sceneObject.Serialize(writer); + var noStop = writer.Position; + totalBytes += (int)(noStop - noStart); + } + + // Size Place Holder -- End + var positionEnd = writer.Position; + var bytesWritten = (uint)(positionEnd - (positionStart + sizeof(uint))); + writer.Seek(positionStart); + // Write the total size written to the stream by NetworkObjects being serialized + writer.WriteValueSafe(bytesWritten); + writer.Seek(positionEnd); + } + + /// + /// Server Side: + /// Called at the end of a event once the scene is loaded and scene placed NetworkObjects + /// have been locally spawned + /// Maximum number of objects that could theoretically be synchronized is 65536 + /// + internal void SerializeScenePlacedObjects(FastBufferWriter writer) + { + var numberOfObjects = (ushort)0; + var headPosition = writer.Position; + + // Write our count place holder (must not be packed!) + writer.WriteValueSafe((ushort)0); + + foreach (var keyValuePairByGlobalObjectIdHash in m_NetworkManager.SceneManager.ScenePlacedObjects) + { + foreach (var keyValuePairBySceneHandle in keyValuePairByGlobalObjectIdHash.Value) + { + if (keyValuePairBySceneHandle.Value.Observers.Contains(TargetClientId)) + { + // Write our server relative scene handle for the NetworkObject being serialized + writer.WriteValueSafe(keyValuePairBySceneHandle.Key); + // Serialize the NetworkObject + var sceneObject = keyValuePairBySceneHandle.Value.GetMessageSceneObject(TargetClientId); + sceneObject.Serialize(writer); + numberOfObjects++; + } + } + } + + var tailPosition = writer.Position; + // Reposition to our count position to the head before we wrote our object count + writer.Seek(headPosition); + // Write number of NetworkObjects serialized (must not be packed!) + writer.WriteValueSafe(numberOfObjects); + // Set our position back to the tail + writer.Seek(tailPosition); + } + + /// + /// Client and Server Side: + /// Deserialize data based on the SceneEvent type. + /// + /// + internal void Deserialize(FastBufferReader reader) + { + reader.ReadValueSafe(out SceneEventType); + reader.ReadValueSafe(out LoadSceneMode); + + if (SceneEventType != SceneEventType.Synchronize) + { + reader.ReadValueSafe(out SceneEventProgressId); + } + + reader.ReadValueSafe(out SceneHash); + reader.ReadValueSafe(out SceneHandle); + + switch (SceneEventType) + { + case SceneEventType.Synchronize: + { + CopySceneSynchronizationData(reader); + break; + } + case SceneEventType.SynchronizeComplete: + { + CheckClientSynchronizationResults(reader); + break; + } + case SceneEventType.Load: + { + unsafe + { + // We store off the trailing in-scene placed serialized NetworkObject data to + // be processed once we are done loading. + m_HasInternalBuffer = true; + // We use Allocator.Persistent since scene loading could take longer than 4 frames + InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, reader.Length - reader.Position); + } + break; + } + case SceneEventType.ReSynchronize: + { + ReadClientReSynchronizationData(reader); + break; + } + case SceneEventType.LoadEventCompleted: + case SceneEventType.UnloadEventCompleted: + { + ReadSceneEventProgressDone(reader); + break; + } + } + } + + /// + /// Client Side: + /// Prepares for a scene synchronization event and copies the scene synchronization data + /// into the internal buffer to be used throughout the synchronization process. + /// + /// + internal void CopySceneSynchronizationData(FastBufferReader reader) + { + m_NetworkObjectsSync.Clear(); + reader.ReadValueSafe(out uint[] scenesToSynchronize); + reader.ReadValueSafe(out uint[] sceneHandlesToSynchronize); + ScenesToSynchronize = new Queue(scenesToSynchronize); + SceneHandlesToSynchronize = new Queue(sceneHandlesToSynchronize); + + // is not packed! + reader.ReadValueSafe(out int sizeToCopy); + unsafe + { + if (!reader.TryBeginRead(sizeToCopy)) + { + throw new OverflowException("Not enough space in the buffer to read recorded synchronization data size."); + } + + m_HasInternalBuffer = true; + // We use Allocator.Persistent since scene synchronization will most likely take longer than 4 frames + InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, sizeToCopy); + } + } + + /// + /// Client Side: + /// This needs to occur at the end of a event when the scene has finished loading + /// Maximum number of objects that could theoretically be synchronized is 65536 + /// + internal void DeserializeScenePlacedObjects() + { + try + { + // is not packed! + InternalBuffer.ReadValueSafe(out ushort newObjectsCount); + + for (ushort i = 0; i < newObjectsCount; i++) + { + InternalBuffer.ReadValueSafe(out int sceneHandle); + // Set our relative scene to the NetworkObject + m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(sceneHandle); + + // Deserialize the NetworkObject + var sceneObject = new NetworkObject.SceneObject(); + sceneObject.Deserialize(InternalBuffer); + NetworkObject.AddSceneObject(sceneObject, InternalBuffer, m_NetworkManager); + } + } + finally + { + InternalBuffer.Dispose(); + m_HasInternalBuffer = false; + } + } + + /// + /// Client Side: + /// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned, + /// the server will compile a list and send back an Event_ReSync message to the client. This is where the + /// client handles any returned values by the server. + /// + /// + internal void ReadClientReSynchronizationData(FastBufferReader reader) + { + reader.ReadValueSafe(out uint[] networkObjectsToRemove); + + if (networkObjectsToRemove.Length > 0) + { + var networkObjects = UnityEngine.Object.FindObjectsOfType(); + var networkObjectIdToNetworkObject = new Dictionary(); + foreach (var networkObject in networkObjects) + { + if (!networkObjectIdToNetworkObject.ContainsKey(networkObject.NetworkObjectId)) + { + networkObjectIdToNetworkObject.Add(networkObject.NetworkObjectId, networkObject); + } + } + + foreach (var networkObjectId in networkObjectsToRemove) + { + if (networkObjectIdToNetworkObject.ContainsKey(networkObjectId)) + { + var networkObject = networkObjectIdToNetworkObject[networkObjectId]; + networkObjectIdToNetworkObject.Remove(networkObjectId); + + 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); + } + if (m_NetworkManager.SpawnManager.SpawnedObjectsList.Contains(networkObject)) + { + m_NetworkManager.SpawnManager.SpawnedObjectsList.Remove(networkObject); + } + NetworkManager.Singleton.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); + } + else + { + UnityEngine.Object.DestroyImmediate(networkObject.gameObject); + } + } + } + } + } + + /// + /// Server Side: + /// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned, + /// the server will compile a list and send back an Event_ReSync message to the client. + /// + /// + internal void WriteClientReSynchronizationData(FastBufferWriter writer) + { + //Write how many objects need to be removed + writer.WriteValueSafe(m_NetworkObjectsToBeRemoved.ToArray()); + } + + /// + /// Server Side: + /// Determines if the client needs to be slightly re-synchronized if during the deserialization + /// process the server finds NetworkObjects that the client still thinks are spawned. + /// + /// + internal bool ClientNeedsReSynchronization() + { + return (m_NetworkObjectsToBeRemoved.Count > 0); + } + + /// + /// Server Side: + /// Determines if the client needs to be re-synchronized if during the deserialization + /// process the server finds NetworkObjects that the client still thinks are spawned but + /// have since been despawned. + /// + /// + internal void CheckClientSynchronizationResults(FastBufferReader reader) + { + m_NetworkObjectsToBeRemoved.Clear(); + reader.ReadValueSafe(out uint networkObjectIdCount); + for (int i = 0; i < networkObjectIdCount; i++) + { + reader.ReadValueSafe(out uint networkObjectId); + if (!m_NetworkManager.SpawnManager.SpawnedObjects.ContainsKey(networkObjectId)) + { + m_NetworkObjectsToBeRemoved.Add(networkObjectId); + } + } + } + + /// + /// Client Side: + /// During the deserialization process of the servers Event_Sync, the client builds a list of + /// all NetworkObjectIds that were spawned. Upon responding to the server with the Event_Sync_Complete + /// this list is included for the server to review over and determine if the client needs a minor resynchronization + /// of NetworkObjects that might have been despawned while the client was processing the Event_Sync. + /// + /// + internal void WriteClientSynchronizationResults(FastBufferWriter writer) + { + //Write how many objects were spawned + writer.WriteValueSafe((uint)m_NetworkObjectsSync.Count); + foreach (var networkObject in m_NetworkObjectsSync) + { + writer.WriteValueSafe((uint)networkObject.NetworkObjectId); + } + } + + /// + /// Client Side: + /// During the processing of a server sent Event_Sync, this method will be called for each scene once + /// it is finished loading. The client will also build a list of NetworkObjects that it spawned during + /// this process which will be used as part of the Event_Sync_Complete response. + /// + /// + internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) + { + try + { + // Process all NetworkObjects for this scene + InternalBuffer.ReadValueSafe(out int newObjectsCount); + + for (int i = 0; i < newObjectsCount; i++) + { + // We want to make sure for each NetworkObject we have the appropriate scene selected as the scene that is + // currently being synchronized. This assures in-scene placed NetworkObjects will use the right NetworkObject + // from the list of populated + InternalBuffer.ReadValueSafe(out int handle); + m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(handle); + + var sceneObject = new NetworkObject.SceneObject(); + sceneObject.Deserialize(InternalBuffer); + + var spawnedNetworkObject = NetworkObject.AddSceneObject(sceneObject, InternalBuffer, networkManager); + if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject)) + { + m_NetworkObjectsSync.Add(spawnedNetworkObject); + } + } + } + finally + { + InternalBuffer.Dispose(); + m_HasInternalBuffer = false; + } + } + + /// + /// Writes the all clients loaded or unloaded completed and timed out lists + /// + /// + internal void WriteSceneEventProgressDone(FastBufferWriter writer) + { + writer.WriteValueSafe((ushort)ClientsCompleted.Count); + foreach (var clientId in ClientsCompleted) + { + writer.WriteValueSafe(clientId); + } + + writer.WriteValueSafe((ushort)ClientsTimedOut.Count); + foreach (var clientId in ClientsTimedOut) + { + writer.WriteValueSafe(clientId); + } + } + + /// + /// Reads the all clients loaded or unloaded completed and timed out lists + /// + /// + internal void ReadSceneEventProgressDone(FastBufferReader reader) + { + reader.ReadValueSafe(out ushort completedCount); + ClientsCompleted = new List(); + for (int i = 0; i < completedCount; i++) + { + reader.ReadValueSafe(out ulong clientId); + ClientsCompleted.Add(clientId); + } + + reader.ReadValueSafe(out ushort timedOutCount); + ClientsTimedOut = new List(); + for (int i = 0; i < timedOutCount; i++) + { + reader.ReadValueSafe(out ulong clientId); + ClientsTimedOut.Add(clientId); + } + } + + /// + /// Used to release the pooled network buffer + /// + public void Dispose() + { + if (m_HasInternalBuffer) + { + InternalBuffer.Dispose(); + m_HasInternalBuffer = false; + } + } + + /// + /// Constructor for SceneEventData + /// + internal SceneEventData(NetworkManager networkManager) + { + m_NetworkManager = networkManager; + SceneEventId = XXHash.Hash32(Guid.NewGuid().ToString()); + } + } +} diff --git a/Runtime/SceneManagement/SceneEventData.cs.meta b/Runtime/SceneManagement/SceneEventData.cs.meta new file mode 100644 index 0000000..1d7827c --- /dev/null +++ b/Runtime/SceneManagement/SceneEventData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d4e25f9d4b699684183b2a06c55349fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SceneManagement/SceneEventProgress.cs b/Runtime/SceneManagement/SceneEventProgress.cs new file mode 100644 index 0000000..12143a3 --- /dev/null +++ b/Runtime/SceneManagement/SceneEventProgress.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; +using AsyncOperation = UnityEngine.AsyncOperation; + +namespace Unity.Netcode +{ + /// + /// Used by to determine if a server invoked scene event has started. + /// The returned status is stored in the property. + /// Note: This was formally known as SwitchSceneProgress which contained the . + /// All s are now delivered by the event handler + /// via the parameter. + /// + public enum SceneEventProgressStatus + { + /// + /// No scene event progress status can be used to initialize a variable that will be checked over time. + /// + None, + /// + /// The scene event was successfully started + /// + Started, + /// + /// Returned if you try to unload a scene that was not yet loaded + /// + SceneNotLoaded, + /// + /// Returned if you try to start a new scene event before a previous one is finished + /// + SceneEventInProgress, + /// + /// Returned if the scene name used with + /// or is invalid + /// + InvalidSceneName, + /// + /// Server side: Returned if the delegate handler returns false + /// (i.e. scene is considered not valid/safe to load) + /// + SceneFailedVerification, + /// + /// This is used for internal error notifications. + /// If you receive this event then it is most likely due to a bug. + /// If you receive this event repeatedly, then please open a GitHub issue with steps to replicate + /// + InternalNetcodeError, + } + + /// + /// Server side only: + /// This tracks the progress of clients during a load or unload scene event + /// + internal class SceneEventProgress + { + /// + /// List of clientIds of those clients that is done loading the scene. + /// + internal List DoneClients { get; } = new List(); + + /// + /// The NetworkTime at the moment the scene switch was initiated by the server. + /// + internal NetworkTime TimeAtInitiation { get; } + + /// + /// Delegate type for when the switch scene progress is completed. Either by all clients done loading the scene or by time out. + /// + internal delegate bool OnCompletedDelegate(SceneEventProgress sceneEventProgress); + + /// + /// The callback invoked when the switch scene progress is completed. Either by all clients done loading the scene or by time out. + /// + internal OnCompletedDelegate OnComplete; + + /// + /// Is this scene switch progresses completed, all clients are done loading the scene or a timeout has occurred. + /// + internal bool IsCompleted { get; private set; } + + internal bool TimedOut { get; private set; } + + /// + /// If all clients are done loading the scene, at the moment of completed. + /// + internal bool AreAllClientsDoneLoading { get; private set; } + + /// + /// The hash value generated from the full scene path + /// + internal uint SceneHash { get; set; } + + internal Guid Guid { get; } = Guid.NewGuid(); + + private Coroutine m_TimeOutCoroutine; + private AsyncOperation m_SceneLoadOperation; + + private NetworkManager m_NetworkManager { get; } + + internal SceneEventProgressStatus Status { get; set; } + + internal SceneEventType SceneEventType { get; set; } + + internal LoadSceneMode LoadSceneMode; + + internal SceneEventProgress(NetworkManager networkManager, SceneEventProgressStatus status = SceneEventProgressStatus.Started) + { + if (status == SceneEventProgressStatus.Started) + { + m_NetworkManager = networkManager; + m_TimeOutCoroutine = m_NetworkManager.StartCoroutine(TimeOutSceneEventProgress()); + TimeAtInitiation = networkManager.LocalTime; + } + Status = status; + } + + internal IEnumerator TimeOutSceneEventProgress() + { + yield return new WaitForSecondsRealtime(m_NetworkManager.NetworkConfig.LoadSceneTimeOut); + TimedOut = true; + CheckCompletion(); + } + + internal void AddClientAsDone(ulong clientId) + { + DoneClients.Add(clientId); + CheckCompletion(); + } + + internal void RemoveClientAsDone(ulong clientId) + { + DoneClients.Remove(clientId); + CheckCompletion(); + } + + internal void SetSceneLoadOperation(AsyncOperation sceneLoadOperation) + { + m_SceneLoadOperation = sceneLoadOperation; + m_SceneLoadOperation.completed += operation => CheckCompletion(); + } + + internal void CheckCompletion() + { + if ((!IsCompleted && DoneClients.Count == m_NetworkManager.ConnectedClientsList.Count && m_SceneLoadOperation.isDone) || (!IsCompleted && TimedOut)) + { + IsCompleted = true; + AreAllClientsDoneLoading = true; + + // If OnComplete is not registered or it is and returns true then remove this from the progress tracking + if (OnComplete == null || (OnComplete != null && OnComplete.Invoke(this))) + { + m_NetworkManager.SceneManager.SceneEventProgressTracking.Remove(Guid); + } + m_NetworkManager.StopCoroutine(m_TimeOutCoroutine); + } + } + } +} diff --git a/Runtime/SceneManagement/SceneEventProgress.cs.meta b/Runtime/SceneManagement/SceneEventProgress.cs.meta new file mode 100644 index 0000000..918b2bd --- /dev/null +++ b/Runtime/SceneManagement/SceneEventProgress.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 270ba1c92dd7ebe4aa15145612a9880e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization.meta b/Runtime/Serialization.meta new file mode 100644 index 0000000..7ec99bd --- /dev/null +++ b/Runtime/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4fd73dafe8658fb4cb3c87f77e783073 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/Arithmetic.cs b/Runtime/Serialization/Arithmetic.cs new file mode 100644 index 0000000..34d5430 --- /dev/null +++ b/Runtime/Serialization/Arithmetic.cs @@ -0,0 +1,57 @@ +namespace Unity.Netcode +{ + /// + /// Arithmetic helper class + /// + public static class Arithmetic + { + // Sign bits for different data types + internal const long SIGN_BIT_64 = -9223372036854775808; + internal const int SIGN_BIT_32 = -2147483648; + internal const short SIGN_BIT_16 = -32768; + internal const sbyte SIGN_BIT_8 = -128; + + // Ceiling function that doesn't deal with floating point values + // these only work correctly with possitive numbers + internal static ulong CeilingExact(ulong u1, ulong u2) => (u1 + u2 - 1) / u2; + internal static long CeilingExact(long u1, long u2) => (u1 + u2 - 1) / u2; + internal static uint CeilingExact(uint u1, uint u2) => (u1 + u2 - 1) / u2; + internal static int CeilingExact(int u1, int u2) => (u1 + u2 - 1) / u2; + internal static ushort CeilingExact(ushort u1, ushort u2) => (ushort)((u1 + u2 - 1) / u2); + internal static short CeilingExact(short u1, short u2) => (short)((u1 + u2 - 1) / u2); + internal static byte CeilingExact(byte u1, byte u2) => (byte)((u1 + u2 - 1) / u2); + internal static sbyte CeilingExact(sbyte u1, sbyte u2) => (sbyte)((u1 + u2 - 1) / u2); + + /// + /// ZigZag encodes a signed integer and maps it to a unsigned integer + /// + /// The signed integer to encode + /// A ZigZag encoded version of the integer + public static ulong ZigZagEncode(long value) => (ulong)((value >> 63) ^ (value << 1)); + + /// + /// Decides a ZigZag encoded integer back to a signed integer + /// + /// The unsigned integer + /// The signed version of the integer + public static long ZigZagDecode(ulong value) => (((long)(value >> 1) & 0x7FFFFFFFFFFFFFFFL) ^ ((long)(value << 63) >> 63)); + + /// + /// Gets the output size in bytes after VarInting a unsigned integer + /// + /// The unsigned integer whose length to get + /// The amount of bytes + public static int VarIntSize(ulong value) => + value <= 240 ? 1 : + value <= 2287 ? 2 : + value <= 67823 ? 3 : + value <= 16777215 ? 4 : + value <= 4294967295 ? 5 : + value <= 1099511627775 ? 6 : + value <= 281474976710655 ? 7 : + value <= 72057594037927935 ? 8 : + 9; + + internal static long Div8Ceil(ulong value) => (long)((value >> 3) + ((value & 1UL) | ((value >> 1) & 1UL) | ((value >> 2) & 1UL))); + } +} diff --git a/Runtime/Serialization/Arithmetic.cs.meta b/Runtime/Serialization/Arithmetic.cs.meta new file mode 100644 index 0000000..3bb614b --- /dev/null +++ b/Runtime/Serialization/Arithmetic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4e08651e5a4a45549926322c8957dfaa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/BitCounter.cs b/Runtime/Serialization/BitCounter.cs new file mode 100644 index 0000000..4feb447 --- /dev/null +++ b/Runtime/Serialization/BitCounter.cs @@ -0,0 +1,126 @@ +using System.Runtime.CompilerServices; + +namespace Unity.Netcode +{ + public static class BitCounter + { + // Since we don't have access to BitOperations.LeadingZeroCount() (which would have been the fastest) + // we use the De Bruijn sequence to do this calculation + // See https://en.wikipedia.org/wiki/De_Bruijn_sequence and https://www.chessprogramming.org/De_Bruijn_Sequence + private const ulong k_DeBruijnMagic64 = 0x37E84A99DAE458F; + private const uint k_DeBruijnMagic32 = 0x06EB14F9; + + // We're counting bytes, not bits, so these have all had the operation x/8 + 1 applied + private static readonly int[] k_DeBruijnTableBytes64 = + { + 0/8+1, 1/8+1, 17/8+1, 2/8+1, 18/8+1, 50/8+1, 3/8+1, 57/8+1, + 47/8+1, 19/8+1, 22/8+1, 51/8+1, 29/8+1, 4/8+1, 33/8+1, 58/8+1, + 15/8+1, 48/8+1, 20/8+1, 27/8+1, 25/8+1, 23/8+1, 52/8+1, 41/8+1, + 54/8+1, 30/8+1, 38/8+1, 5/8+1, 43/8+1, 34/8+1, 59/8+1, 8/8+1, + 63/8+1, 16/8+1, 49/8+1, 56/8+1, 46/8+1, 21/8+1, 28/8+1, 32/8+1, + 14/8+1, 26/8+1, 24/8+1, 40/8+1, 53/8+1, 37/8+1, 42/8+1, 7/8+1, + 62/8+1, 55/8+1, 45/8+1, 31/8+1, 13/8+1, 39/8+1, 36/8+1, 6/8+1, + 61/8+1, 44/8+1, 12/8+1, 35/8+1, 60/8+1, 11/8+1, 10/8+1, 9/8+1, + }; + + private static readonly int[] k_DeBruijnTableBytes32 = + { + 0/8+1, 1/8+1, 16/8+1, 2/8+1, 29/8+1, 17/8+1, 3/8+1, 22/8+1, + 30/8+1, 20/8+1, 18/8+1, 11/8+1, 13/8+1, 4/8+1, 7/8+1, 23/8+1, + 31/8+1, 15/8+1, 28/8+1, 21/8+1, 19/8+1, 10/8+1, 12/8+1, 6/8+1, + 14/8+1, 27/8+1, 9/8+1, 5/8+1, 26/8+1, 8/8+1, 25/8+1, 24/8+1, + }; + + // And here we're counting the number of set bits, not the position of the highest set, + // so these still have +1 applied - unfortunately 0 and 1 both return the same value. + private static readonly int[] k_DeBruijnTableBits64 = + { + 0+1, 1+1, 17+1, 2+1, 18+1, 50+1, 3+1, 57+1, + 47+1, 19+1, 22+1, 51+1, 29+1, 4+1, 33+1, 58+1, + 15+1, 48+1, 20+1, 27+1, 25+1, 23+1, 52+1, 41+1, + 54+1, 30+1, 38+1, 5+1, 43+1, 34+1, 59+1, 8+1, + 63+1, 16+1, 49+1, 56+1, 46+1, 21+1, 28+1, 32+1, + 14+1, 26+1, 24+1, 40+1, 53+1, 37+1, 42+1, 7+1, + 62+1, 55+1, 45+1, 31+1, 13+1, 39+1, 36+1, 6+1, + 61+1, 44+1, 12+1, 35+1, 60+1, 11+1, 10+1, 9+1, + }; + + private static readonly int[] k_DeBruijnTableBits32 = + { + 0+1, 1+1, 16+1, 2+1, 29+1, 17+1, 3+1, 22+1, + 30+1, 20+1, 18+1, 11+1, 13+1, 4+1, 7+1, 23+1, + 31+1, 15+1, 28+1, 21+1, 19+1, 10+1, 12+1, 6+1, + 14+1, 27+1, 9+1, 5+1, 26+1, 8+1, 25+1, 24+1, + }; + + /// + /// Get the minimum number of bytes required to represent the given value + /// + /// The value + /// The number of bytes required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedByteCount(uint value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value = value & ~(value >> 1); + return k_DeBruijnTableBytes32[value * k_DeBruijnMagic32 >> 27]; + } + + /// + /// Get the minimum number of bytes required to represent the given value + /// + /// The value + /// The number of bytes required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedByteCount(ulong value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + value = value & ~(value >> 1); + return k_DeBruijnTableBytes64[value * k_DeBruijnMagic64 >> 58]; + } + + /// + /// Get the minimum number of bits required to represent the given value + /// + /// The value + /// The number of bits required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedBitCount(uint value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value = value & ~(value >> 1); + return k_DeBruijnTableBits32[value * k_DeBruijnMagic32 >> 27]; + } + + /// + /// Get the minimum number of bits required to represent the given value + /// + /// The value + /// The number of bits required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedBitCount(ulong value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + value = value & ~(value >> 1); + return k_DeBruijnTableBits64[value * k_DeBruijnMagic64 >> 58]; + } + } +} diff --git a/Runtime/Serialization/BitCounter.cs.meta b/Runtime/Serialization/BitCounter.cs.meta new file mode 100644 index 0000000..f9d96d1 --- /dev/null +++ b/Runtime/Serialization/BitCounter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6983de23935090341bf45d5564401b9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/BitReader.cs b/Runtime/Serialization/BitReader.cs new file mode 100644 index 0000000..313e0c4 --- /dev/null +++ b/Runtime/Serialization/BitReader.cs @@ -0,0 +1,218 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + /// + /// Helper class for doing bitwise reads for a FastBufferReader. + /// Ensures all bitwise reads end on proper byte alignment so FastBufferReader doesn't have to be concerned + /// with misaligned reads. + /// + public ref struct BitReader + { + private FastBufferReader m_Reader; + private readonly unsafe byte* m_BufferPointer; + private readonly int m_Position; + private int m_BitPosition; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + private int m_AllowedBitwiseReadMark; +#endif + + private const int k_BitsPerByte = 8; + + /// + /// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary. + /// + public bool BitAligned + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (m_BitPosition & 7) == 0; + } + + internal unsafe BitReader(FastBufferReader reader) + { + m_Reader = reader; + + m_BufferPointer = m_Reader.Handle->BufferPointer + m_Reader.Handle->Position; + m_Position = m_Reader.Handle->Position; + m_BitPosition = 0; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseReadMark = (m_Reader.Handle->AllowedReadMark - m_Position) * k_BitsPerByte; +#endif + } + + /// + /// Pads the read bit count to byte alignment and commits the read back to the reader + /// + public void Dispose() + { + var bytesWritten = m_BitPosition >> 3; + if (!BitAligned) + { + // Accounting for the partial read + ++bytesWritten; + } + + m_Reader.CommitBitwiseReads(bytesWritten); + } + + /// + /// Verifies the requested bit count can be read from the buffer. + /// This exists as a separate method to allow multiple bit reads to be bounds checked with a single call. + /// If it returns false, you may not read, and in editor and development builds, attempting to do so will + /// throw an exception. In release builds, attempting to do so will read junk memory. + /// + /// Number of bits you want to read, in total + /// True if you can read, false if that would exceed buffer bounds + public unsafe bool TryBeginReadBits(uint bitCount) + { + var newBitPosition = m_BitPosition + bitCount; + var totalBytesWrittenInBitwiseContext = newBitPosition >> 3; + if ((newBitPosition & 7) != 0) + { + // Accounting for the partial read + ++totalBytesWrittenInBitwiseContext; + } + + if (m_Reader.Handle->Position + totalBytesWrittenInBitwiseContext > m_Reader.Handle->Length) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseReadMark = (int)newBitPosition; +#endif + return true; + } + + /// + /// Read a certain amount of bits from the stream. + /// + /// Value to store bits into. + /// Amount of bits to read + public unsafe void ReadBits(out ulong value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (bitCount > 64) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 64 bits from a 64-bit value!"); + } + + if (bitCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read fewer than 0 bits!"); + } + + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()"); + } +#endif + ulong val = 0; + + int wholeBytes = (int)bitCount / k_BitsPerByte; + byte* asBytes = (byte*)&val; + if (BitAligned) + { + if (wholeBytes != 0) + { + ReadPartialValue(out val, wholeBytes); + } + } + else + { + for (var i = 0; i < wholeBytes; ++i) + { + ReadMisaligned(out asBytes[i]); + } + } + + val |= (ulong)ReadByteBits((int)bitCount & 7) << ((int)bitCount & ~7); + value = val; + } + + /// + /// Read bits from stream. + /// + /// Value to store bits into. + /// Amount of bits to read. + public void ReadBits(out byte value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()"); + } +#endif + value = ReadByteBits((int)bitCount); + } + + /// + /// Read a single bit from the buffer + /// + /// Out value of the bit. True represents 1, False represents 0 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBit(out bool bit) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (m_BitPosition + 1); + if (checkPos > m_AllowedBitwiseReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()"); + } +#endif + + int offset = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + bit = (m_BufferPointer[pos] & (1 << offset)) != 0; + ++m_BitPosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ReadPartialValue(out T value, int bytesToRead, int offsetBytes = 0) where T : unmanaged + { + var val = new T(); + byte* ptr = ((byte*)&val) + offsetBytes; + byte* bufferPointer = m_BufferPointer + m_Position; + UnsafeUtility.MemCpy(ptr, bufferPointer, bytesToRead); + + m_BitPosition += bytesToRead * k_BitsPerByte; + value = val; + } + + private byte ReadByteBits(int bitCount) + { + if (bitCount > 8) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 8 bits into an 8-bit value!"); + } + + if (bitCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read fewer than 0 bits!"); + } + + int result = 0; + var convert = new ByteBool(); + for (int i = 0; i < bitCount; ++i) + { + ReadBit(out bool bit); + result |= convert.Collapse(bit) << i; + } + + return (byte)result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ReadMisaligned(out byte value) + { + int off = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + int shift1 = 8 - off; + + value = (byte)((m_BufferPointer[pos] >> off) | (m_BufferPointer[(m_BitPosition += 8) >> 3] << shift1)); + } + } +} diff --git a/Runtime/Serialization/BitReader.cs.meta b/Runtime/Serialization/BitReader.cs.meta new file mode 100644 index 0000000..9b0d159 --- /dev/null +++ b/Runtime/Serialization/BitReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72e2d94a96ca96a4fb2921df9adc2fdf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/BitWriter.cs b/Runtime/Serialization/BitWriter.cs new file mode 100644 index 0000000..842522a --- /dev/null +++ b/Runtime/Serialization/BitWriter.cs @@ -0,0 +1,211 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + /// + /// Helper class for doing bitwise writes for a FastBufferWriter. + /// Ensures all bitwise writes end on proper byte alignment so FastBufferWriter doesn't have to be concerned + /// with misaligned writes. + /// + public ref struct BitWriter + { + private FastBufferWriter m_Writer; + private unsafe byte* m_BufferPointer; + private readonly int m_Position; + private int m_BitPosition; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + private int m_AllowedBitwiseWriteMark; +#endif + private const int k_BitsPerByte = 8; + + /// + /// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary. + /// + public bool BitAligned + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (m_BitPosition & 7) == 0; + } + + internal unsafe BitWriter(FastBufferWriter writer) + { + m_Writer = writer; + m_BufferPointer = writer.Handle->BufferPointer + writer.Handle->Position; + m_Position = writer.Handle->Position; + m_BitPosition = 0; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseWriteMark = (m_Writer.Handle->AllowedWriteMark - m_Writer.Handle->Position) * k_BitsPerByte; +#endif + } + + /// + /// Pads the written bit count to byte alignment and commits the write back to the writer + /// + public void Dispose() + { + var bytesWritten = m_BitPosition >> 3; + if (!BitAligned) + { + // Accounting for the partial write + ++bytesWritten; + } + + m_Writer.CommitBitwiseWrites(bytesWritten); + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call TryBeginWriteBits() once on the total size, and then follow it with calls to + /// WriteBit() or WriteBits(). + /// + /// Bitwise write operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginWriteBits(). 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 + /// operations in release builds. Instead, attempting to write past the marked position in release builds + /// will write to random memory and cause undefined behavior, likely including instability and crashes. + /// + /// Number of bits you want to write, in total + /// True if you can write, false if that would exceed buffer bounds + public unsafe bool TryBeginWriteBits(int bitCount) + { + var newBitPosition = m_BitPosition + bitCount; + var totalBytesWrittenInBitwiseContext = newBitPosition >> 3; + if ((newBitPosition & 7) != 0) + { + // Accounting for the partial write + ++totalBytesWrittenInBitwiseContext; + } + + if (m_Position + totalBytesWrittenInBitwiseContext > m_Writer.Handle->Capacity) + { + if (m_Position + totalBytesWrittenInBitwiseContext > m_Writer.Handle->MaxCapacity) + { + return false; + } + if (m_Writer.Handle->Capacity < m_Writer.Handle->MaxCapacity) + { + m_Writer.Grow(totalBytesWrittenInBitwiseContext); + m_BufferPointer = m_Writer.Handle->BufferPointer + m_Writer.Handle->Position; + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseWriteMark = newBitPosition; +#endif + return true; + } + + /// + /// Write s certain amount of bits to the stream. + /// + /// Value to get bits from. + /// Amount of bits to write + public unsafe void WriteBits(ulong value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (bitCount > 64) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot write more than 64 bits from a 64-bit value!"); + } + + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWriteBits)}()"); + } +#endif + + int wholeBytes = (int)bitCount / k_BitsPerByte; + byte* asBytes = (byte*)&value; + if (BitAligned) + { + if (wholeBytes != 0) + { + WritePartialValue(value, wholeBytes); + } + } + else + { + for (var i = 0; i < wholeBytes; ++i) + { + WriteMisaligned(asBytes[i]); + } + } + + for (var count = wholeBytes * k_BitsPerByte; count < bitCount; ++count) + { + WriteBit((value & (1UL << count)) != 0); + } + } + + /// + /// Write bits to stream. + /// + /// Value to get bits from. + /// Amount of bits to write. + public void WriteBits(byte value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWriteBits)}()"); + } +#endif + + for (int i = 0; i < bitCount; ++i) + { + WriteBit(((value >> i) & 1) != 0); + } + } + + /// + /// Write a single bit to the buffer + /// + /// Value of the bit. True represents 1, False represents 0 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBit(bool bit) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (m_BitPosition + 1); + if (checkPos > m_AllowedBitwiseWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWriteBits)}()"); + } +#endif + + int offset = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + ++m_BitPosition; + m_BufferPointer[pos] = (byte)(bit ? (m_BufferPointer[pos] & ~(1 << offset)) | (1 << offset) : (m_BufferPointer[pos] & ~(1 << offset))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void WritePartialValue(T value, int bytesToWrite, int offsetBytes = 0) where T : unmanaged + { + byte* ptr = ((byte*)&value) + offsetBytes; + byte* bufferPointer = m_BufferPointer + m_Position; + UnsafeUtility.MemCpy(bufferPointer, ptr, bytesToWrite); + + m_BitPosition += bytesToWrite * k_BitsPerByte; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void WriteMisaligned(byte value) + { + int off = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + int shift1 = 8 - off; + m_BufferPointer[pos + 1] = (byte)((m_BufferPointer[pos + 1] & (0xFF << off)) | (value >> shift1)); + m_BufferPointer[pos] = (byte)((m_BufferPointer[pos] & (0xFF >> shift1)) | (value << off)); + + m_BitPosition += 8; + } + } +} diff --git a/Runtime/Serialization/BitWriter.cs.meta b/Runtime/Serialization/BitWriter.cs.meta new file mode 100644 index 0000000..604982f --- /dev/null +++ b/Runtime/Serialization/BitWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d6360e096142c149a11a2e86560c350 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/BufferSerializer.cs b/Runtime/Serialization/BufferSerializer.cs new file mode 100644 index 0000000..661975c --- /dev/null +++ b/Runtime/Serialization/BufferSerializer.cs @@ -0,0 +1,225 @@ +namespace Unity.Netcode +{ + /// + /// Two-way serializer wrapping FastBufferReader or FastBufferWriter. + /// + /// Implemented as a ref struct for two reasons: + /// 1. The BufferSerializer cannot outlive the FBR/FBW it wraps or using it will cause a crash + /// 2. The BufferSerializer must always be passed by reference and can't be copied + /// + /// Ref structs help enforce both of those rules: they can't out live the stack context in which they were + /// created, and they're always passed by reference no matter what. + /// + /// BufferSerializer doesn't wrapp FastBufferReader or FastBufferWriter directly because it can't. + /// ref structs can't implement interfaces, and in order to be able to have two different implementations with + /// the same interface (which allows us to avoid an "if(IsReader)" on every call), the thing directly wrapping + /// the struct has to implement an interface. So IReaderWriter exists as the interface, + /// which is implemented by a normal struct, while the ref struct wraps the normal one to enforce the two above + /// requirements. (Allowing direct access to the IReaderWriter struct would allow dangerous + /// things to happen because the struct's lifetime could outlive the Reader/Writer's.) + /// + /// The implementation struct + public ref struct BufferSerializer where TReaderWriter : IReaderWriter + { + private TReaderWriter m_Implementation; + + /// + /// Check if the contained implementation is a reader + /// + public bool IsReader => m_Implementation.IsReader; + + /// + /// Check if the contained implementation is a writer + /// + public bool IsWriter => m_Implementation.IsWriter; + + internal BufferSerializer(TReaderWriter implementation) + { + m_Implementation = implementation; + } + + /// + /// Retrieves the FastBufferReader instance. Only valid if IsReader = true, throws + /// InvalidOperationException otherwise. + /// + /// Reader instance + public FastBufferReader GetFastBufferReader() + { + return m_Implementation.GetFastBufferReader(); + } + + /// + /// Retrieves the FastBufferWriter instance. Only valid if IsWriter = true, throws + /// InvalidOperationException otherwise. + /// + /// Writer instance + public FastBufferWriter GetFastBufferWriter() + { + return m_Implementation.GetFastBufferWriter(); + } + + /// + /// Serialize an INetworkSerializable + /// + /// Throws OverflowException if the end of the buffer has been reached. + /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. + /// + /// Value to serialize + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() + { + m_Implementation.SerializeNetworkSerializable(ref value); + } + + /// + /// Serialize a string. + /// + /// Note: Will ALWAYS allocate a new string when reading. + /// + /// Throws OverflowException if the end of the buffer has been reached. + /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. + /// + /// Value to serialize + /// + /// If true, will truncate each char to one byte. + /// This is slower than two-byte chars, but uses less bandwidth. + /// + public void SerializeValue(ref string s, bool oneByteChars = false) + { + m_Implementation.SerializeValue(ref s, oneByteChars); + } + + /// + /// Serialize an array value. + /// + /// Note: Will ALWAYS allocate a new array when reading. + /// If you have a statically-sized array that you know is large enough, it's recommended to + /// serialize the size yourself and iterate serializing array members. + /// + /// (This is because C# doesn't allow setting an array's length value, so deserializing + /// into an existing array of larger size would result in an array that doesn't have as many values + /// as its Length indicates it should.) + /// + /// Throws OverflowException if the end of the buffer has been reached. + /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. + /// + /// Value to serialize + public void SerializeValue(ref T[] array) where T : unmanaged + { + m_Implementation.SerializeValue(ref array); + } + + /// + /// Serialize a single byte + /// + /// Throws OverflowException if the end of the buffer has been reached. + /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. + /// + /// Value to serialize + public void SerializeValue(ref byte value) + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// Serialize an unmanaged type. Supports basic value types as well as structs. + /// The provided type will be copied to/from the buffer as it exists in memory. + /// + /// Throws OverflowException if the end of the buffer has been reached. + /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. + /// + /// Value to serialize + public void SerializeValue(ref T value) where T : unmanaged + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call PreCheck() once on the total size, and then follow it with calls to + /// SerializeValuePreChecked() for faster serialization. Write buffers will grow during PreCheck() + /// if needed. + /// + /// PreChecked serialization operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using PreCheck(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using PreCheck is to avoid bounds checking in the following + /// operations in release builds. + /// + /// To get the correct size to check for, use FastBufferWriter.GetWriteSize(value) or + /// FastBufferWriter.GetWriteSize<type>() + /// + /// Number of bytes you plan to read or write + /// True if the read/write can proceed, false otherwise. + public bool PreCheck(int amount) + { + return m_Implementation.PreCheck(amount); + } + + /// + /// Serialize a string. + /// + /// Note: Will ALWAYS allocate a new string when reading. + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + /// + /// If true, will truncate each char to one byte. + /// This is slower than two-byte chars, but uses less bandwidth. + /// + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) + { + m_Implementation.SerializeValuePreChecked(ref s, oneByteChars); + } + + /// + /// Serialize an array value. + /// + /// Note: Will ALWAYS allocate a new array when reading. + /// If you have a statically-sized array that you know is large enough, it's recommended to + /// serialize the size yourself and iterate serializing array members. + /// + /// (This is because C# doesn't allow setting an array's length value, so deserializing + /// into an existing array of larger size would result in an array that doesn't have as many values + /// as its Length indicates it should.) + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref T[] array) where T : unmanaged + { + m_Implementation.SerializeValuePreChecked(ref array); + } + + /// + /// Serialize a single byte + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref byte value) + { + m_Implementation.SerializeValuePreChecked(ref value); + } + + /// + /// Serialize an unmanaged type. Supports basic value types as well as structs. + /// The provided type will be copied to/from the buffer as it exists in memory. + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref T value) where T : unmanaged + { + m_Implementation.SerializeValuePreChecked(ref value); + } + } +} diff --git a/Runtime/Serialization/BufferSerializer.cs.meta b/Runtime/Serialization/BufferSerializer.cs.meta new file mode 100644 index 0000000..fe70dbe --- /dev/null +++ b/Runtime/Serialization/BufferSerializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fca519b9bc3b32e4f9bd7cd138d690af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/BufferSerializerReader.cs b/Runtime/Serialization/BufferSerializerReader.cs new file mode 100644 index 0000000..4796b6c --- /dev/null +++ b/Runtime/Serialization/BufferSerializerReader.cs @@ -0,0 +1,77 @@ +using System; + +namespace Unity.Netcode +{ + internal struct BufferSerializerReader : IReaderWriter + { + private FastBufferReader m_Reader; + + public BufferSerializerReader(FastBufferReader reader) + { + m_Reader = reader; + } + + public bool IsReader => true; + public bool IsWriter => false; + + public FastBufferReader GetFastBufferReader() + { + return m_Reader; + } + + public FastBufferWriter GetFastBufferWriter() + { + throw new InvalidOperationException("Cannot retrieve a FastBufferWriter from a serializer where IsWriter = false"); + } + + public void SerializeValue(ref string s, bool oneByteChars = false) + { + m_Reader.ReadValueSafe(out s, oneByteChars); + } + + public void SerializeValue(ref T[] array) where T : unmanaged + { + m_Reader.ReadValueSafe(out array); + } + + public void SerializeValue(ref byte value) + { + m_Reader.ReadByteSafe(out value); + } + + public void SerializeValue(ref T value) where T : unmanaged + { + m_Reader.ReadValueSafe(out value); + } + + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() + { + m_Reader.ReadNetworkSerializable(out value); + } + + public bool PreCheck(int amount) + { + return m_Reader.TryBeginRead(amount); + } + + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) + { + m_Reader.ReadValue(out s, oneByteChars); + } + + public void SerializeValuePreChecked(ref T[] array) where T : unmanaged + { + m_Reader.ReadValue(out array); + } + + public void SerializeValuePreChecked(ref byte value) + { + m_Reader.ReadValue(out value); + } + + public void SerializeValuePreChecked(ref T value) where T : unmanaged + { + m_Reader.ReadValue(out value); + } + } +} diff --git a/Runtime/Serialization/BufferSerializerReader.cs.meta b/Runtime/Serialization/BufferSerializerReader.cs.meta new file mode 100644 index 0000000..ed94cf1 --- /dev/null +++ b/Runtime/Serialization/BufferSerializerReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 70dd17b6c14f7cd43ba5380d01cf91ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/BufferSerializerWriter.cs b/Runtime/Serialization/BufferSerializerWriter.cs new file mode 100644 index 0000000..f13d989 --- /dev/null +++ b/Runtime/Serialization/BufferSerializerWriter.cs @@ -0,0 +1,77 @@ +using System; + +namespace Unity.Netcode +{ + internal struct BufferSerializerWriter : IReaderWriter + { + private FastBufferWriter m_Writer; + + public BufferSerializerWriter(FastBufferWriter writer) + { + m_Writer = writer; + } + + public bool IsReader => false; + public bool IsWriter => true; + + public FastBufferReader GetFastBufferReader() + { + throw new InvalidOperationException("Cannot retrieve a FastBufferReader from a serializer where IsReader = false"); + } + + public FastBufferWriter GetFastBufferWriter() + { + return m_Writer; + } + + public void SerializeValue(ref string s, bool oneByteChars = false) + { + m_Writer.WriteValueSafe(s, oneByteChars); + } + + public void SerializeValue(ref T[] array) where T : unmanaged + { + m_Writer.WriteValueSafe(array); + } + + public void SerializeValue(ref byte value) + { + m_Writer.WriteByteSafe(value); + } + + public void SerializeValue(ref T value) where T : unmanaged + { + m_Writer.WriteValueSafe(value); + } + + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() + { + m_Writer.WriteNetworkSerializable(value); + } + + public bool PreCheck(int amount) + { + return m_Writer.TryBeginWrite(amount); + } + + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) + { + m_Writer.WriteValue(s, oneByteChars); + } + + public void SerializeValuePreChecked(ref T[] array) where T : unmanaged + { + m_Writer.WriteValue(array); + } + + public void SerializeValuePreChecked(ref byte value) + { + m_Writer.WriteByte(value); + } + + public void SerializeValuePreChecked(ref T value) where T : unmanaged + { + m_Writer.WriteValue(value); + } + } +} diff --git a/Runtime/Serialization/BufferSerializerWriter.cs.meta b/Runtime/Serialization/BufferSerializerWriter.cs.meta new file mode 100644 index 0000000..183a7ba --- /dev/null +++ b/Runtime/Serialization/BufferSerializerWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c05ed8e3061e62147a012cc01a64b5a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/BytePacker.cs b/Runtime/Serialization/BytePacker.cs new file mode 100644 index 0000000..3a50f7a --- /dev/null +++ b/Runtime/Serialization/BytePacker.cs @@ -0,0 +1,477 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Utility class for packing values in serialization. + /// + 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 + /// + /// Write a packed enum value. + /// + /// The writer to write to + /// The value to write + /// An enum type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void WriteValuePacked(FastBufferWriter writer, TEnum value) where TEnum : unmanaged, Enum + { + TEnum enumValue = value; + switch (sizeof(TEnum)) + { + case sizeof(int): + WriteValuePacked(writer, *(int*)&enumValue); + break; + case sizeof(byte): + WriteValuePacked(writer, *(byte*)&enumValue); + break; + case sizeof(short): + WriteValuePacked(writer, *(short*)&enumValue); + break; + case sizeof(long): + WriteValuePacked(writer, *(long*)&enumValue); + break; + } + } + + /// + /// Write single-precision floating point value to the buffer as a varint + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, float value) + { + WriteUInt32Packed(writer, ToUint(value)); + } + + /// + /// Write double-precision floating point value to the buffer as a varint + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, double value) + { + WriteUInt64Packed(writer, ToUlong(value)); + } + + /// + /// Write a byte to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, byte value) => writer.WriteByteSafe(value); + + /// + /// Write a signed byte to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, sbyte value) => writer.WriteByteSafe((byte)value); + + /// + /// Write a bool to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, bool value) => writer.WriteValueSafe(value); + + + /// + /// Write a signed short (Int16) as a ZigZag encoded varint to the buffer. + /// WARNING: If the value you're writing is > 2287, this will use MORE space + /// (3 bytes instead of 2), and if your value is > 240 you'll get no savings at all. + /// Only use this if you're certain your value will be small. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, short value) => WriteUInt32Packed(writer, (ushort)Arithmetic.ZigZagEncode(value)); + + /// + /// Write an unsigned short (UInt16) as a varint to the buffer. + /// WARNING: If the value you're writing is > 2287, this will use MORE space + /// (3 bytes instead of 2), and if your value is > 240 you'll get no savings at all. + /// Only use this if you're certain your value will be small. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, ushort value) => WriteUInt32Packed(writer, value); + + /// + /// Write a two-byte character as a varint to the buffer. + /// WARNING: If the value you're writing is > 2287, this will use MORE space + /// (3 bytes instead of 2), and if your value is > 240 you'll get no savings at all. + /// Only use this if you're certain your value will be small. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, char c) => WriteUInt32Packed(writer, c); + + /// + /// Write a signed int (Int32) as a ZigZag encoded varint to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, int value) => WriteUInt32Packed(writer, (uint)Arithmetic.ZigZagEncode(value)); + + /// + /// Write an unsigned int (UInt32) to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, uint value) => WriteUInt32Packed(writer, value); + + /// + /// Write an unsigned long (UInt64) to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, ulong value) => WriteUInt64Packed(writer, value); + + /// + /// Write a signed long (Int64) as a ZigZag encoded varint to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, long value) => WriteUInt64Packed(writer, Arithmetic.ZigZagEncode(value)); + + /// + /// Convenience method that writes two packed Vector3 from the ray to the buffer + /// + /// The writer to write to + /// Ray to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Ray ray) + { + WriteValuePacked(writer, ray.origin); + WriteValuePacked(writer, ray.direction); + } + + /// + /// Convenience method that writes two packed Vector2 from the ray to the buffer + /// + /// The writer to write to + /// Ray2D to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Ray2D ray2d) + { + WriteValuePacked(writer, ray2d.origin); + WriteValuePacked(writer, ray2d.direction); + } + + /// + /// Convenience method that writes four varint floats from the color to the buffer + /// + /// The writer to write to + /// Color to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Color color) + { + WriteValuePacked(writer, color.r); + WriteValuePacked(writer, color.g); + WriteValuePacked(writer, color.b); + WriteValuePacked(writer, color.a); + } + + /// + /// Convenience method that writes four varint floats from the color to the buffer + /// + /// The writer to write to + /// Color to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Color32 color) + { + WriteValuePacked(writer, color.r); + WriteValuePacked(writer, color.g); + WriteValuePacked(writer, color.b); + WriteValuePacked(writer, color.a); + } + + /// + /// Convenience method that writes two varint floats from the vector to the buffer + /// + /// The writer to write to + /// Vector to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Vector2 vector2) + { + WriteValuePacked(writer, vector2.x); + WriteValuePacked(writer, vector2.y); + } + + /// + /// Convenience method that writes three varint floats from the vector to the buffer + /// + /// The writer to write to + /// Vector to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Vector3 vector3) + { + WriteValuePacked(writer, vector3.x); + WriteValuePacked(writer, vector3.y); + WriteValuePacked(writer, vector3.z); + } + + /// + /// Convenience method that writes four varint floats from the vector to the buffer + /// + /// The writer to write to + /// Vector to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Vector4 vector4) + { + WriteValuePacked(writer, vector4.x); + WriteValuePacked(writer, vector4.y); + WriteValuePacked(writer, vector4.z); + WriteValuePacked(writer, vector4.w); + } + + /// + /// Writes the rotation to the buffer. + /// + /// The writer to write to + /// Rotation to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, Quaternion rotation) + { + WriteValuePacked(writer, rotation.x); + WriteValuePacked(writer, rotation.y); + WriteValuePacked(writer, rotation.z); + WriteValuePacked(writer, rotation.w); + } + + /// + /// Writes a string in a packed format + /// + /// The writer to write to + /// The value to pack + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(FastBufferWriter writer, string s) + { + WriteValuePacked(writer, (uint)s.Length); + int target = s.Length; + for (int i = 0; i < target; ++i) + { + WriteValuePacked(writer, s[i]); + } + } +#endif + + +#if UNITY_NETCODE_DEBUG_NO_PACKING + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueBitPacked(FastBufferWriter writer, T value) where T: unmanaged => writer.WriteValueSafe(value); +#else + /// + /// 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. + /// The sign bit takes up another bit. + /// That leaves 14 bits for the value. + /// A value greater than 2^14-1 or less than -2^14 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its two + /// most significant bits after zig-zag encoding. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(FastBufferWriter writer, short value) => WriteValueBitPacked(writer, (ushort)Arithmetic.ZigZagEncode(value)); + + /// + /// Writes a 15-bit unsigned short to the buffer in a bit-encoded packed format. + /// The first bit indicates whether the value is 1 byte or 2. + /// That leaves 15 bits for the value. + /// A value greater than 2^15-1 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its + /// most significant bit. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(FastBufferWriter writer, ushort value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (value >= 0b1000_0000_0000_0000) + { + throw new ArgumentException("BitPacked ushorts must be <= 15 bits"); + } +#endif + + if (value <= 0b0111_1111) + { + if (!writer.TryBeginWriteInternal(1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteByte((byte)(value << 1)); + return; + } + + if (!writer.TryBeginWriteInternal(2)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteValue((ushort)((value << 1) | 0b1)); + } + + /// + /// Writes a 29-bit signed int to the buffer in a bit-encoded packed format. + /// The first two bits indicate whether the value is 1, 2, 3, or 4 bytes. + /// The sign bit takes up another bit. + /// That leaves 29 bits for the value. + /// A value greater than 2^29-1 or less than -2^29 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its three + /// most significant bits after zig-zag encoding. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(FastBufferWriter writer, int value) => WriteValueBitPacked(writer, (uint)Arithmetic.ZigZagEncode(value)); + + /// + /// Writes a 30-bit unsigned int to the buffer in a bit-encoded packed format. + /// The first two bits indicate whether the value is 1, 2, 3, or 4 bytes. + /// That leaves 30 bits for the value. + /// A value greater than 2^30-1 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its two + /// most significant bits. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(FastBufferWriter writer, uint value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (value >= 0b0100_0000_0000_0000_0000_0000_0000_0000) + { + throw new ArgumentException("BitPacked uints must be <= 30 bits"); + } +#endif + value <<= 2; + var numBytes = BitCounter.GetUsedByteCount(value); + if (!writer.TryBeginWriteInternal(numBytes)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WritePartialValue(value | (uint)(numBytes - 1), numBytes); + } + + /// + /// Writes a 60-bit signed long to the buffer in a bit-encoded packed format. + /// The first three bits indicate whether the value is 1, 2, 3, 4, 5, 6, 7, or 8 bytes. + /// The sign bit takes up another bit. + /// That leaves 60 bits for the value. + /// A value greater than 2^60-1 or less than -2^60 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its four + /// most significant bits after zig-zag encoding. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(FastBufferWriter writer, long value) => WriteValueBitPacked(writer, Arithmetic.ZigZagEncode(value)); + + /// + /// Writes a 61-bit unsigned long to the buffer in a bit-encoded packed format. + /// The first three bits indicate whether the value is 1, 2, 3, 4, 5, 6, 7, or 8 bytes. + /// That leaves 31 bits for the value. + /// A value greater than 2^61-1 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its three + /// most significant bits. + /// + /// The writer to write to + /// The value to pack + 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) + { + throw new ArgumentException("BitPacked ulongs must be <= 61 bits"); + } +#endif + value <<= 3; + var numBytes = BitCounter.GetUsedByteCount(value); + if (!writer.TryBeginWriteInternal(numBytes)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WritePartialValue(value | (uint)(numBytes - 1), numBytes); + } +#endif + + private static void WriteUInt64Packed(FastBufferWriter writer, ulong value) + { + if (value <= 240) + { + writer.WriteByteSafe((byte)value); + return; + } + if (value <= 2287) + { + writer.WriteByteSafe((byte)(((value - 240) >> 8) + 241)); + writer.WriteByteSafe((byte)(value - 240)); + return; + } + var writeBytes = BitCounter.GetUsedByteCount(value); + + if (!writer.TryBeginWriteInternal(writeBytes + 1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteByte((byte)(247 + writeBytes)); + writer.WritePartialValue(value, writeBytes); + } + + // Looks like the same code as WriteUInt64Packed? + // It's actually different because it will call the more efficient 32-bit version + // of BytewiseUtility.GetUsedByteCount(). + private static void WriteUInt32Packed(FastBufferWriter writer, uint value) + { + if (value <= 240) + { + writer.WriteByteSafe((byte)value); + return; + } + if (value <= 2287) + { + writer.WriteByteSafe((byte)(((value - 240) >> 8) + 241)); + writer.WriteByteSafe((byte)(value - 240)); + return; + } + var writeBytes = BitCounter.GetUsedByteCount(value); + + if (!writer.TryBeginWriteInternal(writeBytes + 1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteByte((byte)(247 + writeBytes)); + writer.WritePartialValue(value, writeBytes); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe uint ToUint(T value) where T : unmanaged + { + uint* asUint = (uint*)&value; + return *asUint; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong ToUlong(T value) where T : unmanaged + { + ulong* asUlong = (ulong*)&value; + return *asUlong; + } + } +} diff --git a/Runtime/Serialization/BytePacker.cs.meta b/Runtime/Serialization/BytePacker.cs.meta new file mode 100644 index 0000000..dfb5e75 --- /dev/null +++ b/Runtime/Serialization/BytePacker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3ec13587ae68cb49b82af8612d47698 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/ByteUnpacker.cs b/Runtime/Serialization/ByteUnpacker.cs new file mode 100644 index 0000000..5cf2ace --- /dev/null +++ b/Runtime/Serialization/ByteUnpacker.cs @@ -0,0 +1,558 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Unity.Netcode +{ + public static class ByteUnpacker + { + +#if UNITY_NETCODE_DEBUG_NO_PACKING + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValuePacked(FastBufferReader reader, out T value) where T: unmanaged => reader.ReadValueSafe(out value); +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void ReadValuePacked(FastBufferReader reader, out TEnum value) where TEnum : unmanaged, Enum + { + switch (sizeof(TEnum)) + { + case sizeof(int): + ReadValuePacked(reader, out int asInt); + value = *(TEnum*)&asInt; + break; + case sizeof(byte): + ReadValuePacked(reader, out byte asByte); + value = *(TEnum*)&asByte; + break; + case sizeof(short): + ReadValuePacked(reader, out short asShort); + value = *(TEnum*)&asShort; + break; + case sizeof(long): + ReadValuePacked(reader, out long asLong); + value = *(TEnum*)&asLong; + break; + default: + throw new InvalidOperationException("Enum is a size that cannot exist?!"); + } + } + + /// + /// Read single-precision floating point value from the stream as a varint + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out float value) + { + ReadUInt32Packed(reader, out uint asUInt); + value = ToSingle(asUInt); + } + + /// + /// Read double-precision floating point value from the stream as a varint + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out double value) + { + ReadUInt64Packed(reader, out ulong asULong); + value = ToDouble(asULong); + } + + /// + /// Read a byte from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out byte value) => reader.ReadByteSafe(out value); + + /// + /// Read a signed byte from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out sbyte value) + { + reader.ReadByteSafe(out byte byteVal); + value = (sbyte)byteVal; + } + + /// + /// Read a boolean from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out bool value) => reader.ReadValueSafe(out value); + + + /// + /// Read an usigned short (Int16) as a varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out short value) + { + ReadUInt32Packed(reader, out uint readValue); + value = (short)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read an unsigned short (UInt16) as a varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out ushort value) + { + ReadUInt32Packed(reader, out uint readValue); + value = (ushort)readValue; + } + + /// + /// Read a two-byte character as a varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out char c) + { + ReadUInt32Packed(reader, out uint readValue); + c = (char)readValue; + } + + /// + /// Read a signed int (Int32) as a ZigZag encoded varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out int value) + { + ReadUInt32Packed(reader, out uint readValue); + value = (int)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read an unsigned int (UInt32) from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out uint value) => ReadUInt32Packed(reader, out value); + + /// + /// Read an unsigned long (UInt64) from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out ulong value) => ReadUInt64Packed(reader, out value); + + /// + /// Read a signed long (Int64) as a ZigZag encoded varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out long value) + { + ReadUInt64Packed(reader, out ulong readValue); + value = Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Convenience method that reads two packed Vector3 from the ray from the stream + /// + /// The reader to read from + /// Ray to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Ray ray) + { + ReadValuePacked(reader, out Vector3 origin); + ReadValuePacked(reader, out Vector3 direction); + ray = new Ray(origin, direction); + } + + /// + /// Convenience method that reads two packed Vector2 from the ray from the stream + /// + /// The reader to read from + /// Ray2D to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Ray2D ray2d) + { + ReadValuePacked(reader, out Vector2 origin); + ReadValuePacked(reader, out Vector2 direction); + ray2d = new Ray2D(origin, direction); + } + + /// + /// Convenience method that reads four varint floats from the color from the stream + /// + /// The reader to read from + /// Color to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Color color) + { + color = new Color(); + ReadValuePacked(reader, out color.r); + ReadValuePacked(reader, out color.g); + ReadValuePacked(reader, out color.b); + ReadValuePacked(reader, out color.a); + } + + /// + /// Convenience method that reads four varint floats from the color from the stream + /// + /// The reader to read from + /// Color to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Color32 color) + { + color = new Color32(); + ReadValuePacked(reader, out color.r); + ReadValuePacked(reader, out color.g); + ReadValuePacked(reader, out color.b); + ReadValuePacked(reader, out color.a); + } + + /// + /// Convenience method that reads two varint floats from the vector from the stream + /// + /// The reader to read from + /// Vector to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Vector2 vector2) + { + vector2 = new Vector2(); + ReadValuePacked(reader, out vector2.x); + ReadValuePacked(reader, out vector2.y); + } + + /// + /// Convenience method that reads three varint floats from the vector from the stream + /// + /// The reader to read from + /// Vector to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Vector3 vector3) + { + vector3 = new Vector3(); + ReadValuePacked(reader, out vector3.x); + ReadValuePacked(reader, out vector3.y); + ReadValuePacked(reader, out vector3.z); + } + + /// + /// Convenience method that reads four varint floats from the vector from the stream + /// + /// The reader to read from + /// Vector to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Vector4 vector4) + { + vector4 = new Vector4(); + ReadValuePacked(reader, out vector4.x); + ReadValuePacked(reader, out vector4.y); + ReadValuePacked(reader, out vector4.z); + ReadValuePacked(reader, out vector4.w); + } + + /// + /// Reads the rotation from the stream. + /// + /// The reader to read from + /// Rotation to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(FastBufferReader reader, out Quaternion rotation) + { + rotation = new Quaternion(); + ReadValuePacked(reader, out rotation.x); + ReadValuePacked(reader, out rotation.y); + ReadValuePacked(reader, out rotation.z); + ReadValuePacked(reader, out rotation.w); + } + + /// + /// Reads a string in a packed format + /// + /// The reader to read from + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void ReadValuePacked(FastBufferReader reader, out string s) + { + ReadValuePacked(reader, out uint length); + s = "".PadRight((int)length); + int target = s.Length; + fixed (char* c = s) + { + for (int i = 0; i < target; ++i) + { + ReadValuePacked(reader, out c[i]); + } + } + } +#endif + +#if UNITY_NETCODE_DEBUG_NO_PACKING + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueBitPacked(FastBufferReader reader, T value) where T: unmanaged => reader.ReadValueSafe(out value); +#else + /// + /// Read a bit-packed 14-bit signed short from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static void ReadValueBitPacked(FastBufferReader reader, out short value) + { + ReadValueBitPacked(reader, out ushort readValue); + value = (short)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read a bit-packed 15-bit unsigned short from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static unsafe void ReadValueBitPacked(FastBufferReader reader, out ushort value) + { + ushort returnValue = 0; + byte* ptr = ((byte*)&returnValue); + byte* data = reader.GetUnsafePtrAtCurrentPosition(); + int numBytes = (data[0] & 0b1) + 1; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.MarkBytesRead(numBytes); + switch (numBytes) + { + case 1: + *ptr = *data; + break; + case 2: + *ptr = *data; + *(ptr + 1) = *(data + 1); + break; + default: + throw new InvalidOperationException("Could not read bit-packed value: impossible byte count"); + } + + value = (ushort)(returnValue >> 1); + } + + /// + /// Read a bit-packed 29-bit signed int from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static void ReadValueBitPacked(FastBufferReader reader, out int value) + { + ReadValueBitPacked(reader, out uint readValue); + value = (int)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read a bit-packed 30-bit unsigned int from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static unsafe void ReadValueBitPacked(FastBufferReader reader, out uint value) + { + uint returnValue = 0; + byte* ptr = ((byte*)&returnValue); + byte* data = reader.GetUnsafePtrAtCurrentPosition(); + int numBytes = (data[0] & 0b11) + 1; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.MarkBytesRead(numBytes); + switch (numBytes) + { + case 1: + *ptr = *data; + break; + case 2: + *ptr = *data; + *(ptr + 1) = *(data + 1); + break; + case 3: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + break; + case 4: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + break; + } + + value = returnValue >> 2; + } + + /// + /// Read a bit-packed 60-bit signed long from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static void ReadValueBitPacked(FastBufferReader reader, out long value) + { + ReadValueBitPacked(reader, out ulong readValue); + value = Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read a bit-packed 61-bit signed long from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static unsafe void ReadValueBitPacked(FastBufferReader reader, out ulong value) + { + ulong returnValue = 0; + byte* ptr = ((byte*)&returnValue); + byte* data = reader.GetUnsafePtrAtCurrentPosition(); + int numBytes = (data[0] & 0b111) + 1; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.MarkBytesRead(numBytes); + switch (numBytes) + { + case 1: + *ptr = *data; + break; + case 2: + *ptr = *data; + *(ptr + 1) = *(data + 1); + break; + case 3: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + break; + case 4: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + break; + case 5: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + break; + case 6: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + *(ptr + 5) = *(data + 5); + break; + case 7: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + *(ptr + 5) = *(data + 5); + *(ptr + 6) = *(data + 6); + break; + case 8: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + *(ptr + 5) = *(data + 5); + *(ptr + 6) = *(data + 6); + *(ptr + 7) = *(data + 7); + break; + } + + value = returnValue >> 3; + } +#endif + private static void ReadUInt64Packed(FastBufferReader reader, out ulong value) + { + reader.ReadByteSafe(out byte firstByte); + if (firstByte <= 240) + { + value = firstByte; + return; + } + + if (firstByte <= 248) + { + reader.ReadByteSafe(out byte secondByte); + value = 240UL + ((firstByte - 241UL) << 8) + secondByte; + return; + } + + var numBytes = firstByte - 247; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.ReadPartialValue(out value, numBytes); + } + + private static void ReadUInt32Packed(FastBufferReader reader, out uint value) + { + reader.ReadByteSafe(out byte firstByte); + if (firstByte <= 240) + { + value = firstByte; + return; + } + + if (firstByte <= 248) + { + reader.ReadByteSafe(out byte secondByte); + value = 240U + ((firstByte - 241U) << 8) + secondByte; + return; + } + + var numBytes = firstByte - 247; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.ReadPartialValue(out value, numBytes); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe float ToSingle(T value) where T : unmanaged + { + float* asFloat = (float*)&value; + return *asFloat; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe double ToDouble(T value) where T : unmanaged + { + double* asDouble = (double*)&value; + return *asDouble; + } + } +} diff --git a/Runtime/Serialization/ByteUnpacker.cs.meta b/Runtime/Serialization/ByteUnpacker.cs.meta new file mode 100644 index 0000000..f3784bc --- /dev/null +++ b/Runtime/Serialization/ByteUnpacker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 73484532f9cd8a7418b6a7ac770df851 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/FastBufferReader.cs b/Runtime/Serialization/FastBufferReader.cs new file mode 100644 index 0000000..0d8dac7 --- /dev/null +++ b/Runtime/Serialization/FastBufferReader.cs @@ -0,0 +1,773 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + public struct FastBufferReader : IDisposable + { + internal struct ReaderHandle + { + internal unsafe byte* BufferPointer; + internal int Position; + internal int Length; + internal Allocator Allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + internal int AllowedReadMark; + internal bool InBitwiseContext; +#endif + } + + internal readonly unsafe ReaderHandle* Handle; + + /// + /// Get the current read position + /// + public unsafe int Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Handle->Position; + } + + /// + /// Get the total length of the buffer + /// + public unsafe int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Handle->Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe void CommitBitwiseReads(int amount) + { + Handle->Position += amount; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->InBitwiseContext = false; +#endif + } + + private static unsafe ReaderHandle* CreateHandle(byte* buffer, int length, int offset, Allocator allocator) + { + ReaderHandle* readerHandle = null; + if (allocator == Allocator.None) + { + readerHandle = (ReaderHandle*)UnsafeUtility.Malloc(sizeof(ReaderHandle) + length, UnsafeUtility.AlignOf(), Allocator.Temp); + readerHandle->BufferPointer = buffer; + readerHandle->Position = offset; + } + else + { + readerHandle = (ReaderHandle*)UnsafeUtility.Malloc(sizeof(ReaderHandle) + length, UnsafeUtility.AlignOf(), allocator); + UnsafeUtility.MemCpy(readerHandle + 1, buffer + offset, length); + readerHandle->BufferPointer = (byte*)(readerHandle + 1); + readerHandle->Position = 0; + } + + readerHandle->Length = length; + readerHandle->Allocator = allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + readerHandle->AllowedReadMark = 0; + readerHandle->InBitwiseContext = false; +#endif + return readerHandle; + } + + /// + /// Create a FastBufferReader from a NativeArray. + /// + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The exception to this is when the allocator passed in is Allocator.None. In this scenario, + /// ownership of the data remains with the caller and the reader will point at it directly. + /// When created with Allocator.None, FastBufferReader will allocate some internal data using + /// Allocator.Temp, so it should be treated as if it's a ref struct and not allowed to outlive + /// the context in which it was created (it should neither be returned from that function nor + /// stored anywhere in heap memory). + /// + /// + /// + /// + /// + public unsafe FastBufferReader(NativeArray buffer, Allocator allocator, int length = -1, int offset = 0) + { + Handle = CreateHandle((byte*)buffer.GetUnsafePtr(), Math.Max(1, length == -1 ? buffer.Length : length), offset, allocator); + } + + /// + /// Create a FastBufferReader from an ArraySegment. + /// + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// Allocator.None is not supported for byte[]. If you need this functionality, use a fixed() block + /// and ensure the FastBufferReader isn't used outside that block. + /// + /// The buffer to copy from + /// The allocator to use + /// The number of bytes to copy (all if this is -1) + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(ArraySegment buffer, Allocator allocator, int length = -1, int offset = 0) + { + if (allocator == Allocator.None) + { + throw new NotSupportedException("Allocator.None cannot be used with managed source buffers."); + } + fixed (byte* data = buffer.Array) + { + Handle = CreateHandle(data, Math.Max(1, length == -1 ? buffer.Count : length), offset, allocator); + } + } + + /// + /// Create a FastBufferReader from an existing byte array. + /// + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// Allocator.None is not supported for byte[]. If you need this functionality, use a fixed() block + /// and ensure the FastBufferReader isn't used outside that block. + /// + /// The buffer to copy from + /// The allocator to use + /// The number of bytes to copy (all if this is -1) + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(byte[] buffer, Allocator allocator, int length = -1, int offset = 0) + { + if (allocator == Allocator.None) + { + throw new NotSupportedException("Allocator.None cannot be used with managed source buffers."); + } + fixed (byte* data = buffer) + { + Handle = CreateHandle(data, Math.Max(1, length == -1 ? buffer.Length : length), offset, allocator); + } + } + + /// + /// Create a FastBufferReader from an existing byte buffer. + /// + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The exception to this is when the allocator passed in is Allocator.None. In this scenario, + /// ownership of the data remains with the caller and the reader will point at it directly. + /// When created with Allocator.None, FastBufferReader will allocate some internal data using + /// Allocator.Temp, so it should be treated as if it's a ref struct and not allowed to outlive + /// the context in which it was created (it should neither be returned from that function nor + /// stored anywhere in heap memory). + /// + /// The buffer to copy from + /// The allocator to use + /// The number of bytes to copy + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(byte* buffer, Allocator allocator, int length, int offset = 0) + { + Handle = CreateHandle(buffer, Math.Max(1, length), offset, allocator); + } + + /// + /// Create a FastBufferReader from a FastBufferWriter. + /// + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The exception to this is when the allocator passed in is Allocator.None. In this scenario, + /// ownership of the data remains with the caller and the reader will point at it directly. + /// When created with Allocator.None, FastBufferReader will allocate some internal data using + /// Allocator.Temp, so it should be treated as if it's a ref struct and not allowed to outlive + /// the context in which it was created (it should neither be returned from that function nor + /// stored anywhere in heap memory). + /// + /// The writer to copy from + /// The allocator to use + /// The number of bytes to copy (all if this is -1) + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(FastBufferWriter writer, Allocator allocator, int length = -1, int offset = 0) + { + Handle = CreateHandle(writer.GetUnsafePtr(), Math.Max(1, length == -1 ? writer.Length : length), offset, allocator); + } + + /// + /// Frees the allocated buffer + /// + public unsafe void Dispose() + { + UnsafeUtility.Free(Handle, Handle->Allocator); + } + + /// + /// Move the read position in the stream + /// + /// Absolute value to move the position to, truncated to Length + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void Seek(int where) + { + Handle->Position = Math.Min(Length, where); + } + + /// + /// Mark that some bytes are going to be read via GetUnsafePtr(). + /// + /// Amount that will be read + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe void MarkBytesRead(int amount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (Handle->Position + amount > Handle->AllowedReadMark) + { + throw new OverflowException("Attempted to read without first calling TryBeginRead()"); + } +#endif + Handle->Position += amount; + } + + /// + /// Retrieve a BitReader to be able to perform bitwise operations on the buffer. + /// No bytewise operations can be performed on the buffer until bitReader.Dispose() has been called. + /// At the end of the operation, FastBufferReader will remain byte-aligned. + /// + /// A BitReader + public unsafe BitReader EnterBitwiseContext() + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->InBitwiseContext = true; +#endif + return new BitReader(this); + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be reading multiple fields back-to-back and you know the total size, + /// you can call TryBeginRead() once on the total size, and then follow it with calls to + /// ReadValue() instead of ReadValueSafe() for faster serialization. + /// + /// Unsafe read operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginRead(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using TryBeginRead is to avoid bounds checking in the following + /// operations in release builds. + /// + /// Amount of bytes to read + /// True if the read is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryBeginRead(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + if (Handle->Position + bytes > Handle->Length) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->AllowedReadMark = Handle->Position + bytes; +#endif + return true; + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be reading multiple fields back-to-back and you know the total size, + /// you can call TryBeginRead() once on the total size, and then follow it with calls to + /// ReadValue() instead of ReadValueSafe() for faster serialization. + /// + /// Unsafe read operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginRead(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using TryBeginRead is to avoid bounds checking in the following + /// operations in release builds. + /// + /// The value you want to read + /// True if the read is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryBeginReadValue(in T value) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + int len = sizeof(T); + if (Handle->Position + len > Handle->Length) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->AllowedReadMark = Handle->Position + len; +#endif + return true; + } + + /// + /// Internal version of TryBeginRead. + /// Differs from TryBeginRead only in that it won't ever move the AllowedReadMark backward. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe bool TryBeginReadInternal(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + if (Handle->Position + bytes > Handle->Length) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->Position + bytes > Handle->AllowedReadMark) + { + Handle->AllowedReadMark = Handle->Position + bytes; + } +#endif + return true; + } + + /// + /// Returns an array representation of the underlying byte buffer. + /// !!Allocates a new array!! + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte[] ToArray() + { + byte[] ret = new byte[Length]; + fixed (byte* b = ret) + { + UnsafeUtility.MemCpy(b, Handle->BufferPointer, Length); + } + return ret; + } + + /// + /// Gets a direct pointer to the underlying buffer + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtr() + { + return Handle->BufferPointer; + } + + /// + /// Gets a direct pointer to the underlying buffer at the current read position + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtrAtCurrentPosition() + { + return Handle->BufferPointer + Handle->Position; + } + + /// + /// Read an INetworkSerializable + /// + /// INetworkSerializable instance + /// + /// + public void ReadNetworkSerializable(out T value) where T : INetworkSerializable, new() + { + value = new T(); + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(this)); + value.NetworkSerialize(bufferSerializer); + } + + /// + /// Read an array of INetworkSerializables + /// + /// INetworkSerializable instance + /// + /// + public void ReadNetworkSerializable(out T[] value) where T : INetworkSerializable, new() + { + ReadValueSafe(out int size); + value = new T[size]; + for (var i = 0; i < size; ++i) + { + ReadNetworkSerializable(out value[i]); + } + } + + /// + /// Reads a string + /// NOTE: ALLOCATES + /// + /// Stores the read string + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void ReadValue(out string s, bool oneByteChars = false) + { + ReadValue(out uint length); + s = "".PadRight((int)length); + int target = s.Length; + fixed (char* native = s) + { + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + ReadByte(out byte b); + native[i] = (char)b; + } + } + else + { + ReadBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Reads a string. + /// NOTE: ALLOCATES + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Stores the read string + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void ReadValueSafe(out string s, bool oneByteChars = false) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(sizeof(uint))) + { + throw new OverflowException("Reading past the end of the buffer"); + } + + ReadValue(out uint length); + + if (!TryBeginReadInternal((int)length * (oneByteChars ? 1 : sizeof(char)))) + { + throw new OverflowException("Reading past the end of the buffer"); + } + s = "".PadRight((int)length); + int target = s.Length; + fixed (char* native = s) + { + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + ReadByte(out byte b); + native[i] = (char)b; + } + } + else + { + ReadBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Writes an unmanaged array + /// NOTE: ALLOCATES + /// + /// Stores the read array + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValue(out T[] array) where T : unmanaged + { + ReadValue(out int sizeInTs); + int sizeInBytes = sizeInTs * sizeof(T); + array = new T[sizeInTs]; + fixed (T* native = array) + { + byte* bytes = (byte*)(native); + ReadBytes(bytes, sizeInBytes); + } + } + + /// + /// Reads an unmanaged array + /// NOTE: ALLOCATES + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Stores the read array + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValueSafe(out T[] array) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(sizeof(int))) + { + throw new OverflowException("Reading past the end of the buffer"); + } + ReadValue(out int sizeInTs); + int sizeInBytes = sizeInTs * sizeof(T); + if (!TryBeginReadInternal(sizeInBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + array = new T[sizeInTs]; + fixed (T* native = array) + { + byte* bytes = (byte*)(native); + ReadBytes(bytes, sizeInBytes); + } + } + + /// + /// Read a partial value. The value is zero-initialized and then the specified number of bytes is read into it. + /// + /// Value to read + /// Number of bytes + /// Offset into the value to write the bytes + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadPartialValue(out T value, int bytesToRead, int offsetBytes = 0) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (Handle->Position + bytesToRead > Handle->AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + + var val = new T(); + byte* ptr = ((byte*)&val) + offsetBytes; + byte* bufferPointer = Handle->BufferPointer + Handle->Position; + UnsafeUtility.MemCpy(ptr, bufferPointer, bytesToRead); + + Handle->Position += bytesToRead; + value = val; + } + + /// + /// Read a byte to the stream. + /// + /// Stores the read value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadByte(out byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (Handle->Position + 1 > Handle->AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + value = Handle->BufferPointer[Handle->Position++]; + } + + /// + /// Read a byte to the stream. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Stores the read value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadByteSafe(out byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(1)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + value = Handle->BufferPointer[Handle->Position++]; + } + + /// + /// Read multiple bytes to the stream + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytes(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (Handle->Position + size > Handle->AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + UnsafeUtility.MemCpy(value + offset, (Handle->BufferPointer + Handle->Position), size); + Handle->Position += size; + } + + /// + /// Read multiple bytes to the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytesSafe(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(size)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + UnsafeUtility.MemCpy(value + offset, (Handle->BufferPointer + Handle->Position), size); + Handle->Position += size; + } + + /// + /// Read multiple bytes from the stream + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytes(ref byte[] value, int size, int offset = 0) + { + fixed (byte* ptr = value) + { + ReadBytes(ptr, size, offset); + } + } + + /// + /// Read multiple bytes from the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytesSafe(ref byte[] value, int size, int offset = 0) + { + fixed (byte* ptr = value) + { + ReadBytesSafe(ptr, size, offset); + } + } + + /// + /// Read a value of any unmanaged type to the buffer. + /// It will be copied from the buffer exactly as it existed in memory on the writing end. + /// + /// The read value + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValue(out T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (Handle->Position + len > Handle->AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy((byte*)ptr, Handle->BufferPointer + Handle->Position, len); + } + Handle->Position += len; + } + + /// + /// Read a value of any unmanaged type to the buffer. + /// It will be copied from the buffer exactly as it existed in memory on the writing end. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// The read value + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValueSafe(out T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(len)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy((byte*)ptr, Handle->BufferPointer + Handle->Position, len); + } + Handle->Position += len; + } + } +} diff --git a/Runtime/Serialization/FastBufferReader.cs.meta b/Runtime/Serialization/FastBufferReader.cs.meta new file mode 100644 index 0000000..3667f73 --- /dev/null +++ b/Runtime/Serialization/FastBufferReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5479786a4b1f57648a0fe56bd37a823b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/FastBufferWriter.cs b/Runtime/Serialization/FastBufferWriter.cs new file mode 100644 index 0000000..2f636ff --- /dev/null +++ b/Runtime/Serialization/FastBufferWriter.cs @@ -0,0 +1,822 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + public struct FastBufferWriter : IDisposable + { + internal struct WriterHandle + { + internal unsafe byte* BufferPointer; + internal int Position; + internal int Length; + internal int Capacity; + internal int MaxCapacity; + internal Allocator Allocator; + internal bool BufferGrew; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + internal int AllowedWriteMark; + internal bool InBitwiseContext; +#endif + } + + internal readonly unsafe WriterHandle* Handle; + + /// + /// The current write position + /// + public unsafe int Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Handle->Position; + } + + /// + /// The current total buffer size + /// + public unsafe int Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Handle->Capacity; + } + + /// + /// The maximum possible total buffer size + /// + public unsafe int MaxCapacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Handle->MaxCapacity; + } + + /// + /// The total amount of bytes that have been written to the stream + /// + public unsafe int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Handle->Position > Handle->Length ? Handle->Position : Handle->Length; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal unsafe void CommitBitwiseWrites(int amount) + { + Handle->Position += amount; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->InBitwiseContext = false; +#endif + } + + /// + /// Create a FastBufferWriter. + /// + /// Size of the buffer to create + /// Allocator to use in creating it + /// Maximum size the buffer can grow to. If less than size, buffer cannot grow. + public unsafe FastBufferWriter(int size, Allocator allocator, int maxSize = -1) + { + Handle = (WriterHandle*)UnsafeUtility.Malloc(sizeof(WriterHandle) + size, UnsafeUtility.AlignOf(), allocator); +#if DEVELOPMENT_BUILD || UNITY_EDITOR + UnsafeUtility.MemSet(Handle, 0, sizeof(WriterHandle) + size); +#endif + Handle->BufferPointer = (byte*)(Handle + 1); + Handle->Position = 0; + Handle->Length = 0; + Handle->Capacity = size; + Handle->Allocator = allocator; + Handle->MaxCapacity = maxSize < size ? size : maxSize; + Handle->BufferGrew = false; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->AllowedWriteMark = 0; + Handle->InBitwiseContext = false; +#endif + } + + /// + /// Frees the allocated buffer + /// + public unsafe void Dispose() + { + if (Handle->BufferGrew) + { + UnsafeUtility.Free(Handle->BufferPointer, Handle->Allocator); + } + UnsafeUtility.Free(Handle, Handle->Allocator); + } + + /// + /// Move the write position in the stream. + /// Note that moving forward past the current length will extend the buffer's Length value even if you don't write. + /// + /// Absolute value to move the position to, truncated to Capacity + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void Seek(int where) + { + // This avoids us having to synchronize length all the time. + // Writing things is a much more common operation than seeking + // or querying length. The length here is a high watermark of + // what's been written. So before we seek, if the current position + // is greater than the length, we update that watermark. + // When querying length later, we'll return whichever of the two + // values is greater, thus if we write past length, length increases + // because position increases, and if we seek backward, length remembers + // the position it was in. + // Seeking forward will not update the length. + where = Math.Min(where, Handle->Capacity); + if (Handle->Position > Handle->Length && where < Handle->Position) + { + Handle->Length = Handle->Position; + } + + Handle->Position = where; + } + + /// + /// Truncate the stream by setting Length to the specified value. + /// If Position is greater than the specified value, it will be moved as well. + /// + /// The value to truncate to. If -1, the current position will be used. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void Truncate(int where = -1) + { + if (where == -1) + { + where = Position; + } + + if (Handle->Position > where) + { + Handle->Position = where; + } + + if (Handle->Length > where) + { + Handle->Length = where; + } + } + + /// + /// Retrieve a BitWriter to be able to perform bitwise operations on the buffer. + /// No bytewise operations can be performed on the buffer until bitWriter.Dispose() has been called. + /// At the end of the operation, FastBufferWriter will remain byte-aligned. + /// + /// A BitWriter + public unsafe BitWriter EnterBitwiseContext() + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->InBitwiseContext = true; +#endif + return new BitWriter(this); + } + + internal unsafe void Grow(int additionalSizeRequired) + { + var desiredSize = Handle->Capacity * 2; + while (desiredSize < Position + additionalSizeRequired) + { + desiredSize *= 2; + } + + var newSize = Math.Min(desiredSize, Handle->MaxCapacity); + byte* newBuffer = (byte*)UnsafeUtility.Malloc(newSize, UnsafeUtility.AlignOf(), Handle->Allocator); +#if DEVELOPMENT_BUILD || UNITY_EDITOR + UnsafeUtility.MemSet(newBuffer, 0, sizeof(WriterHandle) + newSize); +#endif + UnsafeUtility.MemCpy(newBuffer, Handle->BufferPointer, Length); + if (Handle->BufferGrew) + { + UnsafeUtility.Free(Handle->BufferPointer, Handle->Allocator); + } + + Handle->BufferGrew = true; + Handle->BufferPointer = newBuffer; + Handle->Capacity = newSize; + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call 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 + /// operations in release builds. + /// + /// Amount of bytes to write + /// True if the write is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryBeginWrite(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + if (Handle->Position + bytes > Handle->Capacity) + { + if (Handle->Position + bytes > Handle->MaxCapacity) + { + return false; + } + + if (Handle->Capacity < Handle->MaxCapacity) + { + Grow(bytes); + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->AllowedWriteMark = Handle->Position + bytes; +#endif + return true; + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call 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 + /// operations in release builds. Instead, attempting to write past the marked position in release builds + /// will write to random memory and cause undefined behavior, likely including instability and crashes. + /// + /// The value you want to write + /// True if the write is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryBeginWriteValue(in T value) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + int len = sizeof(T); + if (Handle->Position + len > Handle->Capacity) + { + if (Handle->Position + len > Handle->MaxCapacity) + { + return false; + } + + if (Handle->Capacity < Handle->MaxCapacity) + { + Grow(len); + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + Handle->AllowedWriteMark = Handle->Position + len; +#endif + return true; + } + + /// + /// Internal version of TryBeginWrite. + /// Differs from TryBeginWrite only in that it won't ever move the AllowedWriteMark backward. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryBeginWriteInternal(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + if (Handle->Position + bytes > Handle->Capacity) + { + if (Handle->Position + bytes > Handle->MaxCapacity) + { + return false; + } + + if (Handle->Capacity < Handle->MaxCapacity) + { + Grow(bytes); + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->Position + bytes > Handle->AllowedWriteMark) + { + Handle->AllowedWriteMark = Handle->Position + bytes; + } +#endif + return true; + } + + /// + /// Returns an array representation of the underlying byte buffer. + /// !!Allocates a new array!! + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte[] ToArray() + { + byte[] ret = new byte[Length]; + fixed (byte* b = ret) + { + UnsafeUtility.MemCpy(b, Handle->BufferPointer, Length); + } + + return ret; + } + + /// + /// Gets a direct pointer to the underlying buffer + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtr() + { + return Handle->BufferPointer; + } + + /// + /// Gets a direct pointer to the underlying buffer at the current read position + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtrAtCurrentPosition() + { + return Handle->BufferPointer + Handle->Position; + } + + /// + /// Get the required size to write a string + /// + /// The string to write + /// Whether or not to use one byte per character. This will only allow ASCII + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetWriteSize(string s, bool oneByteChars = false) + { + return sizeof(int) + s.Length * (oneByteChars ? sizeof(byte) : sizeof(char)); + } + + /// + /// Write an INetworkSerializable + /// + /// The value to write + /// + public void WriteNetworkSerializable(in T value) where T : INetworkSerializable + { + var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(this)); + value.NetworkSerialize(bufferSerializer); + } + + /// + /// Write an array of INetworkSerializables + /// + /// The value to write + /// + /// + /// + public void WriteNetworkSerializable(INetworkSerializable[] array, int count = -1, int offset = 0) where T : INetworkSerializable + { + int sizeInTs = count != -1 ? count : array.Length - offset; + WriteValueSafe(sizeInTs); + foreach (var item in array) + { + WriteNetworkSerializable(item); + } + } + + /// + /// Writes a string + /// + /// The string to write + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void WriteValue(string s, bool oneByteChars = false) + { + WriteValue((uint)s.Length); + int target = s.Length; + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + WriteByte((byte)s[i]); + } + } + else + { + fixed (char* native = s) + { + WriteBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Writes a string + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The string to write + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void WriteValueSafe(string s, bool oneByteChars = false) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + int sizeInBytes = GetWriteSize(s, oneByteChars); + + if (!TryBeginWriteInternal(sizeInBytes)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + + WriteValue((uint)s.Length); + int target = s.Length; + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + WriteByte((byte)s[i]); + } + } + else + { + fixed (char* native = s) + { + WriteBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Get the required size to write an unmanaged array + /// + /// The array to write + /// The amount of elements to write + /// Where in the array to start + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int GetWriteSize(T[] array, int count = -1, int offset = 0) where T : unmanaged + { + int sizeInTs = count != -1 ? count : array.Length - offset; + int sizeInBytes = sizeInTs * sizeof(T); + return sizeof(int) + sizeInBytes; + } + + /// + /// Writes an unmanaged array + /// + /// The array to write + /// The amount of elements to write + /// Where in the array to start + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValue(T[] array, int count = -1, int offset = 0) where T : unmanaged + { + int sizeInTs = count != -1 ? count : array.Length - offset; + int sizeInBytes = sizeInTs * sizeof(T); + WriteValue(sizeInTs); + fixed (T* native = array) + { + byte* bytes = (byte*)(native + offset); + WriteBytes(bytes, sizeInBytes); + } + } + + /// + /// Writes an unmanaged array + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The array to write + /// The amount of elements to write + /// Where in the array to start + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValueSafe(T[] array, int count = -1, int offset = 0) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + int sizeInTs = count != -1 ? count : array.Length - offset; + int sizeInBytes = sizeInTs * sizeof(T); + + if (!TryBeginWriteInternal(sizeInBytes + sizeof(int))) + { + throw new OverflowException("Writing past the end of the buffer"); + } + WriteValue(sizeInTs); + fixed (T* native = array) + { + byte* bytes = (byte*)(native + offset); + WriteBytes(bytes, sizeInBytes); + } + } + + /// + /// Write a partial value. The specified number of bytes is written from the value and the rest is ignored. + /// + /// Value to write + /// Number of bytes + /// Offset into the value to begin reading the bytes + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WritePartialValue(T value, int bytesToWrite, int offsetBytes = 0) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (Handle->Position + bytesToWrite > Handle->AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + + byte* ptr = ((byte*)&value) + offsetBytes; + byte* bufferPointer = Handle->BufferPointer + Handle->Position; + UnsafeUtility.MemCpy(bufferPointer, ptr, bytesToWrite); + + Handle->Position += bytesToWrite; + } + + /// + /// Write a byte to the stream. + /// + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteByte(byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (Handle->Position + 1 > Handle->AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + Handle->BufferPointer[Handle->Position++] = value; + } + + /// + /// Write a byte to the stream. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteByteSafe(byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginWriteInternal(1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + Handle->BufferPointer[Handle->Position++] = value; + } + + /// + /// Write multiple bytes to the stream + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytes(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (Handle->Position + size > Handle->AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + UnsafeUtility.MemCpy((Handle->BufferPointer + Handle->Position), value + offset, size); + Handle->Position += size; + } + + /// + /// Write multiple bytes to the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytesSafe(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginWriteInternal(size)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + UnsafeUtility.MemCpy((Handle->BufferPointer + Handle->Position), value + offset, size); + Handle->Position += size; + } + + /// + /// Write multiple bytes to the stream + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytes(byte[] value, int size = -1, int offset = 0) + { + fixed (byte* ptr = value) + { + WriteBytes(ptr, size == -1 ? value.Length : size, offset); + } + } + + /// + /// Write multiple bytes to the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytesSafe(byte[] value, int size = -1, int offset = 0) + { + fixed (byte* ptr = value) + { + WriteBytesSafe(ptr, size == -1 ? value.Length : size, offset); + } + } + + /// + /// Copy the contents of this writer into another writer. + /// The contents will be copied from the beginning of this writer to its current position. + /// They will be copied to the other writer starting at the other writer's current position. + /// + /// Writer to copy to + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void CopyTo(FastBufferWriter other) + { + other.WriteBytes(Handle->BufferPointer, Handle->Position); + } + + /// + /// Copy the contents of another writer into this writer. + /// The contents will be copied from the beginning of the other writer to its current position. + /// They will be copied to this writer starting at this writer's current position. + /// + /// Writer to copy to + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void CopyFrom(FastBufferWriter other) + { + WriteBytes(other.Handle->BufferPointer, other.Handle->Position); + } + + /// + /// Get the size required to write an unmanaged value + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int GetWriteSize(in T value) where T : unmanaged + { + return sizeof(T); + } + + /// + /// Get the size required to write an unmanaged value of type T + /// + /// + /// + public static unsafe int GetWriteSize() where T : unmanaged + { + return sizeof(T); + } + + /// + /// Write a value of any unmanaged type (including unmanaged structs) to the buffer. + /// It will be copied into the buffer exactly as it exists in memory. + /// + /// The value to copy + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValue(in T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (Handle->Position + len > Handle->AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy(Handle->BufferPointer + Handle->Position, (byte*)ptr, len); + } + Handle->Position += len; + } + + /// + /// Write a value of any unmanaged type (including unmanaged structs) to the buffer. + /// It will be copied into the buffer exactly as it exists in memory. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The value to copy + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValueSafe(in T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (Handle->InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginWriteInternal(len)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy(Handle->BufferPointer + Handle->Position, (byte*)ptr, len); + } + Handle->Position += len; + } + } +} diff --git a/Runtime/Serialization/FastBufferWriter.cs.meta b/Runtime/Serialization/FastBufferWriter.cs.meta new file mode 100644 index 0000000..0c31b46 --- /dev/null +++ b/Runtime/Serialization/FastBufferWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 819a511316a46104db673c8a0eab9e72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/INetworkSerializable.cs b/Runtime/Serialization/INetworkSerializable.cs new file mode 100644 index 0000000..da68a37 --- /dev/null +++ b/Runtime/Serialization/INetworkSerializable.cs @@ -0,0 +1,18 @@ +namespace Unity.Netcode +{ + /// + /// Interface for implementing custom serializable types. + /// + public interface INetworkSerializable + { + /// + /// Provides bi-directional serialization to read and write the desired data to serialize this type. + /// + /// The serializer to use to read and write the data. + /// + /// Either BufferSerializerReader or BufferSerializerWriter, depending whether the serializer + /// is in read mode or write mode. + /// + void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter; + } +} diff --git a/Runtime/Serialization/INetworkSerializable.cs.meta b/Runtime/Serialization/INetworkSerializable.cs.meta new file mode 100644 index 0000000..13aa4d1 --- /dev/null +++ b/Runtime/Serialization/INetworkSerializable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dcbf989721df344779c6c845cf79444f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/IReaderWriter.cs b/Runtime/Serialization/IReaderWriter.cs new file mode 100644 index 0000000..a50e536 --- /dev/null +++ b/Runtime/Serialization/IReaderWriter.cs @@ -0,0 +1,25 @@ +namespace Unity.Netcode +{ + public interface IReaderWriter + { + bool IsReader { get; } + bool IsWriter { get; } + + FastBufferReader GetFastBufferReader(); + FastBufferWriter GetFastBufferWriter(); + + void SerializeValue(ref string s, bool oneByteChars = false); + void SerializeValue(ref T[] array) where T : unmanaged; + void SerializeValue(ref byte value); + void SerializeValue(ref T value) where T : unmanaged; + + // Has to have a different name to avoid conflicting with "where T: unmananged" + void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new(); + + bool PreCheck(int amount); + void SerializeValuePreChecked(ref string s, bool oneByteChars = false); + void SerializeValuePreChecked(ref T[] array) where T : unmanaged; + void SerializeValuePreChecked(ref byte value); + void SerializeValuePreChecked(ref T value) where T : unmanaged; + } +} diff --git a/Runtime/Serialization/IReaderWriter.cs.meta b/Runtime/Serialization/IReaderWriter.cs.meta new file mode 100644 index 0000000..61e9aae --- /dev/null +++ b/Runtime/Serialization/IReaderWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: daea292c2c9ec794fb78018d4530c1d6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/MemoryStructures.meta b/Runtime/Serialization/MemoryStructures.meta new file mode 100644 index 0000000..a08e170 --- /dev/null +++ b/Runtime/Serialization/MemoryStructures.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0a4a44dfad228154db7945948c115efa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/MemoryStructures/ByteBool.cs b/Runtime/Serialization/MemoryStructures/ByteBool.cs new file mode 100644 index 0000000..54e46b9 --- /dev/null +++ b/Runtime/Serialization/MemoryStructures/ByteBool.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; + +namespace Unity.Netcode +{ + [StructLayout(LayoutKind.Explicit)] + internal struct ByteBool + { + [FieldOffset(0)] + public bool BoolValue; + + [FieldOffset(0)] + public byte ByteValue; + + public byte Collapse() => + ByteValue = (byte)(( + // Collapse all bits to position 1 and reassign as bit + (ByteValue >> 7) | + (ByteValue >> 6) | + (ByteValue >> 5) | + (ByteValue >> 4) | + (ByteValue >> 3) | + (ByteValue >> 2) | + (ByteValue >> 1) | + ByteValue + ) & 1); + + public byte Collapse(bool b) + { + BoolValue = b; + return Collapse(); + } + } +} diff --git a/Runtime/Serialization/MemoryStructures/ByteBool.cs.meta b/Runtime/Serialization/MemoryStructures/ByteBool.cs.meta new file mode 100644 index 0000000..5a840d3 --- /dev/null +++ b/Runtime/Serialization/MemoryStructures/ByteBool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a92848ab7f6eed144b5247f7b06bbbb0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/MemoryStructures/UIntFloat.cs b/Runtime/Serialization/MemoryStructures/UIntFloat.cs new file mode 100644 index 0000000..5878e30 --- /dev/null +++ b/Runtime/Serialization/MemoryStructures/UIntFloat.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; + +namespace Unity.Netcode +{ + /// + /// A struct with a explicit memory layout. The struct has 4 fields. float,uint,double and ulong. + /// Every field has the same starting point in memory. If you insert a float value, it can be extracted as a uint. + /// This is to allow for lockless and garbage free conversion from float to uint and double to ulong. + /// This allows for VarInt encoding and other integer encodings. + /// + [StructLayout(LayoutKind.Explicit)] + internal struct UIntFloat + { + [FieldOffset(0)] + public float FloatValue; + + [FieldOffset(0)] + public uint UIntValue; + + [FieldOffset(0)] + public double DoubleValue; + + [FieldOffset(0)] + public ulong ULongValue; + } +} diff --git a/Runtime/Serialization/MemoryStructures/UIntFloat.cs.meta b/Runtime/Serialization/MemoryStructures/UIntFloat.cs.meta new file mode 100644 index 0000000..6f7f2e4 --- /dev/null +++ b/Runtime/Serialization/MemoryStructures/UIntFloat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b04241ccff0cf024d81c9d62d24fe077 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/NetworkBehaviourReference.cs b/Runtime/Serialization/NetworkBehaviourReference.cs new file mode 100644 index 0000000..5bb904a --- /dev/null +++ b/Runtime/Serialization/NetworkBehaviourReference.cs @@ -0,0 +1,103 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Unity.Netcode +{ + /// + /// A helper struct for serializing s over the network. Can be used in RPCs and . + /// Note: network ids get recycled by the NetworkManager after a while. So a reference pointing to + /// + public struct NetworkBehaviourReference : INetworkSerializable, IEquatable + { + private NetworkObjectReference m_NetworkObjectReference; + private ushort m_NetworkBehaviourId; + + /// + /// Creates a new instance of the struct. + /// + /// The to reference. + /// + public NetworkBehaviourReference(NetworkBehaviour networkBehaviour) + { + if (networkBehaviour == null) + { + throw new ArgumentNullException(nameof(networkBehaviour)); + } + if (networkBehaviour.NetworkObject == null) + { + throw new ArgumentException($"Cannot create {nameof(NetworkBehaviourReference)} from {nameof(NetworkBehaviour)} without a {nameof(NetworkObject)}."); + } + + m_NetworkObjectReference = networkBehaviour.NetworkObject; + m_NetworkBehaviourId = networkBehaviour.NetworkBehaviourId; + } + + /// + /// Tries to get the referenced by this reference. + /// + /// The which was found. Null if the corresponding was not found. + /// The networkmanager. Uses to resolve if null. + /// True if the was found; False if the was not found. This can happen if the corresponding has not been spawned yet. you can try getting the reference at a later point in time. + public bool TryGet(out NetworkBehaviour networkBehaviour, NetworkManager networkManager = null) + { + networkBehaviour = GetInternal(this, null); + return networkBehaviour != null; + } + + /// + /// Tries to get the referenced by this reference. + /// + /// The which was found. Null if the corresponding was not found. + /// The networkmanager. Uses to resolve if null. + /// The type of the networkBehaviour for convenience. + /// True if the was found; False if the was not found. This can happen if the corresponding has not been spawned yet. you can try getting the reference at a later point in time. + public bool TryGet(out T networkBehaviour, NetworkManager networkManager = null) where T : NetworkBehaviour + { + networkBehaviour = (T)GetInternal(this, null); + return networkBehaviour != null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static NetworkBehaviour GetInternal(NetworkBehaviourReference networkBehaviourRef, NetworkManager networkManager = null) + { + if (networkBehaviourRef.m_NetworkObjectReference.TryGet(out NetworkObject networkObject, networkManager)) + { + return networkObject.GetNetworkBehaviourAtOrderIndex(networkBehaviourRef.m_NetworkBehaviourId); + } + + return null; + } + + /// + public bool Equals(NetworkBehaviourReference other) + { + return m_NetworkObjectReference.Equals(other.m_NetworkObjectReference) && m_NetworkBehaviourId == other.m_NetworkBehaviourId; + } + + /// + public override bool Equals(object obj) + { + return obj is NetworkBehaviourReference other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (m_NetworkObjectReference.GetHashCode() * 397) ^ m_NetworkBehaviourId.GetHashCode(); + } + } + + /// + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + m_NetworkObjectReference.NetworkSerialize(serializer); + serializer.SerializeValue(ref m_NetworkBehaviourId); + } + + public static implicit operator NetworkBehaviour(NetworkBehaviourReference networkBehaviourRef) => GetInternal(networkBehaviourRef); + + public static implicit operator NetworkBehaviourReference(NetworkBehaviour networkBehaviour) => new NetworkBehaviourReference(networkBehaviour); + } +} diff --git a/Runtime/Serialization/NetworkBehaviourReference.cs.meta b/Runtime/Serialization/NetworkBehaviourReference.cs.meta new file mode 100644 index 0000000..a65b9ef --- /dev/null +++ b/Runtime/Serialization/NetworkBehaviourReference.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a9cea52f48ea70499020aebe4073ba8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Serialization/NetworkObjectReference.cs b/Runtime/Serialization/NetworkObjectReference.cs new file mode 100644 index 0000000..547e43f --- /dev/null +++ b/Runtime/Serialization/NetworkObjectReference.cs @@ -0,0 +1,131 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// A helper struct for serializing s over the network. Can be used in RPCs and . + /// + public struct NetworkObjectReference : INetworkSerializable, IEquatable + { + private ulong m_NetworkObjectId; + + /// + /// The of the referenced . + /// + public ulong NetworkObjectId + { + get => m_NetworkObjectId; + internal set => m_NetworkObjectId = value; + } + + /// + /// Creates a new instance of the struct. + /// + /// The to reference. + /// + /// + public NetworkObjectReference(NetworkObject networkObject) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject)); + } + + if (networkObject.IsSpawned == false) + { + throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s."); + } + + m_NetworkObjectId = networkObject.NetworkObjectId; + } + + /// + /// Creates a new instance of the struct. + /// + /// The GameObject from which the component will be referenced. + /// + /// + public NetworkObjectReference(GameObject gameObject) + { + if (gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + var networkObject = gameObject.GetComponent(); + + if (networkObject == null) + { + throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component."); + } + + if (networkObject.IsSpawned == false) + { + throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s."); + } + + m_NetworkObjectId = networkObject.NetworkObjectId; + } + + /// + /// Tries to get the referenced by this reference. + /// + /// The which was found. Null if no object was found. + /// The networkmanager. Uses to resolve if null. + /// True if the was found; False if the was not found. This can happen if the has not been spawned yet. you can try getting the reference at a later point in time. + public bool TryGet(out NetworkObject networkObject, NetworkManager networkManager = null) + { + networkObject = Resolve(this, networkManager); + return networkObject != null; + } + + /// + /// Resolves the corresponding for this reference. + /// + /// The reference. + /// The networkmanager. Uses to resolve if null. + /// The resolves . Returns null if the networkobject was not found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static NetworkObject Resolve(NetworkObjectReference networkObjectRef, NetworkManager networkManager = null) + { + networkManager = networkManager != null ? networkManager : NetworkManager.Singleton; + networkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectRef.m_NetworkObjectId, out NetworkObject networkObject); + + return networkObject; + } + + /// + public bool Equals(NetworkObjectReference other) + { + return m_NetworkObjectId == other.m_NetworkObjectId; + } + + /// + public override bool Equals(object obj) + { + return obj is NetworkObjectReference other && Equals(other); + } + + /// + public override int GetHashCode() + { + return m_NetworkObjectId.GetHashCode(); + } + + /// + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref m_NetworkObjectId); + } + + public static implicit operator NetworkObject(NetworkObjectReference networkObjectRef) => Resolve(networkObjectRef); + + public static implicit operator NetworkObjectReference(NetworkObject networkObject) => new NetworkObjectReference(networkObject); + + public static implicit operator GameObject(NetworkObjectReference networkObjectRef) => Resolve(networkObjectRef).gameObject; + + public static implicit operator NetworkObjectReference(GameObject gameObject) => new NetworkObjectReference(gameObject); + } +} diff --git a/Runtime/Serialization/NetworkObjectReference.cs.meta b/Runtime/Serialization/NetworkObjectReference.cs.meta new file mode 100644 index 0000000..fecff26 --- /dev/null +++ b/Runtime/Serialization/NetworkObjectReference.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 463f3b530aad5d849964ee157646818e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Spawning.meta b/Runtime/Spawning.meta new file mode 100644 index 0000000..993b53b --- /dev/null +++ b/Runtime/Spawning.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 39220c123d44f6847a391c87be47d3a7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Spawning/NetworkPrefabHandler.cs b/Runtime/Spawning/NetworkPrefabHandler.cs new file mode 100644 index 0000000..ec8ca8d --- /dev/null +++ b/Runtime/Spawning/NetworkPrefabHandler.cs @@ -0,0 +1,301 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Interface for customizing, overriding, spawning, and destroying Network Prefabs + /// Used by + /// + public interface INetworkPrefabInstanceHandler + { + /// + /// Client Side Only + /// Once an implementation is registered with the , this method will be called every time + /// a Network Prefab associated is spawned on clients + /// + /// Note On Hosts: Use the + /// method to register all targeted NetworkPrefab overrides manually since the host will be acting as both a server and client. + /// + /// Note on Pooling: If you are using a NetworkObject pool, don't forget to make the NetworkObject active + /// via the method. + /// + /// the owner for the to be instantiated + /// the initial/default position for the to be instantiated + /// the initial/default rotation for the to be instantiated + /// + NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); + + /// + /// Invoked on Client and Server + /// Once an implementation is registered with the , this method will be called when + /// a Network Prefab associated is: + /// + /// Server Side: destroyed or despawned with the destroy parameter equal to true + /// If is invoked with the default destroy parameter (i.e. false) then this method will NOT be invoked! + /// + /// Client Side: destroyed when the client receives a destroy object message from the server or host. + /// + /// Note on Pooling: When this method is invoked, you do not need to destroy the NetworkObject as long as you want your pool to persist. + /// The most common approach is to make the inactive by calling . + /// + /// The being destroyed + void Destroy(NetworkObject networkObject); + } + + /// + /// Primary handler to add or remove customized spawn and destroy handlers for a network prefab (i.e. a prefab with a NetworkObject component) + /// Register custom prefab handlers by implementing the interface. + /// + public class NetworkPrefabHandler + { + /// + /// Links a network prefab asset to a class with the INetworkPrefabInstanceHandler interface + /// + private readonly Dictionary m_PrefabAssetToPrefabHandler = new Dictionary(); + + /// + /// Links the custom prefab instance's GlobalNetworkObjectId to the original prefab asset's GlobalNetworkObjectId. (Needed for HandleNetworkPrefabDestroy) + /// [PrefabInstance][PrefabAsset] + /// + private readonly Dictionary m_PrefabInstanceToPrefabAsset = new Dictionary(); + + /// + /// Use a to register a class that implements the interface with the + /// + /// the of the network prefab asset to be overridden + /// class that implements the interface to be registered + /// true (registered) false (failed to register) + public bool AddHandler(GameObject networkPrefabAsset, INetworkPrefabInstanceHandler instanceHandler) + { + return AddHandler(networkPrefabAsset.GetComponent().GlobalObjectIdHash, instanceHandler); + } + + /// + /// Use a to register a class that implements the interface with the + /// + /// the of the network prefab asset to be overridden + /// the class that implements the interface to be registered + /// + public bool AddHandler(NetworkObject prefabAssetNetworkObject, INetworkPrefabInstanceHandler instanceHandler) + { + return AddHandler(prefabAssetNetworkObject.GlobalObjectIdHash, instanceHandler); + } + + /// + /// Use a to register a class that implements the interface with the + /// + /// the value of the network prefab asset being overridden + /// a class that implements the interface + /// + public bool AddHandler(uint globalObjectIdHash, INetworkPrefabInstanceHandler instanceHandler) + { + if (!m_PrefabAssetToPrefabHandler.ContainsKey(globalObjectIdHash)) + { + m_PrefabAssetToPrefabHandler.Add(globalObjectIdHash, instanceHandler); + return true; + } + + return false; + } + + /// + /// HOST ONLY! + /// Since a host is unique and is considered both a client and a server, for each source NetworkPrefab you must manually + /// register all potential target overrides that have the component. + /// + /// source NetworkPrefab to be overridden + /// one or more NetworkPrefabs could be used to override the source NetworkPrefab + public void RegisterHostGlobalObjectIdHashValues(GameObject sourceNetworkPrefab, List networkPrefabOverrides) + { + if (NetworkManager.Singleton.IsListening) + { + if (NetworkManager.Singleton.IsHost) + { + var sourceNetworkObject = sourceNetworkPrefab.GetComponent(); + if (sourceNetworkPrefab != null) + { + var sourceGlobalObjectIdHash = sourceNetworkObject.GlobalObjectIdHash; + // Now we register all + foreach (var gameObject in networkPrefabOverrides) + { + var targetNetworkObject = gameObject.GetComponent(); + if (targetNetworkObject != null) + { + if (!m_PrefabInstanceToPrefabAsset.ContainsKey(targetNetworkObject.GlobalObjectIdHash)) + { + m_PrefabInstanceToPrefabAsset.Add(targetNetworkObject.GlobalObjectIdHash, sourceGlobalObjectIdHash); + } + else + { + Debug.LogWarning($"{targetNetworkObject.name} appears to be a duplicate entry!"); + } + } + else + { + throw new System.Exception($"{targetNetworkObject.name} does not have a {nameof(NetworkObject)} component!"); + } + } + } + else + { + throw new System.Exception($"{sourceNetworkPrefab.name} does not have a {nameof(NetworkObject)} component!"); + } + } + else + { + throw new System.Exception($"You should only call {nameof(RegisterHostGlobalObjectIdHashValues)} as a Host!"); + } + } + else + { + throw new System.Exception($"You can only call {nameof(RegisterHostGlobalObjectIdHashValues)} once NetworkManager is listening!"); + } + } + + /// + /// Use the of the overridden network prefab asset to remove a registered class that implements the interface. + /// + /// of the network prefab asset that was being overridden + /// true (success) or false (failure) + public bool RemoveHandler(GameObject networkPrefabAsset) + { + return RemoveHandler(networkPrefabAsset.GetComponent().GlobalObjectIdHash); + } + + /// + /// Use the of the overridden network prefab asset to remove a registered class that implements the interface. + /// + /// of the source NetworkPrefab that was being overridden + /// true (success) or false (failure) + public bool RemoveHandler(NetworkObject networkObject) + { + return RemoveHandler(networkObject.GlobalObjectIdHash); + } + + /// + /// Use the of the overridden network prefab asset to remove a registered class that implements the interface. + /// + /// of the source NetworkPrefab that was being overridden + /// true (success) or false (failure) + public bool RemoveHandler(uint globalObjectIdHash) + { + if (m_PrefabInstanceToPrefabAsset.ContainsValue(globalObjectIdHash)) + { + uint networkPrefabHashKey = 0; + foreach (var kvp in m_PrefabInstanceToPrefabAsset) + { + if (kvp.Value == globalObjectIdHash) + { + networkPrefabHashKey = kvp.Key; + break; + } + } + m_PrefabInstanceToPrefabAsset.Remove(networkPrefabHashKey); + } + + return m_PrefabAssetToPrefabHandler.Remove(globalObjectIdHash); + } + + /// + /// Check to see if a with a is registered to an implementation + /// + /// + /// true or false + internal bool ContainsHandler(GameObject networkPrefab) + { + return ContainsHandler(networkPrefab.GetComponent().GlobalObjectIdHash); + } + + /// + /// Check to see if a is registered to an implementation + /// + /// + /// true or false + internal bool ContainsHandler(NetworkObject networkObject) + { + return ContainsHandler(networkObject.GlobalObjectIdHash); + } + + /// + /// Check to see if a is registered to an implementation + /// + /// + /// true or false + internal bool ContainsHandler(uint networkPrefabHash) + { + return m_PrefabAssetToPrefabHandler.ContainsKey(networkPrefabHash) || m_PrefabInstanceToPrefabAsset.ContainsKey(networkPrefabHash); + } + + /// + /// Returns the source NetworkPrefab's + /// + /// + /// + internal uint GetSourceGlobalObjectIdHash(uint networkPrefabHash) + { + if (m_PrefabAssetToPrefabHandler.ContainsKey(networkPrefabHash)) + { + return networkPrefabHash; + } + else if (m_PrefabInstanceToPrefabAsset.ContainsKey(networkPrefabHash)) + { + return m_PrefabInstanceToPrefabAsset[networkPrefabHash]; + } + + return 0; + } + + /// + /// Will return back a generated via an implementation + /// Note: Invoked only on the client side and called within NetworkSpawnManager.CreateLocalNetworkObject + /// + /// typically the "server-side" asset's prefab hash + /// + /// + /// + /// + internal NetworkObject HandleNetworkPrefabSpawn(uint networkPrefabAssetHash, ulong ownerClientId, Vector3 position, Quaternion rotation) + { + if (m_PrefabAssetToPrefabHandler.ContainsKey(networkPrefabAssetHash)) + { + var networkObjectInstance = m_PrefabAssetToPrefabHandler[networkPrefabAssetHash].Instantiate(ownerClientId, position, rotation); + + //Now we must make sure this alternate PrefabAsset spawned in place of the prefab asset with the networkPrefabAssetHash (GlobalObjectIdHash) + //is registered and linked to the networkPrefabAssetHash so during the HandleNetworkPrefabDestroy process we can identify the alternate prefab asset. + if (networkObjectInstance != null && !m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstance.GlobalObjectIdHash)) + { + m_PrefabInstanceToPrefabAsset.Add(networkObjectInstance.GlobalObjectIdHash, networkPrefabAssetHash); + } + + return networkObjectInstance; + } + + return null; + } + + /// + /// Will invoke the implementation's Destroy method + /// + /// + internal void HandleNetworkPrefabDestroy(NetworkObject networkObjectInstance) + { + var networkObjectInstanceHash = networkObjectInstance.GlobalObjectIdHash; + + // Do we have custom overrides registered? + if (m_PrefabInstanceToPrefabAsset.ContainsKey(networkObjectInstanceHash)) + { + var networkPrefabAssetHash = m_PrefabInstanceToPrefabAsset[networkObjectInstanceHash]; + if (m_PrefabAssetToPrefabHandler.ContainsKey(networkPrefabAssetHash)) + { + m_PrefabAssetToPrefabHandler[networkPrefabAssetHash].Destroy(networkObjectInstance); + } + } + else // Otherwise the NetworkObject is the source NetworkPrefab + if (m_PrefabAssetToPrefabHandler.ContainsKey(networkObjectInstanceHash)) + { + m_PrefabAssetToPrefabHandler[networkObjectInstanceHash].Destroy(networkObjectInstance); + } + } + } +} diff --git a/Runtime/Spawning/NetworkPrefabHandler.cs.meta b/Runtime/Spawning/NetworkPrefabHandler.cs.meta new file mode 100644 index 0000000..65bd0f2 --- /dev/null +++ b/Runtime/Spawning/NetworkPrefabHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 03b691ec0c2fc1a44bc560431e6c2745 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs new file mode 100644 index 0000000..ad10715 --- /dev/null +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -0,0 +1,798 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Collections; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Class that handles object spawning + /// + public class NetworkSpawnManager + { + /// + /// The currently spawned objects + /// + public readonly Dictionary SpawnedObjects = new Dictionary(); + + /// + /// A list of the spawned objects + /// + public readonly HashSet SpawnedObjectsList = new HashSet(); + + private struct TriggerData + { + public FastBufferReader Reader; + public MessageHeader Header; + public ulong SenderId; + public float Timestamp; + } + private struct TriggerInfo + { + public float Expiry; + public NativeList TriggerData; + } + + private readonly Dictionary m_Triggers = new Dictionary(); + + /// + /// Gets the NetworkManager associated with this SpawnManager. + /// + public NetworkManager NetworkManager { get; } + + internal NetworkSpawnManager(NetworkManager networkManager) + { + NetworkManager = networkManager; + } + + internal readonly Queue ReleasedNetworkObjectIds = new Queue(); + private ulong m_NetworkObjectIdCounter; + + // A list of target ClientId, use when sending despawn commands. Kept as a member to reduce memory allocations + private List m_TargetClientIds = new List(); + + internal ulong GetNetworkObjectId() + { + if (ReleasedNetworkObjectIds.Count > 0 && NetworkManager.NetworkConfig.RecycleNetworkIds && (Time.unscaledTime - ReleasedNetworkObjectIds.Peek().ReleaseTime) >= NetworkManager.NetworkConfig.NetworkIdRecycleDelay) + { + return ReleasedNetworkObjectIds.Dequeue().NetworkId; + } + + m_NetworkObjectIdCounter++; + + return m_NetworkObjectIdCounter; + } + + /// + /// Returns the local player object or null if one does not exist + /// + /// The local player object or null if one does not exist + public NetworkObject GetLocalPlayerObject() + { + return GetPlayerNetworkObject(NetworkManager.LocalClientId); + } + + /// + /// Returns the player object with a given clientId or null if one does not exist. This is only valid server side. + /// + /// The player object with a given clientId or null if one does not exist + public NetworkObject GetPlayerNetworkObject(ulong clientId) + { + if (!NetworkManager.IsServer && NetworkManager.LocalClientId != clientId) + { + throw new NotServerException("Only the server can find player objects from other clients."); + } + + if (TryGetNetworkClient(clientId, out NetworkClient networkClient)) + { + return networkClient.PlayerObject; + } + + return null; + } + + /// + /// Defers processing of a message until the moment a specific networkObjectId is spawned. + /// This is to handle situations where an RPC or other object-specific message arrives before the spawn does, + /// either due to it being requested in OnNetworkSpawn before the spawn call has been executed, or with + /// snapshot spawns enabled where the spawn is sent unreliably and not until the end of the frame. + /// + /// There is a one second maximum lifetime of triggers to avoid memory leaks. After one second has passed + /// without the requested object ID being spawned, the triggers for it are automatically deleted. + /// + internal unsafe void TriggerOnSpawn(ulong networkObjectId, FastBufferReader reader, in NetworkContext context) + { + if (!m_Triggers.ContainsKey(networkObjectId)) + { + m_Triggers[networkObjectId] = new TriggerInfo + { + Expiry = Time.realtimeSinceStartup + 1, + TriggerData = new NativeList(Allocator.Persistent) + }; + } + + m_Triggers[networkObjectId].TriggerData.Add(new TriggerData + { + Reader = new FastBufferReader(reader.GetUnsafePtr(), Allocator.Persistent, reader.Length), + Header = context.Header, + Timestamp = context.Timestamp, + SenderId = context.SenderId + }); + } + + /// + /// Cleans up any trigger that's existed for more than a second. + /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. + /// + internal unsafe void CleanupStaleTriggers() + { + ulong* staleKeys = stackalloc ulong[m_Triggers.Count()]; + int index = 0; + foreach (var kvp in m_Triggers) + { + if (kvp.Value.Expiry < Time.realtimeSinceStartup) + { + + staleKeys[index++] = kvp.Key; + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"Deferred messages were received for {nameof(NetworkObject)} #{kvp.Key}, but it did not spawn within 1 second."); + } + + foreach (var data in kvp.Value.TriggerData) + { + data.Reader.Dispose(); + } + + kvp.Value.TriggerData.Dispose(); + } + } + + for (var i = 0; i < index; ++i) + { + m_Triggers.Remove(staleKeys[i]); + } + } + + internal void RemoveOwnership(NetworkObject networkObject) + { + if (!NetworkManager.IsServer) + { + throw new NotServerException("Only the server can change ownership"); + } + + if (!networkObject.IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + for (int i = NetworkManager.ConnectedClients[networkObject.OwnerClientId].OwnedObjects.Count - 1; + i > -1; + i--) + { + if (NetworkManager.ConnectedClients[networkObject.OwnerClientId].OwnedObjects[i] == networkObject) + { + NetworkManager.ConnectedClients[networkObject.OwnerClientId].OwnedObjects.RemoveAt(i); + } + } + + networkObject.OwnerClientIdInternal = null; + + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId + }; + var size = NetworkManager.SendMessage(message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds); + + foreach (var client in NetworkManager.ConnectedClients) + { + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } + } + + /// + /// Helper function to get a network client for a clientId from the NetworkManager. + /// On the server this will check the list. + /// On a non-server this will check the only. + /// + /// The clientId for which to try getting the NetworkClient for. + /// The found NetworkClient. Null if no client was found. + /// True if a NetworkClient with a matching id was found else false. + private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient) + { + if (NetworkManager.IsServer) + { + return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient); + } + + if (clientId == NetworkManager.LocalClient.ClientId) + { + networkClient = NetworkManager.LocalClient; + return true; + } + + networkClient = null; + return false; + } + + internal void ChangeOwnership(NetworkObject networkObject, ulong clientId) + { + if (!NetworkManager.IsServer) + { + throw new NotServerException("Only the server can change ownership"); + } + + if (!networkObject.IsSpawned) + { + 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; + + + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId + }; + var size = NetworkManager.SendMessage(message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds); + + foreach (var client in NetworkManager.ConnectedClients) + { + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } + } + + /// + /// Should only run on the client + /// + internal NetworkObject CreateLocalNetworkObject(bool isSceneObject, uint globalObjectIdHash, ulong ownerClientId, ulong? parentNetworkId, Vector3? position, Quaternion? rotation, bool isReparented = false) + { + NetworkObject parentNetworkObject = null; + + if (parentNetworkId != null && !isReparented) + { + if (SpawnedObjects.TryGetValue(parentNetworkId.Value, out NetworkObject networkObject)) + { + parentNetworkObject = networkObject; + } + else + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning("Cannot find parent. Parent objects always have to be spawned and replicated BEFORE the child"); + } + } + } + + if (!NetworkManager.NetworkConfig.EnableSceneManagement || !isSceneObject) + { + // If the prefab hash has a registered INetworkPrefabInstanceHandler derived class + if (NetworkManager.PrefabHandler.ContainsHandler(globalObjectIdHash)) + { + // Let the handler spawn the NetworkObject + var networkObject = NetworkManager.PrefabHandler.HandleNetworkPrefabSpawn(globalObjectIdHash, ownerClientId, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity)); + + networkObject.NetworkManagerOwner = NetworkManager; + + if (parentNetworkObject != null) + { + networkObject.transform.SetParent(parentNetworkObject.transform, true); + } + + if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + { + UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + } + + return networkObject; + } + else + { + // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash + GameObject networkPrefabReference = null; + if (NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks.ContainsKey(globalObjectIdHash)) + { + switch (NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks[globalObjectIdHash].Override) + { + default: + case NetworkPrefabOverride.None: + networkPrefabReference = NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks[globalObjectIdHash].Prefab; + break; + case NetworkPrefabOverride.Hash: + case NetworkPrefabOverride.Prefab: + networkPrefabReference = NetworkManager.NetworkConfig.NetworkPrefabOverrideLinks[globalObjectIdHash].OverridingTargetPrefab; + break; + } + } + + // If not, then there is an issue (user possibly didn't register the prefab properly?) + if (networkPrefabReference == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Failed to create object locally. [{nameof(globalObjectIdHash)}={globalObjectIdHash}]. {nameof(NetworkPrefab)} could not be found. Is the prefab registered with {nameof(NetworkManager)}?"); + } + + return null; + } + + // Otherwise, instantiate an instance of the NetworkPrefab linked to the prefabHash + var networkObject = ((position == null && rotation == null) ? UnityEngine.Object.Instantiate(networkPrefabReference) : UnityEngine.Object.Instantiate(networkPrefabReference, position.GetValueOrDefault(Vector3.zero), rotation.GetValueOrDefault(Quaternion.identity))).GetComponent(); + + networkObject.NetworkManagerOwner = NetworkManager; + + if (parentNetworkObject != null) + { + networkObject.transform.SetParent(parentNetworkObject.transform, true); + } + + if (NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + { + UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + } + + return networkObject; + } + } + else + { + var networkObject = NetworkManager.SceneManager.GetSceneRelativeInSceneNetworkObject(globalObjectIdHash); + + if (networkObject == null) + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); + } + + return null; + } + + if (parentNetworkObject != null) + { + networkObject.transform.SetParent(parentNetworkObject.transform, true); + } + + return networkObject; + } + } + + // Ran on both server and client + internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong? ownerClientId, bool destroyWithScene) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + } + + if (networkObject.IsSpawned) + { + throw new SpawnStateException("Object is already spawned"); + } + + SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); + } + + // Ran on both server and client + internal void SpawnNetworkObjectLocally(NetworkObject networkObject, in NetworkObject.SceneObject sceneObject, + FastBufferReader variableData, bool destroyWithScene) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + } + + if (networkObject.IsSpawned) + { + throw new SpawnStateException("Object is already spawned"); + } + + if (sceneObject.Header.HasNetworkVariables) + { + 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) + { + if (SpawnedObjects.ContainsKey(networkId)) + { + Debug.LogWarning($"Trying to spawn {nameof(NetworkObject.NetworkObjectId)} {networkId} that already exists!"); + return; + } + + // 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 + // 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 + networkObject.IsSceneObject = sceneObject; + networkObject.NetworkObjectId = networkId; + + networkObject.DestroyWithScene = sceneObject || destroyWithScene; + + networkObject.OwnerClientIdInternal = ownerClientId; + networkObject.IsPlayerObject = playerObject; + + SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); + SpawnedObjectsList.Add(networkObject); + + if (ownerClientId != null) + { + if (NetworkManager.IsServer) + { + if (playerObject) + { + NetworkManager.ConnectedClients[ownerClientId.Value].PlayerObject = networkObject; + } + else + { + NetworkManager.ConnectedClients[ownerClientId.Value].OwnedObjects.Add(networkObject); + } + } + else if (playerObject && ownerClientId.Value == NetworkManager.LocalClientId) + { + NetworkManager.LocalClient.PlayerObject = networkObject; + } + } + + if (NetworkManager.IsServer) + { + for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++) + { + if (networkObject.CheckObjectVisibility == null || networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsList[i].ClientId)) + { + networkObject.Observers.Add(NetworkManager.ConnectedClientsList[i].ClientId); + } + } + } + + networkObject.SetCachedParent(networkObject.transform.parent); + networkObject.ApplyNetworkParenting(); + NetworkObject.CheckOrphanChildren(); + + networkObject.InvokeBehaviourNetworkSpawn(); + + // This must happen after InvokeBehaviourNetworkSpawn, otherwise ClientRPCs and other messages can be + // processed before the object is fully spawned. This must be the last thing done in the spawn process. + if (m_Triggers.ContainsKey(networkId)) + { + var triggerInfo = m_Triggers[networkId]; + foreach (var trigger in triggerInfo.TriggerData) + { + // Reader will be disposed within HandleMessage + NetworkManager.MessagingSystem.HandleMessage(trigger.Header, trigger.Reader, trigger.SenderId, trigger.Timestamp); + } + + triggerInfo.TriggerData.Dispose(); + m_Triggers.Remove(networkId); + } + } + + 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) + { + return; + } + + var message = new CreateObjectMessage + { + ObjectInfo = networkObject.GetMessageSceneObject(clientId) + }; + var size = NetworkManager.SendMessage(message, NetworkDelivery.ReliableFragmentedSequenced, clientId); + NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); + + networkObject.MarkVariablesDirty(); + } + } + + internal ulong? GetSpawnParentId(NetworkObject networkObject) + { + NetworkObject parentNetworkObject = null; + + if (!networkObject.AlwaysReplicateAsRoot && networkObject.transform.parent != null) + { + parentNetworkObject = networkObject.transform.parent.GetComponent(); + } + + if (parentNetworkObject == null) + { + return null; + } + + return parentNetworkObject.NetworkObjectId; + } + + internal void DespawnObject(NetworkObject networkObject, bool destroyObject = false) + { + if (!networkObject.IsSpawned) + { + throw new SpawnStateException("Object is not spawned"); + } + + if (!NetworkManager.IsServer) + { + throw new NotServerException("Only server can despawn objects"); + } + + OnDespawnObject(networkObject, destroyObject); + } + + // Makes scene objects ready to be reused + internal void ServerResetShudownStateForSceneObjects() + { + foreach (var sobj in SpawnedObjectsList) + { + if ((sobj.IsSceneObject != null && sobj.IsSceneObject == true) || sobj.DestroyWithScene) + { + sobj.IsSpawned = false; + sobj.DestroyWithScene = false; + sobj.IsSceneObject = null; + } + } + } + + /// + /// Gets called only by NetworkSceneManager.SwitchScene + /// + internal void ServerDestroySpawnedSceneObjects() + { + // This Allocation is "OK" for now because this code only executes when a new scene is switched to + // We need to create a new copy the HashSet of NetworkObjects (SpawnedObjectsList) so we can remove + // objects from the HashSet (SpawnedObjectsList) without causing a list has been modified exception to occur. + var spawnedObjects = SpawnedObjectsList.ToList(); + + foreach (var sobj in spawnedObjects) + { + if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) + { + SpawnedObjectsList.Remove(sobj); + UnityEngine.Object.Destroy(sobj.gameObject); + } + } + } + + internal void DestroyNonSceneObjects() + { + var networkObjects = UnityEngine.Object.FindObjectsOfType(); + + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + if (networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value == false) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) + { + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); + OnDespawnObject(networkObjects[i], false); + } + else + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); + } + } + } + } + } + + internal void DestroySceneObjects() + { + var networkObjects = UnityEngine.Object.FindObjectsOfType(); + + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + if (networkObjects[i].IsSceneObject == null || networkObjects[i].IsSceneObject.Value == true) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) + { + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); + if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId)) + { + OnDespawnObject(networkObjects[i], false); + } + } + else + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); + } + } + } + } + } + + internal void ServerSpawnSceneObjectsOnStartSweep() + { + var networkObjects = UnityEngine.Object.FindObjectsOfType(); + + for (int i = 0; i < networkObjects.Length; i++) + { + if (networkObjects[i].NetworkManager == NetworkManager) + { + if (networkObjects[i].IsSceneObject == null) + { + SpawnNetworkObjectLocally(networkObjects[i], GetNetworkObjectId(), true, false, null, true); + } + } + } + } + + internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObject) + { + if (NetworkManager == null) + { + return; + } + + // We have to do this check first as subsequent checks assume we can access NetworkObjectId. + if (networkObject == null) + { + Debug.LogWarning($"Trying to destroy network object but it is null"); + return; + } + + // Removal of spawned object + if (!SpawnedObjects.ContainsKey(networkObject.NetworkObjectId)) + { + Debug.LogWarning($"Trying to destroy object {networkObject.NetworkObjectId} but it doesn't seem to exist anymore!"); + return; + } + + // Move child NetworkObjects to the root when parent NetworkObject is destroyed + foreach (var spawnedNetObj in SpawnedObjectsList) + { + var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting(); + if (isReparented && latestParent == networkObject.NetworkObjectId) + { + spawnedNetObj.gameObject.transform.parent = null; + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); + } + } + } + + 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) + { + if (NetworkManager.NetworkConfig.RecycleNetworkIds) + { + ReleasedNetworkObjectIds.Enqueue(new ReleasedNetworkId() + { + NetworkId = networkObject.NetworkObjectId, + ReleaseTime = Time.unscaledTime + }); + } + + if (NetworkManager.NetworkConfig.UseSnapshotSpawn) + { + networkObject.SnapshotDespawn(); + } + else + { + if (networkObject != null) + { + // As long as we have any remaining clients, then notify of the object being destroy. + if (NetworkManager.ConnectedClientsList.Count > 0) + { + 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) + { + if (networkObject.IsNetworkVisibleTo(clientId)) + { + m_TargetClientIds.Add(clientId); + } + } + + var message = new DestroyObjectMessage + { + NetworkObjectId = networkObject.NetworkObjectId + }; + var size = NetworkManager.SendMessage(message, NetworkDelivery.ReliableSequenced, m_TargetClientIds); + foreach (var targetClientId in m_TargetClientIds) + { + NetworkManager.NetworkMetrics.TrackObjectDestroySent(targetClientId, networkObject, size); + } + } + } + } + } + + networkObject.IsSpawned = false; + + if (SpawnedObjects.Remove(networkObject.NetworkObjectId)) + { + SpawnedObjectsList.Remove(networkObject); + } + + var gobj = networkObject.gameObject; + if (destroyGameObject && gobj != null) + { + if (NetworkManager.PrefabHandler.ContainsHandler(networkObject)) + { + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); + } + else + { + UnityEngine.Object.Destroy(gobj); + } + } + } + + /// + /// Updates all spawned for the specified client + /// Note: if the clientId is the server then it is observable to all spawned 's + /// + internal void UpdateObservedNetworkObjects(ulong clientId) + { + foreach (var sobj in SpawnedObjectsList) + { + if (sobj.CheckObjectVisibility == null || NetworkManager.IsServer) + { + if (!sobj.Observers.Contains(clientId)) + { + sobj.Observers.Add(clientId); + } + } + else + { + if (sobj.CheckObjectVisibility(clientId)) + { + sobj.Observers.Add(clientId); + } + else if (sobj.Observers.Contains(clientId)) + { + sobj.Observers.Remove(clientId); + } + } + } + } + } +} diff --git a/Runtime/Spawning/NetworkSpawnManager.cs.meta b/Runtime/Spawning/NetworkSpawnManager.cs.meta new file mode 100644 index 0000000..1d55052 --- /dev/null +++ b/Runtime/Spawning/NetworkSpawnManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ec7ce70f58ec904eaf00b4f47514e27 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Spawning/ReleasedNetworkId.cs b/Runtime/Spawning/ReleasedNetworkId.cs new file mode 100644 index 0000000..a30dab2 --- /dev/null +++ b/Runtime/Spawning/ReleasedNetworkId.cs @@ -0,0 +1,8 @@ +namespace Unity.Netcode +{ + internal struct ReleasedNetworkId + { + public ulong NetworkId; + public float ReleaseTime; + } +} diff --git a/Runtime/Spawning/ReleasedNetworkId.cs.meta b/Runtime/Spawning/ReleasedNetworkId.cs.meta new file mode 100644 index 0000000..669138a --- /dev/null +++ b/Runtime/Spawning/ReleasedNetworkId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6ef01d2364d98a409c2b7a1ec0e6a19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Timing.meta b/Runtime/Timing.meta new file mode 100644 index 0000000..d408cd9 --- /dev/null +++ b/Runtime/Timing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: af5e955b406e71a42aef6a74d7c96ff2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Timing/NetworkTickSystem.cs b/Runtime/Timing/NetworkTickSystem.cs new file mode 100644 index 0000000..103e185 --- /dev/null +++ b/Runtime/Timing/NetworkTickSystem.cs @@ -0,0 +1,107 @@ +using System; +using Unity.Profiling; + +namespace Unity.Netcode +{ + public class NetworkTickSystem + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + private static ProfilerMarker s_Tick = new ProfilerMarker($"{nameof(NetworkTickSystem)}.Tick"); +#endif + + /// + /// Special value to indicate "No tick information" + /// + public const int NoTick = int.MinValue; + + /// + /// The TickRate of the tick system. This is used to decide how often a fixed network tick is run. + /// + public uint TickRate { get; internal set; } + + /// + /// The current local time. This is the time at which predicted or client authoritative objects move. + /// This value is accurate when called in Update or during the event but does not work correctly for FixedUpdate. + /// + public NetworkTime LocalTime { get; internal set; } + + /// + /// The current server time. This value is mostly used for internal purposes and to interpolate state received from the server. + /// This value is accurate when called in Update or during the event but does not work correctly for FixedUpdate. + /// + public NetworkTime ServerTime { get; internal set; } + + /// + /// Gets invoked before every network tick. + /// + public event Action Tick; + + /// + /// Creates a new instance of the class. + /// + /// The tick rate + /// The initial local time to start at. + /// The initial server time to start at. + public NetworkTickSystem(uint tickRate, double localTimeSec, double serverTimeSec) + { + if (tickRate == 0) + { + throw new ArgumentException("Tickrate must be a positive value.", nameof(tickRate)); + } + + TickRate = tickRate; + Tick = null; + LocalTime = new NetworkTime(tickRate, localTimeSec); + ServerTime = new NetworkTime(tickRate, serverTimeSec); + } + + /// + /// Resets the tick system to the given network time. + /// + /// The local time in seconds. + /// The server time in seconds. + public void Reset(double localTimeSec, double serverTimeSec) + { + LocalTime = new NetworkTime(TickRate, localTimeSec); + ServerTime = new NetworkTime(TickRate, serverTimeSec); + } + + /// + /// Called after advancing the time system to run ticks based on the difference in time. + /// + public void UpdateTick(double localTimeSec, double serverTimeSec) + { + // store old local tick to know how many fixed ticks passed + var previousLocalTick = LocalTime.Tick; + + LocalTime = new NetworkTime(TickRate, localTimeSec); + ServerTime = new NetworkTime(TickRate, serverTimeSec); + + // cache times here so that we can adjust them to temporary values while simulating ticks. + var cacheLocalTime = LocalTime; + var cacheServerTime = ServerTime; + + var currentLocalTick = LocalTime.Tick; + var localToServerDifference = currentLocalTick - ServerTime.Tick; + + for (int i = previousLocalTick + 1; i <= currentLocalTick; i++) + { + // set exposed time values to correct fixed values + LocalTime = new NetworkTime(TickRate, i); + ServerTime = new NetworkTime(TickRate, i - localToServerDifference); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_Tick.Begin(); +#endif + Tick?.Invoke(); +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_Tick.End(); +#endif + } + + // Set exposed time to values from tick system + LocalTime = cacheLocalTime; + ServerTime = cacheServerTime; + } + } +} diff --git a/Runtime/Timing/NetworkTickSystem.cs.meta b/Runtime/Timing/NetworkTickSystem.cs.meta new file mode 100644 index 0000000..91ee5f0 --- /dev/null +++ b/Runtime/Timing/NetworkTickSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f2654d93ced8d14093426a6fea32c32 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Timing/NetworkTime.cs b/Runtime/Timing/NetworkTime.cs new file mode 100644 index 0000000..f247189 --- /dev/null +++ b/Runtime/Timing/NetworkTime.cs @@ -0,0 +1,149 @@ +using System; +using UnityEngine; +using UnityEngine.Assertions; + +namespace Unity.Netcode +{ + /// + /// A struct to represent a point of time in a networked game. + /// Time is stored as a combination of amount of passed ticks + a duration offset. + /// This struct is meant to replace the Unity API for multiplayer gameplay. + /// + public struct NetworkTime + { + private double m_TimeSec; + + private uint m_TickRate; + private double m_TickInterval; + + private int m_CachedTick; + private double m_CachedTickOffset; + + /// + /// Gets the amount of time which has passed since the last network tick. + /// + public double TickOffset => m_CachedTickOffset; + + /// + /// Gets the current time. This is a non fixed time value and similar to + /// + public double Time => m_TimeSec; + + /// + /// Gets the current time as a float. + /// + public float TimeAsFloat => (float)m_TimeSec; + + /// + /// Gets he current fixed network time. This is the time value of the last network tick. Similar to + /// + public double FixedTime => m_CachedTick * m_TickInterval; + + /// + /// Gets the fixed delta time. This value is based on the and stays constant. + /// Similar to There is no equivalent to + /// + public float FixedDeltaTime => (float)m_TickInterval; + + /// + /// Gets the amount of network ticks which have passed until reaching the current time value. + /// + public int Tick => m_CachedTick; + + /// + /// Gets the tickrate of the system of this . + /// Ticks per second. + /// + public uint TickRate => m_TickRate; + + /// + /// Creates a new instance of the struct. + /// + /// The tickrate. + public NetworkTime(uint tickRate) + { + Assert.IsTrue(tickRate > 0, "Tickrate must be a positive value."); + + m_TickRate = tickRate; + m_TickInterval = 1f / m_TickRate; // potential floating point precision issue, could result in different interval on different machines + m_CachedTickOffset = 0; + m_CachedTick = 0; + m_TimeSec = 0; + } + + /// + /// Creates a new instance of the struct. + /// + /// The tickrate. + /// The time will be created with a value where this many tick have already passed. + /// Can be used to create a with a non fixed time value by adding an offset to the given tick value. + public NetworkTime(uint tickRate, int tick, double tickOffset = 0d) + : this(tickRate) + { + Assert.IsTrue(tickOffset < 1d / tickRate); + this += tick * m_TickInterval + tickOffset; + } + + /// + /// Creates a new instance of the struct. + /// + /// The tickrate. + /// The time value as a float. + public NetworkTime(uint tickRate, double timeSec) + : this(tickRate) + { + this += timeSec; + } + + + /// + /// Converts the network time into a fixed time value. + /// + /// A where Time is the FixedTime value of this instance. + public NetworkTime ToFixedTime() + { + return new NetworkTime(m_TickRate, m_CachedTick); + } + + public NetworkTime TimeTicksAgo(int ticks) + { + return this - new NetworkTime(TickRate, ticks); + } + + private void UpdateCache() + { + double d = m_TimeSec / m_TickInterval; + m_CachedTick = (int)d; + m_CachedTickOffset = ((d - Math.Truncate(d)) * m_TickInterval); + + // This handles negative time, decreases tick by 1 and makes offset positive. + if (m_CachedTick < 0 && m_CachedTickOffset != 0d) + { + m_CachedTick--; + m_CachedTickOffset = m_TickInterval + m_CachedTickOffset; + } + } + + public static NetworkTime operator -(NetworkTime a, NetworkTime b) + { + return new NetworkTime(a.TickRate, a.Time - b.Time); + } + + public static NetworkTime operator +(NetworkTime a, NetworkTime b) + { + return new NetworkTime(a.TickRate, a.Time + b.Time); + } + + public static NetworkTime operator +(NetworkTime a, double b) + { + a.m_TimeSec += b; + a.UpdateCache(); + return a; + } + + public static NetworkTime operator -(NetworkTime a, double b) + { + return a + -b; + } + } +} diff --git a/Runtime/Timing/NetworkTime.cs.meta b/Runtime/Timing/NetworkTime.cs.meta new file mode 100644 index 0000000..b9c4f42 --- /dev/null +++ b/Runtime/Timing/NetworkTime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 675ab548d27069b4c97d61781ec01231 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Timing/NetworkTimeSystem.cs b/Runtime/Timing/NetworkTimeSystem.cs new file mode 100644 index 0000000..25c7766 --- /dev/null +++ b/Runtime/Timing/NetworkTimeSystem.cs @@ -0,0 +1,118 @@ +using System; + +namespace Unity.Netcode +{ + /// + /// is a standalone system which can be used to run a network time simulation. + /// The network time system maintains both a local and a server time. The local time is based on + /// + public class NetworkTimeSystem + { + private double m_TimeSec; + private double m_CurrentLocalTimeOffset; + private double m_DesiredLocalTimeOffset; + private double m_CurrentServerTimeOffset; + private double m_DesiredServerTimeOffset; + + /// + /// Gets or sets the amount of time in seconds the server should buffer incoming client messages. + /// This increases the difference between local and server time so that messages arrive earlier on the server. + /// + public double LocalBufferSec { get; set; } + + /// + /// Gets or sets the amount of the time in seconds the client should buffer incoming messages from the server. This increases server time. + /// A higher value increases latency but makes the game look more smooth in bad networking conditions. + /// This value must be higher than the tick length client side. + /// + public double ServerBufferSec { get; set; } + + /// + /// Gets or sets a threshold in seconds used to force a hard catchup of network time. + /// + public double HardResetThresholdSec { get; set; } + + /// + /// Gets or sets the ratio at which the NetworkTimeSystem speeds up or slows down time. + /// + public double AdjustmentRatio { get; set; } + public double LocalTime => m_TimeSec + m_CurrentLocalTimeOffset; + public double ServerTime => m_TimeSec + m_CurrentServerTimeOffset; + + internal double LastSyncedServerTimeSec { get; private set; } + internal double LastSyncedRttSec { get; private set; } + + public NetworkTimeSystem(double localBufferSec, double serverBufferSec, double hardResetThresholdSec, double adjustmentRatio = 0.01d) + { + LocalBufferSec = localBufferSec; + ServerBufferSec = serverBufferSec; + HardResetThresholdSec = hardResetThresholdSec; + AdjustmentRatio = adjustmentRatio; + } + + /// + /// Creates a new instance of the class for a server instance. + /// The server will not apply any buffer values which ensures that local time equals server time. + /// + /// The instance. + public static NetworkTimeSystem ServerTimeSystem() + { + return new NetworkTimeSystem(0, 0, double.MaxValue); + } + + /// + /// Advances the time system by a certain amount of time. Should be called once per frame with Time.deltaTime or similar. + /// + /// The amount of time to advance. The delta time which passed since Advance was last called. + /// + public bool Advance(double deltaTimeSec) + { + m_TimeSec += deltaTimeSec; + + if (Math.Abs(m_DesiredLocalTimeOffset - m_CurrentLocalTimeOffset) > HardResetThresholdSec || Math.Abs(m_DesiredServerTimeOffset - m_CurrentServerTimeOffset) > HardResetThresholdSec) + { + m_TimeSec += m_DesiredServerTimeOffset; + + m_DesiredLocalTimeOffset -= m_DesiredServerTimeOffset; + m_CurrentLocalTimeOffset = m_DesiredLocalTimeOffset; + + m_DesiredServerTimeOffset = 0; + m_CurrentServerTimeOffset = 0; + + return true; + } + + m_CurrentLocalTimeOffset += deltaTimeSec * (m_DesiredLocalTimeOffset > m_CurrentLocalTimeOffset ? AdjustmentRatio : -AdjustmentRatio); + m_CurrentServerTimeOffset += deltaTimeSec * (m_DesiredServerTimeOffset > m_CurrentServerTimeOffset ? AdjustmentRatio : -AdjustmentRatio); + + return false; + } + + /// + /// Resets the time system to a time based on the given network parameters. + /// + /// The most recent server time value received in seconds. + /// The current RTT in seconds. Can be an averaged or a raw value. + public void Reset(double serverTimeSec, double rttSec) + { + Sync(serverTimeSec, rttSec); + Advance(0); + } + + /// + /// Synchronizes the time system with up-to-date network statistics but does not change any time values or advance the time. + /// + /// The most recent server time value received in seconds. + /// The current RTT in seconds. Can be an averaged or a raw value. + public void Sync(double serverTimeSec, double rttSec) + { + LastSyncedRttSec = rttSec; + LastSyncedServerTimeSec = serverTimeSec; + + var timeDif = serverTimeSec - m_TimeSec; + + m_DesiredServerTimeOffset = timeDif - ServerBufferSec; + m_DesiredLocalTimeOffset = timeDif + rttSec + LocalBufferSec; + } + } +} diff --git a/Runtime/Timing/NetworkTimeSystem.cs.meta b/Runtime/Timing/NetworkTimeSystem.cs.meta new file mode 100644 index 0000000..655b341 --- /dev/null +++ b/Runtime/Timing/NetworkTimeSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d802d09776f224e44af7dc71f09779d6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports.meta b/Runtime/Transports.meta new file mode 100644 index 0000000..f7da1bc --- /dev/null +++ b/Runtime/Transports.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a47160f3ea0898241b4677d75dfeb471 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/NetworkDelivery.cs b/Runtime/Transports/NetworkDelivery.cs new file mode 100644 index 0000000..19a4d95 --- /dev/null +++ b/Runtime/Transports/NetworkDelivery.cs @@ -0,0 +1,33 @@ +namespace Unity.Netcode +{ + /// + /// Delivery methods + /// + public enum NetworkDelivery + { + /// + /// Unreliable message + /// + Unreliable, + + /// + /// Unreliable with sequencing + /// + UnreliableSequenced, + + /// + /// Reliable message + /// + Reliable, + + /// + /// Reliable message where messages are guaranteed to be in the right order + /// + ReliableSequenced, + + /// + /// A reliable message with guaranteed order with fragmentation support + /// + ReliableFragmentedSequenced + } +} diff --git a/Runtime/Transports/NetworkDelivery.cs.meta b/Runtime/Transports/NetworkDelivery.cs.meta new file mode 100644 index 0000000..f693d45 --- /dev/null +++ b/Runtime/Transports/NetworkDelivery.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d0151d4283b329e498204f86534292ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/NetworkEvent.cs b/Runtime/Transports/NetworkEvent.cs new file mode 100644 index 0000000..b940d80 --- /dev/null +++ b/Runtime/Transports/NetworkEvent.cs @@ -0,0 +1,28 @@ +namespace Unity.Netcode +{ + /// + /// Represents a netEvent when polling + /// + public enum NetworkEvent + { + /// + /// New data is received + /// + Data, + + /// + /// A client is connected, or client connected to server + /// + Connect, + + /// + /// A client disconnected, or client disconnected from server + /// + Disconnect, + + /// + /// No new event + /// + Nothing + } +} diff --git a/Runtime/Transports/NetworkEvent.cs.meta b/Runtime/Transports/NetworkEvent.cs.meta new file mode 100644 index 0000000..5c7280c --- /dev/null +++ b/Runtime/Transports/NetworkEvent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c875a40b1475a5419ffd2a6b002cad2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/NetworkTransport.cs b/Runtime/Transports/NetworkTransport.cs new file mode 100644 index 0000000..57e7cfc --- /dev/null +++ b/Runtime/Transports/NetworkTransport.cs @@ -0,0 +1,100 @@ +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + public abstract class NetworkTransport : MonoBehaviour + { + /// + /// A constant `clientId` that represents the server + /// When this value is found in methods such as `Send`, it should be treated as a placeholder that means "the server" + /// + public abstract ulong ServerClientId { get; } + + /// + /// Gets a value indicating whether this is supported in the current runtime context + /// This is used by multiplex adapters + /// + /// true if is supported; otherwise, false. + public virtual bool IsSupported => true; + + /// + /// Delegate for transport network events + /// + public delegate void TransportEventDelegate(NetworkEvent eventType, ulong clientId, ArraySegment payload, float receiveTime); + + /// + /// Occurs when the transport has a new transport network event. + /// Can be used to make an event based transport instead of a poll based. + /// Invocation has to occur on the Unity thread in the Update loop. + /// + public event TransportEventDelegate OnTransportEvent; + + /// + /// Invokes the . Invokation has to occur on the Unity thread in the Update loop. + /// + /// The event type + /// The clientId this event is for + /// The incoming data payload + /// The time the event was received, as reported by Time.realtimeSinceStartup. + protected void InvokeOnTransportEvent(NetworkEvent eventType, ulong clientId, ArraySegment payload, float receiveTime) + { + OnTransportEvent?.Invoke(eventType, clientId, payload, receiveTime); + } + + /// + /// Send a payload to the specified clientId, data and channelName. + /// + /// The clientId to send to + /// The data to send + /// The delivery type (QoS) to send data with + public abstract void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery); + + /// + /// Polls for incoming events, with an extra output parameter to report the precise time the event was received. + /// + /// The clientId this event is for + /// The incoming data payload + /// The time the event was received, as reported by Time.realtimeSinceStartup. + /// Returns the event type + public abstract NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime); + + /// + /// Connects client to the server + /// + public abstract bool StartClient(); + + /// + /// Starts to listening for incoming clients + /// + public abstract bool StartServer(); + + /// + /// Disconnects a client from the server + /// + /// The clientId to disconnect + public abstract void DisconnectRemoteClient(ulong clientId); + + /// + /// Disconnects the local client from the server + /// + public abstract void DisconnectLocalClient(); + + /// + /// Gets the round trip time for a specific client. This method is optional + /// + /// The clientId to get the RTT from + /// Returns the round trip time in milliseconds + public abstract ulong GetCurrentRtt(ulong clientId); + + /// + /// Shuts down the transport + /// + public abstract void Shutdown(); + + /// + /// Initializes the transport + /// + public abstract void Initialize(); + } +} diff --git a/Runtime/Transports/NetworkTransport.cs.meta b/Runtime/Transports/NetworkTransport.cs.meta new file mode 100644 index 0000000..165d7cc --- /dev/null +++ b/Runtime/Transports/NetworkTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e47bade9963ff5b41b86d331006b5f67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/UNET.meta b/Runtime/Transports/UNET.meta new file mode 100644 index 0000000..4429818 --- /dev/null +++ b/Runtime/Transports/UNET.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 901256c415b484744b9da9551d85073a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/UNET/UNetChannel.cs b/Runtime/Transports/UNET/UNetChannel.cs new file mode 100644 index 0000000..1e1afc0 --- /dev/null +++ b/Runtime/Transports/UNET/UNetChannel.cs @@ -0,0 +1,52 @@ +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 + } +} diff --git a/Runtime/Transports/UNET/UNetChannel.cs.meta b/Runtime/Transports/UNET/UNetChannel.cs.meta new file mode 100644 index 0000000..9605368 --- /dev/null +++ b/Runtime/Transports/UNET/UNetChannel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e864534da30ef604992c0ed33c75d3c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Transports/UNET/UNetTransport.cs b/Runtime/Transports/UNET/UNetTransport.cs new file mode 100644 index 0000000..1a102b3 --- /dev/null +++ b/Runtime/Transports/UNET/UNetTransport.cs @@ -0,0 +1,281 @@ +#pragma warning disable 618 // disable is obsolete +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +using System; +using UnityEngine; +using UnityEngine.Networking; + +namespace Unity.Netcode.Transports.UNET +{ + public class UNetTransport : NetworkTransport + { + public enum SendMode + { + Immediately, + Queued + } + + private int m_UnreliableChannelId; + private int m_UnreliableSequencedChannelId; + private int m_ReliableChannelId; + private int m_ReliableSequencedChannelId; + private int m_ReliableFragmentedSequencedChannelId; + + // Inspector / settings + public int MessageBufferSize = 1024 * 5; + public int MaxConnections = 100; + public int MaxSentMessageQueueSize = 128; + + public string ConnectAddress = "127.0.0.1"; + public int ConnectPort = 7777; + public int ServerListenPort = 7777; + + public SendMode MessageSendMode = SendMode.Immediately; + + // Runtime / state + private byte[] m_MessageBuffer; + private WeakReference m_TemporaryBufferReference; + + // Lookup / translation + private int m_ServerConnectionId; + private int m_ServerHostId; + + public override ulong ServerClientId => GetNetcodeClientId(0, 0, true); + + protected void LateUpdate() + { + if (UnityEngine.Networking.NetworkTransport.IsStarted && MessageSendMode == SendMode.Queued) + { +#if UNITY_WEBGL + Debug.LogError("Cannot use queued sending mode for WebGL"); +#else + if (NetworkManager.Singleton.IsServer) + { + foreach (var targetClient in NetworkManager.Singleton.ConnectedClientsList) + { + SendQueued(targetClient.ClientId); + } + } + else + { + SendQueued(NetworkManager.Singleton.LocalClientId); + } +#endif + } + } + + public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) + { + GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId); + + byte[] buffer; + if (payload.Offset > 0) + { + // UNET cant handle this, do a copy + + if (m_MessageBuffer.Length >= payload.Count) + { + buffer = m_MessageBuffer; + } + else + { + object bufferRef; + if (m_TemporaryBufferReference != null && ((bufferRef = m_TemporaryBufferReference.Target) != null) && ((byte[])bufferRef).Length >= payload.Count) + { + buffer = (byte[])bufferRef; + } + else + { + buffer = new byte[payload.Count]; + m_TemporaryBufferReference = new WeakReference(buffer); + } + } + + Buffer.BlockCopy(payload.Array, payload.Offset, buffer, 0, payload.Count); + } + else + { + buffer = payload.Array; + } + + int channelId = -1; + switch (networkDelivery) + { + case NetworkDelivery.Unreliable: + channelId = m_UnreliableChannelId; + break; + case NetworkDelivery.UnreliableSequenced: + channelId = m_UnreliableSequencedChannelId; + break; + case NetworkDelivery.Reliable: + channelId = m_ReliableChannelId; + break; + case NetworkDelivery.ReliableSequenced: + channelId = m_ReliableSequencedChannelId; + break; + case NetworkDelivery.ReliableFragmentedSequenced: + channelId = m_ReliableFragmentedSequencedChannelId; + break; + } + + if (MessageSendMode == SendMode.Queued) + { +#if UNITY_WEBGL + Debug.LogError("Cannot use queued sending mode for WebGL"); +#else + UnityEngine.Networking.NetworkTransport.QueueMessageForSending(hostId, connectionId, channelId, buffer, payload.Count, out byte error); +#endif + } + else + { + UnityEngine.Networking.NetworkTransport.Send(hostId, connectionId, channelId, buffer, payload.Count, out byte error); + } + } + +#if !UNITY_WEBGL + private void SendQueued(ulong clientId) + { + GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId); + + UnityEngine.Networking.NetworkTransport.SendQueuedMessages(hostId, connectionId, out _); + } +#endif + + public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime) + { + var eventType = UnityEngine.Networking.NetworkTransport.Receive(out int hostId, out int connectionId, out _, m_MessageBuffer, m_MessageBuffer.Length, out int receivedSize, out byte error); + + clientId = GetNetcodeClientId((byte)hostId, (ushort)connectionId, false); + receiveTime = Time.realtimeSinceStartup; + + var networkError = (NetworkError)error; + if (networkError == NetworkError.MessageToLong) + { + byte[] tempBuffer; + + if (m_TemporaryBufferReference != null && m_TemporaryBufferReference.IsAlive && ((byte[])m_TemporaryBufferReference.Target).Length >= receivedSize) + { + tempBuffer = (byte[])m_TemporaryBufferReference.Target; + } + else + { + tempBuffer = new byte[receivedSize]; + m_TemporaryBufferReference = new WeakReference(tempBuffer); + } + + eventType = UnityEngine.Networking.NetworkTransport.Receive(out hostId, out connectionId, out _, tempBuffer, tempBuffer.Length, out receivedSize, out error); + payload = new ArraySegment(tempBuffer, 0, receivedSize); + } + else + { + payload = new ArraySegment(m_MessageBuffer, 0, receivedSize); + } + + if (networkError == NetworkError.Timeout) + { + // In UNET. Timeouts are not disconnects. We have to translate that here. + eventType = NetworkEventType.DisconnectEvent; + } + + // Translate NetworkEventType to NetEventType + switch (eventType) + { + case NetworkEventType.DataEvent: + return NetworkEvent.Data; + case NetworkEventType.ConnectEvent: + return NetworkEvent.Connect; + case NetworkEventType.DisconnectEvent: + return NetworkEvent.Disconnect; + case NetworkEventType.BroadcastEvent: + case NetworkEventType.Nothing: + default: + return NetworkEvent.Nothing; + } + } + + public override bool StartClient() + { + m_ServerHostId = UnityEngine.Networking.NetworkTransport.AddHost(new HostTopology(GetConfig(), 1), 0, null); + m_ServerConnectionId = UnityEngine.Networking.NetworkTransport.Connect(m_ServerHostId, ConnectAddress, ConnectPort, 0, out byte error); + return (NetworkError)error == NetworkError.Ok; + } + + public override bool StartServer() + { + var topology = new HostTopology(GetConfig(), MaxConnections); + UnityEngine.Networking.NetworkTransport.AddHost(topology, ServerListenPort, null); + return true; + } + + public override void DisconnectRemoteClient(ulong clientId) + { + GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId); + + UnityEngine.Networking.NetworkTransport.Disconnect((int)hostId, (int)connectionId, out byte error); + } + + public override void DisconnectLocalClient() + { + UnityEngine.Networking.NetworkTransport.Disconnect(m_ServerHostId, m_ServerConnectionId, out byte error); + } + + public override ulong GetCurrentRtt(ulong clientId) + { + GetUNetConnectionDetails(clientId, out byte hostId, out ushort connectionId); + + return (ulong)UnityEngine.Networking.NetworkTransport.GetCurrentRTT((int)hostId, (int)connectionId, out byte error); + } + + public override void Shutdown() + { + UnityEngine.Networking.NetworkTransport.Shutdown(); + } + + public override void Initialize() + { + m_MessageBuffer = new byte[MessageBufferSize]; + + UnityEngine.Networking.NetworkTransport.Init(); + } + + private ulong GetNetcodeClientId(byte hostId, ushort connectionId, bool isServer) + { + if (isServer) + { + return 0; + } + + return (connectionId | (ulong)hostId << 16) + 1; + } + + private void GetUNetConnectionDetails(ulong clientId, out byte hostId, out ushort connectionId) + { + if (clientId == 0) + { + hostId = (byte)m_ServerHostId; + connectionId = (ushort)m_ServerConnectionId; + } + else + { + hostId = (byte)((clientId - 1) >> 16); + connectionId = (ushort)((clientId - 1)); + } + } + + private ConnectionConfig GetConfig() + { + var connectionConfig = new ConnectionConfig(); + + m_UnreliableChannelId = connectionConfig.AddChannel(QosType.Unreliable); + m_UnreliableSequencedChannelId = connectionConfig.AddChannel(QosType.UnreliableSequenced); + m_ReliableChannelId = connectionConfig.AddChannel(QosType.Reliable); + m_ReliableSequencedChannelId = connectionConfig.AddChannel(QosType.ReliableSequenced); + m_ReliableFragmentedSequencedChannelId = connectionConfig.AddChannel(QosType.ReliableFragmentedSequenced); + + connectionConfig.MaxSentMessageQueueSize = (ushort)MaxSentMessageQueueSize; + + return connectionConfig; + } + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +#pragma warning restore 618 // restore is obsolete diff --git a/Runtime/Transports/UNET/UNetTransport.cs.meta b/Runtime/Transports/UNET/UNetTransport.cs.meta new file mode 100644 index 0000000..6af193f --- /dev/null +++ b/Runtime/Transports/UNET/UNetTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b84c2d8dfe509a34fb59e2b81f8e1319 +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 new file mode 100644 index 0000000..0efd561 --- /dev/null +++ b/Runtime/com.unity.netcode.runtime.asmdef @@ -0,0 +1,26 @@ +{ + "name": "Unity.Netcode.Runtime", + "rootNamespace": "Unity.Netcode", + "references": [ + "Unity.Multiplayer.MetricTypes", + "Unity.Multiplayer.NetStats", + "Unity.Multiplayer.NetStatsReporting", + "Unity.Multiplayer.NetworkSolutionInterface", + "Unity.Collections" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.unity.multiplayer.tools", + "expression": "", + "define": "MULTIPLAYER_TOOLS" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/com.unity.netcode.runtime.asmdef.meta b/Runtime/com.unity.netcode.runtime.asmdef.meta new file mode 100644 index 0000000..872cce1 --- /dev/null +++ b/Runtime/com.unity.netcode.runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1491147abca9d7d4bb7105af628b223e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap.meta b/Samples~/Bootstrap.meta new file mode 100644 index 0000000..3d32b89 --- /dev/null +++ b/Samples~/Bootstrap.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 72c642b5eadc8414782ae0e0e2703037 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/.sample.json b/Samples~/Bootstrap/.sample.json new file mode 100644 index 0000000..f62cf30 --- /dev/null +++ b/Samples~/Bootstrap/.sample.json @@ -0,0 +1,4 @@ +{ + "displayName": "Bootstrap", + "description": "A lightweight sample to get started" +} \ No newline at end of file diff --git a/Samples~/Bootstrap/Prefabs.meta b/Samples~/Bootstrap/Prefabs.meta new file mode 100644 index 0000000..75f957e --- /dev/null +++ b/Samples~/Bootstrap/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2ca52adaafe314f728cfd0fd9c0e92fb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/Prefabs/BootstrapPlayer.prefab b/Samples~/Bootstrap/Prefabs/BootstrapPlayer.prefab new file mode 100644 index 0000000..5b7b447 --- /dev/null +++ b/Samples~/Bootstrap/Prefabs/BootstrapPlayer.prefab @@ -0,0 +1,155 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &3439633038736912633 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3439633038736913158} + - component: {fileID: 3439633038736913157} + - component: {fileID: 3439633038736913156} + - component: {fileID: 3439633038736912635} + - component: {fileID: 3439633038736912634} + - component: {fileID: 2776227185612554462} + - component: {fileID: 6046305264893698362} + m_Layer: 0 + m_Name: BootstrapPlayer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3439633038736913158 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3439633038736912633} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!33 &3439633038736913157 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3439633038736912633} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &3439633038736913156 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3439633038736912633} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!135 &3439633038736912635 +SphereCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3439633038736912633} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 2 + m_Radius: 0.5 + m_Center: {x: 0, y: 0, z: 0} +--- !u!114 &3439633038736912634 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3439633038736912633} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 951099334 + AlwaysReplicateAsRoot: 0 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &2776227185612554462 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3439633038736912633} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3} + m_Name: + m_EditorClassIdentifier: + SyncPositionX: 1 + SyncPositionY: 1 + SyncPositionZ: 1 + SyncRotAngleX: 1 + SyncRotAngleY: 1 + SyncRotAngleZ: 1 + SyncScaleX: 1 + SyncScaleY: 1 + SyncScaleZ: 1 + PositionThreshold: 0 + RotAngleThreshold: 0 + ScaleThreshold: 0 + InLocalSpace: 0 + Interpolate: 1 + FixedSendsPerSecond: 30 +--- !u!114 &6046305264893698362 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3439633038736912633} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1aaab5fe31dc3423e89747f088d7bcaf, type: 3} + m_Name: + m_EditorClassIdentifier: diff --git a/Samples~/Bootstrap/Prefabs/BootstrapPlayer.prefab.meta b/Samples~/Bootstrap/Prefabs/BootstrapPlayer.prefab.meta new file mode 100644 index 0000000..1529432 --- /dev/null +++ b/Samples~/Bootstrap/Prefabs/BootstrapPlayer.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 398aad09d8b2a47eba664a076763cdcc +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/Scenes.meta b/Samples~/Bootstrap/Scenes.meta new file mode 100644 index 0000000..7091e64 --- /dev/null +++ b/Samples~/Bootstrap/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 68b3c153b391b40219154be17a973085 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/Scenes/Bootstrap.unity b/Samples~/Bootstrap/Scenes/Bootstrap.unity new file mode 100644 index 0000000..b253454 --- /dev/null +++ b/Samples~/Bootstrap/Scenes/Bootstrap.unity @@ -0,0 +1,401 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &1114774665 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1114774669} + - component: {fileID: 1114774668} + - component: {fileID: 1114774667} + - component: {fileID: 1114774666} + m_Layer: 0 + m_Name: BootstrapManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1114774666 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1114774665} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6960e84d07fb87f47956e7a81d71c4e6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ProtocolType: 0 + m_MessageBufferSize: 6144 + m_ReciveQueueSize: 128 + m_SendQueueSize: 128 + m_SendQueueBatchSize: 4096 + m_ServerAddress: 127.0.0.1 + m_ServerPort: 7777 +--- !u!114 &1114774667 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1114774665} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3} + m_Name: + m_EditorClassIdentifier: + DontDestroy: 1 + RunInBackground: 1 + LogLevel: 1 + NetworkConfig: + ProtocolVersion: 0 + NetworkTransport: {fileID: 1114774666} + PlayerPrefab: {fileID: 3439633038736912633, guid: 398aad09d8b2a47eba664a076763cdcc, + type: 3} + NetworkPrefabs: [] + TickRate: 30 + ClientConnectionBufferTimeout: 10 + ConnectionApproval: 0 + ConnectionData: + EnableTimeResync: 0 + TimeResyncInterval: 30 + EnsureNetworkVariableLengthSafety: 0 + EnableSceneManagement: 1 + ForceSamePrefabs: 1 + RecycleNetworkIds: 1 + NetworkIdRecycleDelay: 120 + RpcHashSize: 0 + LoadSceneTimeOut: 120 + MessageBufferTimeout: 20 + EnableNetworkLogs: 1 +--- !u!114 &1114774668 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1114774665} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5fed568ebf6c14b11928f16219b5675b, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!4 &1114774669 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1114774665} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2011451347 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2011451350} + - component: {fileID: 2011451349} + - component: {fileID: 2011451348} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &2011451348 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011451347} + m_Enabled: 1 +--- !u!20 &2011451349 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011451347} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &2011451350 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2011451347} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2056444517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2056444519} + - component: {fileID: 2056444518} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &2056444518 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2056444517} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &2056444519 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2056444517} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} diff --git a/Samples~/Bootstrap/Scenes/Bootstrap.unity.meta b/Samples~/Bootstrap/Scenes/Bootstrap.unity.meta new file mode 100644 index 0000000..e5c1d3f --- /dev/null +++ b/Samples~/Bootstrap/Scenes/Bootstrap.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7a6e122bd2b01425989087a299b6bb93 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/Scripts.meta b/Samples~/Bootstrap/Scripts.meta new file mode 100644 index 0000000..9221096 --- /dev/null +++ b/Samples~/Bootstrap/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5575d489edb9d4c8392de6bad7982f54 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/Scripts/Bootstrap.asmdef b/Samples~/Bootstrap/Scripts/Bootstrap.asmdef new file mode 100644 index 0000000..07a8ac0 --- /dev/null +++ b/Samples~/Bootstrap/Scripts/Bootstrap.asmdef @@ -0,0 +1,7 @@ +{ + "name": "Bootstrap", + "rootNamespace": "Unity.Netcode.Samples", + "references": [ + "Unity.Netcode.Runtime" + ] +} diff --git a/Samples~/Bootstrap/Scripts/Bootstrap.asmdef.meta b/Samples~/Bootstrap/Scripts/Bootstrap.asmdef.meta new file mode 100644 index 0000000..ebc1bc4 --- /dev/null +++ b/Samples~/Bootstrap/Scripts/Bootstrap.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 219b171db79434558b442780da267dab +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/Scripts/BootstrapManager.cs b/Samples~/Bootstrap/Scripts/BootstrapManager.cs new file mode 100644 index 0000000..00cdd46 --- /dev/null +++ b/Samples~/Bootstrap/Scripts/BootstrapManager.cs @@ -0,0 +1,58 @@ +using UnityEngine; + +namespace Unity.Netcode.Samples +{ + /// + /// Class to display helper buttons and status labels on the GUI, as well as buttons to start host/client/server. + /// Once a connection has been established to the server, the local player can be teleported to random positions via a GUI button. + /// + public class BootstrapManager : MonoBehaviour + { + private void OnGUI() + { + GUILayout.BeginArea(new Rect(10, 10, 300, 300)); + + var networkManager = NetworkManager.Singleton; + if (!networkManager.IsClient && !networkManager.IsServer) + { + if (GUILayout.Button("Host")) + { + networkManager.StartHost(); + } + + if (GUILayout.Button("Client")) + { + networkManager.StartClient(); + } + + if (GUILayout.Button("Server")) + { + networkManager.StartServer(); + } + } + else + { + GUILayout.Label($"Mode: {(networkManager.IsHost ? "Host" : networkManager.IsServer ? "Server" : "Client")}"); + + // "Random Teleport" button will only be shown to clients + if (networkManager.IsClient) + { + if (GUILayout.Button("Random Teleport")) + { + if (networkManager.LocalClient != null) + { + // Get `BootstrapPlayer` component from the player's `PlayerObject` + if (networkManager.LocalClient.PlayerObject.TryGetComponent(out BootstrapPlayer bootstrapPlayer)) + { + // Invoke a `ServerRpc` from client-side to teleport player to a random position on the server-side + bootstrapPlayer.RandomTeleportServerRpc(); + } + } + } + } + } + + GUILayout.EndArea(); + } + } +} diff --git a/Samples~/Bootstrap/Scripts/BootstrapManager.cs.meta b/Samples~/Bootstrap/Scripts/BootstrapManager.cs.meta new file mode 100644 index 0000000..07509d1 --- /dev/null +++ b/Samples~/Bootstrap/Scripts/BootstrapManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5fed568ebf6c14b11928f16219b5675b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/Bootstrap/Scripts/BootstrapPlayer.cs b/Samples~/Bootstrap/Scripts/BootstrapPlayer.cs new file mode 100644 index 0000000..af68e50 --- /dev/null +++ b/Samples~/Bootstrap/Scripts/BootstrapPlayer.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace Unity.Netcode.Samples +{ + /// + /// Component attached to the "Player Prefab" on the `NetworkManager`. + /// + public class BootstrapPlayer : NetworkBehaviour + { + /// + /// If this method is invoked on the client instance of this player, it will invoke a `ServerRpc` on the server-side. + /// If this method is invoked on the server instance of this player, it will teleport player to a random position. + /// + /// + /// Since a `NetworkTransform` component is attached to this player, and the authority on that component is set to "Server", + /// this transform's position modification can only be performed on the server, where it will then be replicated down to all clients through `NetworkTransform`. + /// + [ServerRpc] + public void RandomTeleportServerRpc() + { + var oldPosition = transform.position; + transform.position = GetRandomPositionOnXYPlane(); + var newPosition = transform.position; + print($"{nameof(RandomTeleportServerRpc)}() -> {nameof(OwnerClientId)}: {OwnerClientId} --- {nameof(oldPosition)}: {oldPosition} --- {nameof(newPosition)}: {newPosition}"); + } + + private static Vector3 GetRandomPositionOnXYPlane() + { + return new Vector3(Random.Range(-3f, 3f), Random.Range(-3f, 3f), 0f); + } + } +} diff --git a/Samples~/Bootstrap/Scripts/BootstrapPlayer.cs.meta b/Samples~/Bootstrap/Scripts/BootstrapPlayer.cs.meta new file mode 100644 index 0000000..f87f1ef --- /dev/null +++ b/Samples~/Bootstrap/Scripts/BootstrapPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1aaab5fe31dc3423e89747f088d7bcaf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform.meta b/Samples~/ClientNetworkTransform.meta new file mode 100644 index 0000000..2ffd543 --- /dev/null +++ b/Samples~/ClientNetworkTransform.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3106ae882c6ec416d855a44c97eeaeef +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/.sample.json b/Samples~/ClientNetworkTransform/.sample.json new file mode 100644 index 0000000..0c7e9fc --- /dev/null +++ b/Samples~/ClientNetworkTransform/.sample.json @@ -0,0 +1,4 @@ +{ + "displayName": "ClientNetworkTransform", + "description": "A sample to demonstrate how client-driven NetworkTransform can be implemented" +} \ No newline at end of file diff --git a/Samples~/ClientNetworkTransform/Prefabs.meta b/Samples~/ClientNetworkTransform/Prefabs.meta new file mode 100644 index 0000000..875e321 --- /dev/null +++ b/Samples~/ClientNetworkTransform/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b1ef235ca94b4bbd9a6456f44c69188 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Prefabs/.gitkeep b/Samples~/ClientNetworkTransform/Prefabs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Samples~/ClientNetworkTransform/Scenes.meta b/Samples~/ClientNetworkTransform/Scenes.meta new file mode 100644 index 0000000..cc143c0 --- /dev/null +++ b/Samples~/ClientNetworkTransform/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 03def738b58f746408d456f1f8c99264 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Scenes/.gitkeep b/Samples~/ClientNetworkTransform/Scenes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Samples~/ClientNetworkTransform/Scripts.meta b/Samples~/ClientNetworkTransform/Scripts.meta new file mode 100644 index 0000000..d0f7f18 --- /dev/null +++ b/Samples~/ClientNetworkTransform/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 749af92bd75b44951b56ea583f3f10b5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef new file mode 100644 index 0000000..26e8209 --- /dev/null +++ b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef @@ -0,0 +1,8 @@ +{ + "name": "ClientNetworkTransform", + "rootNamespace": "Unity.Netcode.Samples", + "references": [ + "Unity.Netcode.Runtime", + "Unity.Netcode.Components" + ] +} diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef.meta b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef.meta new file mode 100644 index 0000000..977e18f --- /dev/null +++ b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 78ac2a8d1365141f68da5d0a9e10dbc6 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs new file mode 100644 index 0000000..e1bdca9 --- /dev/null +++ b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs @@ -0,0 +1,39 @@ +using Unity.Netcode.Components; +using UnityEngine; + +namespace Unity.Netcode.Samples +{ + /// + /// Used for syncing a transform with client side changes. This includes host. Pure server as owner isn't supported by this. Please use NetworkTransform + /// for transforms that'll always be owned by the server. + /// + [DisallowMultipleComponent] + public class ClientNetworkTransform : NetworkTransform + { + /// + /// Used to determine who can write to this transform. Owner client only. + /// Changing this value alone will not allow you to create a NetworkTransform which can be written to by clients. + /// We're using RPCs to send updated values from client to server. Netcode doesn't support client side network variable writing. + /// This imposes state to the server. This is putting trust on your clients. Make sure no security-sensitive features use this transform. + /// + // This is public to make sure that users don't depend on this IsClient && IsOwner check in their code. If this logic changes in the future, we can make it invisible here + + public override void OnNetworkSpawn() + { + base.OnNetworkSpawn(); + CanCommitToTransform = IsOwner; + } + + protected override void Update() + { + base.Update(); + if (NetworkManager.Singleton != null && (NetworkManager.Singleton.IsConnectedClient || NetworkManager.Singleton.IsListening)) + { + if (CanCommitToTransform) + { + TryCommitTransformToServer(transform, NetworkManager.LocalTime.Time); + } + } + } + } +} diff --git a/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs.meta b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs.meta new file mode 100644 index 0000000..a19c929 --- /dev/null +++ b/Samples~/ClientNetworkTransform/Scripts/ClientNetworkTransform.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54c9647dc784a46bca664910f182491e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..5be7b0d --- /dev/null +++ b/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e5a2c06211b0c224c94202c2cda733fd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor.meta b/Tests/Editor.meta new file mode 100644 index 0000000..ce7ba7d --- /dev/null +++ b/Tests/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d656bc8bb2502584ab6883d254d64782 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/ArithmeticTests.cs b/Tests/Editor/ArithmeticTests.cs new file mode 100644 index 0000000..45baf5a --- /dev/null +++ b/Tests/Editor/ArithmeticTests.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; + +namespace Unity.Netcode.EditorTests +{ + public class ArithmeticTests + { + [Test] + public void TestCeil() + { + Assert.That(Arithmetic.CeilingExact(10, 5), Is.EqualTo(2)); + Assert.That(Arithmetic.CeilingExact(11, 5), Is.EqualTo(3)); + Assert.That(Arithmetic.CeilingExact(0, 5), Is.EqualTo(0)); + Assert.That(Arithmetic.CeilingExact(1, 5), Is.EqualTo(1)); + Assert.That(Arithmetic.CeilingExact(2, 5), Is.EqualTo(1)); + Assert.That(Arithmetic.CeilingExact(3, 5), Is.EqualTo(1)); + Assert.That(Arithmetic.CeilingExact(4, 5), Is.EqualTo(1)); + Assert.That(Arithmetic.CeilingExact(5, 5), Is.EqualTo(1)); + Assert.That(Arithmetic.CeilingExact(6, 5), Is.EqualTo(2)); + } + + [Test] + public void TestZigZag() + { + Assert.That(Arithmetic.ZigZagDecode(Arithmetic.ZigZagEncode(1234)), Is.EqualTo(1234)); + Assert.That(Arithmetic.ZigZagDecode(Arithmetic.ZigZagEncode(-1)), Is.EqualTo(-1)); + Assert.That(Arithmetic.ZigZagDecode(Arithmetic.ZigZagEncode(0)), Is.EqualTo(0)); + Assert.That(Arithmetic.ZigZagDecode(Arithmetic.ZigZagEncode(long.MaxValue)), Is.EqualTo(long.MaxValue)); + Assert.That(Arithmetic.ZigZagDecode(Arithmetic.ZigZagEncode(long.MinValue)), Is.EqualTo(long.MinValue)); + } + } +} diff --git a/Tests/Editor/ArithmeticTests.cs.meta b/Tests/Editor/ArithmeticTests.cs.meta new file mode 100644 index 0000000..d169f79 --- /dev/null +++ b/Tests/Editor/ArithmeticTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc0dc1cbf78a5486db1ccbc245d90992 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Build.meta b/Tests/Editor/Build.meta new file mode 100644 index 0000000..ecc75f3 --- /dev/null +++ b/Tests/Editor/Build.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2a46bbc7e63044f498612ed996afa274 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Build/BuildTestScene.unity b/Tests/Editor/Build/BuildTestScene.unity new file mode 100644 index 0000000..0b80327 --- /dev/null +++ b/Tests/Editor/Build/BuildTestScene.unity @@ -0,0 +1,371 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.44657874, g: 0.49641275, b: 0.5748172, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &1290761662 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1290761665} + - component: {fileID: 1290761664} + - component: {fileID: 1290761663} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1290761663 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1290761662} + m_Enabled: 1 +--- !u!20 &1290761664 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1290761662} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1290761665 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1290761662} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1858588911 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1858588913} + - component: {fileID: 1858588912} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &1858588912 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1858588911} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &1858588913 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1858588911} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1896907459 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1896907460} + - component: {fileID: 1896907461} + m_Layer: 0 + m_Name: '[NetworkManager]' + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1896907460 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1896907459} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1896907461 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1896907459} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 593a2fe42fa9d37498c96f9a383b6521, type: 3} + m_Name: + m_EditorClassIdentifier: + DontDestroy: 1 + RunInBackground: 1 + LogLevel: 1 + NetworkConfig: + ProtocolVersion: 0 + NetworkTransport: {fileID: 0} + RegisteredScenes: + - BuildTestScene + AllowRuntimeSceneChanges: 0 + PlayerPrefab: {fileID: 0} + NetworkPrefabs: [] + TickRate: 30 + ClientConnectionBufferTimeout: 10 + ConnectionApproval: 0 + ConnectionData: + EnableTimeResync: 0 + TimeResyncInterval: 30 + EnableNetworkVariable: 1 + EnsureNetworkVariableLengthSafety: 0 + EnableSceneManagement: 1 + ForceSamePrefabs: 1 + RecycleNetworkIds: 1 + NetworkIdRecycleDelay: 120 + RpcHashSize: 0 + LoadSceneTimeOut: 120 + MessageBufferTimeout: 20 + EnableNetworkLogs: 1 diff --git a/Tests/Editor/Build/BuildTestScene.unity.meta b/Tests/Editor/Build/BuildTestScene.unity.meta new file mode 100644 index 0000000..0a8a789 --- /dev/null +++ b/Tests/Editor/Build/BuildTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d45c09a54b1da466ab6c385ee45fd3e4 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Build/BuildTests.cs b/Tests/Editor/Build/BuildTests.cs new file mode 100644 index 0000000..0862d80 --- /dev/null +++ b/Tests/Editor/Build/BuildTests.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Reflection; +using NUnit.Framework; +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + public class BuildTests + { + public const string DefaultBuildScenePath = "Tests/Editor/Build/BuildTestScene.unity"; + + [Test] + public void BasicBuildTest() + { + var execAssembly = Assembly.GetExecutingAssembly(); + var packagePath = UnityEditor.PackageManager.PackageInfo.FindForAssembly(execAssembly).assetPath; + var buildReport = BuildPipeline.BuildPlayer( + new[] { Path.Combine(packagePath, DefaultBuildScenePath) }, + Path.Combine(Path.GetDirectoryName(Application.dataPath), "Builds", nameof(BuildTests)), + EditorUserBuildSettings.activeBuildTarget, + BuildOptions.None + ); + Assert.AreEqual(BuildResult.Succeeded, buildReport.summary.result); + } + } +} diff --git a/Tests/Editor/Build/BuildTests.cs.meta b/Tests/Editor/Build/BuildTests.cs.meta new file mode 100644 index 0000000..705e96c --- /dev/null +++ b/Tests/Editor/Build/BuildTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efb549c2173d5478e93a71fbd05c0392 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/IndexAllocatorTests.cs b/Tests/Editor/IndexAllocatorTests.cs new file mode 100644 index 0000000..2a04a97 --- /dev/null +++ b/Tests/Editor/IndexAllocatorTests.cs @@ -0,0 +1,116 @@ +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 new file mode 100644 index 0000000..760a4cc --- /dev/null +++ b/Tests/Editor/IndexAllocatorTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85ac488e1432d49668c711fa625a0743 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/InterpolatorTests.cs b/Tests/Editor/InterpolatorTests.cs new file mode 100644 index 0000000..ee71ad4 --- /dev/null +++ b/Tests/Editor/InterpolatorTests.cs @@ -0,0 +1,359 @@ +using System; +using NUnit.Framework; + +namespace Unity.Netcode.EditorTests +{ + public class InterpolatorTests + { + private const float k_Precision = 0.00000001f; + private const int k_MockTickRate = 1; + + private NetworkTime T(float time, uint tickRate = k_MockTickRate) + { + return new NetworkTime(tickRate, timeSec: time); + } + + [Test] + public void TestReset() + { + var interpolator = new BufferedLinearInterpolatorFloat(); + + var serverTime = new NetworkTime(k_MockTickRate, 100f); + interpolator.AddMeasurement(5, 1.0f); + var initVal = interpolator.Update(10f, serverTime.Time, serverTime.TimeTicksAgo(1).Time); // big value + Assert.That(initVal, Is.EqualTo(5f)); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(5f)); + + interpolator.ResetTo(100f, serverTime.Time); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(100f)); + var val = interpolator.Update(1f, serverTime.Time, serverTime.TimeTicksAgo(1).Time); + Assert.That(val, Is.EqualTo(100f)); + } + + [Test] + public void NormalUsage() + { + // Testing float instead of Vector3. The only difference with Vector3 is the lerp method used. + var interpolator = new BufferedLinearInterpolatorFloat(); + + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0f)); + + interpolator.AddMeasurement(0f, 1.0f); + interpolator.AddMeasurement(1f, 2.0f); + + // too small update, nothing happens, doesn't consume from buffer yet + var serverTime = new NetworkTime(k_MockTickRate, 0.01d); // t = 0.1d + interpolator.Update(.01f, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0f)); + + // consume first measurement, still can't interpolate with just one tick consumed + serverTime += 1.0d; // t = 1.01 + interpolator.Update(1.0f, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0f)); + + // consume second measurement, start to interpolate + serverTime += 1.0d; // t = 2.01 + var valueFromUpdate = interpolator.Update(1.0f, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0.01f).Within(k_Precision)); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0.01f).Within(k_Precision)); // test a second time, to make sure the get doesn't update the value + Assert.That(valueFromUpdate, Is.EqualTo(interpolator.GetInterpolatedValue()).Within(k_Precision)); + + // continue interpolation + serverTime = new NetworkTime(k_MockTickRate, 2.5d); // t = 2.5d + interpolator.Update(2.5f - 2.01f, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0.5f).Within(k_Precision)); + + // check when reaching end + serverTime += 0.5d; // t = 3 + interpolator.Update(0.5f, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f).Within(k_Precision)); + } + + /// + /// Out of order or 'ACB' problem + /// Given two measurements have already arrived A and C, if a new measurement B arrives, the interpolation shouldn't go to B, but continue + /// to C. + /// Adding B should be ignored if interpolation is already interpolating between A and C + /// + [Test] + public void OutOfOrderShouldStillWork() + { + var serverTime = new NetworkTime(k_MockTickRate, 0.01d); + var interpolator = new BufferedLinearInterpolatorFloat(); + double timeStep = 0.5d; + + interpolator.AddMeasurement(0f, 0d); + interpolator.AddMeasurement(2f, 2d); + + serverTime = new NetworkTime(k_MockTickRate, 1.5d); + interpolator.Update(1.5f, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(0f).Within(k_Precision)); + + serverTime += timeStep; // t = 2.0 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f).Within(k_Precision)); + + serverTime += timeStep; // t = 2.5 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1.5f).Within(k_Precision)); + + // makes sure that interpolation still continues in right direction + interpolator.AddMeasurement(1, 1d); + + serverTime += timeStep; // t = 3 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(2f).Within(k_Precision)); + } + + [Test] + public void MessageLoss() + { + var serverTime = new NetworkTime(k_MockTickRate, 0.01d); + var interpolator = new BufferedLinearInterpolatorFloat(); + double timeStep = 0.5d; + + interpolator.AddMeasurement(1f, 1d); + interpolator.AddMeasurement(2f, 2d); + // message time=3 was lost + interpolator.AddMeasurement(4f, 4d); + interpolator.AddMeasurement(5f, 5d); + // message time=6 was lost + interpolator.AddMeasurement(100f, 7d); // high value to produce a misprediction + + // first value teleports interpolator + serverTime = new NetworkTime(k_MockTickRate, 1d); + interpolator.Update(1f, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f)); + + // nothing happens, not ready to consume second value yet + serverTime += timeStep; // t = 1.5 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f)); + + // beginning of interpolation, second value consumed, currently at start + serverTime += timeStep; // t = 2 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1f)); + + // interpolation starts + serverTime += timeStep; // t = 2.5 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(1.5f)); + + serverTime += timeStep; // t = 3 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(2f)); + + // extrapolating to 2.5 + serverTime += timeStep; // t = 3.5d + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(2.5f)); + + // next value skips to where it was supposed to be once buffer time is showing the next value + serverTime += timeStep; // t = 4 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(3f)); + + // interpolation continues as expected + serverTime += timeStep; // t = 4.5 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(3.5f)); + + serverTime += timeStep; // t = 5 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(4f)); + + // lost time=6, extrapolating + serverTime += timeStep; // t = 5.5 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(4.5f)); + + serverTime += timeStep; // t = 6.0 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(5f)); + + // misprediction + serverTime += timeStep; // t = 6.5 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.EqualTo(5.5f)); + + // lerp to right value + serverTime += timeStep; // t = 7.0 + interpolator.Update((float)timeStep, serverTime); + Assert.That(interpolator.GetInterpolatedValue(), Is.GreaterThan(6.0f)); + Assert.That(interpolator.GetInterpolatedValue(), Is.LessThanOrEqualTo(100f)); + } + + [Test] + public void AddFirstMeasurement() + { + var interpolator = new BufferedLinearInterpolatorFloat(); + + var serverTime = new NetworkTime(k_MockTickRate, 0d); + interpolator.AddMeasurement(2f, 1d); + interpolator.AddMeasurement(3f, 2d); + + serverTime += 1d; // t = 1 + var interpolatedValue = interpolator.Update(1f, serverTime); + // when consuming only one measurement and it's the first one consumed, teleport to it + Assert.That(interpolatedValue, Is.EqualTo(2f)); + + // then interpolation should work as usual + serverTime += 1d; // t = 2 + interpolatedValue = interpolator.Update(1f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(2f)); + + serverTime += 0.5d; // t = 2.5 + interpolatedValue = interpolator.Update(0.5f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(2.5f)); + + serverTime += 0.5d; // t = 3 + interpolatedValue = interpolator.Update(.5f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(3f)); + } + + [Test] + public void JumpToEachValueIfDeltaTimeTooBig() + { + var interpolator = new BufferedLinearInterpolatorFloat(); + + var serverTime = new NetworkTime(k_MockTickRate, 0d); + interpolator.AddMeasurement(2f, 1d); + interpolator.AddMeasurement(3f, 2d); + + serverTime += 1d; // t = 1 + var interpolatedValue = interpolator.Update(1f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(2f)); + + // big deltaTime, jumping to latest value + serverTime += 9f; // t = 10 + interpolatedValue = interpolator.Update(8f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(3)); + } + + [Test] + public void JumpToLastValueFromStart() + { + var interpolator = new BufferedLinearInterpolatorFloat(); + + var serverTime = new NetworkTime(k_MockTickRate, 0d); + + serverTime += 1d; // t = 1 + interpolator.AddMeasurement(1f, serverTime.Time); + serverTime += 1d; // t = 2 + interpolator.AddMeasurement(2f, serverTime.Time); + serverTime += 1d; // t = 3 + interpolator.AddMeasurement(3f, serverTime.Time); + + // big time jump + serverTime += 7d; // t = 10 + var interpolatedValue = interpolator.Update(10f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(3f)); + + // interpolation continues as normal + serverTime = new NetworkTime(k_MockTickRate, 11d); // t = 11 + interpolator.AddMeasurement(11f, serverTime.Time); // out of order + + serverTime = new NetworkTime(k_MockTickRate, 10.5d); // t = 10.5 + interpolatedValue = interpolator.Update(0.5f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(3f)); + + serverTime += 0.5d; // t = 11 + interpolatedValue = interpolator.Update(0.5f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(10f)); + + serverTime += 0.5d; // t = 11.5 + interpolatedValue = interpolator.Update(0.5f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(10.5f)); + + serverTime += 0.5d; // t = 12 + interpolatedValue = interpolator.Update(0.5f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(11f)); + } + + [Test] + public void TestBufferSizeLimit() + { + var interpolator = new BufferedLinearInterpolatorFloat(); + + // set first value + var serverTime = new NetworkTime(k_MockTickRate, 0d); + serverTime += 1.0d; // t = 1 + interpolator.AddMeasurement(-1f, serverTime.Time); + interpolator.Update(1f, serverTime); + + // max + 1 + serverTime += 1.0d; // t = 2 + interpolator.AddMeasurement(2, serverTime.Time); // +1, this should trigger a burst and teleport to last value + for (int i = 0; i < 100; i++) + { + interpolator.AddMeasurement(i + 3, i + 3d); + } + + // client was paused for a while, some time has past, we just got a burst of values from the server that teleported us to the last value received + serverTime = new NetworkTime(k_MockTickRate, 102d); + var interpolatedValue = interpolator.Update(101f, serverTime); + Assert.That(interpolatedValue, Is.EqualTo(102)); + } + + [Test] + public void TestUpdatingInterpolatorWithNoData() + { + var interpolator = new BufferedLinearInterpolatorFloat(); + var serverTime = new NetworkTime(k_MockTickRate, 0.0d); + // invalid case, this is undefined behaviour + Assert.Throws(() => interpolator.Update(1f, serverTime)); + } + + [Test] + public void TestDuplicatedValues() + { + var interpolator = new BufferedLinearInterpolatorFloat(); + + var serverTime = new NetworkTime(k_MockTickRate, 0.0d); + + serverTime += 1d; // t = 1 + interpolator.AddMeasurement(1f, serverTime.Time); + serverTime += 1d; // t = 2 + interpolator.AddMeasurement(2f, serverTime.Time); + interpolator.AddMeasurement(2f, serverTime.Time); + + // empty interpolator teleports to initial value + serverTime = new NetworkTime(k_MockTickRate, 0.0d); + serverTime += 1d; // t = 1 + var interp = interpolator.Update(1f, serverTime); + Assert.That(interp, Is.EqualTo(1f)); + + // consume value, start interp, currently at start value + serverTime += 1d; // t = 2 + interp = interpolator.Update(1f, serverTime); + Assert.That(interp, Is.EqualTo(1f)); + + // interp + serverTime += 0.5d; // t = 2.5 + interp = interpolator.Update(0.5f, serverTime); + Assert.That(interp, Is.EqualTo(1.5f)); + + // reach end + serverTime += 0.5d; // t = 3 + interp = interpolator.Update(0.5f, serverTime); + Assert.That(interp, Is.EqualTo(2f)); + + // with unclamped interpolation, we continue mispredicting since the two last values are actually treated as the same. Therefore we're not stopping at "2" + serverTime += 0.5d; // t = 3.5 + interp = interpolator.Update(0.5f, serverTime); + Assert.That(interp, Is.EqualTo(2.5f)); + + serverTime += 0.5d; // t = 4 + interp = interpolator.Update(0.5f, serverTime); + Assert.That(interp, Is.EqualTo(3f)); + + // we add a measurement with an updated time + var pastServerTime = new NetworkTime(k_MockTickRate, 3.0d); + interpolator.AddMeasurement(2f, pastServerTime.Time); + + interp = interpolator.Update(0.5f, serverTime); + Assert.That(interp, Is.EqualTo(2f)); + } + } +} diff --git a/Tests/Editor/InterpolatorTests.cs.meta b/Tests/Editor/InterpolatorTests.cs.meta new file mode 100644 index 0000000..973fe50 --- /dev/null +++ b/Tests/Editor/InterpolatorTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5cfbc170161c4e95ac6124ee43068b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Messaging.meta b/Tests/Editor/Messaging.meta new file mode 100644 index 0000000..c03aaa7 --- /dev/null +++ b/Tests/Editor/Messaging.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 55531000b0344935b665541f089df60e +timeCreated: 1630354914 \ No newline at end of file diff --git a/Tests/Editor/Messaging/MessageReceivingTests.cs b/Tests/Editor/Messaging/MessageReceivingTests.cs new file mode 100644 index 0000000..ef8bfda --- /dev/null +++ b/Tests/Editor/Messaging/MessageReceivingTests.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using NUnit.Framework.Internal; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode.EditorTests +{ + public class MessageReceivingTests + { + private struct TestMessage : INetworkMessage + { + public int A; + public int B; + public int C; + public static bool Deserialized; + public static List DeserializedValues = new List(); + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValueSafe(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + Deserialized = true; + reader.ReadValueSafe(out TestMessage value); + DeserializedValues.Add(value); + } + } + + private class TestMessageProvider : IMessageProvider + { + public List GetMessages() + { + return new List + { + new MessagingSystem.MessageWithHandler + { + MessageType = typeof(TestMessage), + Handler = TestMessage.Receive + } + }; + } + } + + private MessagingSystem m_MessagingSystem; + + [SetUp] + public void SetUp() + { + TestMessage.Deserialized = false; + TestMessage.DeserializedValues.Clear(); + + m_MessagingSystem = new MessagingSystem(new NopMessageSender(), this, new TestMessageProvider()); + } + + [TearDown] + public void TearDown() + { + m_MessagingSystem.Dispose(); + } + + private TestMessage GetMessage() + { + var random = new Random(); + return new TestMessage + { + A = random.Next(), + B = random.Next(), + C = random.Next(), + }; + } + + [Test] + public void WhenHandlingAMessage_ReceiveMethodIsCalled() + { + var messageHeader = new MessageHeader + { + MessageSize = (ushort)UnsafeUtility.SizeOf(), + MessageType = m_MessagingSystem.GetMessageType(typeof(TestMessage)), + }; + var message = GetMessage(); + + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(FastBufferWriter.GetWriteSize(message)); + writer.WriteValue(message); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + m_MessagingSystem.HandleMessage(messageHeader, reader, 0, 0); + Assert.IsTrue(TestMessage.Deserialized); + Assert.AreEqual(1, TestMessage.DeserializedValues.Count); + Assert.AreEqual(message, TestMessage.DeserializedValues[0]); + } + } + } + + [Test] + public void WhenHandlingIncomingData_ReceiveIsNotCalledBeforeProcessingIncomingMessageQueue() + { + var batchHeader = new BatchHeader + { + BatchSize = 1 + }; + var messageHeader = new MessageHeader + { + MessageSize = (ushort)UnsafeUtility.SizeOf(), + MessageType = m_MessagingSystem.GetMessageType(typeof(TestMessage)), + }; + var message = GetMessage(); + + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(FastBufferWriter.GetWriteSize(batchHeader) + + FastBufferWriter.GetWriteSize(messageHeader) + + FastBufferWriter.GetWriteSize(message)); + writer.WriteValue(batchHeader); + writer.WriteValue(messageHeader); + writer.WriteValue(message); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + m_MessagingSystem.HandleIncomingData(0, new ArraySegment(writer.ToArray()), 0); + Assert.IsFalse(TestMessage.Deserialized); + Assert.IsEmpty(TestMessage.DeserializedValues); ; + } + } + } + + [Test] + public void WhenReceivingAMessageAndProcessingMessageQueue_ReceiveMethodIsCalled() + { + var batchHeader = new BatchHeader + { + BatchSize = 1 + }; + var messageHeader = new MessageHeader + { + MessageSize = (ushort)UnsafeUtility.SizeOf(), + MessageType = m_MessagingSystem.GetMessageType(typeof(TestMessage)), + }; + var message = GetMessage(); + + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(FastBufferWriter.GetWriteSize(batchHeader) + + FastBufferWriter.GetWriteSize(messageHeader) + + FastBufferWriter.GetWriteSize(message)); + writer.WriteValue(batchHeader); + writer.WriteValue(messageHeader); + writer.WriteValue(message); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + m_MessagingSystem.HandleIncomingData(0, new ArraySegment(writer.ToArray()), 0); + m_MessagingSystem.ProcessIncomingMessageQueue(); + Assert.IsTrue(TestMessage.Deserialized); + Assert.AreEqual(1, TestMessage.DeserializedValues.Count); + Assert.AreEqual(message, TestMessage.DeserializedValues[0]); + } + } + } + + [Test] + public void WhenReceivingMultipleMessagesAndProcessingMessageQueue_ReceiveMethodIsCalledMultipleTimes() + { + var batchHeader = new BatchHeader + { + BatchSize = 2 + }; + var messageHeader = new MessageHeader + { + MessageSize = (ushort)UnsafeUtility.SizeOf(), + MessageType = m_MessagingSystem.GetMessageType(typeof(TestMessage)), + }; + var message = GetMessage(); + var message2 = GetMessage(); + + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(FastBufferWriter.GetWriteSize(batchHeader) + + FastBufferWriter.GetWriteSize(messageHeader) * 2 + + FastBufferWriter.GetWriteSize(message) * 2); + writer.WriteValue(batchHeader); + writer.WriteValue(messageHeader); + writer.WriteValue(message); + writer.WriteValue(messageHeader); + writer.WriteValue(message2); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + m_MessagingSystem.HandleIncomingData(0, new ArraySegment(writer.ToArray()), 0); + Assert.IsFalse(TestMessage.Deserialized); + Assert.IsEmpty(TestMessage.DeserializedValues); + + m_MessagingSystem.ProcessIncomingMessageQueue(); + Assert.IsTrue(TestMessage.Deserialized); + Assert.AreEqual(2, TestMessage.DeserializedValues.Count); + Assert.AreEqual(message, TestMessage.DeserializedValues[0]); + Assert.AreEqual(message2, TestMessage.DeserializedValues[1]); + } + } + } + } +} diff --git a/Tests/Editor/Messaging/MessageReceivingTests.cs.meta b/Tests/Editor/Messaging/MessageReceivingTests.cs.meta new file mode 100644 index 0000000..45178e3 --- /dev/null +++ b/Tests/Editor/Messaging/MessageReceivingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cbc8fb6cf75f52d46a5d74971ce4b240 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Messaging/MessageRegistrationTests.cs b/Tests/Editor/Messaging/MessageRegistrationTests.cs new file mode 100644 index 0000000..5f25cbb --- /dev/null +++ b/Tests/Editor/Messaging/MessageRegistrationTests.cs @@ -0,0 +1,179 @@ +using System.Collections.Generic; +using NUnit.Framework; + +namespace Unity.Netcode.EditorTests +{ + public class MessageRegistrationTests + { + + private struct TestMessageOne : INetworkMessage + { + public int A; + public int B; + public int C; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValue(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + + } + } + + private struct TestMessageTwo : INetworkMessage + { + public int A; + public int B; + public int C; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValue(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + + } + } + private class TestMessageProviderOne : IMessageProvider + { + public List GetMessages() + { + return new List + { + new MessagingSystem.MessageWithHandler + { + MessageType = typeof(TestMessageOne), + Handler = TestMessageOne.Receive + }, + new MessagingSystem.MessageWithHandler + { + MessageType = typeof(TestMessageTwo), + Handler = TestMessageTwo.Receive + } + }; + } + } + + private struct TestMessageThree : INetworkMessage + { + public int A; + public int B; + public int C; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValue(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + + } + } + private class TestMessageProviderTwo : IMessageProvider + { + public List GetMessages() + { + return new List + { + new MessagingSystem.MessageWithHandler + { + MessageType = typeof(TestMessageThree), + Handler = TestMessageThree.Receive + } + }; + } + } + + private struct TestMessageFour : INetworkMessage + { + public int A; + public int B; + public int C; + + public void Serialize(FastBufferWriter writer) + { + writer.WriteValue(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + + } + } + private class TestMessageProviderThree : IMessageProvider + { + public List GetMessages() + { + return new List + { + new MessagingSystem.MessageWithHandler + { + MessageType = typeof(TestMessageFour), + Handler = TestMessageFour.Receive + } + }; + } + } + + [Test] + public void WhenCreatingMessageSystem_OnlyProvidedTypesAreRegistered() + { + var sender = new NopMessageSender(); + + var systemOne = new MessagingSystem(sender, null, new TestMessageProviderOne()); + var systemTwo = new MessagingSystem(sender, null, new TestMessageProviderTwo()); + var systemThree = new MessagingSystem(sender, null, new TestMessageProviderThree()); + + using (systemOne) + using (systemTwo) + using (systemThree) + { + Assert.AreEqual(2, systemOne.MessageHandlerCount); + Assert.AreEqual(1, systemTwo.MessageHandlerCount); + Assert.AreEqual(1, systemThree.MessageHandlerCount); + + Assert.Contains(typeof(TestMessageOne), systemOne.MessageTypes); + Assert.Contains(typeof(TestMessageTwo), systemOne.MessageTypes); + Assert.Contains(typeof(TestMessageThree), systemTwo.MessageTypes); + Assert.Contains(typeof(TestMessageFour), systemThree.MessageTypes); + } + } + + [Test] + public void WhenCreatingMessageSystem_BoundTypeMessageHandlersAreRegistered() + { + var sender = new NopMessageSender(); + + var systemOne = new MessagingSystem(sender, null, new TestMessageProviderOne()); + var systemTwo = new MessagingSystem(sender, null, new TestMessageProviderTwo()); + var systemThree = new MessagingSystem(sender, null, new TestMessageProviderThree()); + + using (systemOne) + using (systemTwo) + using (systemThree) + { + MessagingSystem.MessageHandler handlerOne = TestMessageOne.Receive; + MessagingSystem.MessageHandler handlerTwo = TestMessageTwo.Receive; + MessagingSystem.MessageHandler handlerThree = TestMessageThree.Receive; + MessagingSystem.MessageHandler handlerFour = TestMessageFour.Receive; + + var foundHandlerOne = systemOne.MessageHandlers[systemOne.GetMessageType(typeof(TestMessageOne))]; + + Assert.AreEqual(handlerOne, + systemOne.MessageHandlers[systemOne.GetMessageType(typeof(TestMessageOne))]); + Assert.AreEqual(handlerTwo, + systemOne.MessageHandlers[systemOne.GetMessageType(typeof(TestMessageTwo))]); + Assert.AreEqual(handlerThree, + systemTwo.MessageHandlers[systemTwo.GetMessageType(typeof(TestMessageThree))]); + Assert.AreEqual(handlerFour, + systemThree.MessageHandlers[systemThree.GetMessageType(typeof(TestMessageFour))]); + } + } + } +} diff --git a/Tests/Editor/Messaging/MessageRegistrationTests.cs.meta b/Tests/Editor/Messaging/MessageRegistrationTests.cs.meta new file mode 100644 index 0000000..860ab64 --- /dev/null +++ b/Tests/Editor/Messaging/MessageRegistrationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 03e7cc2b6f0c5c540a529429f48529f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Messaging/MessageSendingTests.cs b/Tests/Editor/Messaging/MessageSendingTests.cs new file mode 100644 index 0000000..d23c18a --- /dev/null +++ b/Tests/Editor/Messaging/MessageSendingTests.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode.EditorTests +{ + public class MessageSendingTests + { + private struct TestMessage : INetworkMessage + { + public int A; + public int B; + public int C; + public static bool Serialized; + + public void Serialize(FastBufferWriter writer) + { + Serialized = true; + writer.WriteValueSafe(this); + } + + public static void Receive(FastBufferReader reader, in NetworkContext context) + { + } + } + + private class TestMessageSender : IMessageSender + { + public List MessageQueue = new List(); + + public void Send(ulong clientId, NetworkDelivery delivery, FastBufferWriter batchData) + { + MessageQueue.Add(batchData.ToArray()); + } + } + + private class TestMessageProvider : IMessageProvider + { + public List GetMessages() + { + return new List + { + new MessagingSystem.MessageWithHandler + { + MessageType = typeof(TestMessage), + Handler = TestMessage.Receive + } + }; + } + } + + private TestMessageSender m_MessageSender; + private MessagingSystem m_MessagingSystem; + private ulong[] m_Clients = { 0 }; + + [SetUp] + public void SetUp() + { + TestMessage.Serialized = false; + + m_MessageSender = new TestMessageSender(); + m_MessagingSystem = new MessagingSystem(m_MessageSender, this, new TestMessageProvider()); + m_MessagingSystem.ClientConnected(0); + } + + [TearDown] + public void TearDown() + { + m_MessagingSystem.Dispose(); + } + + private TestMessage GetMessage() + { + var random = new Random(); + return new TestMessage + { + A = random.Next(), + B = random.Next(), + C = random.Next(), + }; + } + + [Test] + public void WhenSendingMessage_SerializeIsCalled() + { + var message = GetMessage(); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + Assert.IsTrue(TestMessage.Serialized); + } + + [Test] + public void WhenSendingMessage_NothingIsSentBeforeProcessingSendQueue() + { + var message = GetMessage(); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + Assert.IsEmpty(m_MessageSender.MessageQueue); + } + + [Test] + public void WhenProcessingSendQueue_MessageIsSent() + { + var message = GetMessage(); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + + m_MessagingSystem.ProcessSendQueues(); + Assert.AreEqual(1, m_MessageSender.MessageQueue.Count); + } + + [Test] + public void WhenSendingMultipleMessages_MessagesAreBatched() + { + var message = GetMessage(); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + + m_MessagingSystem.ProcessSendQueues(); + Assert.AreEqual(1, m_MessageSender.MessageQueue.Count); + } + + [Test] + public void WhenNotExceedingBatchSize_NewBatchesAreNotCreated() + { + var message = GetMessage(); + var size = UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf(); + for (var i = 0; i < 1300 / size; ++i) + { + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + } + + m_MessagingSystem.ProcessSendQueues(); + Assert.AreEqual(1, m_MessageSender.MessageQueue.Count); + } + + [Test] + public void WhenExceedingBatchSize_NewBatchesAreCreated() + { + var message = GetMessage(); + var size = UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf(); + for (var i = 0; i < (1300 / size) + 1; ++i) + { + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + } + + m_MessagingSystem.ProcessSendQueues(); + Assert.AreEqual(2, m_MessageSender.MessageQueue.Count); + } + + [Test] + public void WhenExceedingMTUSizeWithFragmentedDelivery_NewBatchesAreNotCreated() + { + var message = GetMessage(); + var size = UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf(); + for (var i = 0; i < (1300 / size) + 1; ++i) + { + m_MessagingSystem.SendMessage(message, NetworkDelivery.ReliableFragmentedSequenced, m_Clients); + } + + m_MessagingSystem.ProcessSendQueues(); + Assert.AreEqual(1, m_MessageSender.MessageQueue.Count); + } + + [Test] + public void WhenSwitchingDelivery_NewBatchesAreCreated() + { + var message = GetMessage(); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Unreliable, m_Clients); + + m_MessagingSystem.ProcessSendQueues(); + Assert.AreEqual(2, m_MessageSender.MessageQueue.Count); + } + + [Test] + public void WhenSwitchingChannel_NewBatchesAreNotCreated() + { + var message = GetMessage(); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + + m_MessagingSystem.ProcessSendQueues(); + Assert.AreEqual(1, m_MessageSender.MessageQueue.Count); + } + + [Test] + public void WhenSendingMessaged_SentDataIsCorrect() + { + var message = GetMessage(); + var message2 = GetMessage(); + m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); + m_MessagingSystem.SendMessage(message2, NetworkDelivery.Reliable, m_Clients); + + m_MessagingSystem.ProcessSendQueues(); + var reader = new FastBufferReader(m_MessageSender.MessageQueue[0], Allocator.Temp); + using (reader) + { + reader.TryBeginRead( + FastBufferWriter.GetWriteSize() + + FastBufferWriter.GetWriteSize() * 2 + + FastBufferWriter.GetWriteSize() * 2 + ); + reader.ReadValue(out BatchHeader header); + Assert.AreEqual(2, header.BatchSize); + + reader.ReadValue(out MessageHeader messageHeader); + Assert.AreEqual(m_MessagingSystem.GetMessageType(typeof(TestMessage)), messageHeader.MessageType); + Assert.AreEqual(UnsafeUtility.SizeOf(), messageHeader.MessageSize); + reader.ReadValue(out TestMessage receivedMessage); + Assert.AreEqual(message, receivedMessage); + + reader.ReadValue(out MessageHeader messageHeader2); + Assert.AreEqual(m_MessagingSystem.GetMessageType(typeof(TestMessage)), messageHeader2.MessageType); + Assert.AreEqual(UnsafeUtility.SizeOf(), messageHeader2.MessageSize); + reader.ReadValue(out TestMessage receivedMessage2); + Assert.AreEqual(message2, receivedMessage2); + } + } + } +} diff --git a/Tests/Editor/Messaging/MessageSendingTests.cs.meta b/Tests/Editor/Messaging/MessageSendingTests.cs.meta new file mode 100644 index 0000000..8589132 --- /dev/null +++ b/Tests/Editor/Messaging/MessageSendingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea2fc218c5e07c54795fc9bed4a6a62c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Messaging/NopMessageSender.cs b/Tests/Editor/Messaging/NopMessageSender.cs new file mode 100644 index 0000000..39fac90 --- /dev/null +++ b/Tests/Editor/Messaging/NopMessageSender.cs @@ -0,0 +1,9 @@ +namespace Unity.Netcode.EditorTests +{ + internal class NopMessageSender : IMessageSender + { + public void Send(ulong clientId, NetworkDelivery delivery, FastBufferWriter batchData) + { + } + } +} diff --git a/Tests/Editor/Messaging/NopMessageSender.cs.meta b/Tests/Editor/Messaging/NopMessageSender.cs.meta new file mode 100644 index 0000000..321f2a7 --- /dev/null +++ b/Tests/Editor/Messaging/NopMessageSender.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 817c58672ba39a74da57082ed176956e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Metrics.meta b/Tests/Editor/Metrics.meta new file mode 100644 index 0000000..bc962b1 --- /dev/null +++ b/Tests/Editor/Metrics.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 87ddfad8823c4fe192fff56b7acc241b +timeCreated: 1629386688 \ No newline at end of file diff --git a/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs b/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs new file mode 100644 index 0000000..56fb132 --- /dev/null +++ b/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs @@ -0,0 +1,41 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Multiplayer.Tools.NetStats; + +namespace Unity.Netcode.EditorTests.Metrics +{ + public class NetworkMetricsRegistrationTests + { + static Type[] s_MetricTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(x => x.GetTypes()) + .Where(x => x.GetInterfaces().Contains(typeof(INetworkMetricEvent))) + .ToArray(); + + [TestCaseSource(nameof(s_MetricTypes))][Ignore("Disable test while we reevaluate the assumption that INetworkMetricEvent interfaces must be reported from MLAPI.")] + public void ValidateThatAllMetricTypesAreRegistered(Type metricType) + { + var dispatcher = new NetworkMetrics().Dispatcher as MetricDispatcher; + Assert.NotNull(dispatcher); + + var collection = typeof(MetricDispatcher) + .GetField("m_Collection", BindingFlags.NonPublic | BindingFlags.Instance)? + .GetValue(dispatcher) as MetricCollection; + Assert.NotNull(collection); + + Assert.That( + collection.Metrics.OfType(), + Has.Exactly(2).Matches( + eventMetric => + { + var eventType = eventMetric.GetType().GetGenericArguments()?.FirstOrDefault(); + return eventType == metricType; + })); + } + } +} + +#endif diff --git a/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs.meta b/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs.meta new file mode 100644 index 0000000..89ebdab --- /dev/null +++ b/Tests/Editor/Metrics/NetworkMetricsRegistrationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af741f5e3d4f5544eaa68bb9bcaf54c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/NetworkBehaviourTests.cs b/Tests/Editor/NetworkBehaviourTests.cs new file mode 100644 index 0000000..395aa79 --- /dev/null +++ b/Tests/Editor/NetworkBehaviourTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.EditorTests +{ + public class NetworkBehaviourTests + { + [Test] + public void HasNetworkObjectTest() + { + var gameObject = new GameObject(nameof(HasNetworkObjectTest)); + var networkBehaviour = gameObject.AddComponent(); + + Assert.That(networkBehaviour.HasNetworkObject, Is.False); + + var networkObject = gameObject.AddComponent(); + + Assert.That(networkBehaviour.HasNetworkObject, Is.True); + + Object.DestroyImmediate(networkObject); + + Assert.That(networkBehaviour.HasNetworkObject, Is.False); + + // Cleanup + Object.DestroyImmediate(gameObject); + } + + [Test] + public void AccessNetworkObjectTest() + { + var gameObject = new GameObject(nameof(AccessNetworkObjectTest)); + var networkBehaviour = gameObject.AddComponent(); + + Assert.That(networkBehaviour.NetworkObject, Is.Null); + + var networkObject = gameObject.AddComponent(); + + Assert.That(networkBehaviour.NetworkObject, Is.EqualTo(networkObject)); + + Object.DestroyImmediate(networkObject); + + Assert.That(networkBehaviour.NetworkObject, Is.Null); + + // Cleanup + Object.DestroyImmediate(gameObject); + } + + [Test] + public void GivenClassDerivesFromNetworkBehaviour_GetTypeNameReturnsCorrectValue() + { + var gameObject = new GameObject(nameof(GivenClassDerivesFromNetworkBehaviour_GetTypeNameReturnsCorrectValue)); + var networkBehaviour = gameObject.AddComponent(); + + Assert.AreEqual(nameof(EmptyNetworkBehaviour), networkBehaviour.__getTypeName()); + } + + [Test] + public void GivenClassDerivesFromNetworkBehaviourDerivedClass_GetTypeNameReturnsCorrectValue() + { + var gameObject = new GameObject(nameof(GivenClassDerivesFromNetworkBehaviourDerivedClass_GetTypeNameReturnsCorrectValue)); + var networkBehaviour = gameObject.AddComponent(); + + Assert.AreEqual(nameof(DerivedNetworkBehaviour), networkBehaviour.__getTypeName()); + } + + // Note: in order to repro https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues/1078 + // this child class must be defined before its parent to assure it is processed first by ILPP + public class DerivedNetworkBehaviour : EmptyNetworkBehaviour + { + + } + + public class EmptyNetworkBehaviour : NetworkBehaviour + { + + } + } +} diff --git a/Tests/Editor/NetworkBehaviourTests.cs.meta b/Tests/Editor/NetworkBehaviourTests.cs.meta new file mode 100644 index 0000000..7c741c7 --- /dev/null +++ b/Tests/Editor/NetworkBehaviourTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 73e18571452c102e4b209671741f3b51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/NetworkManagerCustomMessageManagerTests.cs b/Tests/Editor/NetworkManagerCustomMessageManagerTests.cs new file mode 100644 index 0000000..2094b1e --- /dev/null +++ b/Tests/Editor/NetworkManagerCustomMessageManagerTests.cs @@ -0,0 +1,30 @@ +using NUnit.Framework; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + public class NetworkManagerCustomMessageManagerTests + { + [Test] + public void CustomMessageManagerAssigned() + { + var gameObject = new GameObject(nameof(CustomMessageManagerAssigned)); + var networkManager = gameObject.AddComponent(); + var transport = gameObject.AddComponent(); + + networkManager.NetworkConfig = new NetworkConfig(); + // Set dummy transport that does nothing + networkManager.NetworkConfig.NetworkTransport = transport; + + CustomMessagingManager preManager = networkManager.CustomMessagingManager; + + // Start server to cause initialization + networkManager.StartServer(); + + Debug.Assert(preManager == null); + Debug.Assert(networkManager.CustomMessagingManager != null); + + Object.DestroyImmediate(gameObject); + } + } +} diff --git a/Tests/Editor/NetworkManagerCustomMessageManagerTests.cs.meta b/Tests/Editor/NetworkManagerCustomMessageManagerTests.cs.meta new file mode 100644 index 0000000..a25df43 --- /dev/null +++ b/Tests/Editor/NetworkManagerCustomMessageManagerTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 315ffe39806441839400d21871d566a0 +timeCreated: 1618478909 \ No newline at end of file diff --git a/Tests/Editor/NetworkManagerMessageHandlerTests.cs b/Tests/Editor/NetworkManagerMessageHandlerTests.cs new file mode 100644 index 0000000..df53283 --- /dev/null +++ b/Tests/Editor/NetworkManagerMessageHandlerTests.cs @@ -0,0 +1,54 @@ +using System; +using Unity.Netcode.Editor; + +namespace Unity.Netcode.EditorTests +{ + // Should probably have one of these for more files? In the future we could use the SIPTransport? + [DontShowInTransportDropdown] + internal class DummyTransport : NetworkTransport + { + public override ulong ServerClientId { get; } = 0; + public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) + { + } + + 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() + { + return true; + } + + public override bool StartServer() + { + return true; + } + + public override void DisconnectRemoteClient(ulong clientId) + { + } + + public override void DisconnectLocalClient() + { + } + + public override ulong GetCurrentRtt(ulong clientId) + { + return 0; + } + + public override void Shutdown() + { + } + + public override void Initialize() + { + } + } +} diff --git a/Tests/Editor/NetworkManagerMessageHandlerTests.cs.meta b/Tests/Editor/NetworkManagerMessageHandlerTests.cs.meta new file mode 100644 index 0000000..5a9c34f --- /dev/null +++ b/Tests/Editor/NetworkManagerMessageHandlerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 976ca592c7fa4bcb854203dfbadc0ad9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/NetworkManagerSceneManagerTests.cs b/Tests/Editor/NetworkManagerSceneManagerTests.cs new file mode 100644 index 0000000..c1f5b30 --- /dev/null +++ b/Tests/Editor/NetworkManagerSceneManagerTests.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + public class NetworkManagerSceneManagerTests + { + [Test] + public void SceneManagerAssigned() + { + var gameObject = new GameObject(nameof(SceneManagerAssigned)); + var networkManager = gameObject.AddComponent(); + var transport = gameObject.AddComponent(); + networkManager.NetworkConfig = new NetworkConfig(); + // Set dummy transport that does nothing + networkManager.NetworkConfig.NetworkTransport = transport; + + NetworkSceneManager preManager = networkManager.SceneManager; + + // Start server to cause initialization process + networkManager.StartServer(); + + Debug.Assert(preManager == null); + Debug.Assert(networkManager.SceneManager != null); + + Object.DestroyImmediate(gameObject); + } + } +} diff --git a/Tests/Editor/NetworkManagerSceneManagerTests.cs.meta b/Tests/Editor/NetworkManagerSceneManagerTests.cs.meta new file mode 100644 index 0000000..fe94876 --- /dev/null +++ b/Tests/Editor/NetworkManagerSceneManagerTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9753bf4088484cbebee95d917699dec6 +timeCreated: 1618482634 \ No newline at end of file diff --git a/Tests/Editor/NetworkObjectTests.cs b/Tests/Editor/NetworkObjectTests.cs new file mode 100644 index 0000000..d887dcf --- /dev/null +++ b/Tests/Editor/NetworkObjectTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.EditorTests +{ + public class NetworkObjectTests + { + [Test] + public void NetworkManagerOverrideTest() + { + // Create "bait" + var singletonNetworkManager = new GameObject(nameof(NetworkManager)).AddComponent(); + singletonNetworkManager.SetSingleton(); + + // Create override + var networkManager = new GameObject(nameof(NetworkManager)).AddComponent(); + + // NetworkObject + var gameObject = new GameObject(nameof(NetworkManagerOverrideTest)); + var networkObject = gameObject.AddComponent(); + + // Set override + networkObject.NetworkManagerOwner = networkManager; + + Debug.Assert(networkObject.NetworkManager == networkManager); + + Object.DestroyImmediate(singletonNetworkManager.gameObject); + Object.DestroyImmediate(networkManager.gameObject); + Object.DestroyImmediate(gameObject); + } + + [Test] + public void GetBehaviourIndexNone() + { + var gameObject = new GameObject(nameof(GetBehaviourIndexNone)); + var networkObject = gameObject.AddComponent(); + + // TODO: Maybe not hardcode message? + LogAssert.Expect(LogType.Error, $"[Netcode] Behaviour index was out of bounds. Did you mess up the order of your {nameof(NetworkBehaviour)}s?"); + LogAssert.Expect(LogType.Error, $"[Netcode] Behaviour index was out of bounds. Did you mess up the order of your {nameof(NetworkBehaviour)}s?"); + LogAssert.Expect(LogType.Error, $"[Netcode] Behaviour index was out of bounds. Did you mess up the order of your {nameof(NetworkBehaviour)}s?"); + + Assert.That(networkObject.GetNetworkBehaviourAtOrderIndex(0), Is.Null); + Assert.That(networkObject.GetNetworkBehaviourAtOrderIndex(1), Is.Null); + Assert.That(networkObject.GetNetworkBehaviourAtOrderIndex(2), Is.Null); + + // Cleanup + Object.DestroyImmediate(gameObject); + } + + [Test] + public void GetBehaviourIndexOne() + { + var gameObject = new GameObject(nameof(GetBehaviourIndexOne)); + var networkObject = gameObject.AddComponent(); + var networkBehaviour = gameObject.AddComponent(); + + // TODO: Maybe not hardcode message? + LogAssert.Expect(LogType.Error, $"[Netcode] Behaviour index was out of bounds. Did you mess up the order of your {nameof(NetworkBehaviour)}s?"); + LogAssert.Expect(LogType.Error, $"[Netcode] Behaviour index was out of bounds. Did you mess up the order of your {nameof(NetworkBehaviour)}s?"); + + Assert.That(networkObject.GetNetworkBehaviourAtOrderIndex(0), Is.EqualTo(networkBehaviour)); + Assert.That(networkObject.GetNetworkBehaviourAtOrderIndex(1), Is.Null); + Assert.That(networkObject.GetNetworkBehaviourAtOrderIndex(2), Is.Null); + + // Cleanup + Object.DestroyImmediate(gameObject); + } + + public class EmptyNetworkBehaviour : NetworkBehaviour + { + + } + } +} diff --git a/Tests/Editor/NetworkObjectTests.cs.meta b/Tests/Editor/NetworkObjectTests.cs.meta new file mode 100644 index 0000000..70ddc85 --- /dev/null +++ b/Tests/Editor/NetworkObjectTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 542af4c12ffee14f59967bc8e41f5e9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Serialization.meta b/Tests/Editor/Serialization.meta new file mode 100644 index 0000000..b3c7bee --- /dev/null +++ b/Tests/Editor/Serialization.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e12c4be6e89f459aa2826abba8c8d301 +timeCreated: 1628799671 \ No newline at end of file diff --git a/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs new file mode 100644 index 0000000..e08d038 --- /dev/null +++ b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs @@ -0,0 +1,587 @@ +using System; +using NUnit.Framework; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode +{ + public abstract class BaseFastBufferReaderWriterTest + { + + #region Test Types + protected enum ByteEnum : byte + { + A, + B, + C + }; + protected enum SByteEnum : sbyte + { + A, + B, + C + }; + protected enum ShortEnum : short + { + A, + B, + C + }; + protected enum UShortEnum : ushort + { + A, + B, + C + }; + protected enum IntEnum : int + { + A, + B, + C + }; + protected enum UIntEnum : uint + { + A, + B, + C + }; + protected enum LongEnum : long + { + A, + B, + C + }; + protected enum ULongEnum : ulong + { + A, + B, + C + }; + + protected struct TestStruct + { + public byte A; + public short B; + public ushort C; + public int D; + public uint E; + public long F; + public ulong G; + public bool H; + public char I; + public float J; + public double K; + } + + public enum WriteType + { + WriteDirect, + WriteSafe + } + #endregion + + + protected abstract void RunTypeTest(T valueToTest) where T : unmanaged; + + protected abstract void RunTypeTestSafe(T valueToTest) where T : unmanaged; + + protected abstract void RunTypeArrayTest(T[] valueToTest) where T : unmanaged; + + protected abstract void RunTypeArrayTestSafe(T[] valueToTest) where T : unmanaged; + + #region Helpers + protected TestStruct GetTestStruct() + { + var random = new Random(); + + var testStruct = new TestStruct + { + A = (byte)random.Next(), + B = (short)random.Next(), + C = (ushort)random.Next(), + D = (int)random.Next(), + E = (uint)random.Next(), + F = ((long)random.Next() << 32) + random.Next(), + G = ((ulong)random.Next() << 32) + (ulong)random.Next(), + H = true, + I = '\u263a', + J = (float)random.NextDouble(), + K = random.NextDouble(), + }; + + return testStruct; + } + + #endregion + + public void BaseTypeTest(Type testType, WriteType writeType) + { + var random = new Random(); + + void RunTypeTestLocal(T val, WriteType wt) where T : unmanaged + { + switch (wt) + { + case WriteType.WriteDirect: + RunTypeTest(val); + break; + case WriteType.WriteSafe: + RunTypeTestSafe(val); + break; + } + } + + if (testType == typeof(byte)) + { + RunTypeTestLocal((byte)random.Next(), writeType); + } + else if (testType == typeof(sbyte)) + { + RunTypeTestLocal((sbyte)random.Next(), writeType); + } + else if (testType == typeof(short)) + { + RunTypeTestLocal((short)random.Next(), writeType); + } + else if (testType == typeof(ushort)) + { + RunTypeTestLocal((ushort)random.Next(), writeType); + } + else if (testType == typeof(int)) + { + RunTypeTestLocal((int)random.Next(), writeType); + } + else if (testType == typeof(uint)) + { + RunTypeTestLocal((uint)random.Next(), writeType); + } + else if (testType == typeof(long)) + { + RunTypeTestLocal(((long)random.Next() << 32) + random.Next(), writeType); + } + else if (testType == typeof(ulong)) + { + RunTypeTestLocal(((ulong)random.Next() << 32) + (ulong)random.Next(), writeType); + } + else if (testType == typeof(bool)) + { + RunTypeTestLocal(true, writeType); + } + else if (testType == typeof(char)) + { + RunTypeTestLocal('a', writeType); + RunTypeTestLocal('\u263a', writeType); + } + else if (testType == typeof(float)) + { + RunTypeTestLocal((float)random.NextDouble(), writeType); + } + else if (testType == typeof(double)) + { + RunTypeTestLocal(random.NextDouble(), writeType); + } + else if (testType == typeof(ByteEnum)) + { + RunTypeTestLocal(ByteEnum.C, writeType); + } + else if (testType == typeof(SByteEnum)) + { + RunTypeTestLocal(SByteEnum.C, writeType); + } + else if (testType == typeof(ShortEnum)) + { + RunTypeTestLocal(ShortEnum.C, writeType); + } + else if (testType == typeof(UShortEnum)) + { + RunTypeTestLocal(UShortEnum.C, writeType); + } + else if (testType == typeof(IntEnum)) + { + RunTypeTestLocal(IntEnum.C, writeType); + } + else if (testType == typeof(UIntEnum)) + { + RunTypeTestLocal(UIntEnum.C, writeType); + } + else if (testType == typeof(LongEnum)) + { + RunTypeTestLocal(LongEnum.C, writeType); + } + else if (testType == typeof(ULongEnum)) + { + RunTypeTestLocal(ULongEnum.C, writeType); + } + else if (testType == typeof(Vector2)) + { + RunTypeTestLocal(new Vector2((float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Vector3)) + { + RunTypeTestLocal(new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Vector4)) + { + RunTypeTestLocal(new Vector4((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Quaternion)) + { + RunTypeTestLocal(new Quaternion((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Color)) + { + RunTypeTestLocal(new Color((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Color32)) + { + RunTypeTestLocal(new Color32((byte)random.Next(), (byte)random.Next(), (byte)random.Next(), (byte)random.Next()), writeType); + } + else if (testType == typeof(Ray)) + { + RunTypeTestLocal(new Ray( + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble())), writeType); + } + else if (testType == typeof(Ray2D)) + { + RunTypeTestLocal(new Ray2D( + new Vector2((float)random.NextDouble(), (float)random.NextDouble()), + new Vector2((float)random.NextDouble(), (float)random.NextDouble())), writeType); + } + else if (testType == typeof(TestStruct)) + { + RunTypeTestLocal(GetTestStruct(), writeType); + } + else + { + Assert.Fail("No type handler was provided for this type in the test!"); + } + } + + public void BaseArrayTypeTest(Type testType, WriteType writeType) + { + var random = new Random(); + void RunTypeTestLocal(T[] val, WriteType wt) where T : unmanaged + { + switch (wt) + { + case WriteType.WriteDirect: + RunTypeArrayTest(val); + break; + case WriteType.WriteSafe: + RunTypeArrayTestSafe(val); + break; + } + } + + if (testType == typeof(byte)) + { + RunTypeTestLocal(new[]{ + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next() + }, writeType); + } + else if (testType == typeof(sbyte)) + { + RunTypeTestLocal(new[]{ + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next() + }, writeType); + } + else if (testType == typeof(short)) + { + RunTypeTestLocal(new[]{ + (short) random.Next(), + (short) random.Next(), + (short) random.Next(), + (short) random.Next(), + (short) random.Next() + }, writeType); + } + else if (testType == typeof(ushort)) + { + RunTypeTestLocal(new[]{ + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next() + }, writeType); + } + else if (testType == typeof(int)) + { + RunTypeTestLocal(new[]{ + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next() + }, writeType); + } + else if (testType == typeof(uint)) + { + RunTypeTestLocal(new[]{ + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next() + }, writeType); + } + else if (testType == typeof(long)) + { + RunTypeTestLocal(new[]{ + ((long)random.Next() << 32) + (long)random.Next(), + ((long)random.Next() << 32) + (long)random.Next(), + ((long)random.Next() << 32) + (long)random.Next(), + ((long)random.Next() << 32) + (long)random.Next() + }, writeType); + } + else if (testType == typeof(ulong)) + { + RunTypeTestLocal(new[]{ + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next() + }, writeType); + } + else if (testType == typeof(bool)) + { + RunTypeTestLocal(new[]{ + true, + false, + true, + true, + false, + false, + true, + false, + true + }, writeType); + } + else if (testType == typeof(char)) + { + RunTypeTestLocal(new[]{ + 'a', + '\u263a' + }, writeType); + } + else if (testType == typeof(float)) + { + RunTypeTestLocal(new[]{ + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble() + }, writeType); + } + else if (testType == typeof(double)) + { + RunTypeTestLocal(new[]{ + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble() + }, writeType); + } + else if (testType == typeof(ByteEnum)) + { + RunTypeTestLocal(new[]{ + ByteEnum.C, + ByteEnum.A, + ByteEnum.B + }, writeType); + } + else if (testType == typeof(SByteEnum)) + { + RunTypeTestLocal(new[]{ + SByteEnum.C, + SByteEnum.A, + SByteEnum.B + }, writeType); + } + else if (testType == typeof(ShortEnum)) + { + RunTypeTestLocal(new[]{ + ShortEnum.C, + ShortEnum.A, + ShortEnum.B + }, writeType); + } + else if (testType == typeof(UShortEnum)) + { + RunTypeTestLocal(new[]{ + UShortEnum.C, + UShortEnum.A, + UShortEnum.B + }, writeType); + } + else if (testType == typeof(IntEnum)) + { + RunTypeTestLocal(new[]{ + IntEnum.C, + IntEnum.A, + IntEnum.B + }, writeType); + } + else if (testType == typeof(UIntEnum)) + { + RunTypeTestLocal(new[]{ + UIntEnum.C, + UIntEnum.A, + UIntEnum.B + }, writeType); + } + else if (testType == typeof(LongEnum)) + { + RunTypeTestLocal(new[]{ + LongEnum.C, + LongEnum.A, + LongEnum.B + }, writeType); + } + else if (testType == typeof(ULongEnum)) + { + RunTypeTestLocal(new[]{ + ULongEnum.C, + ULongEnum.A, + ULongEnum.B + }, writeType); + } + else if (testType == typeof(Vector2)) + { + RunTypeTestLocal(new[]{ + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Vector3)) + { + RunTypeTestLocal(new[]{ + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Vector4)) + { + RunTypeTestLocal(new[]{ + new Vector4((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector4((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector4((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Quaternion)) + { + RunTypeTestLocal(new[]{ + new Quaternion((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble(), (float) random.NextDouble()), + new Quaternion((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble(), (float) random.NextDouble()), + new Quaternion((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble(), (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Color)) + { + RunTypeTestLocal(new[]{ + new Color((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Color((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Color((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Color32)) + { + RunTypeTestLocal(new[]{ + new Color32((byte) random.Next(), (byte) random.Next(), (byte) random.Next(), (byte) random.Next()), + new Color32((byte) random.Next(), (byte) random.Next(), (byte) random.Next(), (byte) random.Next()), + new Color32((byte) random.Next(), (byte) random.Next(), (byte) random.Next(), (byte) random.Next()), + }, writeType); + } + else if (testType == typeof(Ray)) + { + RunTypeTestLocal(new[]{ + new Ray( + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble())), + new Ray( + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble())), + new Ray( + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble())), + }, writeType); + } + else if (testType == typeof(Ray2D)) + { + RunTypeTestLocal(new[]{ + new Ray2D( + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble())), + new Ray2D( + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble())), + new Ray2D( + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble())), + }, writeType); + } + else if (testType == typeof(TestStruct)) + { + RunTypeTestLocal(new[] { + GetTestStruct(), + GetTestStruct(), + GetTestStruct(), + }, writeType); + } + else + { + Assert.Fail("No type handler was provided for this type in the test!"); + } + } + } +} diff --git a/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs.meta b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs.meta new file mode 100644 index 0000000..f0b683d --- /dev/null +++ b/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 573b1f36caed496a9c6e0eaa788d0c29 +timeCreated: 1629917174 \ No newline at end of file diff --git a/Tests/Editor/Serialization/BitCounterTests.cs b/Tests/Editor/Serialization/BitCounterTests.cs new file mode 100644 index 0000000..5e0f4c8 --- /dev/null +++ b/Tests/Editor/Serialization/BitCounterTests.cs @@ -0,0 +1,71 @@ +using NUnit.Framework; + +namespace Unity.Netcode.EditorTests +{ + public class BitCounterTests + { + [Test] + public void WhenCountingUsedBitsIn64BitValue_ResultMatchesHighBitSetPlusOne([Range(0, 63)] int highBit) + { + if (highBit == 0) + { + ulong value = 0; + // 0 is a special case. All values are considered at least 1 bit. + Assert.AreEqual(1, BitCounter.GetUsedBitCount(value)); + } + else + { + ulong value = 1UL << highBit; + Assert.AreEqual(highBit + 1, BitCounter.GetUsedBitCount(value)); + } + } + + [Test] + public void WhenCountingUsedBitsIn32BitValue_ResultMatchesHighBitSetPlusOne([Range(0, 31)] int highBit) + { + if (highBit == 0) + { + uint value = 0; + // 0 is a special case. All values are considered at least 1 bit. + Assert.AreEqual(1, BitCounter.GetUsedBitCount(value)); + } + else + { + uint value = 1U << highBit; + Assert.AreEqual(highBit + 1, BitCounter.GetUsedBitCount(value)); + } + } + + [Test] + public void WhenCountingUsedBytesIn64BitValue_ResultMatchesHighBitSetOver8PlusOne([Range(0, 63)] int highBit) + { + if (highBit == 0) + { + ulong value = 0; + // 0 is a special case. All values are considered at least 1 byte. + Assert.AreEqual(1, BitCounter.GetUsedByteCount(value)); + } + else + { + ulong value = 1UL << highBit; + Assert.AreEqual(highBit / 8 + 1, BitCounter.GetUsedByteCount(value)); + } + } + + [Test] + public void WhenCountingUsedBytesIn32BitValue_ResultMatchesHighBitSetOver8PlusOne([Range(0, 31)] int highBit) + { + if (highBit == 0) + { + uint value = 0; + // 0 is a special case. All values are considered at least 1 byte. + Assert.AreEqual(1, BitCounter.GetUsedByteCount(value)); + } + else + { + uint value = 1U << highBit; + Assert.AreEqual(highBit / 8 + 1, BitCounter.GetUsedByteCount(value)); + } + } + } +} diff --git a/Tests/Editor/Serialization/BitCounterTests.cs.meta b/Tests/Editor/Serialization/BitCounterTests.cs.meta new file mode 100644 index 0000000..64a1375 --- /dev/null +++ b/Tests/Editor/Serialization/BitCounterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76e459b9c2aeea94ebf448c237061485 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Serialization/BitReaderTests.cs b/Tests/Editor/Serialization/BitReaderTests.cs new file mode 100644 index 0000000..b6e4ef5 --- /dev/null +++ b/Tests/Editor/Serialization/BitReaderTests.cs @@ -0,0 +1,371 @@ +using System; +using NUnit.Framework; +using Unity.Collections; + +namespace Unity.Netcode.EditorTests +{ + public class BitReaderTests + { + [Test] + public void TestReadingOneBit() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBit(true); + + bitWriter.WriteBit(true); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + } + + writer.WriteByte(0b11111111); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(3)); + using (var bitReader = reader.EnterBitwiseContext()) + { + bool b; + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + } + + reader.ReadByte(out byte lastByte); + Assert.AreEqual(0b11111111, lastByte); + } + } + } + [Test] + public unsafe void TestTryBeginReadBits() + { + var nativeArray = new NativeArray(4, Allocator.Temp); + var reader = new FastBufferReader(nativeArray, Allocator.Temp); + nativeArray.Dispose(); + using (reader) + { + int* asInt = (int*)reader.GetUnsafePtr(); + *asInt = 0b11111111_00001010_10101011; + + using (var bitReader = reader.EnterBitwiseContext()) + { + Assert.Throws(() => reader.TryBeginRead(1)); + Assert.Throws(() => reader.TryBeginReadValue(1)); + Assert.IsTrue(bitReader.TryBeginReadBits(1)); + bitReader.ReadBit(out bool b); + Assert.IsTrue(b); + + // Can't use Assert.Throws() because ref struct BitWriter can't be captured in a lambda + try + { + bitReader.ReadBit(out b); + } + catch (OverflowException) + { + // Should get called here. + } + Assert.IsTrue(bitReader.TryBeginReadBits(3)); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + + byte byteVal; + try + { + bitReader.ReadBits(out byteVal, 4); + } + catch (OverflowException) + { + // Should get called here. + } + + try + { + bitReader.ReadBits(out byteVal, 1); + } + catch (OverflowException) + { + // Should get called here. + } + Assert.IsTrue(bitReader.TryBeginReadBits(3)); + + try + { + bitReader.ReadBits(out byteVal, 4); + } + catch (OverflowException) + { + // Should get called here. + } + Assert.IsTrue(bitReader.TryBeginReadBits(4)); + bitReader.ReadBits(out byteVal, 3); + Assert.AreEqual(0b010, byteVal); + + Assert.IsTrue(bitReader.TryBeginReadBits(5)); + + bitReader.ReadBits(out byteVal, 5); + Assert.AreEqual(0b10101, byteVal); + } + + Assert.AreEqual(2, reader.Position); + + Assert.IsTrue(reader.TryBeginRead(1)); + reader.ReadByte(out byte nextByte); + Assert.AreEqual(0b11111111, nextByte); + + Assert.IsTrue(reader.TryBeginRead(1)); + reader.ReadByte(out nextByte); + Assert.AreEqual(0b00000000, nextByte); + + Assert.IsFalse(reader.TryBeginRead(1)); + using (var bitReader = reader.EnterBitwiseContext()) + { + Assert.IsFalse(bitReader.TryBeginReadBits(1)); + } + } + } + + [Test] + public void TestReadingMultipleBits() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111, 1); + bitWriter.WriteBits(0b11111111, 1); + bitWriter.WriteBits(0b11111110, 2); + bitWriter.WriteBits(0b11111000, 4); + bitWriter.WriteBits(0b11111010, 4); + } + writer.WriteByte(0b11111111); + + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(3)); + using (var bitReader = reader.EnterBitwiseContext()) + { + byte b; + bitReader.ReadBits(out b, 1); + Assert.AreEqual(0b1, b); + + bitReader.ReadBits(out b, 1); + Assert.AreEqual(0b1, b); + + bitReader.ReadBits(out b, 2); + Assert.AreEqual(0b10, b); + + bitReader.ReadBits(out b, 4); + Assert.AreEqual(0b1000, b); + + bitReader.ReadBits(out b, 4); + Assert.AreEqual(0b1010, b); + } + + reader.ReadByte(out byte lastByte); + Assert.AreEqual(0b11111111, lastByte); + } + } + } + + [Test] + public void TestReadingMultipleBitsToLongs() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111UL, 1); + bitWriter.WriteBits(0b11111111UL, 1); + bitWriter.WriteBits(0b11111110UL, 2); + bitWriter.WriteBits(0b11111000UL, 4); + bitWriter.WriteBits(0b11111010UL, 4); + } + + writer.WriteByte(0b11111111); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(3)); + using (var bitReader = reader.EnterBitwiseContext()) + { + ulong ul; + bitReader.ReadBits(out ul, 1); + Assert.AreEqual(0b1, ul); + + bitReader.ReadBits(out ul, 1); + Assert.AreEqual(0b1, ul); + + bitReader.ReadBits(out ul, 2); + Assert.AreEqual(0b10, ul); + + bitReader.ReadBits(out ul, 4); + Assert.AreEqual(0b1000, ul); + + bitReader.ReadBits(out ul, 4); + Assert.AreEqual(0b1010, ul); + } + + reader.ReadByte(out byte lastByte); + Assert.AreEqual(0b11111111, lastByte); + } + } + } + + [Test] + public unsafe void TestReadingMultipleBytesToLongs([Range(1U, 64U)] uint numBits) + { + ulong value = 0xFFFFFFFFFFFFFFFF; + var reader = new FastBufferReader((byte*)&value, Allocator.Temp, sizeof(ulong)); + using (reader) + { + ulong* asUlong = (ulong*)reader.GetUnsafePtr(); + + Assert.AreEqual(value, *asUlong); + var mask = 0UL; + for (var i = 0; i < numBits; ++i) + { + mask |= (1UL << i); + } + + ulong readValue; + + Assert.IsTrue(reader.TryBeginRead(sizeof(ulong))); + using (var bitReader = reader.EnterBitwiseContext()) + { + bitReader.ReadBits(out readValue, numBits); + } + Assert.AreEqual(value & mask, readValue); + } + } + + [Test] + public unsafe void TestReadingMultipleBytesToLongsMisaligned([Range(1U, 63U)] uint numBits) + { + ulong value = 0b01010101_10101010_01010101_10101010_01010101_10101010_01010101_10101010; + var reader = new FastBufferReader((byte*)&value, Allocator.Temp, sizeof(ulong)); + using (reader) + { + ulong* asUlong = (ulong*)reader.GetUnsafePtr(); + + Assert.AreEqual(value, *asUlong); + var mask = 0UL; + for (var i = 0; i < numBits; ++i) + { + mask |= (1UL << i); + } + + ulong readValue; + + Assert.IsTrue(reader.TryBeginRead(sizeof(ulong))); + using (var bitReader = reader.EnterBitwiseContext()) + { + bitReader.ReadBit(out bool unused); + bitReader.ReadBits(out readValue, numBits); + } + Assert.AreEqual((value >> 1) & mask, readValue); + } + } + + [Test] + public unsafe void TestReadingBitsThrowsIfTryBeginReadNotCalled() + { + var nativeArray = new NativeArray(4, Allocator.Temp); + var reader = new FastBufferReader(nativeArray, Allocator.Temp); + nativeArray.Dispose(); + using (reader) + { + int* asInt = (int*)reader.GetUnsafePtr(); + *asInt = 0b11111111_00001010_10101011; + + Assert.Throws(() => + { + using var bitReader = reader.EnterBitwiseContext(); + bitReader.ReadBit(out bool b); + }); + + Assert.Throws(() => + { + using var bitReader = reader.EnterBitwiseContext(); + bitReader.ReadBits(out byte b, 1); + }); + + Assert.Throws(() => + { + using var bitReader = reader.EnterBitwiseContext(); + bitReader.ReadBits(out ulong ul, 1); + }); + + Assert.AreEqual(0, reader.Position); + + Assert.Throws(() => + { + Assert.IsTrue(reader.TryBeginRead(1)); + using var bitReader = reader.EnterBitwiseContext(); + ulong ul; + try + { + bitReader.ReadBits(out ul, 4); + bitReader.ReadBits(out ul, 4); + } + catch (OverflowException) + { + Assert.Fail("Overflow exception was thrown too early."); + throw; + } + bitReader.ReadBits(out ul, 4); + }); + + } + } + } +} diff --git a/Tests/Editor/Serialization/BitReaderTests.cs.meta b/Tests/Editor/Serialization/BitReaderTests.cs.meta new file mode 100644 index 0000000..0dc0f36 --- /dev/null +++ b/Tests/Editor/Serialization/BitReaderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 67df11865abcd5843a4e142cf6bbd901 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Serialization/BitWriterTests.cs b/Tests/Editor/Serialization/BitWriterTests.cs new file mode 100644 index 0000000..f35f725 --- /dev/null +++ b/Tests/Editor/Serialization/BitWriterTests.cs @@ -0,0 +1,333 @@ +using System; +using NUnit.Framework; +using Unity.Collections; + +namespace Unity.Netcode.EditorTests +{ + public class BitWriterTests + { + [Test] + public unsafe void TestWritingOneBit() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBit(true); + Assert.AreEqual(0b1, *asInt); + + bitWriter.WriteBit(true); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1011, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b10001011, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1010_10001011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10001011, *asInt); + + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10001011, *asInt); + } + } + [Test] + public unsafe void TestTryBeginWriteBits() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + using (var bitWriter = writer.EnterBitwiseContext()) + { + Assert.Throws(() => writer.TryBeginWrite(1)); + Assert.Throws(() => writer.TryBeginWriteValue(1)); + Assert.IsTrue(bitWriter.TryBeginWriteBits(1)); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1, *asInt); + + // Can't use Assert.Throws() because ref struct BitWriter can't be captured in a lambda + try + { + bitWriter.WriteBit(true); + } + catch (OverflowException) + { + // Should get called here. + } + + Assert.IsTrue(bitWriter.TryBeginWriteBits(3)); + bitWriter.WriteBit(true); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1011, *asInt); + + try + { + bitWriter.WriteBits(0b11111111, 4); + } + catch (OverflowException) + { + // Should get called here. + } + + try + { + bitWriter.WriteBits(0b11111111, 1); + } + catch (OverflowException) + { + // Should get called here. + } + Assert.IsTrue(bitWriter.TryBeginWriteBits(3)); + + try + { + bitWriter.WriteBits(0b11111111, 4); + } + catch (OverflowException) + { + // Should get called here. + } + Assert.IsTrue(bitWriter.TryBeginWriteBits(4)); + + bitWriter.WriteBits(0b11111010, 3); + + Assert.AreEqual(0b00101011, *asInt); + + Assert.IsTrue(bitWriter.TryBeginWriteBits(5)); + + bitWriter.WriteBits(0b11110101, 5); + Assert.AreEqual(0b1010_10101011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10101011, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(1)); + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10101011, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(1)); + writer.WriteByte(0b00000000); + Assert.AreEqual(0b11111111_00001010_10101011, *asInt); + + Assert.IsFalse(writer.TryBeginWrite(1)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + Assert.IsFalse(bitWriter.TryBeginWriteBits(1)); + } + } + } + + [Test] + public unsafe void TestWritingMultipleBits() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111, 1); + Assert.AreEqual(0b1, *asInt); + + bitWriter.WriteBits(0b11111111, 1); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBits(0b11111110, 2); + Assert.AreEqual(0b1011, *asInt); + + bitWriter.WriteBits(0b11111000, 4); + Assert.AreEqual(0b10001011, *asInt); + + bitWriter.WriteBits(0b11111010, 4); + Assert.AreEqual(0b1010_10001011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10001011, *asInt); + + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10001011, *asInt); + } + } + + [Test] + public unsafe void TestWritingMultipleBitsFromLongs() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111UL, 1); + Assert.AreEqual(0b1, *asInt); + + bitWriter.WriteBits(0b11111111UL, 1); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBits(0b11111110UL, 2); + Assert.AreEqual(0b1011, *asInt); + + bitWriter.WriteBits(0b11111000UL, 4); + Assert.AreEqual(0b10001011, *asInt); + + bitWriter.WriteBits(0b11111010UL, 4); + Assert.AreEqual(0b1010_10001011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10001011, *asInt); + + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10001011, *asInt); + } + } + + [Test] + public unsafe void TestWritingMultipleBytesFromLongs([Range(1U, 64U)] uint numBits) + { + var writer = new FastBufferWriter(sizeof(ulong), Allocator.Temp); + using (writer) + { + ulong* asUlong = (ulong*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asUlong); + var mask = 0UL; + for (var i = 0; i < numBits; ++i) + { + mask |= (1UL << i); + } + + ulong value = 0xFFFFFFFFFFFFFFFF; + + Assert.IsTrue(writer.TryBeginWrite(sizeof(ulong))); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(value, numBits); + } + Assert.AreEqual(value & mask, *asUlong); + } + } + + [Test] + public unsafe void TestWritingMultipleBytesFromLongsMisaligned([Range(1U, 63U)] uint numBits) + { + var writer = new FastBufferWriter(sizeof(ulong), Allocator.Temp); + using (writer) + { + ulong* asUlong = (ulong*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asUlong); + var mask = 0UL; + for (var i = 0; i < numBits; ++i) + { + mask |= (1UL << i); + } + + ulong value = 0xFFFFFFFFFFFFFFFF; + + Assert.IsTrue(writer.TryBeginWrite(sizeof(ulong))); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBit(false); + bitWriter.WriteBits(value, numBits); + } + Assert.AreEqual(value & mask, *asUlong >> 1); + } + } + + [Test] + public unsafe void TestWritingBitsThrowsIfTryBeginWriteNotCalled() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBit(true); + }); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBit(false); + }); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBits(0b11111111, 1); + }); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBits(0b11111111UL, 1); + }); + + Assert.AreEqual(0, writer.Position); + Assert.AreEqual(0, *asInt); + + writer.WriteByteSafe(0b11111111); + Assert.AreEqual(0b11111111, *asInt); + + + Assert.Throws(() => + { + Assert.IsTrue(writer.TryBeginWrite(1)); + using var bitWriter = writer.EnterBitwiseContext(); + try + { + bitWriter.WriteBits(0b11111111UL, 4); + bitWriter.WriteBits(0b11111111UL, 4); + } + catch (OverflowException) + { + Assert.Fail("Overflow exception was thrown too early."); + } + bitWriter.WriteBits(0b11111111UL, 1); + }); + + } + } + } +} diff --git a/Tests/Editor/Serialization/BitWriterTests.cs.meta b/Tests/Editor/Serialization/BitWriterTests.cs.meta new file mode 100644 index 0000000..3a8da0e --- /dev/null +++ b/Tests/Editor/Serialization/BitWriterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fed657e0516a72f469fbf886e3e5149a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Serialization/BufferSerializerTests.cs b/Tests/Editor/Serialization/BufferSerializerTests.cs new file mode 100644 index 0000000..c36d738 --- /dev/null +++ b/Tests/Editor/Serialization/BufferSerializerTests.cs @@ -0,0 +1,353 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class BufferSerializerTests + { + [Test] + public void TestIsReaderIsWriter() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + Assert.IsFalse(serializer.IsReader); + Assert.IsTrue(serializer.IsWriter); + } + byte[] readBuffer = new byte[4]; + var reader = new FastBufferReader(readBuffer, Allocator.Temp); + using (reader) + { + var serializer = + new BufferSerializer(new BufferSerializerReader(reader)); + Assert.IsTrue(serializer.IsReader); + Assert.IsFalse(serializer.IsWriter); + } + } + [Test] + public unsafe void TestGetUnderlyingStructs() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + FastBufferWriter underlyingWriter = serializer.GetFastBufferWriter(); + Assert.IsTrue(underlyingWriter.Handle == writer.Handle); + // Can't use Assert.Throws() because ref structs can't be passed into lambdas. + try + { + serializer.GetFastBufferReader(); + } + catch (InvalidOperationException) + { + // pass + } + + } + byte[] readBuffer = new byte[4]; + var reader = new FastBufferReader(readBuffer, Allocator.Temp); + using (reader) + { + var serializer = + new BufferSerializer(new BufferSerializerReader(reader)); + FastBufferReader underlyingReader = serializer.GetFastBufferReader(); + Assert.IsTrue(underlyingReader.Handle == reader.Handle); + // Can't use Assert.Throws() because ref structs can't be passed into lambdas. + try + { + serializer.GetFastBufferWriter(); + } + catch (InvalidOperationException) + { + // pass + } + } + } + + [Test] + public void TestSerializingValues() + { + var random = new Random(); + int value = random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + serializer.SerializeValue(ref value); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + int readValue = 0; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingBytes() + { + var random = new Random(); + byte value = (byte)random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + serializer.SerializeValue(ref value); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + byte readValue = 0; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingArrays() + { + var random = new Random(); + int[] value = { random.Next(), random.Next(), random.Next() }; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + serializer.SerializeValue(ref value); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + int[] readValue = null; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingStrings([Values] bool oneBytChars) + { + string value = "I am a test string"; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + serializer.SerializeValue(ref value, oneBytChars); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + string readValue = null; + deserializer.SerializeValue(ref readValue, oneBytChars); + + Assert.AreEqual(value, readValue); + } + } + } + + + [Test] + public void TestSerializingValuesPreChecked() + { + var random = new Random(); + int value = random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + try + { + serializer.SerializeValuePreChecked(ref value); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + serializer.SerializeValuePreChecked(ref value); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + int readValue = 0; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingBytesPreChecked() + { + var random = new Random(); + byte value = (byte)random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + try + { + serializer.SerializeValuePreChecked(ref value); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + serializer.SerializeValuePreChecked(ref value); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + byte readValue = 0; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingArraysPreChecked() + { + var random = new Random(); + int[] value = { random.Next(), random.Next(), random.Next() }; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + try + { + serializer.SerializeValuePreChecked(ref value); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + serializer.SerializeValuePreChecked(ref value); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + int[] readValue = null; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingStringsPreChecked([Values] bool oneBytChars) + { + string value = "I am a test string"; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(writer)); + try + { + serializer.SerializeValuePreChecked(ref value, oneBytChars); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value, oneBytChars))); + serializer.SerializeValuePreChecked(ref value, oneBytChars); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(reader)); + string readValue = null; + try + { + deserializer.SerializeValuePreChecked(ref readValue, oneBytChars); + } + catch (OverflowException) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value, oneBytChars))); + deserializer.SerializeValuePreChecked(ref readValue, oneBytChars); + + Assert.AreEqual(value, readValue); + } + } + } + } +} diff --git a/Tests/Editor/Serialization/BufferSerializerTests.cs.meta b/Tests/Editor/Serialization/BufferSerializerTests.cs.meta new file mode 100644 index 0000000..cb62c7e --- /dev/null +++ b/Tests/Editor/Serialization/BufferSerializerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0cf899c97866c76498b71585a61a8142 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Serialization/BytePackerTests.cs b/Tests/Editor/Serialization/BytePackerTests.cs new file mode 100644 index 0000000..c4e039b --- /dev/null +++ b/Tests/Editor/Serialization/BytePackerTests.cs @@ -0,0 +1,1103 @@ +using System; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class BytePackerTests + { + #region Test Types + + private enum ByteEnum : byte + { + A, + B, + C + } + + private enum SByteEnum : sbyte + { + A, + B, + C + } + + private enum ShortEnum : short + { + A, + B, + C + } + + private enum UShortEnum : ushort + { + A, + B, + C + } + + private enum IntEnum + { + A, + B, + C + } + + private enum UIntEnum : uint + { + A, + B, + C + } + + private enum LongEnum : long + { + A, + B, + C + } + + private enum ULongEnum : ulong + { + A, + B, + C + } + + public enum WriteType + { + WriteDirect, + WriteAsObject + } + + #endregion + + private void CheckUnsignedPackedSize64(FastBufferWriter writer, ulong value) + { + + if (value <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (value <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(value) + 1, writer.Position); + } + } + + private void CheckUnsignedPackedValue64(FastBufferWriter writer, ulong value) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(reader, out ulong readValue); + Assert.AreEqual(readValue, value); + } + } + + private void CheckUnsignedPackedSize32(FastBufferWriter writer, uint value) + { + + if (value <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (value <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(value) + 1, writer.Position); + } + } + + private void CheckUnsignedPackedValue32(FastBufferWriter writer, uint value) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(reader, out uint readValue); + Assert.AreEqual(readValue, value); + } + } + + private void CheckSignedPackedSize64(FastBufferWriter writer, long value) + { + ulong asUlong = Arithmetic.ZigZagEncode(value); + + if (asUlong <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (asUlong <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(asUlong) + 1, writer.Position); + } + } + + private void CheckSignedPackedValue64(FastBufferWriter writer, long value) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(reader, out long readValue); + Assert.AreEqual(readValue, value); + } + } + + private void CheckSignedPackedSize32(FastBufferWriter writer, int value) + { + ulong asUlong = Arithmetic.ZigZagEncode(value); + + if (asUlong <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (asUlong <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(asUlong) + 1, writer.Position); + } + } + + private void CheckSignedPackedValue32(FastBufferWriter writer, int value) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(reader, out int readValue); + Assert.AreEqual(readValue, value); + } + } + + private unsafe void VerifyBytewiseEquality(T value, T otherValue) where T : unmanaged + { + byte* asBytePointer = (byte*)&value; + byte* otherBytePointer = (byte*)&otherValue; + for (var i = 0; i < sizeof(T); ++i) + { + Assert.AreEqual(asBytePointer[i], otherBytePointer[i]); + } + } + + private unsafe void RunTypeTest(T value) where T : unmanaged + { + var writer = new FastBufferWriter(sizeof(T) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteValuePacked(writer, (dynamic)value); + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + + var outVal = new T(); + MethodInfo method; + if (value is Enum) + { + method = typeof(ByteUnpacker).GetMethods().Single(x => + x.Name == "ReadValuePacked" && x.IsGenericMethodDefinition) + .MakeGenericMethod(typeof(T)); + } + else + { + method = typeof(ByteUnpacker).GetMethod("ReadValuePacked", + new[] { typeof(FastBufferReader), typeof(T).MakeByRefType() }); + } + + object[] args = { reader, outVal }; + method.Invoke(null, args); + outVal = (T)args[1]; + Assert.AreEqual(value, outVal); + VerifyBytewiseEquality(value, outVal); + } + } + } + + + [Test] + public void TestPacking64BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(9); + ulong value = 0; + BytePacker.WriteValuePacked(writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1UL << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckUnsignedPackedSize64(writer, value); + CheckUnsignedPackedValue64(writer, value); + for (var j = 0; j < 8; ++j) + { + value = (1UL << i) | (1UL << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckUnsignedPackedSize64(writer, value); + CheckUnsignedPackedValue64(writer, value); + } + } + } + } + + [Test] + public void TestPacking32BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(9); + uint value = 0; + BytePacker.WriteValuePacked(writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1U << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckUnsignedPackedSize32(writer, value); + CheckUnsignedPackedValue32(writer, value); + for (var j = 0; j < 8; ++j) + { + value = (1U << i) | (1U << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckUnsignedPackedSize32(writer, value); + CheckUnsignedPackedValue32(writer, value); + } + } + } + } + + [Test] + public void TestPacking64BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(9); + long value = 0; + BytePacker.WriteValuePacked(writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1L << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckSignedPackedSize64(writer, value); + CheckSignedPackedValue64(writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, -value); + CheckSignedPackedSize64(writer, -value); + CheckSignedPackedValue64(writer, -value); + for (var j = 0; j < 8; ++j) + { + value = (1L << i) | (1L << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckSignedPackedSize64(writer, value); + CheckSignedPackedValue64(writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, -value); + CheckSignedPackedSize64(writer, -value); + CheckSignedPackedValue64(writer, -value); + } + } + } + } + + [Test] + public void TestPacking32BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(5); + int value = 0; + BytePacker.WriteValuePacked(writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1 << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckSignedPackedSize32(writer, value); + CheckSignedPackedValue32(writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, -value); + CheckSignedPackedSize32(writer, -value); + CheckSignedPackedValue32(writer, -value); + for (var j = 0; j < 8; ++j) + { + value = (1 << i) | (1 << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, value); + CheckSignedPackedSize32(writer, value); + CheckSignedPackedValue32(writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(writer, -value); + CheckSignedPackedSize32(writer, -value); + CheckSignedPackedValue32(writer, -value); + } + } + } + } + + private int GetByteCount61Bits(ulong value) + { + + if (value <= 0b0001_1111) + { + return 1; + } + + if (value <= 0b0001_1111_1111_1111) + { + return 2; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111) + { + return 3; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111) + { + return 4; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111) + { + return 5; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + { + return 6; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + { + return 7; + } + + return 8; + } + + private int GetByteCount30Bits(uint value) + { + + if (value <= 0b0011_1111) + { + return 1; + } + + if (value <= 0b0011_1111_1111_1111) + { + return 2; + } + + if (value <= 0b0011_1111_1111_1111_1111_1111) + { + return 3; + } + + return 4; + } + + private int GetByteCount15Bits(ushort value) + { + + if (value <= 0b0111_1111) + { + return 1; + } + + return 2; + } + + private ulong Get61BitEncodedValue(FastBufferWriter writer) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(reader, out ulong value); + return value; + } + } + + private long Get60BitSignedEncodedValue(FastBufferWriter writer) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(reader, out long value); + return value; + } + } + + private uint Get30BitEncodedValue(FastBufferWriter writer) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(reader, out uint value); + return value; + } + } + + private int Get29BitSignedEncodedValue(FastBufferWriter writer) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(reader, out int value); + return value; + } + } + + private ushort Get15BitEncodedValue(FastBufferWriter writer) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(reader, out ushort value); + return value; + } + } + + private short Get14BitSignedEncodedValue(FastBufferWriter writer) + { + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(reader, out short value); + return value; + } + } + + [Test] + public void TestBitPacking61BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(8); + ulong value = 0; + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b111); + Assert.AreEqual(value, Get61BitEncodedValue(writer)); + + for (var i = 0; i < 61; ++i) + { + value = 1UL << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount61Bits(value), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount61Bits(value) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get61BitEncodedValue(writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1UL << i) | (1UL << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount61Bits(value), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount61Bits(value) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get61BitEncodedValue(writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(writer, 1UL << 61); }); + } + } + + [Test] + public void TestBitPacking60BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(8); + long value = 0; + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b111); + Assert.AreEqual(value, Get60BitSignedEncodedValue(writer)); + + for (var i = 0; i < 61; ++i) + { + value = 1U << i; + ulong zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(writer)); + + value = -value; + zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1U << i) | (1U << j); + zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(writer)); + + value = -value; + zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(writer, 1UL << 61); }); + } + } + + [Test] + public void TestBitPacking30BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(4); + uint value = 0; + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b11); + Assert.AreEqual(value, Get30BitEncodedValue(writer)); + + for (var i = 0; i < 30; ++i) + { + value = 1U << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount30Bits(value), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount30Bits(value) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get30BitEncodedValue(writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1U << i) | (1U << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount30Bits(value), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount30Bits(value) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get30BitEncodedValue(writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(writer, 1U << 30); }); + } + } + + [Test] + public void TestBitPacking29BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(4); + int value = 0; + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b11); + Assert.AreEqual(value, Get30BitEncodedValue(writer)); + + for (var i = 0; i < 29; ++i) + { + value = 1 << i; + uint zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(writer)); + + value = -value; + zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1 << i) | (1 << j); + zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(writer)); + + value = -value; + zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(writer)); + } + } + } + } + + [Test] + public void TestBitPacking15BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(2); + ushort value = 0; + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b1); + Assert.AreEqual(value, Get15BitEncodedValue(writer)); + + for (var i = 0; i < 15; ++i) + { + value = (ushort)(1U << i); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount15Bits(value), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount15Bits(value) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get15BitEncodedValue(writer)); + + for (var j = 0; j < 8; ++j) + { + value = (ushort)((1U << i) | (1U << j)); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount15Bits(value), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount15Bits(value) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get15BitEncodedValue(writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(writer, (ushort)(1U << 15)); }); + } + } + [Test] + public void TestBitPacking14BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(2); + short value = 0; + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b1); + Assert.AreEqual(value, Get15BitEncodedValue(writer)); + + for (var i = 0; i < 14; ++i) + { + value = (short)(1 << i); + ushort zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(writer)); + + value = (short)-value; + zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(writer)); + + for (var j = 0; j < 8; ++j) + { + value = (short)((1 << i) | (1 << j)); + zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(writer)); + + value = (short)-value; + zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(writer)); + } + } + } + } + + [Test] + public void TestPackingBasicTypes( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D))] + Type testType, + [Values] WriteType writeType) + { + var random = new Random(); + + if (testType == typeof(byte)) + { + byte b = (byte)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(b); + } + } + else if (testType == typeof(sbyte)) + { + sbyte sb = (sbyte)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(sb); + } + } + else if (testType == typeof(short)) + { + short s = (short)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(s); + } + } + else if (testType == typeof(ushort)) + { + ushort us = (ushort)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(us); + } + } + else if (testType == typeof(int)) + { + int i = random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(i); + } + } + else if (testType == typeof(uint)) + { + uint ui = (uint)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(ui); + } + } + else if (testType == typeof(long)) + { + long l = ((long)random.Next() << 32) + random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(l); + } + } + else if (testType == typeof(ulong)) + { + ulong ul = ((ulong)random.Next() << 32) + (ulong)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(ul); + } + } + else if (testType == typeof(bool)) + { + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(true); + } + } + else if (testType == typeof(char)) + { + char c = 'a'; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(c); + } + + c = '\u263a'; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(c); + } + } + else if (testType == typeof(float)) + { + float f = (float)random.NextDouble(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(f); + } + } + else if (testType == typeof(double)) + { + double d = random.NextDouble(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(d); + } + } + else if (testType == typeof(ByteEnum)) + { + ByteEnum e = ByteEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(SByteEnum)) + { + SByteEnum e = SByteEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(ShortEnum)) + { + ShortEnum e = ShortEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(UShortEnum)) + { + UShortEnum e = UShortEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(IntEnum)) + { + IntEnum e = IntEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(UIntEnum)) + { + UIntEnum e = UIntEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(LongEnum)) + { + LongEnum e = LongEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(ULongEnum)) + { + ULongEnum e = ULongEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + } + else if (testType == typeof(Vector2)) + { + var v = new Vector2((float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + } + else if (testType == typeof(Vector3)) + { + var v = new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + } + else if (testType == typeof(Vector4)) + { + var v = new Vector4((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + } + else if (testType == typeof(Quaternion)) + { + var v = new Quaternion((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + } + else if (testType == typeof(Color)) + { + var v = new Color((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + } + else if (testType == typeof(Color32)) + { + var v = new Color32((byte)random.Next(), (byte)random.Next(), (byte)random.Next(), (byte)random.Next()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + } + else if (testType == typeof(Ray)) + { + // Rays need special handling on the equality checks because the constructor normalizes direction + // Which can cause slight variations in the result + var v = new Ray( + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble())); + if (writeType == WriteType.WriteDirect) + { + unsafe + { + var writer = new FastBufferWriter(sizeof(Ray) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteValuePacked(writer, v); + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(reader, out Ray outVal); + Assert.AreEqual(v.origin, outVal.origin); + Assert.AreEqual(v.direction.x, outVal.direction.x, 0.00001); + Assert.AreEqual(v.direction.y, outVal.direction.y, 0.00001); + Assert.AreEqual(v.direction.z, outVal.direction.z, 0.00001); + } + } + } + } + } + else if (testType == typeof(Ray2D)) + { + // Rays need special handling on the equality checks because the constructor normalizes direction + // Which can cause slight variations in the result + var v = new Ray2D( + new Vector2((float)random.NextDouble(), (float)random.NextDouble()), + new Vector2((float)random.NextDouble(), (float)random.NextDouble())); + if (writeType == WriteType.WriteDirect) + { + unsafe + { + var writer = new FastBufferWriter(sizeof(Ray2D) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteValuePacked(writer, v); + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(reader, out Ray2D outVal); + Assert.AreEqual(v.origin, outVal.origin); + Assert.AreEqual(v.direction.x, outVal.direction.x, 0.00001); + Assert.AreEqual(v.direction.y, outVal.direction.y, 0.00001); + } + } + } + } + } + else + { + Assert.Fail("No type handler was provided for this type in the test!"); + } + } + } +} diff --git a/Tests/Editor/Serialization/BytePackerTests.cs.meta b/Tests/Editor/Serialization/BytePackerTests.cs.meta new file mode 100644 index 0000000..4c07179 --- /dev/null +++ b/Tests/Editor/Serialization/BytePackerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b50db056cd7443b4eb2e00b603d4c15c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Serialization/FastBufferReaderTests.cs b/Tests/Editor/Serialization/FastBufferReaderTests.cs new file mode 100644 index 0000000..6156ffa --- /dev/null +++ b/Tests/Editor/Serialization/FastBufferReaderTests.cs @@ -0,0 +1,830 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class FastBufferReaderTests : BaseFastBufferReaderWriterTest + { + #region Common Checks + private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "") + { + Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission"); + writer.WriteValue((byte)0x80); + Assert.AreEqual(writeSize + 1, writer.Position, failMessage); + Assert.AreEqual(writeSize + 1, writer.Length, failMessage); + writer.WriteValue((byte)0xFF); + Assert.AreEqual(writeSize + 2, writer.Position, failMessage); + Assert.AreEqual(writeSize + 2, writer.Length, failMessage); + } + + private void VerifyCheckBytes(FastBufferReader reader, int checkPosition, string failMessage = "") + { + reader.Seek(checkPosition); + reader.TryBeginRead(2); + + reader.ReadByte(out byte value); + Assert.AreEqual(0x80, value, failMessage); + reader.ReadByte(out value); + Assert.AreEqual(0xFF, value, failMessage); + } + + private void VerifyPositionAndLength(FastBufferReader reader, int length, string failMessage = "") + { + Assert.AreEqual(0, reader.Position, failMessage); + Assert.AreEqual(length, reader.Length, failMessage); + } + + private FastBufferReader CommonChecks(FastBufferWriter writer, T valueToTest, int writeSize, string failMessage = "") where T : unmanaged + { + WriteCheckBytes(writer, writeSize, failMessage); + + var reader = new FastBufferReader(writer, Allocator.Temp); + + VerifyPositionAndLength(reader, writer.Length, failMessage); + + VerifyCheckBytes(reader, writeSize, failMessage); + + reader.Seek(0); + + return reader; + } + #endregion + + #region Generic Checks + protected override unsafe void RunTypeTest(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + Assert.AreEqual(sizeof(T), writeSize); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValue(valueToTest); + + var reader = CommonChecks(writer, valueToTest, writeSize, failMessage); + + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(FastBufferWriter.GetWriteSize())); + reader.ReadValue(out T result); + Assert.AreEqual(valueToTest, result); + } + } + } + protected override unsafe void RunTypeTestSafe(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.AreEqual(sizeof(T), writeSize); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValueSafe(valueToTest); + + + var reader = CommonChecks(writer, valueToTest, writeSize, failMessage); + + using (reader) + { + reader.ReadValueSafe(out T result); + Assert.AreEqual(valueToTest, result); + } + } + } + + private void VerifyArrayEquality(T[] value, T[] compareValue, int offset) where T : unmanaged + { + Assert.AreEqual(value.Length, compareValue.Length); + + for (var i = 0; i < value.Length; ++i) + { + Assert.AreEqual(value[i], compareValue[i]); + } + } + + protected override unsafe void RunTypeArrayTest(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + writer.WriteValue(valueToTest); + + WriteCheckBytes(writer, writeSize); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(reader, writer.Length); + + Assert.IsTrue(reader.TryBeginRead(writeSize)); + reader.ReadValue(out T[] result); + VerifyArrayEquality(valueToTest, result, 0); + + VerifyCheckBytes(reader, writeSize); + } + } + } + + protected override unsafe void RunTypeArrayTestSafe(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + + writer.WriteValueSafe(valueToTest); + + WriteCheckBytes(writer, writeSize); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(reader, writer.Length); + + reader.ReadValueSafe(out T[] result); + VerifyArrayEquality(valueToTest, result, 0); + + VerifyCheckBytes(reader, writeSize); + } + } + } + + #endregion + + #region Tests + [Test] + public void GivenFastBufferWriterContainingValue_WhenReadingUnmanagedType_ValueMatchesWhatWasWritten( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseTypeTest(testType, writeType); + } + + [Test] + public void GivenFastBufferWriterContainingValue_WhenReadingArrayOfUnmanagedElementType_ValueMatchesWhatWasWritten( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseArrayTypeTest(testType, writeType); + } + + [TestCase(false, WriteType.WriteDirect)] + [TestCase(false, WriteType.WriteSafe)] + [TestCase(true, WriteType.WriteDirect)] + [TestCase(true, WriteType.WriteSafe)] + public void GivenFastBufferWriterContainingValue_WhenReadingString_ValueMatchesWhatWasWritten(bool oneByteChars, WriteType writeType) + { + string valueToTest = "Hello, I am a test string!"; + + var serializedValueSize = FastBufferWriter.GetWriteSize(valueToTest, oneByteChars); + + var writer = new FastBufferWriter(serializedValueSize + 3, Allocator.Temp); + using (writer) + { + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(writer.TryBeginWrite(serializedValueSize + 2), "Writer denied write permission"); + writer.WriteValue(valueToTest, oneByteChars); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(valueToTest, oneByteChars); + break; + } + + WriteCheckBytes(writer, serializedValueSize); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(reader, writer.Length); + + string result = null; + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(reader.TryBeginRead(serializedValueSize + 2), "Reader denied read permission"); + reader.ReadValue(out result, oneByteChars); + break; + case WriteType.WriteSafe: + reader.ReadValueSafe(out result, oneByteChars); + break; + } + Assert.AreEqual(valueToTest, result); + + VerifyCheckBytes(reader, serializedValueSize); + } + } + } + + + [TestCase(1, 0)] + [TestCase(2, 0)] + [TestCase(3, 0)] + [TestCase(4, 0)] + [TestCase(5, 0)] + [TestCase(6, 0)] + [TestCase(7, 0)] + [TestCase(8, 0)] + + [TestCase(1, 1)] + [TestCase(2, 1)] + [TestCase(3, 1)] + [TestCase(4, 1)] + [TestCase(5, 1)] + [TestCase(6, 1)] + [TestCase(7, 1)] + + [TestCase(1, 2)] + [TestCase(2, 2)] + [TestCase(3, 2)] + [TestCase(4, 2)] + [TestCase(5, 2)] + [TestCase(6, 2)] + + [TestCase(1, 3)] + [TestCase(2, 3)] + [TestCase(3, 3)] + [TestCase(4, 3)] + [TestCase(5, 3)] + + [TestCase(1, 4)] + [TestCase(2, 4)] + [TestCase(3, 4)] + [TestCase(4, 4)] + + [TestCase(1, 5)] + [TestCase(2, 5)] + [TestCase(3, 5)] + + [TestCase(1, 6)] + [TestCase(2, 6)] + + [TestCase(1, 7)] + public void GivenFastBufferWriterContainingValue_WhenReadingPartialValue_ValueMatchesWhatWasWritten(int count, int offset) + { + var random = new Random(); + var valueToTest = ((ulong)random.Next() << 32) + (ulong)random.Next(); + var writer = new FastBufferWriter(sizeof(ulong) + 2, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(count + 2), "Writer denied write permission"); + writer.WritePartialValue(valueToTest, count, offset); + + var failMessage = $"TestReadingPartialValues failed with value {valueToTest}"; + WriteCheckBytes(writer, count, failMessage); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(reader, writer.Length, failMessage); + Assert.IsTrue(reader.TryBeginRead(count + 2), "Reader denied read permission"); + + ulong mask = 0; + for (var i = 0; i < count; ++i) + { + mask = (mask << 8) | 0b11111111; + } + + mask <<= (offset * 8); + + reader.ReadPartialValue(out ulong result, count, offset); + Assert.AreEqual(valueToTest & mask, result & mask, failMessage); + VerifyCheckBytes(reader, count, failMessage); + } + } + } + + + [Test] + public unsafe void GivenFastBufferReaderInitializedFromFastBufferWriterContainingValue_WhenCallingToArray_ReturnedArrayMatchesContentOfWriter() + { + var testStruct = GetTestStruct(); + var requiredSize = FastBufferWriter.GetWriteSize(testStruct); + var writer = new FastBufferWriter(requiredSize, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(requiredSize); + writer.WriteValue(testStruct); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + var array = reader.ToArray(); + var underlyingArray = writer.GetUnsafePtr(); + for (var i = 0; i < array.Length; ++i) + { + Assert.AreEqual(array[i], underlyingArray[i]); + } + } + } + } + + + [Test] + public void WhenCallingReadByteWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadByte(out byte b); }); + } + } + + [Test] + public void WhenCallingReadBytesWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + byte[] b = { 0, 1, 2 }; + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadBytes(ref b, 3); }); + } + } + + [Test] + public void WhenCallingReadValueWithUnmanagedTypeWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadValue(out int i); }); + } + } + + [Test] + public void WhenCallingReadValueWithByteArrayWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadValue(out byte[] b); }); + } + } + + [Test] + public void WhenCallingReadValueWithStringWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadValue(out string s); }); + } + } + + [Test] + public void WhenCallingReadValueAfterCallingTryBeginWriteWithTooFewBytes_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(sizeof(int) - 1); + Assert.Throws(() => { emptyReader.ReadValue(out int i); }); + } + } + + [Test] + public void WhenCallingReadBytePastBoundaryMarkedByTryBeginWrite_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(sizeof(int) - 1); + emptyReader.ReadByte(out byte b); + emptyReader.ReadByte(out b); + emptyReader.ReadByte(out b); + Assert.Throws(() => { emptyReader.ReadByte(out b); }); + } + } + + [Test] + public void WhenCallingReadByteDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadByte(out byte b); }); + } + } + + [Test] + public void WhenCallingReadBytesDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + byte[] b = { 0, 1, 2 }; + Assert.Throws(() => { emptyReader.ReadBytes(ref b, 3); }); + } + } + + [Test] + public void WhenCallingReadValueWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValue(out int i); }); + } + } + + [Test] + public void WhenCallingReadValueWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValue(out byte[] b); }); + } + } + + [Test] + public void WhenCallingReadValueWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValue(out string s); }); + } + } + + [Test] + public void WhenCallingReadByteSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadByteSafe(out byte b); }); + } + } + + [Test] + public void WhenCallingReadBytesSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + byte[] b = { 0, 1, 2 }; + Assert.Throws(() => { emptyReader.ReadBytesSafe(ref b, 3); }); + } + } + + [Test] + public void WhenCallingReadValueSafeWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValueSafe(out int i); }); + } + } + + [Test] + public void WhenCallingReadValueSafeWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValueSafe(out byte[] b); }); + } + } + + [Test] + public void WhenCallingReadValueSafeWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValueSafe(out string s); }); + } + } + + [Test] + public void WhenCallingReadByteAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadByte(out byte theByte); + } + } + + [Test] + public void WhenCallingReadBytesAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + + byte[] theBytes = { 0, 1, 2 }; + emptyReader.ReadBytes(ref theBytes, 3); + } + } + + [Test] + public void WhenCallingReadValueWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValue(out int i); + } + } + + [Test] + public void WhenCallingReadValueWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValue(out byte[] theBytes); + } + } + + [Test] + public void WhenCallingReadValueWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValue(out string s); + } + } + + [Test] + public void WhenCallingReadByteSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadByteSafe(out byte theByte); + } + } + + [Test] + public void WhenCallingReadBytesSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + + byte[] theBytes = { 0, 1, 2 }; + emptyReader.ReadBytesSafe(ref theBytes, 3); + } + } + + [Test] + public void WhenCallingReadValueSafeWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValueSafe(out int i); + } + } + + [Test] + public void WhenCallingReadValueSafeWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValueSafe(out byte[] theBytes); + } + } + + [Test] + public void WhenCallingReadValueSafeWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValueSafe(out string s); + } + } + + [Test] + public void WhenCallingTryBeginRead_TheAllowedReadPositionIsMarkedRelativeToCurrentPosition() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + emptyReader.ReadByte(out byte b); + emptyReader.TryBeginRead(1); + emptyReader.ReadByte(out b); + Assert.Throws(() => { emptyReader.ReadByte(out byte b); }); + } + } + + [Test] + public void WhenReadingAfterSeeking_TheNewReadComesFromTheCorrectPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.WriteByteSafe(1); + writer.WriteByteSafe(3); + writer.WriteByteSafe(2); + writer.WriteByteSafe(5); + writer.WriteByteSafe(4); + writer.WriteByteSafe(0); + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + reader.Seek(5); + reader.ReadByteSafe(out byte b); + Assert.AreEqual(reader.Position, 6); + Assert.AreEqual(reader.Length, writer.Length); + Assert.AreEqual(0, b); + + reader.Seek(0); + reader.ReadByteSafe(out b); + Assert.AreEqual(reader.Position, 1); + Assert.AreEqual(reader.Length, writer.Length); + Assert.AreEqual(1, b); + + reader.Seek(10); + Assert.AreEqual(reader.Position, writer.Length); + Assert.AreEqual(reader.Length, writer.Length); + + reader.Seek(2); + reader.ReadByteSafe(out b); + Assert.AreEqual(2, b); + + reader.Seek(1); + reader.ReadByteSafe(out b); + Assert.AreEqual(3, b); + + reader.Seek(4); + reader.ReadByteSafe(out b); + Assert.AreEqual(4, b); + + reader.Seek(3); + reader.ReadByteSafe(out b); + Assert.AreEqual(5, b); + + Assert.AreEqual(reader.Position, 4); + Assert.AreEqual(reader.Length, writer.Length); + } + } + } + + [Test] + public unsafe void WhenCallingTryBeginReadInternal_AllowedReadPositionDoesNotMoveBackward() + { + var reader = new FastBufferReader(new NativeArray(100, Allocator.Temp), Allocator.Temp); + using (reader) + { + reader.TryBeginRead(25); + reader.TryBeginReadInternal(5); + Assert.AreEqual(reader.Handle->AllowedReadMark, 25); + } + } + + #endregion + } +} diff --git a/Tests/Editor/Serialization/FastBufferReaderTests.cs.meta b/Tests/Editor/Serialization/FastBufferReaderTests.cs.meta new file mode 100644 index 0000000..52af796 --- /dev/null +++ b/Tests/Editor/Serialization/FastBufferReaderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2881f8138b479c34389b76687e5307ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Serialization/FastBufferWriterTests.cs b/Tests/Editor/Serialization/FastBufferWriterTests.cs new file mode 100644 index 0000000..6b141e6 --- /dev/null +++ b/Tests/Editor/Serialization/FastBufferWriterTests.cs @@ -0,0 +1,1069 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class FastBufferWriterTests : BaseFastBufferReaderWriterTest + { + + #region Common Checks + private void WriteCheckBytes(FastBufferWriter writer, int writeSize, string failMessage = "") + { + Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission"); + writer.WriteValue((byte)0x80); + Assert.AreEqual(writeSize + 1, writer.Position, failMessage); + Assert.AreEqual(writeSize + 1, writer.Length, failMessage); + writer.WriteValue((byte)0xFF); + Assert.AreEqual(writeSize + 2, writer.Position, failMessage); + Assert.AreEqual(writeSize + 2, writer.Length, failMessage); + } + + private void VerifyCheckBytes(byte[] underlyingArray, int writeSize, string failMessage = "") + { + Assert.AreEqual(0x80, underlyingArray[writeSize], failMessage); + Assert.AreEqual(0xFF, underlyingArray[writeSize + 1], failMessage); + } + + private unsafe void VerifyBytewiseEquality(T value, byte[] underlyingArray, int valueOffset, int bufferOffset, int size, string failMessage = "") where T : unmanaged + { + byte* asBytePointer = (byte*)&value; + for (var i = 0; i < size; ++i) + { + Assert.AreEqual(asBytePointer[i + valueOffset], underlyingArray[i + bufferOffset], failMessage); + } + } + + private unsafe void VerifyTypedEquality(T value, byte* unsafePtr) where T : unmanaged + { + var checkValue = (T*)unsafePtr; + Assert.AreEqual(value, *checkValue); + } + + private void VerifyPositionAndLength(FastBufferWriter writer, int position, string failMessage = "") + { + Assert.AreEqual(position, writer.Position, failMessage); + Assert.AreEqual(position, writer.Length, failMessage); + } + + private unsafe void CommonChecks(FastBufferWriter writer, T valueToTest, int writeSize, string failMessage = "") where T : unmanaged + { + + VerifyPositionAndLength(writer, writeSize, failMessage); + + WriteCheckBytes(writer, writeSize, failMessage); + + var underlyingArray = writer.ToArray(); + + VerifyBytewiseEquality(valueToTest, underlyingArray, 0, 0, writeSize, failMessage); + + VerifyCheckBytes(underlyingArray, writeSize, failMessage); + + VerifyTypedEquality(valueToTest, writer.GetUnsafePtr()); + } + #endregion + + #region Generic Checks + protected override unsafe void RunTypeTest(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var alternateWriteSize = FastBufferWriter.GetWriteSize(); + Assert.AreEqual(sizeof(T), writeSize); + Assert.AreEqual(sizeof(T), alternateWriteSize); + + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValue(valueToTest); + + CommonChecks(writer, valueToTest, writeSize, failMessage); + } + } + protected override unsafe void RunTypeTestSafe(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.AreEqual(sizeof(T), writeSize); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValueSafe(valueToTest); + + CommonChecks(writer, valueToTest, writeSize, failMessage); + } + } + + private unsafe void VerifyArrayEquality(T[] value, byte* unsafePtr, int offset) where T : unmanaged + { + int* sizeValue = (int*)(unsafePtr + offset); + Assert.AreEqual(value.Length, *sizeValue); + + fixed (T* asTPointer = value) + { + var underlyingTArray = (T*)(unsafePtr + sizeof(int) + offset); + for (var i = 0; i < value.Length; ++i) + { + Assert.AreEqual(asTPointer[i], underlyingTArray[i]); + } + } + } + + protected override unsafe void RunTypeArrayTest(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + writer.WriteValue(valueToTest); + VerifyPositionAndLength(writer, writeSize); + + WriteCheckBytes(writer, writeSize); + + VerifyArrayEquality(valueToTest, writer.GetUnsafePtr(), 0); + + var underlyingArray = writer.ToArray(); + VerifyCheckBytes(underlyingArray, writeSize); + } + } + + protected override unsafe void RunTypeArrayTestSafe(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + + writer.WriteValueSafe(valueToTest); + VerifyPositionAndLength(writer, writeSize); + + WriteCheckBytes(writer, writeSize); + + VerifyArrayEquality(valueToTest, writer.GetUnsafePtr(), 0); + + var underlyingArray = writer.ToArray(); + VerifyCheckBytes(underlyingArray, writeSize); + } + } + #endregion + + + #region Tests + [Test, Description("Tests ")] + public void WhenWritingUnmanagedType_ValueIsWrittenCorrectly( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseTypeTest(testType, writeType); + } + + [Test] + public void WhenWritingArrayOfUnmanagedElementType_ArrayIsWrittenCorrectly( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseArrayTypeTest(testType, writeType); + } + + [TestCase(false, WriteType.WriteDirect)] + [TestCase(false, WriteType.WriteSafe)] + [TestCase(true, WriteType.WriteDirect)] + [TestCase(true, WriteType.WriteSafe)] + public unsafe void WhenWritingString_ValueIsWrittenCorrectly(bool oneByteChars, WriteType writeType) + { + string valueToTest = "Hello, I am a test string!"; + + var serializedValueSize = FastBufferWriter.GetWriteSize(valueToTest, oneByteChars); + + var writer = new FastBufferWriter(serializedValueSize + 3, Allocator.Temp); + using (writer) + { + var offset = 0; + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(writer.TryBeginWrite(serializedValueSize + 2), "Writer denied write permission"); + writer.WriteValue(valueToTest, oneByteChars); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(valueToTest, oneByteChars); + break; + + } + + VerifyPositionAndLength(writer, serializedValueSize + offset); + WriteCheckBytes(writer, serializedValueSize + offset); + + int* sizeValue = (int*)(writer.GetUnsafePtr() + offset); + Assert.AreEqual(valueToTest.Length, *sizeValue); + + fixed (char* asCharPointer = valueToTest) + { + if (oneByteChars) + { + byte* underlyingByteArray = writer.GetUnsafePtr() + sizeof(int) + offset; + for (var i = 0; i < valueToTest.Length; ++i) + { + Assert.AreEqual((byte)asCharPointer[i], underlyingByteArray[i]); + } + + } + else + { + char* underlyingCharArray = (char*)(writer.GetUnsafePtr() + sizeof(int) + offset); + for (var i = 0; i < valueToTest.Length; ++i) + { + Assert.AreEqual(asCharPointer[i], underlyingCharArray[i]); + } + } + } + + var underlyingArray = writer.ToArray(); + VerifyCheckBytes(underlyingArray, serializedValueSize + offset); + } + } + + [TestCase(1, 0)] + [TestCase(2, 0)] + [TestCase(3, 0)] + [TestCase(4, 0)] + [TestCase(5, 0)] + [TestCase(6, 0)] + [TestCase(7, 0)] + [TestCase(8, 0)] + + [TestCase(1, 1)] + [TestCase(2, 1)] + [TestCase(3, 1)] + [TestCase(4, 1)] + [TestCase(5, 1)] + [TestCase(6, 1)] + [TestCase(7, 1)] + + [TestCase(1, 2)] + [TestCase(2, 2)] + [TestCase(3, 2)] + [TestCase(4, 2)] + [TestCase(5, 2)] + [TestCase(6, 2)] + + [TestCase(1, 3)] + [TestCase(2, 3)] + [TestCase(3, 3)] + [TestCase(4, 3)] + [TestCase(5, 3)] + + [TestCase(1, 4)] + [TestCase(2, 4)] + [TestCase(3, 4)] + [TestCase(4, 4)] + + [TestCase(1, 5)] + [TestCase(2, 5)] + [TestCase(3, 5)] + + [TestCase(1, 6)] + [TestCase(2, 6)] + + [TestCase(1, 7)] + public unsafe void WhenWritingPartialValueWithCountAndOffset_ValueIsWrittenCorrectly(int count, int offset) + { + var random = new Random(); + var valueToTest = ((ulong)random.Next() << 32) + (ulong)random.Next(); + var writer = new FastBufferWriter(sizeof(ulong) + 2, Allocator.Temp); + using (writer) + { + + Assert.IsTrue(writer.TryBeginWrite(count + 2), "Writer denied write permission"); + writer.WritePartialValue(valueToTest, count, offset); + + var failMessage = $"TestWritingPartialValues failed with value {valueToTest}"; + VerifyPositionAndLength(writer, count, failMessage); + WriteCheckBytes(writer, count, failMessage); + var underlyingArray = writer.ToArray(); + VerifyBytewiseEquality(valueToTest, underlyingArray, offset, 0, count, failMessage); + VerifyCheckBytes(underlyingArray, count, failMessage); + + ulong mask = 0; + for (var i = 0; i < count; ++i) + { + mask = (mask << 8) | 0b11111111; + } + + ulong* checkValue = (ulong*)writer.GetUnsafePtr(); + Assert.AreEqual((valueToTest >> (offset * 8)) & mask, *checkValue & mask); + } + } + + [Test] + public void WhenCallingToArray_ReturnedArrayContainsCorrectData() + { + var testStruct = GetTestStruct(); + var requiredSize = FastBufferWriter.GetWriteSize(testStruct); + var writer = new FastBufferWriter(requiredSize, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(requiredSize); + writer.WriteValue(testStruct); + var array = writer.ToArray(); + var underlyingArray = writer.ToArray(); + for (var i = 0; i < array.Length; ++i) + { + Assert.AreEqual(array[i], underlyingArray[i]); + } + } + } + + [Test] + public void WhenCallingWriteByteWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + Assert.Throws(() => { writer.WriteByte(1); }); + } + } + + [Test] + public void WhenCallingWriteBytesWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + var bytes = new byte[] { 0, 1, 2 }; + Assert.Throws(() => { writer.WriteBytes(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueWithUnmanagedTypeWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + int i = 1; + Assert.Throws(() => { writer.WriteValue(i); }); + } + } + + [Test] + public void WhenCallingWriteValueWithByteArrayWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + var bytes = new byte[] { 0, 1, 2 }; + Assert.Throws(() => { writer.WriteValue(bytes); }); + } + } + + [Test] + public void WhenCallingWriteValueWithStringWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + Assert.Throws(() => { writer.WriteValue(""); }); + } + } + + [Test] + public void WhenCallingWriteValueAfterCallingTryBeginWriteWithTooFewBytes_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + int i = 0; + writer.TryBeginWrite(sizeof(int) - 1); + Assert.Throws(() => { writer.WriteValue(i); }); + } + } + + [Test] + public void WhenCallingWriteBytePastBoundaryMarkedByTryBeginWrite_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(sizeof(int) - 1); + writer.WriteByte(1); + writer.WriteByte(2); + writer.WriteByte(3); + Assert.Throws(() => { writer.WriteByte(4); }); + } + } + + [Test] + public void WhenCallingWriteByteDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteByte(1); }); + } + } + + [Test] + public void WhenCallingWriteBytesDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytes(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + int i = 1; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValue(i); }); + } + } + + [Test] + public void WhenCallingWriteValueWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytes(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValue(""); }); + } + } + + [Test] + public void WhenCallingWriteByteSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteByteSafe(1); }); + } + } + + [Test] + public void WhenCallingWriteBytesSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytesSafe(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + int i = 1; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValueSafe(i); }); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytesSafe(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValueSafe(""); }); + } + } + + [Test] + public void WhenCallingWriteByteAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteByte(1); + } + } + + [Test] + public void WhenCallingWriteBytesAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteBytes(bytes, bytes.Length); + } + } + + [Test] + public void WhenCallingWriteValueWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + int i = 1; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValue(i); + } + } + + [Test] + public void WhenCallingWriteValueWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValue(bytes); + } + } + + [Test] + public void WhenCallingWriteValueWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValue(""); + } + } + + [Test] + public void WhenCallingWriteByteSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteByteSafe(1); + } + } + + [Test] + public void WhenCallingWriteBytesSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteBytesSafe(bytes, bytes.Length); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + int i = 1; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValueSafe(i); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValueSafe(bytes); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValueSafe(""); + } + } + + [Test] + public void WhenCallingTryBeginWrite_TheAllowedWritePositionIsMarkedRelativeToCurrentPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(100); + writer.WriteByte(1); + writer.TryBeginWrite(1); + writer.WriteByte(1); + Assert.Throws(() => { writer.WriteByte(1); }); + } + } + + [Test] + public void WhenWritingAfterSeeking_TheNewWriteGoesToTheCorrectPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(5); + writer.WriteByteSafe(0); + Assert.AreEqual(writer.Position, 6); + + writer.Seek(0); + writer.WriteByteSafe(1); + Assert.AreEqual(writer.Position, 1); + + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + + writer.Seek(2); + writer.WriteByteSafe(2); + + writer.Seek(1); + writer.WriteByteSafe(3); + + writer.Seek(4); + writer.WriteByteSafe(4); + + writer.Seek(3); + writer.WriteByteSafe(5); + + Assert.AreEqual(writer.Position, 4); + + var expected = new byte[] { 1, 3, 2, 5, 4, 0 }; + var underlyingArray = writer.ToArray(); + for (var i = 0; i < expected.Length; ++i) + { + Assert.AreEqual(expected[i], underlyingArray[i]); + } + } + } + + [Test] + public void WhenSeekingForward_LengthUpdatesToNewPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + Assert.AreEqual(writer.Length, 0); + writer.Seek(5); + Assert.AreEqual(writer.Length, 5); + writer.Seek(10); + Assert.AreEqual(writer.Length, 10); + } + } + + [Test] + public void WhenSeekingBackward_LengthDoesNotChange() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + Assert.AreEqual(writer.Length, 0); + writer.Seek(5); + Assert.AreEqual(writer.Length, 5); + writer.Seek(0); + Assert.AreEqual(writer.Length, 5); + } + } + + [Test] + public void WhenTruncatingToSpecificPositionAheadOfWritePosition_LengthIsUpdatedAndPositionIsNot() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + Assert.AreEqual(writer.Length, 10); + + writer.Seek(5); + Assert.AreEqual(writer.Position, 5); + Assert.AreEqual(writer.Length, 10); + + writer.Truncate(8); + Assert.AreEqual(writer.Position, 5); + Assert.AreEqual(writer.Length, 8); + } + } + + [Test] + public void WhenTruncatingToSpecificPositionBehindWritePosition_BothLengthAndPositionAreUpdated() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + Assert.AreEqual(writer.Length, 10); + + writer.Truncate(8); + Assert.AreEqual(writer.Position, 8); + Assert.AreEqual(writer.Length, 8); + } + } + + [Test] + public void WhenTruncatingToCurrentPosition_LengthIsUpdated() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + Assert.AreEqual(writer.Length, 10); + + writer.Seek(5); + writer.Truncate(); + Assert.AreEqual(writer.Position, 5); + Assert.AreEqual(writer.Length, 5); + } + } + + [Test] + public void WhenCreatingNewFastBufferWriter_CapacityIsCorrect() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + Assert.AreEqual(100, writer.Capacity); + writer.Dispose(); + + writer = new FastBufferWriter(200, Allocator.Temp); + Assert.AreEqual(200, writer.Capacity); + writer.Dispose(); + } + + [Test] + public void WhenCreatingNewFastBufferWriter_MaxCapacityIsCorrect() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + Assert.AreEqual(100, writer.MaxCapacity); + writer.Dispose(); + + writer = new FastBufferWriter(100, Allocator.Temp, 200); + Assert.AreEqual(200, writer.MaxCapacity); + writer.Dispose(); + } + + [Test] + public void WhenRequestingWritePastBoundsForNonGrowingWriter_TryBeginWriteReturnsFalse() + { + var writer = new FastBufferWriter(150, Allocator.Temp); + using (writer) + { + var testStruct = GetTestStruct(); + writer.TryBeginWriteValue(testStruct); + writer.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + writer.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + // Writer isn't allowed to grow because it didn't specify a maxSize + Assert.IsFalse(writer.TryBeginWriteValue(testStruct)); + } + } + + [Test] + public void WhenTryBeginWriteReturnsFalse_WritingThrowsOverflowException() + { + var writer = new FastBufferWriter(150, Allocator.Temp); + using (writer) + { + var testStruct = GetTestStruct(); + writer.TryBeginWriteValue(testStruct); + writer.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + writer.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + // Writer isn't allowed to grow because it didn't specify a maxSize + Assert.IsFalse(writer.TryBeginWriteValue(testStruct)); + Assert.Throws(() => writer.WriteValue(testStruct)); + } + } + + [Test] + public void WhenTryBeginWriteReturnsFalseAndOverflowExceptionIsThrown_DataIsNotAffected() + { + var writer = new FastBufferWriter(150, Allocator.Temp); + using (writer) + { + var testStruct = GetTestStruct(); + writer.TryBeginWriteValue(testStruct); + writer.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + writer.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + // Writer isn't allowed to grow because it didn't specify a maxSize + Assert.IsFalse(writer.TryBeginWriteValue(testStruct)); + Assert.Throws(() => writer.WriteValue(testStruct)); + VerifyBytewiseEquality(testStruct, writer.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenRequestingWritePastBoundsForGrowingWriter_BufferGrowsWithoutLosingData() + { + var growingWriter = new FastBufferWriter(150, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + growingWriter.TryBeginWriteValue(testStruct); + growingWriter.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + growingWriter.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + + // Growth doubles the size + Assert.AreEqual(300, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + growingWriter.WriteValue(testStruct); + + // Verify the growth properly copied the existing data + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 150 - FastBufferWriter.GetWriteSize(testStruct) + 1, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenRequestingWriteExactlyAtBoundsForGrowingWriter_BufferDoesntGrow() + { + var growingWriter = new FastBufferWriter(300, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + growingWriter.TryBeginWriteValue(testStruct); + growingWriter.WriteValue(testStruct); + + growingWriter.Seek(300 - FastBufferWriter.GetWriteSize(testStruct)); + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + Assert.AreEqual(300, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, growingWriter.ToArray().Length); + growingWriter.WriteValue(testStruct); + Assert.AreEqual(300, growingWriter.Position); + + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 300 - FastBufferWriter.GetWriteSize(testStruct), FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenBufferGrows_MaxCapacityIsNotExceeded() + { + var growingWriter = new FastBufferWriter(300, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + growingWriter.TryBeginWriteValue(testStruct); + growingWriter.WriteValue(testStruct); + + growingWriter.Seek(300); + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + + Assert.AreEqual(500, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 300); + + growingWriter.WriteValue(testStruct); + + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 300, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenBufferGrowthRequiredIsMoreThanDouble_BufferGrowsEnoughToContainRequestedValue() + { + var growingWriter = new FastBufferWriter(1, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + + // Buffer size doubles with each growth, so since we're starting with a size of 1, that means + // the resulting size should be the next power of 2 above the size of testStruct. + Assert.AreEqual(Math.Pow(2, Math.Ceiling(Mathf.Log(FastBufferWriter.GetWriteSize(testStruct), 2))), + growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 0); + + growingWriter.WriteValue(testStruct); + + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenTryingToWritePastMaxCapacity_GrowthDoesNotOccurAndTryBeginWriteReturnsFalse() + { + var growingWriter = new FastBufferWriter(300, Allocator.Temp, 500); + using (growingWriter) + { + Assert.IsFalse(growingWriter.TryBeginWrite(501)); + + Assert.AreEqual(300, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 0); + } + } + + [Test] + public unsafe void WhenCallingTryBeginWriteInternal_AllowedWritePositionDoesNotMoveBackward() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(25); + writer.TryBeginWriteInternal(5); + Assert.AreEqual(writer.Handle->AllowedWriteMark, 25); + } + } + #endregion + } +} diff --git a/Tests/Editor/Serialization/FastBufferWriterTests.cs.meta b/Tests/Editor/Serialization/FastBufferWriterTests.cs.meta new file mode 100644 index 0000000..b549311 --- /dev/null +++ b/Tests/Editor/Serialization/FastBufferWriterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1cef42b60935e29469ed1404fb30ba2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/SnapshotRttTests.cs b/Tests/Editor/SnapshotRttTests.cs new file mode 100644 index 0000000..bb3aa7b --- /dev/null +++ b/Tests/Editor/SnapshotRttTests.cs @@ -0,0 +1,73 @@ +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(default); + 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(NetworkManager.Singleton); + 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 new file mode 100644 index 0000000..8cb7310 --- /dev/null +++ b/Tests/Editor/SnapshotRttTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a05afab7f08d44c07b2c5e144ba0b45a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/StartStopTests.cs b/Tests/Editor/StartStopTests.cs new file mode 100644 index 0000000..9db67ae --- /dev/null +++ b/Tests/Editor/StartStopTests.cs @@ -0,0 +1,62 @@ +using NUnit.Framework; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + public class StartStopTests + { + private NetworkManager m_NetworkManager; + + [SetUp] + public void Setup() + { + // Create the reusable NetworkManager + m_NetworkManager = new GameObject(nameof(NetworkManager)).AddComponent(); + var transport = m_NetworkManager.gameObject.AddComponent(); + + m_NetworkManager.NetworkConfig = new NetworkConfig() + { + NetworkTransport = transport + }; + } + + [Test] + public void TestStopAndRestartForExceptions() + { + m_NetworkManager.StartServer(); + m_NetworkManager.Shutdown(); + m_NetworkManager.StartServer(); + m_NetworkManager.Shutdown(); + } + + [Test] + public void TestStartupServerState() + { + m_NetworkManager.StartServer(); + + Assert.True(m_NetworkManager.IsServer); + Assert.False(m_NetworkManager.IsClient); + Assert.False(m_NetworkManager.IsHost); + + m_NetworkManager.Shutdown(); + } + + [Test] + public void TestFlagShutdown() + { + m_NetworkManager.StartServer(); + m_NetworkManager.Shutdown(); + + Assert.False(m_NetworkManager.IsServer); + Assert.False(m_NetworkManager.IsClient); + Assert.False(m_NetworkManager.IsHost); + } + + [TearDown] + public void Teardown() + { + // Cleanup + Object.DestroyImmediate(m_NetworkManager.gameObject); + } + } +} diff --git a/Tests/Editor/StartStopTests.cs.meta b/Tests/Editor/StartStopTests.cs.meta new file mode 100644 index 0000000..6aee4cc --- /dev/null +++ b/Tests/Editor/StartStopTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e8e36047cb5542bcaade2a9a8746d713 +timeCreated: 1630336158 \ No newline at end of file diff --git a/Tests/Editor/Timing.meta b/Tests/Editor/Timing.meta new file mode 100644 index 0000000..6a21bd9 --- /dev/null +++ b/Tests/Editor/Timing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ff3d73d1a9b7596419b0f389d0219f31 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Timing/ClientNetworkTimeSystemTests.cs b/Tests/Editor/Timing/ClientNetworkTimeSystemTests.cs new file mode 100644 index 0000000..8913ebc --- /dev/null +++ b/Tests/Editor/Timing/ClientNetworkTimeSystemTests.cs @@ -0,0 +1,182 @@ +using System; +using NUnit.Framework; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + /// + /// Tests for running a as a client. + /// + public class ClientNetworkTimeSystemTests + { + private const double k_AcceptableRttOffset = 0.03d; // 30ms offset is fine + + /// + /// Tests whether time is stable if RTT is stable. + /// + [Test] + public void StableRttTest() + { + double receivedServerTime = 2; + + var timeSystem = new NetworkTimeSystem(0.05d, 0.05d, 0.1d); + timeSystem.Reset(receivedServerTime, 0.15); + var tickSystem = new NetworkTickSystem(60, timeSystem.LocalTime, timeSystem.ServerTime); + + Assert.True(timeSystem.LocalTime > 2); + + var steps = TimingTestHelper.GetRandomTimeSteps(100f, 0.01f, 0.1f, 42); + var rttSteps = TimingTestHelper.GetRandomTimeSteps(1000f, 0.095f, 0.105f, 42); // 10ms jitter + + // run for a while so that we reach regular RTT offset + TimingTestHelper.ApplySteps(timeSystem, tickSystem, steps, delegate (int step) + { + // sync network stats + receivedServerTime += steps[step]; + timeSystem.Sync(receivedServerTime, rttSteps[step]); + }); + + // check how we close we are to target time. + var expectedRtt = 0.1d; + var offsetToTarget = (timeSystem.LocalTime - timeSystem.ServerTime) - expectedRtt - timeSystem.ServerBufferSec - timeSystem.LocalBufferSec; + Debug.Log($"offset to target time after running for a while: {offsetToTarget}"); + Assert.IsTrue(Math.Abs(offsetToTarget) < k_AcceptableRttOffset); + + // run again, test that we never need to speed up or slow down under stable RTT + TimingTestHelper.ApplySteps(timeSystem, tickSystem, steps, delegate (int step) + { + // sync network stats + receivedServerTime += steps[step]; + timeSystem.Sync(receivedServerTime, rttSteps[step]); + }); + + // check again to ensure we are still close to the target + var newOffsetToTarget = (timeSystem.LocalTime - timeSystem.ServerTime) - expectedRtt - timeSystem.ServerBufferSec - timeSystem.LocalBufferSec; + Debug.Log($"offset to target time after running longer: {newOffsetToTarget}"); + Assert.IsTrue(Math.Abs(newOffsetToTarget) < k_AcceptableRttOffset); + + // difference between first and second offset should be minimal + var dif = offsetToTarget - newOffsetToTarget; + Assert.IsTrue(Math.Abs(dif) < 0.01d); // less than 10ms + } + + /// + /// Tests whether local time can speed up and slow down to catch up when RTT changes. + /// + [Test] + public void RttCatchupSlowdownTest() + { + double receivedServerTime = 2; + + var timeSystem = new NetworkTimeSystem(0.05d, 0.05d, 0.1d); + timeSystem.Reset(receivedServerTime, 0.15); + var tickSystem = new NetworkTickSystem(60, timeSystem.LocalTime, timeSystem.ServerTime); + + var steps = TimingTestHelper.GetRandomTimeSteps(100f, 0.01f, 0.1f, 42); + var rttSteps = TimingTestHelper.GetRandomTimeSteps(1000f, 0.095f, 0.105f, 42); // 10ms jitter + + // run for a while so that we reach regular RTT offset + TimingTestHelper.ApplySteps(timeSystem, tickSystem, steps, delegate (int step) + { + // sync network stats + receivedServerTime += steps[step]; + timeSystem.Sync(receivedServerTime, rttSteps[step]); + }); + + // increase RTT to ~200ms from ~100ms + var rttSteps2 = TimingTestHelper.GetRandomTimeSteps(1000f, 0.195f, 0.205f, 42); + + double unscaledLocalTime = timeSystem.LocalTime; + double unscaledServerTime = timeSystem.ServerTime; + TimingTestHelper.ApplySteps(timeSystem, tickSystem, steps, delegate (int step) + { + // sync network stats + unscaledLocalTime += steps[step]; + unscaledServerTime += steps[step]; + receivedServerTime += steps[step]; + timeSystem.Sync(receivedServerTime, rttSteps2[step]); + }); + + var totalLocalSpeedUpTime = timeSystem.LocalTime - unscaledLocalTime; + var totalServerSpeedUpTime = timeSystem.ServerTime - unscaledServerTime; + + // speed up of 0.1f expected + Debug.Log($"Total local speed up time catch up: {totalLocalSpeedUpTime}"); + Assert.True(Math.Abs(totalLocalSpeedUpTime - 0.1) < k_AcceptableRttOffset); + Assert.True(Math.Abs(totalServerSpeedUpTime) < k_AcceptableRttOffset); // server speedup/slowdowns should not be affected by RTT + + + // run again with RTT ~100ms and see whether we slow down by -0.1f + unscaledLocalTime = timeSystem.LocalTime; + unscaledServerTime = timeSystem.ServerTime; + + TimingTestHelper.ApplySteps(timeSystem, tickSystem, steps, delegate (int step) + { + // sync network stats + unscaledLocalTime += steps[step]; + unscaledServerTime += steps[step]; + receivedServerTime += steps[step]; + timeSystem.Sync(receivedServerTime, rttSteps[step]); + }); + + totalLocalSpeedUpTime = timeSystem.LocalTime - unscaledLocalTime; + totalServerSpeedUpTime = timeSystem.ServerTime - unscaledServerTime; + + // slow down of 0.1f expected + Debug.Log($"Total local speed up time slow down: {totalLocalSpeedUpTime}"); + Assert.True(Math.Abs(totalLocalSpeedUpTime + 0.1) < k_AcceptableRttOffset); + Assert.True(Math.Abs(totalServerSpeedUpTime) < k_AcceptableRttOffset); // server speedup/slowdowns should not be affected by RTT + + + } + + /// + /// Tests whether time resets when there is a huge spike in RTT and is able to stabilize again. + /// + [Test] + public void ResetTest() + { + double receivedServerTime = 2; + + var timeSystem = new NetworkTimeSystem(0.05d, 0.05d, 0.1d); + timeSystem.Reset(receivedServerTime, 0.15); + var tickSystem = new NetworkTickSystem(60, timeSystem.LocalTime, timeSystem.ServerTime); + + var steps = TimingTestHelper.GetRandomTimeSteps(100f, 0.01f, 0.1f, 42); + var rttSteps = TimingTestHelper.GetRandomTimeSteps(1000f, 0.095f, 0.105f, 42); // 10ms jitter + + // run for a while so that we reach regular RTT offset + TimingTestHelper.ApplySteps(timeSystem, tickSystem, steps, delegate (int step) + { + // sync network stats + receivedServerTime += steps[step]; + timeSystem.Sync(receivedServerTime, rttSteps[step]); + }); + + + // increase RTT to ~500ms from ~100ms + var rttSteps2 = TimingTestHelper.GetRandomTimeSteps(1000f, 0.495f, 0.505f, 42); + + // run a single advance expect a hard rest + + receivedServerTime += 1 / 60d; + timeSystem.Sync(receivedServerTime, 0.5); + bool reset = timeSystem.Advance(1 / 60d); + Assert.IsTrue(reset); + + TimingTestHelper.ApplySteps(timeSystem, tickSystem, steps, delegate (int step, bool reset) + { + Assert.IsFalse(reset); + + // sync network stats + receivedServerTime += steps[step]; + timeSystem.Sync(receivedServerTime, rttSteps2[step]); + + // after hard reset time should stay close to rtt + var expectedRtt = 0.5d; + Assert.IsTrue(Math.Abs((timeSystem.LocalTime - timeSystem.ServerTime) - expectedRtt - timeSystem.ServerBufferSec - timeSystem.LocalBufferSec) < k_AcceptableRttOffset); + + }); + } + } +} diff --git a/Tests/Editor/Timing/ClientNetworkTimeSystemTests.cs.meta b/Tests/Editor/Timing/ClientNetworkTimeSystemTests.cs.meta new file mode 100644 index 0000000..4d2ab78 --- /dev/null +++ b/Tests/Editor/Timing/ClientNetworkTimeSystemTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1cd95d37c9e3f904ba19b1fcf33123c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Timing/NetworkTimeTests.cs b/Tests/Editor/Timing/NetworkTimeTests.cs new file mode 100644 index 0000000..c7a3850 --- /dev/null +++ b/Tests/Editor/Timing/NetworkTimeTests.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class NetworkTimeTests + { + [Test] + [TestCase(0d, 0u)] + [TestCase(5d, 0u)] + [TestCase(-5d, 0u)] + public void TestFailCreateInvalidTime(double time, uint tickrate) + { + Assert.Throws(() => new NetworkTime(tickrate, time)); + } + + [Test] + [TestCase(0d, 0f, 20u)] + [TestCase(0d, 0f, 30u)] + [TestCase(0d, 0f, 60u)] + [TestCase(201d, 201f, 20u)] + [TestCase(201d, 201f, 30u)] + [TestCase(201d, 201f, 60u)] + [TestCase(-4301d, -4301f, 20u)] + [TestCase(-4301d, -4301f, 30u)] + [TestCase(-4301d, -4301f, 60u)] + [TestCase(float.MaxValue, float.MaxValue, 20u)] + [TestCase(float.MaxValue, float.MaxValue, 30u)] + [TestCase(float.MaxValue, float.MaxValue, 60u)] + public void TestTimeAsFloat(double d, float f, uint tickRate) + { + var networkTime = new NetworkTime(tickRate, d); + Assert.True(Mathf.Approximately(networkTime.TimeAsFloat, f)); + } + + [Test] + [TestCase(53.55d, 53.5d, 10u)] + [TestCase(1013553.55d, 1013553.5d, 10u)] + [TestCase(0d, 0d, 10u)] + [TestCase(-27.41d, -27.5d, 10u)] + [TestCase(53.55d, 53.54d, 50u)] + [TestCase(1013553.55d, 1013553.54d, 50u)] + [TestCase(0d, 0d, 50u)] + [TestCase(-27.4133d, -27.42d, 50u)] + public void TestToFixedTime(double time, double expectedFixedTime, uint tickRate) + { + Assert.AreEqual(expectedFixedTime, new NetworkTime(tickRate, time).ToFixedTime().Time); + } + + [Test] + [TestCase(34d, 0)] + [TestCase(17.32d, 0.2d / 60d)] + [TestCase(-42.44d, 1d / 60d - 0.4d / 60d)] + [TestCase(-6d, 0)] + [TestCase(int.MaxValue / 61d, 0.00082, 10d)] // Int.Max / 61 / (1/60) to get divisor then: Int.Max - divisor * 1 / 60 + public void NetworkTimeCreate(double time, double tickOffset, double epsilon = 0.0001d) + { + var networkTime = new NetworkTime(60, time); + + Assert.IsTrue(Approximately(time, networkTime.Time)); + Assert.IsTrue(Approximately(networkTime.Tick * networkTime.FixedDeltaTime + networkTime.TickOffset, networkTime.Time, epsilon)); + Assert.IsTrue(Approximately(networkTime.TickOffset, tickOffset)); + } + + [Test] + public void NetworkTimeDefault() + { + NetworkTime defaultTime = default; + + Assert.IsTrue(defaultTime.Time == 0f); + } + + [Test] + [TestCase(17.32d)] + [TestCase(34d)] + [TestCase(-42.4d)] + [TestCase(-6d)] + [TestCase(int.MaxValue / 61d)] + public void NetworkTimeAddFloatTest(double time) + { + double a = 34d; + double floatResultB = a + time; + + var timeA = new NetworkTime(60, a); + NetworkTime timeB = timeA + time; + + Assert.IsTrue(Approximately(floatResultB, timeB.Time)); + } + + [Test] + [TestCase(17.32d)] + [TestCase(34d)] + [TestCase(-42.4d)] + [TestCase(-6d)] + [TestCase(int.MaxValue / 61d)] + public void NetworkTimeSubFloatTest(double time) + { + double a = 34d; + double floatResultB = a - time; + + var timeA = new NetworkTime(60, a); + NetworkTime timeB = timeA - time; + + Assert.IsTrue(Approximately(floatResultB, timeB.Time)); + } + + [Test] + [TestCase(17.32d)] + [TestCase(34d)] + [TestCase(-42.4d)] + [TestCase(-6d)] + [TestCase(int.MaxValue / 61d)] + public void NetworkTimeAddNetworkTimeTest(double time) + { + double a = 34d; + double floatResultB = a + time; + + var timeA = new NetworkTime(60, a); + NetworkTime timeB = timeA + new NetworkTime(60, time); + Assert.IsTrue(Approximately(floatResultB, timeB.Time)); + } + + [Test] + [TestCase(17.32d)] + [TestCase(34d)] + [TestCase(-42.4d)] + [TestCase(-6d)] + [TestCase(int.MaxValue / 61d)] + public void NetworkTimeSubNetworkTimeTest(double time) + { + double a = 34d; + + double floatResultB = a - time; + + var timeA = new NetworkTime(60, a); + NetworkTime timeB = timeA - new NetworkTime(60, time); + Assert.IsTrue(Approximately(floatResultB, timeB.Time)); + } + + [Test] + public void NetworkTimeAdvanceTest() + { + var random = new Random(42); + var randomSteps = Enumerable.Repeat(0f, 1000).Select(t => Mathf.Lerp(1 / 25f, 1.80f, (float)random.NextDouble())).ToList(); + + NetworkTimeAdvanceTestInternal(randomSteps, 60, 0f); + NetworkTimeAdvanceTestInternal(randomSteps, 1, 0f); + NetworkTimeAdvanceTestInternal(randomSteps, 10, 0f); + NetworkTimeAdvanceTestInternal(randomSteps, 20, 0f); + NetworkTimeAdvanceTestInternal(randomSteps, 30, 0f); + NetworkTimeAdvanceTestInternal(randomSteps, 144, 0f); + + NetworkTimeAdvanceTestInternal(randomSteps, 60, 23132.231f); + NetworkTimeAdvanceTestInternal(randomSteps, 1, 23132.231f); + NetworkTimeAdvanceTestInternal(randomSteps, 10, 23132.231f); + NetworkTimeAdvanceTestInternal(randomSteps, 20, 23132.231f); + NetworkTimeAdvanceTestInternal(randomSteps, 30, 23132.231f); + NetworkTimeAdvanceTestInternal(randomSteps, 30, 23132.231f); + NetworkTimeAdvanceTestInternal(randomSteps, 144, 23132.231f); + + var shortSteps = Enumerable.Repeat(1 / 30f, 1000); + + NetworkTimeAdvanceTestInternal(shortSteps, 60, 0f); + NetworkTimeAdvanceTestInternal(shortSteps, 1, 0f); + NetworkTimeAdvanceTestInternal(shortSteps, 10, 0f); + NetworkTimeAdvanceTestInternal(shortSteps, 20, 0f); + NetworkTimeAdvanceTestInternal(shortSteps, 30, 0f); + NetworkTimeAdvanceTestInternal(shortSteps, 144, 0f); + + NetworkTimeAdvanceTestInternal(shortSteps, 60, 1000000f); + NetworkTimeAdvanceTestInternal(shortSteps, 60, 1000000f); + NetworkTimeAdvanceTestInternal(shortSteps, 1, 1000000f); + NetworkTimeAdvanceTestInternal(shortSteps, 10, 1000000f); + NetworkTimeAdvanceTestInternal(shortSteps, 20, 1000000f); + NetworkTimeAdvanceTestInternal(shortSteps, 30, 1000000f); + NetworkTimeAdvanceTestInternal(shortSteps, 144, 1000000f); + } + + private void NetworkTimeAdvanceTestInternal(IEnumerable steps, uint tickRate, float start, float start2 = 0f) + { + float maxAcceptableTotalOffset = 0.005f; + + var startTime = new NetworkTime(tickRate, start); + var startTime2 = new NetworkTime(tickRate, start2); + NetworkTime dif = startTime2 - startTime; + + foreach (var step in steps) + { + startTime += step; + startTime2 += step; + Assert.IsTrue(Approximately(startTime.Time, (startTime2 - dif).Time)); + } + + Assert.IsTrue(Approximately(startTime.Time, (startTime2 - dif).Time, maxAcceptableTotalOffset)); + } + + private static bool Approximately(double a, double b, double epsilon = 0.000001d) + { + var dif = Math.Abs(a - b); + return dif <= epsilon; + } + } +} diff --git a/Tests/Editor/Timing/NetworkTimeTests.cs.meta b/Tests/Editor/Timing/NetworkTimeTests.cs.meta new file mode 100644 index 0000000..74e6d2e --- /dev/null +++ b/Tests/Editor/Timing/NetworkTimeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c9814eeb95ebba4d8cbb67deab9369b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Timing/ServerNetworkTimeSystemTests.cs b/Tests/Editor/Timing/ServerNetworkTimeSystemTests.cs new file mode 100644 index 0000000..ea5fe00 --- /dev/null +++ b/Tests/Editor/Timing/ServerNetworkTimeSystemTests.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + public class ServerNetworkTimeSystemTests + { + + /// + /// On the server local time should always be equal to server time. This test ensures that this is the case. + /// + [Test] + public void LocalTimeEqualServerTimeTest() + { + var steps = TimingTestHelper.GetRandomTimeSteps(100f, 0.01f, 0.1f, 42); + + var serverTimeSystem = NetworkTimeSystem.ServerTimeSystem(); + var serverTickSystem = new NetworkTickSystem(60, 0, 0); + + serverTimeSystem.Reset(0.5d, 0); + + TimingTestHelper.ApplySteps(serverTimeSystem, serverTickSystem, steps, step => + { + Assert.IsTrue(Mathf.Approximately((float)serverTimeSystem.LocalTime, (float)serverTimeSystem.ServerTime)); + Assert.IsTrue(Mathf.Approximately((float)serverTickSystem.LocalTime.Time, (float)serverTimeSystem.ServerTime)); + }); + + Assert.IsTrue(serverTimeSystem.LocalTime > 1d); + + } + + } +} + + diff --git a/Tests/Editor/Timing/ServerNetworkTimeSystemTests.cs.meta b/Tests/Editor/Timing/ServerNetworkTimeSystemTests.cs.meta new file mode 100644 index 0000000..e045dfa --- /dev/null +++ b/Tests/Editor/Timing/ServerNetworkTimeSystemTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 970d8f921595c7249a5ffc92fd74c7fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Timing/TimingTestHelper.cs b/Tests/Editor/Timing/TimingTestHelper.cs new file mode 100644 index 0000000..16c840d --- /dev/null +++ b/Tests/Editor/Timing/TimingTestHelper.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + /// + /// Helper functions for timing related tests. Allows to get a set of time steps and simulate time advancing without the need of a full playmode test. + /// + public static class TimingTestHelper + { + public static List GetRandomTimeSteps(float totalDuration, float min, float max, int seed) + { + var random = new Random(seed); + var steps = new List(); + + while (totalDuration > 0f) + { + var next = Mathf.Lerp(min, max, (float)random.NextDouble()); + steps.Add(next); + totalDuration -= next; + } + + // correct overshoot at the end + steps[steps.Count - 1] -= totalDuration; + + return steps; + } + + public delegate void StepCheckDelegate(int step); + + public delegate void StepCheckResetDelegate(int step, bool reset); + + public static void ApplySteps(NetworkTimeSystem timeSystem, NetworkTickSystem tickSystem, List steps, StepCheckDelegate stepCheck = null) + { + for (var i = 0; i < steps.Count; i++) + { + var step = steps[i]; + timeSystem.Advance(step); + tickSystem.UpdateTick(timeSystem.LocalTime, timeSystem.ServerTime); + if (stepCheck != null) + { + stepCheck(i); + } + } + } + + public static void ApplySteps(NetworkTimeSystem timeSystem, NetworkTickSystem tickSystem, List steps, StepCheckResetDelegate stepCheck = null) + { + for (var i = 0; i < steps.Count; i++) + { + var step = steps[i]; + var reset = timeSystem.Advance(step); + tickSystem.UpdateTick(timeSystem.LocalTime, timeSystem.ServerTime); + if (stepCheck != null) + { + stepCheck(i, reset); + } + } + } + } +} + diff --git a/Tests/Editor/Timing/TimingTestHelper.cs.meta b/Tests/Editor/Timing/TimingTestHelper.cs.meta new file mode 100644 index 0000000..a258213 --- /dev/null +++ b/Tests/Editor/Timing/TimingTestHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9010f986d5ecb994a8dd34076ff41c8e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/UI.meta b/Tests/Editor/UI.meta new file mode 100644 index 0000000..f22cc75 --- /dev/null +++ b/Tests/Editor/UI.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f672293e0efc41a6a7e930fd7ff14436 +timeCreated: 1631650280 \ No newline at end of file diff --git a/Tests/Editor/UI/UITestHelpers.cs b/Tests/Editor/UI/UITestHelpers.cs new file mode 100644 index 0000000..5eefc13 --- /dev/null +++ b/Tests/Editor/UI/UITestHelpers.cs @@ -0,0 +1,15 @@ +using Unity.Netcode.Editor; +using UnityEditor; +using UnityEngine; + +namespace Unity.Netcode.EditorTests +{ + internal static class UITestHelpers + { + [MenuItem("Netcode/UI/Reset Multiplayer Tools Tip Status")] + private static void ResetMultiplayerToolsTipStatus() + { + PlayerPrefs.DeleteKey(NetworkManagerEditor.InstallMultiplayerToolsTipDismissedPlayerPrefKey); + } + } +} diff --git a/Tests/Editor/UI/UITestHelpers.cs.meta b/Tests/Editor/UI/UITestHelpers.cs.meta new file mode 100644 index 0000000..c9addc4 --- /dev/null +++ b/Tests/Editor/UI/UITestHelpers.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bde5fc3349494f77bebd0be12a6957e1 +timeCreated: 1631650292 \ No newline at end of file diff --git a/Tests/Editor/com.unity.netcode.editortests.asmdef b/Tests/Editor/com.unity.netcode.editortests.asmdef new file mode 100644 index 0000000..d70259e --- /dev/null +++ b/Tests/Editor/com.unity.netcode.editortests.asmdef @@ -0,0 +1,36 @@ +{ + "name": "Unity.Netcode.EditorTests", + "rootNamespace": "Unity.Netcode.EditorTests", + "references": [ + "Unity.Netcode.Runtime", + "Unity.Netcode.Editor", + "Unity.Netcode.Components", + "Unity.Multiplayer.MetricTypes", + "Unity.Multiplayer.NetStats" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false, + "versionDefines": [ + { + "name": "com.unity.multiplayer.tools", + "expression": "", + "define": "MULTIPLAYER_TOOLS" + } + ] +} \ No newline at end of file diff --git a/Tests/Editor/com.unity.netcode.editortests.asmdef.meta b/Tests/Editor/com.unity.netcode.editortests.asmdef.meta new file mode 100644 index 0000000..ab7cbd5 --- /dev/null +++ b/Tests/Editor/com.unity.netcode.editortests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 072b82e2b7c1dcf439827d3fbc4f52a1 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime.meta b/Tests/Runtime.meta new file mode 100644 index 0000000..112ba35 --- /dev/null +++ b/Tests/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9d3a545f9ce7b074c95a3b1c3101dee9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/AssemblyInfo.cs b/Tests/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..f3cdeaf --- /dev/null +++ b/Tests/Runtime/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("TestProject.EditorTests")] +[assembly: InternalsVisibleTo("TestProject.RuntimeTests")] +[assembly: InternalsVisibleTo("TestProject.ToolsIntegration.RuntimeTests")] diff --git a/Tests/Runtime/AssemblyInfo.cs.meta b/Tests/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..e1f992f --- /dev/null +++ b/Tests/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5751b3c3bb5621e4686249b8083be068 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/BaseMultiInstanceTest.cs b/Tests/Runtime/BaseMultiInstanceTest.cs new file mode 100644 index 0000000..feed6e8 --- /dev/null +++ b/Tests/Runtime/BaseMultiInstanceTest.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + public abstract class BaseMultiInstanceTest + { + private const string k_FirstPartOfTestRunnerSceneName = "InitTestScene"; + + protected GameObject m_PlayerPrefab; + protected NetworkManager m_ServerNetworkManager; + protected NetworkManager[] m_ClientNetworkManagers; + + protected abstract int NbClients { get; } + + protected bool m_BypassStartAndWaitForClients = false; + + [UnitySetUp] + public virtual IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, _ => { }); + } + + [UnityTearDown] + public virtual IEnumerator Teardown() + { + // Shutdown and clean up both of our NetworkManager instances + try + { + MultiInstanceHelpers.Destroy(); + } + catch (Exception e) { throw e; } + finally + { + if (m_PlayerPrefab != null) + { + Object.Destroy(m_PlayerPrefab); + m_PlayerPrefab = null; + } + } + + // Make sure any NetworkObject with a GlobalObjectIdHash value of 0 is destroyed + // If we are tearing down, we don't want to leave NetworkObjects hanging around + var networkObjects = Object.FindObjectsOfType().ToList(); + foreach (var networkObject in networkObjects) + { + Object.DestroyImmediate(networkObject); + } + + // wait for next frame so everything is destroyed, so following tests can execute from clean environment + int nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + + /// + /// We want to exclude the TestRunner scene on the host-server side so it won't try to tell clients to + /// synchronize to this scene when they connect + /// + private static bool VerifySceneIsValidForClientsToLoad(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode) + { + // exclude test runner scene + if (sceneName.StartsWith(k_FirstPartOfTestRunnerSceneName)) + { + return false; + } + return true; + } + + /// + /// This registers scene validation callback for the server to prevent it from telling connecting + /// clients to synchronize (i.e. load) the test runner scene. This will also register the test runner + /// scene and its handle for both client(s) and server-host. + /// + public static void SceneManagerValidationAndTestRunnerInitialization(NetworkManager networkManager) + { + // If VerifySceneBeforeLoading is not already set, then go ahead and set it so the host/server + // will not try to synchronize clients to the TestRunner scene. We only need to do this for the server. + if (networkManager.IsServer && networkManager.SceneManager.VerifySceneBeforeLoading == null) + { + networkManager.SceneManager.VerifySceneBeforeLoading = VerifySceneIsValidForClientsToLoad; + // If a unit/integration test does not handle this on their own, then Ignore the validation warning + networkManager.SceneManager.DisableValidationWarnings(true); + } + + // Register the test runner scene so it will be able to synchronize NetworkObjects without logging a + // warning about using the currently active scene + var scene = SceneManager.GetActiveScene(); + // As long as this is a test runner scene (or most likely a test runner scene) + if (scene.name.StartsWith(k_FirstPartOfTestRunnerSceneName)) + { + // Register the test runner scene just so we avoid another warning about not being able to find the + // scene to synchronize NetworkObjects. Next, add the currently active test runner scene to the scenes + // loaded and register the server to client scene handle since host-server shares the test runner scene + // with the clients. + networkManager.SceneManager.GetAndAddNewlyLoadedSceneByName(scene.name); + networkManager.SceneManager.ServerSceneHandleToClientSceneHandle.Add(scene.handle, scene.handle); + } + } + + /// + /// Utility to spawn some clients and a server and set them up + /// + /// + /// Update the prefab with whatever is needed before players spawn + /// The targetFrameRate of the Unity engine to use while this multi instance test is running. Will be reset on teardown. + /// + public IEnumerator StartSomeClientsAndServerWithPlayers(bool useHost, int nbClients, Action updatePlayerPrefab = null, int targetFrameRate = 60) + { + // Make sure any NetworkObject with a GlobalObjectIdHash value of 0 is destroyed + // If we are tearing down, we don't want to leave NetworkObjects hanging around + var networkObjects = Object.FindObjectsOfType().ToList(); + var networkObjectsList = networkObjects.Where(c => c.GlobalObjectIdHash == 0); + foreach (var netObject in networkObjects) + { + Object.DestroyImmediate(netObject); + } + + // Create multiple NetworkManager instances + if (!MultiInstanceHelpers.Create(nbClients, out NetworkManager server, out NetworkManager[] clients, targetFrameRate)) + { + Debug.LogError("Failed to create instances"); + Assert.Fail("Failed to create instances"); + } + + m_ClientNetworkManagers = clients; + m_ServerNetworkManager = server; + + // Create playerPrefab + m_PlayerPrefab = new GameObject("Player"); + NetworkObject networkObject = m_PlayerPrefab.AddComponent(); + /* + * Normally we would only allow player prefabs to be set to a prefab. Not runtime created objects. + * In order to prevent having a Resource folder full of a TON of prefabs that we have to maintain, + * MultiInstanceHelper has a helper function that lets you mark a runtime created object to be + * treated as a prefab by the Netcode. That's how we can get away with creating the player prefab + * at runtime without it being treated as a SceneObject or causing other conflicts with the Netcode. + */ + // Make it a prefab + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObject); + + if (updatePlayerPrefab != null) + { + updatePlayerPrefab(m_PlayerPrefab); // update player prefab with whatever is needed before players are spawned + } + + // Set the player prefab + server.NetworkConfig.PlayerPrefab = m_PlayerPrefab; + + for (int i = 0; i < clients.Length; i++) + { + clients[i].NetworkConfig.PlayerPrefab = m_PlayerPrefab; + } + + if (!m_BypassStartAndWaitForClients) + { + // 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 (!MultiInstanceHelpers.Start(useHost, server, clients, SceneManagerValidationAndTestRunnerInitialization)) + { + Debug.LogError("Failed to start instances"); + Assert.Fail("Failed to start instances"); + } + + // Wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients)); + + // Wait for connection on server side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnectedToServer(server, useHost ? nbClients + 1 : nbClients)); + } + } + } +} diff --git a/Tests/Runtime/BaseMultiInstanceTest.cs.meta b/Tests/Runtime/BaseMultiInstanceTest.cs.meta new file mode 100644 index 0000000..94eb219 --- /dev/null +++ b/Tests/Runtime/BaseMultiInstanceTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 789a3189410645aca48f11a51c823418 +timeCreated: 1621620979 \ No newline at end of file diff --git a/Tests/Runtime/Components.meta b/Tests/Runtime/Components.meta new file mode 100644 index 0000000..9b77361 --- /dev/null +++ b/Tests/Runtime/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 09f6601e441556642ab8217941b24e5c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Components/BufferDataValidationComponent.cs b/Tests/Runtime/Components/BufferDataValidationComponent.cs new file mode 100644 index 0000000..759cce1 --- /dev/null +++ b/Tests/Runtime/Components/BufferDataValidationComponent.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Used in conjunction with the RpcQueueTest to validate from 1 byte to (n) MaximumBufferSize + /// - Sending and Receiving a continually growing buffer up to (MaximumBufferSize) + /// - Default maximum buffer size is 1MB + /// + public class BufferDataValidationComponent : NetworkBehaviour + { + /// + /// Allows the external RPCQueueTest to begin testing or stop it + /// + public bool EnableTesting; + + /// + /// The maximum size of the buffer to send + /// + public int MaximumBufferSize = 1 << 15; + + /// + /// The rate at which the buffer size increases until it reaches MaximumBufferSize + /// (the default starting buffer size is 1 bytes) + /// + public int BufferSizeStart = 1; + + /// + /// Is checked to determine if the test exited because it failed + /// + public bool TestFailed { get; internal set; } + + private bool m_WaitForValidation; + private int m_CurrentBufferSize; + + private List m_SendBuffer; + private List m_PreCalculatedBufferValues; + + // Start is called before the first frame update + private void Start() + { + m_WaitForValidation = false; + m_CurrentBufferSize = BufferSizeStart; + m_SendBuffer = new List(MaximumBufferSize + 1); + m_PreCalculatedBufferValues = new List(MaximumBufferSize + 1); + while (m_PreCalculatedBufferValues.Count <= MaximumBufferSize) + { + m_PreCalculatedBufferValues.Add((byte)Random.Range(0, 255)); + } + } + + /// + /// Returns back whether the test has completed the total number of iterations + /// + /// + public bool IsTestComplete() + { + if (m_CurrentBufferSize > MaximumBufferSize || TestFailed) + { + return true; + } + return false; + } + + // Update is called once per frame + private void Update() + { + if (NetworkManager.Singleton.IsListening && EnableTesting && !IsTestComplete() && !m_WaitForValidation) + { + m_SendBuffer.Clear(); + //Keep the current contents of the bufffer and fill the buffer with the delta difference of the buffer's current size and new size from the m_PreCalculatedBufferValues + m_SendBuffer.AddRange(m_PreCalculatedBufferValues.GetRange(0, m_CurrentBufferSize)); + + //Make sure we don't do anything until we finish validating buffer + m_WaitForValidation = true; + + //Send the buffer + SendBufferServerRpc(m_SendBuffer.ToArray()); + } + } + + /// + /// Server side RPC for testing + /// + /// server rpc parameters + [ServerRpc] + private void SendBufferServerRpc(byte[] buffer) + { + TestFailed = !NetworkManagerHelper.BuffersMatch(0, buffer.Length, buffer, m_SendBuffer.ToArray()); + if (!TestFailed) + { + Debug.Log($"Tested buffer size of {m_SendBuffer.Count} -- OK"); + } + + if (m_CurrentBufferSize == MaximumBufferSize) + { + m_CurrentBufferSize++; + } + else + { + //Increasse buffer size + m_CurrentBufferSize = m_CurrentBufferSize << 1; + } + + m_WaitForValidation = false; + } + } +} diff --git a/Tests/Runtime/Components/BufferDataValidationComponent.cs.meta b/Tests/Runtime/Components/BufferDataValidationComponent.cs.meta new file mode 100644 index 0000000..e1b3497 --- /dev/null +++ b/Tests/Runtime/Components/BufferDataValidationComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1bb4bf89a220a8b409a4afb1f0f4eced +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 new file mode 100644 index 0000000..96d11b3 --- /dev/null +++ b/Tests/Runtime/Components/NetworkVariableTestComponent.cs @@ -0,0 +1,242 @@ +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// This provides coverage for all of the predefined NetworkVariable types + /// The initial goal is for generalized full coverage of NetworkVariables: + /// Covers all of the various constructor calls (i.e. various parameters or no parameters) + /// Covers the local NetworkVariable's OnValueChanged functionality (i.e. when a specific type changes do we get a notification?) + /// This was built as a NetworkBehaviour for further client-server unit testing patterns when this capability is available. + /// + internal class NetworkVariableTestComponent : NetworkBehaviour + { + private NetworkVariable m_NetworkVariableBool; + private NetworkVariable m_NetworkVariableByte; + private NetworkVariable m_NetworkVariableColor; + private NetworkVariable m_NetworkVariableColor32; + private NetworkVariable m_NetworkVariableDouble; + private NetworkVariable m_NetworkVariableFloat; + private NetworkVariable m_NetworkVariableInt; + private NetworkVariable m_NetworkVariableLong; + private NetworkVariable m_NetworkVariableSByte; + private NetworkVariable m_NetworkVariableQuaternion; + private NetworkVariable m_NetworkVariableShort; + private NetworkVariable m_NetworkVariableVector4; + private NetworkVariable m_NetworkVariableVector3; + private NetworkVariable m_NetworkVariableVector2; + private NetworkVariable m_NetworkVariableRay; + private NetworkVariable m_NetworkVariableULong; + private NetworkVariable m_NetworkVariableUInt; + private NetworkVariable m_NetworkVariableUShort; + + + public NetworkVariableHelper Bool_Var; + public NetworkVariableHelper Byte_Var; + public NetworkVariableHelper Color_Var; + public NetworkVariableHelper Color32_Var; + public NetworkVariableHelper Double_Var; + public NetworkVariableHelper Float_Var; + public NetworkVariableHelper Int_Var; + public NetworkVariableHelper Long_Var; + public NetworkVariableHelper Sbyte_Var; + public NetworkVariableHelper Quaternion_Var; + public NetworkVariableHelper Short_Var; + public NetworkVariableHelper Vector4_Var; + public NetworkVariableHelper Vector3_Var; + public NetworkVariableHelper Vector2_Var; + public NetworkVariableHelper Ray_Var; + public NetworkVariableHelper Ulong_Var; + public NetworkVariableHelper Uint_Var; + public NetworkVariableHelper Ushort_Var; + + + public bool EnableTesting; + private bool m_Initialized; + private bool m_FinishedTests; + private bool m_ChangesAppliedToNetworkVariables; + + private float m_WaitForChangesTimeout; + + // Start is called before the first frame update + private void InitializeTest() + { + // Generic Constructor Test Coverage + m_NetworkVariableBool = new NetworkVariable(); + m_NetworkVariableByte = new NetworkVariable(); + m_NetworkVariableColor = new NetworkVariable(); + m_NetworkVariableColor32 = new NetworkVariable(); + m_NetworkVariableDouble = new NetworkVariable(); + m_NetworkVariableFloat = new NetworkVariable(); + m_NetworkVariableInt = new NetworkVariable(); + m_NetworkVariableLong = new NetworkVariable(); + m_NetworkVariableSByte = new NetworkVariable(); + m_NetworkVariableQuaternion = new NetworkVariable(); + m_NetworkVariableShort = new NetworkVariable(); + m_NetworkVariableVector4 = new NetworkVariable(); + m_NetworkVariableVector3 = new NetworkVariable(); + m_NetworkVariableVector2 = new NetworkVariable(); + m_NetworkVariableRay = new NetworkVariable(); + m_NetworkVariableULong = new NetworkVariable(); + m_NetworkVariableUInt = new NetworkVariable(); + m_NetworkVariableUShort = new NetworkVariable(); + + + // NetworkVariable Value Type Constructor Test Coverage + m_NetworkVariableBool = new NetworkVariable(true); + m_NetworkVariableByte = new NetworkVariable((byte)0); + m_NetworkVariableColor = new NetworkVariable(new Color(1, 1, 1, 1)); + m_NetworkVariableColor32 = new NetworkVariable(new Color32(1, 1, 1, 1)); + m_NetworkVariableDouble = new NetworkVariable(1.0); + m_NetworkVariableFloat = new NetworkVariable(1.0f); + m_NetworkVariableInt = new NetworkVariable(1); + m_NetworkVariableLong = new NetworkVariable(1); + m_NetworkVariableSByte = new NetworkVariable((sbyte)0); + m_NetworkVariableQuaternion = new NetworkVariable(Quaternion.identity); + m_NetworkVariableShort = new NetworkVariable(256); + m_NetworkVariableVector4 = new NetworkVariable(new Vector4(1, 1, 1, 1)); + m_NetworkVariableVector3 = new NetworkVariable(new Vector3(1, 1, 1)); + m_NetworkVariableVector2 = new NetworkVariable(new Vector2(1, 1)); + m_NetworkVariableRay = new NetworkVariable(new Ray()); + m_NetworkVariableULong = new NetworkVariable(1); + 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); + Byte_Var = new NetworkVariableHelper(m_NetworkVariableByte); + Color_Var = new NetworkVariableHelper(m_NetworkVariableColor); + Color32_Var = new NetworkVariableHelper(m_NetworkVariableColor32); + Double_Var = new NetworkVariableHelper(m_NetworkVariableDouble); + Float_Var = new NetworkVariableHelper(m_NetworkVariableFloat); + Int_Var = new NetworkVariableHelper(m_NetworkVariableInt); + Long_Var = new NetworkVariableHelper(m_NetworkVariableLong); + Sbyte_Var = new NetworkVariableHelper(m_NetworkVariableSByte); + Quaternion_Var = new NetworkVariableHelper(m_NetworkVariableQuaternion); + Short_Var = new NetworkVariableHelper(m_NetworkVariableShort); + Vector4_Var = new NetworkVariableHelper(m_NetworkVariableVector4); + Vector3_Var = new NetworkVariableHelper(m_NetworkVariableVector3); + Vector2_Var = new NetworkVariableHelper(m_NetworkVariableVector2); + Ray_Var = new NetworkVariableHelper(m_NetworkVariableRay); + Ulong_Var = new NetworkVariableHelper(m_NetworkVariableULong); + Uint_Var = new NetworkVariableHelper(m_NetworkVariableUInt); + Ushort_Var = new NetworkVariableHelper(m_NetworkVariableUShort); + } + + /// + /// Test result for all values changed the expected number of times (once per unique NetworkVariable type) + /// + public bool DidAllValuesChange() + { + if (NetworkVariableBaseHelper.VarChangedCount == NetworkVariableBaseHelper.InstanceCount) + { + return true; + } + else + { + return false; + } + } + + /// + /// Returns back whether the test has completed the total number of iterations + /// + public bool IsTestComplete() + { + return m_FinishedTests; + } + + // Update is called once per frame + private void Update() + { + if (EnableTesting) + { + //Added timeout functionality for near future changes to NetworkVariables and the Snapshot system + if (!m_FinishedTests && m_ChangesAppliedToNetworkVariables) + { + //We finish testing if all NetworkVariables changed their value or we timed out waiting for + //all NetworkVariables to change their value + m_FinishedTests = DidAllValuesChange() || (m_WaitForChangesTimeout < Time.realtimeSinceStartup); + } + else + { + if (NetworkManager != null && NetworkManager.IsListening) + { + if (!m_Initialized) + { + InitializeTest(); + m_Initialized = true; + } + else + { + //Now change all of the values to make sure we are at least testing the local callback + m_NetworkVariableBool.Value = false; + m_NetworkVariableByte.Value = 255; + m_NetworkVariableColor.Value = new Color(100, 100, 100); + m_NetworkVariableColor32.Value = new Color32(100, 100, 100, 100); + m_NetworkVariableDouble.Value = 1000; + m_NetworkVariableFloat.Value = 1000.0f; + m_NetworkVariableInt.Value = 1000; + m_NetworkVariableLong.Value = 100000; + m_NetworkVariableSByte.Value = -127; + m_NetworkVariableQuaternion.Value = new Quaternion(100, 100, 100, 100); + m_NetworkVariableShort.Value = short.MaxValue; + m_NetworkVariableVector4.Value = new Vector4(1000, 1000, 1000, 1000); + m_NetworkVariableVector3.Value = new Vector3(1000, 1000, 1000); + m_NetworkVariableVector2.Value = new Vector2(1000, 1000); + m_NetworkVariableRay.Value = new Ray(Vector3.one, Vector3.right); + m_NetworkVariableULong.Value = ulong.MaxValue; + m_NetworkVariableUInt.Value = uint.MaxValue; + m_NetworkVariableUShort.Value = ushort.MaxValue; + + //Set the timeout (i.e. how long we will wait for all NetworkVariables to have registered their changes) + m_WaitForChangesTimeout = Time.realtimeSinceStartup + 0.50f; + m_ChangesAppliedToNetworkVariables = true; + } + } + } + } + } + } +} diff --git a/Tests/Runtime/Components/NetworkVariableTestComponent.cs.meta b/Tests/Runtime/Components/NetworkVariableTestComponent.cs.meta new file mode 100644 index 0000000..d6f1e19 --- /dev/null +++ b/Tests/Runtime/Components/NetworkVariableTestComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef882d7ba8231eb45839424f54a12486 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/ConnectionApproval.cs b/Tests/Runtime/ConnectionApproval.cs new file mode 100644 index 0000000..3b48766 --- /dev/null +++ b/Tests/Runtime/ConnectionApproval.cs @@ -0,0 +1,66 @@ +using System; +using System.Text; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using NUnit.Framework; + +namespace Unity.Netcode.RuntimeTests +{ + public class ConnectionApprovalTests + { + private Guid m_ValidationToken; + private bool m_IsValidated; + + [SetUp] + public void Setup() + { + // Create, instantiate, and host + Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _, NetworkManagerHelper.NetworkManagerOperatingMode.None)); + m_ValidationToken = Guid.NewGuid(); + } + + [UnityTest] + public IEnumerator ConnectionApproval() + { + NetworkManagerHelper.NetworkManagerObject.ConnectionApprovalCallback += NetworkManagerObject_ConnectionApprovalCallback; + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.ConnectionApproval = true; + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.PlayerPrefab = null; + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.ConnectionData = Encoding.UTF8.GetBytes(m_ValidationToken.ToString()); + m_IsValidated = false; + NetworkManagerHelper.NetworkManagerObject.StartHost(); + + var timeOut = Time.realtimeSinceStartup + 3.0f; + var timedOut = false; + while (!m_IsValidated) + { + yield return new WaitForSeconds(0.01f); + if (timeOut < Time.realtimeSinceStartup) + { + timedOut = true; + } + } + + //Make sure we didn't time out + Assert.False(timedOut); + Assert.True(m_IsValidated); + } + + private void NetworkManagerObject_ConnectionApprovalCallback(byte[] connectionData, ulong clientId, NetworkManager.ConnectionApprovedDelegate callback) + { + var stringGuid = Encoding.UTF8.GetString(connectionData); + if (m_ValidationToken.ToString() == stringGuid) + { + m_IsValidated = true; + } + callback(false, null, m_IsValidated, null, null); + } + + [TearDown] + public void TearDown() + { + // Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + } + } +} diff --git a/Tests/Runtime/ConnectionApproval.cs.meta b/Tests/Runtime/ConnectionApproval.cs.meta new file mode 100644 index 0000000..37a4ced --- /dev/null +++ b/Tests/Runtime/ConnectionApproval.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 52ef2017d72b57f418907e98e1d8b90a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/DisconnectTests.cs b/Tests/Runtime/DisconnectTests.cs new file mode 100644 index 0000000..45fb045 --- /dev/null +++ b/Tests/Runtime/DisconnectTests.cs @@ -0,0 +1,53 @@ +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class DisconnectTests + { + [UnityTest] + public IEnumerator RemoteDisconnectPlayerObjectCleanup() + { + // create server and client instances + MultiInstanceHelpers.Create(1, out NetworkManager server, out NetworkManager[] clients); + + // create prefab + var gameObject = new GameObject("PlayerObject"); + var networkObject = gameObject.AddComponent(); + networkObject.DontDestroyWithOwner = true; + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObject); + + server.NetworkConfig.PlayerPrefab = gameObject; + + for (int i = 0; i < clients.Length; i++) + { + clients[i].NetworkConfig.PlayerPrefab = gameObject; + } + + // start server and connect clients + MultiInstanceHelpers.Start(false, server, clients); + + // wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients)); + + // wait for connection on server side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientConnectedToServer(server)); + + // disconnect the remote client + server.DisconnectClient(clients[0].LocalClientId); + + // wait 1 frame because destroys are delayed + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + // ensure the object was destroyed + Assert.False(server.SpawnManager.SpawnedObjects.Any(x => x.Value.IsPlayerObject && x.Value.OwnerClientId == clients[0].LocalClientId)); + + // cleanup + MultiInstanceHelpers.Destroy(); + } + } +} diff --git a/Tests/Runtime/DisconnectTests.cs.meta b/Tests/Runtime/DisconnectTests.cs.meta new file mode 100644 index 0000000..5528fb9 --- /dev/null +++ b/Tests/Runtime/DisconnectTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b05b4daca3854ff6b01a1f002d433dd6 +timeCreated: 1631652586 \ No newline at end of file diff --git a/Tests/Runtime/Helpers.meta b/Tests/Runtime/Helpers.meta new file mode 100644 index 0000000..0602aaa --- /dev/null +++ b/Tests/Runtime/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fb1b6e801936c7f4a9af28dbed5ea2ff +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Helpers/NetworkManagerHelper.cs b/Tests/Runtime/Helpers/NetworkManagerHelper.cs new file mode 100644 index 0000000..0b40a2b --- /dev/null +++ b/Tests/Runtime/Helpers/NetworkManagerHelper.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using NUnit.Framework; +using Unity.Netcode.Transports.UNET; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Helper class to instantiate a NetworkManager + /// This also provides the ability to: + /// --- instantiate GameObjects with NetworkObject components that returns a Guid for accessing it later. + /// --- add NetworkBehaviour components to the instantiated GameObjects + /// --- spawn a NetworkObject using its parent GameObject's Guid + /// Call StartNetworkManager in the constructor of your runtime unit test class. + /// Call ShutdownNetworkManager in the destructor of your runtime unit test class. + /// + /// Includes a useful "BuffersMatch" method that allows you to compare two buffers (returns true if they match false if not) + /// + public static class NetworkManagerHelper + { + public static NetworkManager NetworkManagerObject { get; internal set; } + public static GameObject NetworkManagerGameObject { get; internal set; } + + internal static Dictionary InstantiatedGameObjects = new Dictionary(); + internal static Dictionary InstantiatedNetworkObjects = new Dictionary(); + internal static NetworkManagerOperatingMode CurrentNetworkManagerMode; + + /// + /// This provides the ability to start NetworkManager in various modes + /// + public enum NetworkManagerOperatingMode + { + None, + Host, + Server, + Client, + } + + /// + /// Called upon the RpcQueueTests being instantiated. + /// This creates an instance of the NetworkManager to be used during unit tests. + /// Currently, the best method to run unit tests is by starting in host mode as you can + /// send messages to yourself (i.e. Host-Client to Host-Server and vice versa). + /// As such, the default setting is to start in Host mode. + /// + /// parameter to specify which mode you want to start the NetworkManager + /// parameter to specify custom NetworkConfig settings + /// true if it was instantiated or is already instantiate otherwise false means it failed to instantiate + public static bool StartNetworkManager(out NetworkManager networkManager, NetworkManagerOperatingMode managerMode = NetworkManagerOperatingMode.Host, NetworkConfig networkConfig = null) + { + // If we are changing the current manager mode and the current manager mode is not "None", then stop the NetworkManager mode + if (CurrentNetworkManagerMode != managerMode && CurrentNetworkManagerMode != NetworkManagerOperatingMode.None) + { + StopNetworkManagerMode(); + } + + if (NetworkManagerGameObject == null) + { + NetworkManagerGameObject = new GameObject(nameof(NetworkManager)); + NetworkManagerObject = NetworkManagerGameObject.AddComponent(); + + if (NetworkManagerObject == null) + { + networkManager = null; + return false; + } + + Debug.Log($"{nameof(NetworkManager)} Instantiated."); + + var unetTransport = NetworkManagerGameObject.AddComponent(); + if (networkConfig == null) + { + networkConfig = new NetworkConfig + { + EnableSceneManagement = false, + }; + } + + NetworkManagerObject.NetworkConfig = networkConfig; + + unetTransport.ConnectAddress = "127.0.0.1"; + unetTransport.ConnectPort = 7777; + unetTransport.ServerListenPort = 7777; + unetTransport.MessageBufferSize = 65535; + unetTransport.MaxConnections = 100; + unetTransport.MessageSendMode = UNetTransport.SendMode.Immediately; + NetworkManagerObject.NetworkConfig.NetworkTransport = unetTransport; + + // Starts the network manager in the mode specified + StartNetworkManagerMode(managerMode); + } + + networkManager = NetworkManagerObject; + + return true; + } + + /// + /// Add a GameObject with a NetworkObject component + /// + /// the name of the object + /// + public static Guid AddGameNetworkObject(string nameOfGameObject) + { + var gameObjectId = Guid.NewGuid(); + + // Create the player object that we will spawn as a host + var gameObject = new GameObject(nameOfGameObject); + + Assert.IsNotNull(gameObject); + + var networkObject = gameObject.AddComponent(); + + Assert.IsNotNull(networkObject); + + Assert.IsFalse(InstantiatedGameObjects.ContainsKey(gameObjectId)); + Assert.IsFalse(InstantiatedNetworkObjects.ContainsKey(gameObjectId)); + + InstantiatedGameObjects.Add(gameObjectId, gameObject); + InstantiatedNetworkObjects.Add(gameObjectId, networkObject); + + return gameObjectId; + } + + /// + /// Helper class to add a component to the GameObject with a NetoworkObject component + /// + /// NetworkBehaviour component being added to the GameObject + /// ID returned to reference the game object + /// + public static T AddComponentToObject(Guid gameObjectIdentifier) where T : NetworkBehaviour + { + Assert.IsTrue(InstantiatedGameObjects.ContainsKey(gameObjectIdentifier)); + return InstantiatedGameObjects[gameObjectIdentifier].AddComponent(); + } + + /// + /// Spawn the NetworkObject, so Rpcs can flow + /// + /// ID returned to reference the game object + public static void SpawnNetworkObject(Guid gameObjectIdentifier) + { + Assert.IsTrue(InstantiatedNetworkObjects.ContainsKey(gameObjectIdentifier)); + if (!InstantiatedNetworkObjects[gameObjectIdentifier].IsSpawned) + { + InstantiatedNetworkObjects[gameObjectIdentifier].Spawn(); + } + } + + /// + /// Starts the NetworkManager in the current mode specified by managerMode + /// + /// the mode to start the NetworkManager as + private static void StartNetworkManagerMode(NetworkManagerOperatingMode managerMode) + { + CurrentNetworkManagerMode = managerMode; + switch (CurrentNetworkManagerMode) + { + case NetworkManagerOperatingMode.Host: + { + // Starts the host + NetworkManagerObject.StartHost(); + break; + } + case NetworkManagerOperatingMode.Server: + { + // Starts the server + NetworkManagerObject.StartServer(); + break; + } + case NetworkManagerOperatingMode.Client: + { + // Starts the client + NetworkManagerObject.StartClient(); + break; + } + } + + // If we started an netcode session + if (CurrentNetworkManagerMode != NetworkManagerOperatingMode.None) + { + // With some unit tests the Singleton can still be from a previous unit test + // depending upon the order of operations that occurred. + if (NetworkManager.Singleton != NetworkManagerObject) + { + NetworkManagerObject.SetSingleton(); + } + + // Only log this if we started an netcode session + Debug.Log($"{CurrentNetworkManagerMode} started."); + } + } + + /// + /// Stops the current mode of the NetworkManager + /// + private static void StopNetworkManagerMode() + { + NetworkManagerObject.Shutdown(); + + Debug.Log($"{CurrentNetworkManagerMode} stopped."); + CurrentNetworkManagerMode = NetworkManagerOperatingMode.None; + } + + // This is called, even if we assert and exit early from a test + public static void ShutdownNetworkManager() + { + // clean up any game objects created with custom unit testing components + foreach (var entry in InstantiatedGameObjects) + { + UnityEngine.Object.DestroyImmediate(entry.Value); + } + + InstantiatedGameObjects.Clear(); + + if (NetworkManagerGameObject != null) + { + Debug.Log($"{nameof(NetworkManager)} shutdown."); + + StopNetworkManagerMode(); + UnityEngine.Object.DestroyImmediate(NetworkManagerGameObject); + Debug.Log($"{nameof(NetworkManager)} destroyed."); + } + NetworkManagerGameObject = null; + NetworkManagerObject = null; + } + + public static bool BuffersMatch(int indexOffset, long targetSize, byte[] sourceArray, byte[] originalArray) + { + long largeInt64Blocks = targetSize >> 3; // Divide by 8 + int originalArrayOffset = 0; + // process by 8 byte blocks if we can + for (long i = 0; i < largeInt64Blocks; i++) + { + if (BitConverter.ToInt64(sourceArray, indexOffset) != BitConverter.ToInt64(originalArray, originalArrayOffset)) + { + return false; + } + indexOffset += 8; + originalArrayOffset += 8; + } + + long offset = largeInt64Blocks * 8; + long remainder = targetSize - offset; + + // 4 byte block + if (remainder >= 4) + { + if (BitConverter.ToInt32(sourceArray, indexOffset) != BitConverter.ToInt32(originalArray, originalArrayOffset)) + { + return false; + } + indexOffset += 4; + originalArrayOffset += 4; + offset += 4; + } + + // Remainder of bytes < 4 + if (targetSize - offset > 0) + { + for (long i = 0; i < (targetSize - offset); i++) + { + if (sourceArray[indexOffset + i] != originalArray[originalArrayOffset + i]) + { + return false; + } + } + } + return true; + } + } +} diff --git a/Tests/Runtime/Helpers/NetworkManagerHelper.cs.meta b/Tests/Runtime/Helpers/NetworkManagerHelper.cs.meta new file mode 100644 index 0000000..398e3c4 --- /dev/null +++ b/Tests/Runtime/Helpers/NetworkManagerHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c920be150fd14ad4ca1936e1a259417c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Helpers/NetworkVariableHelper.cs b/Tests/Runtime/Helpers/NetworkVariableHelper.cs new file mode 100644 index 0000000..2b3a16a --- /dev/null +++ b/Tests/Runtime/Helpers/NetworkVariableHelper.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Will automatically register for the NetworkVariable OnValueChanged + /// delegate handler. It then will expose that single delegate invocation + /// to anything that registers for this NetworkVariableHelper's instance's OnValueChanged event. + /// This allows us to register any NetworkVariable type as well as there are basically two "types of types": + /// IEquatable + /// ValueType + /// From both we can then at least determine if the value indeed changed + /// + /// + internal class NetworkVariableHelper : NetworkVariableBaseHelper where T : unmanaged + { + private readonly NetworkVariable m_NetworkVariable; + public delegate void OnMyValueChangedDelegateHandler(T previous, T next); + public event OnMyValueChangedDelegateHandler OnValueChanged; + + /// + /// IEquatable Equals Check + /// + private void CheckVariableChanged(IEquatable previous, IEquatable next) + { + if (!previous.Equals(next)) + { + ValueChanged(); + } + } + + /// + /// ValueType Equals Check + /// + private void CheckVariableChanged(ValueType previous, ValueType next) + { + if (!previous.Equals(next)) + { + ValueChanged(); + } + } + + /// + /// INetworkVariable's OnVariableChanged delegate callback + /// + /// + /// + private void OnVariableChanged(T previous, T next) + { + if (previous is ValueType testValueType) + { + CheckVariableChanged(previous, next); + } + else + { + CheckVariableChanged(previous as IEquatable, next as IEquatable); + } + + OnValueChanged?.Invoke(previous, next); + } + + public NetworkVariableHelper(NetworkVariableBase networkVariable) : base(networkVariable) + { + m_NetworkVariable = networkVariable as NetworkVariable; + m_NetworkVariable.OnValueChanged = OnVariableChanged; + } + } + + /// + /// The BaseNetworkVariableHelper keeps track of: + /// The number of instances and associates the instance with the NetworkVariable + /// The number of times a specific NetworkVariable instance had its value changed (i.e. !Equal) + /// Note: This could be expanded for future tests focuses around NetworkVariables + /// + internal class NetworkVariableBaseHelper + { + private static Dictionary s_Instances; + private static Dictionary s_InstanceChangedCount; + + /// + /// Returns the total number of registered INetworkVariables + /// + public static int InstanceCount + { + get + { + if (s_Instances != null) + { + return s_Instances.Count; + } + return 0; + } + } + + /// + /// Returns total number of changes that occurred for all registered INetworkVariables + /// + public static int VarChangedCount + { + get + { + if (s_InstanceChangedCount != null) + { + var changeCount = 0; + foreach (var keyPair in s_InstanceChangedCount) + { + changeCount += keyPair.Value; + } + return changeCount; + } + return 0; + } + } + + /// + /// Called by the child class NetworkVariableHelper when a value changed + /// + protected void ValueChanged() + { + if (s_Instances.ContainsKey(this)) + { + if (s_InstanceChangedCount.ContainsKey(s_Instances[this])) + { + s_InstanceChangedCount[s_Instances[this]]++; + } + } + } + + public NetworkVariableBaseHelper(NetworkVariableBase networkVariable) + { + if (s_Instances == null) + { + s_Instances = new Dictionary(); + } + + if (s_InstanceChangedCount == null) + { + s_InstanceChangedCount = new Dictionary(); + } + + // Register new instance and associated INetworkVariable + if (!s_Instances.ContainsKey(this)) + { + s_Instances.Add(this, networkVariable); + if (!s_InstanceChangedCount.ContainsKey(networkVariable)) + { + s_InstanceChangedCount.Add(networkVariable, 0); + } + } + } + } +} diff --git a/Tests/Runtime/Helpers/NetworkVariableHelper.cs.meta b/Tests/Runtime/Helpers/NetworkVariableHelper.cs.meta new file mode 100644 index 0000000..e0a4cff --- /dev/null +++ b/Tests/Runtime/Helpers/NetworkVariableHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 218bb185a48c6f9449e1c74f4855a774 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/HiddenVariableTests.cs b/Tests/Runtime/HiddenVariableTests.cs new file mode 100644 index 0000000..96e60f5 --- /dev/null +++ b/Tests/Runtime/HiddenVariableTests.cs @@ -0,0 +1,245 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class HiddenVariableTest : NetworkBehaviour + { + } + + public class HiddenVariableObject : NetworkBehaviour + { + public NetworkVariable MyNetworkVariable = new NetworkVariable(); + public NetworkList MyNetworkList = new NetworkList(); + + public static Dictionary ValueOnClient = new Dictionary(); + public static int ExpectedSize = 0; + public static int SpawnCount = 0; + + public override void OnNetworkSpawn() + { + Debug.Log($"{nameof(HiddenVariableObject)}.{nameof(OnNetworkSpawn)}() with value {MyNetworkVariable.Value}"); + + MyNetworkVariable.OnValueChanged += Changed; + MyNetworkList.OnListChanged += ListChanged; + SpawnCount++; + + base.OnNetworkSpawn(); + } + + public void Changed(int before, int after) + { + Debug.Log($"Value changed from {before} to {after} on {NetworkManager.LocalClientId}"); + ValueOnClient[NetworkManager.LocalClientId] = after; + } + public void ListChanged(NetworkListEvent listEvent) + { + Debug.Log($"ListEvent received: type {listEvent.Type}, index {listEvent.Index}, value {listEvent.Value}"); + Debug.Assert(ExpectedSize == MyNetworkList.Count); + } + } + + public class HiddenVariableTests : BaseMultiInstanceTest + { + protected override int NbClients => 4; + + private NetworkObject m_NetSpawnedObject; + private List m_NetSpawnedObjectOnClient = new List(); + private GameObject m_TestNetworkPrefab; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(useHost: true, nbClients: NbClients, + updatePlayerPrefab: playerPrefab => + { + var networkTransform = playerPrefab.AddComponent(); + m_TestNetworkPrefab = PreparePrefab(); + }); + } + + public GameObject PreparePrefab() + { + var prefabToSpawn = new GameObject("MyTestObject"); + var networkObjectPrefab = prefabToSpawn.AddComponent(); + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObjectPrefab); + prefabToSpawn.AddComponent(); + + m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Prefab = prefabToSpawn }); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + clientNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Prefab = prefabToSpawn }); + } + return prefabToSpawn; + } + + public IEnumerator WaitForConnectedCount(int targetCount) + { + var endTime = Time.realtimeSinceStartup + 1.0; + while (m_ServerNetworkManager.ConnectedClientsList.Count < targetCount && Time.realtimeSinceStartup < endTime) + { + yield return new WaitForSeconds(0.01f); + } + } + + public IEnumerator WaitForSpawnCount(int targetCount) + { + var endTime = Time.realtimeSinceStartup + 1.0; + while (HiddenVariableObject.SpawnCount != targetCount && + Time.realtimeSinceStartup < endTime) + { + yield return new WaitForSeconds(0.01f); + } + } + + public void VerifyLists() + { + NetworkList prev = null; + int numComparison = 0; + + // for all the instances of NetworkList + foreach (var gameObject in m_NetSpawnedObjectOnClient) + { + // this skips despawned/hidden objects + if (gameObject != null) + { + // if we've seen another one before + if (prev != null) + { + var curr = gameObject.GetComponent().MyNetworkList; + + // check that the two lists are identical + Debug.Assert(curr.Count == prev.Count); + for (int index = 0; index < curr.Count; index++) + { + Debug.Assert(curr[index] == prev[index]); + } + numComparison++; + } + // store the list + prev = gameObject.GetComponent().MyNetworkList; + } + } + Debug.Log($"{numComparison} comparisons done."); + } + + public IEnumerator RefreshGameObects() + { + m_NetSpawnedObjectOnClient.Clear(); + + foreach (var netMan in m_ClientNetworkManagers) + { + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run( + MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.NetworkObjectId == m_NetSpawnedObject.NetworkObjectId, + netMan, + serverClientPlayerResult)); + m_NetSpawnedObjectOnClient.Add(serverClientPlayerResult.Result); + } + } + + [UnityTest] + public IEnumerator HiddenVariableTest() + { + HiddenVariableObject.SpawnCount = 0; + HiddenVariableObject.ValueOnClient.Clear(); + HiddenVariableObject.ExpectedSize = 0; + HiddenVariableObject.SpawnCount = 0; + + Debug.Log("Running test"); + + var spawnedObject = Object.Instantiate(m_TestNetworkPrefab); + m_NetSpawnedObject = spawnedObject.GetComponent(); + m_NetSpawnedObject.NetworkManagerOwner = m_ServerNetworkManager; + yield return WaitForConnectedCount(NbClients); + Debug.Log("Clients connected"); + + // ==== Spawn object with ownership on one client + var client = m_ServerNetworkManager.ConnectedClientsList[1]; + var otherClient = m_ServerNetworkManager.ConnectedClientsList[2]; + m_NetSpawnedObject.SpawnWithOwnership(client.ClientId); + + yield return RefreshGameObects(); + + // === Check spawn occured + yield return WaitForSpawnCount(NbClients + 1); + Debug.Assert(HiddenVariableObject.SpawnCount == NbClients + 1); + Debug.Log("Objects spawned"); + + // ==== Set the NetworkVariable value to 2 + HiddenVariableObject.ExpectedSize = 1; + HiddenVariableObject.SpawnCount = 0; + + m_NetSpawnedObject.GetComponent().MyNetworkVariable.Value = 2; + m_NetSpawnedObject.GetComponent().MyNetworkList.Add(2); + + yield return new WaitForSeconds(1.0f); + + foreach (var id in m_ServerNetworkManager.ConnectedClientsIds) + { + Debug.Assert(HiddenVariableObject.ValueOnClient[id] == 2); + } + + VerifyLists(); + + Debug.Log("Value changed"); + + // ==== Hide our object to a different client + HiddenVariableObject.ExpectedSize = 2; + m_NetSpawnedObject.NetworkHide(otherClient.ClientId); + + // ==== Change the NetworkVariable value + // we should get one less notification of value changing and no errors or exception + m_NetSpawnedObject.GetComponent().MyNetworkVariable.Value = 3; + m_NetSpawnedObject.GetComponent().MyNetworkList.Add(3); + + yield return new WaitForSeconds(1.0f); + foreach (var id in m_ServerNetworkManager.ConnectedClientsIds) + { + if (id != otherClient.ClientId) + { + Debug.Assert(HiddenVariableObject.ValueOnClient[id] == 3); + } + } + + VerifyLists(); + Debug.Log("Values changed"); + + // ==== Show our object again to this client + HiddenVariableObject.ExpectedSize = 3; + m_NetSpawnedObject.NetworkShow(otherClient.ClientId); + + // ==== Wait for object to be spawned + yield return WaitForSpawnCount(1); + Debug.Assert(HiddenVariableObject.SpawnCount == 1); + Debug.Log("Object spawned"); + + // ==== We need a refresh for the newly re-spawned object + yield return RefreshGameObects(); + + // ==== Change the NetworkVariable value + // we should get all notifications of value changing and no errors or exception + m_NetSpawnedObject.GetComponent().MyNetworkVariable.Value = 4; + m_NetSpawnedObject.GetComponent().MyNetworkList.Add(4); + + yield return new WaitForSeconds(1.0f); + + foreach (var id in m_ServerNetworkManager.ConnectedClientsIds) + { + Debug.Assert(HiddenVariableObject.ValueOnClient[id] == 4); + } + + VerifyLists(); + Debug.Log("Values changed"); + + // ==== Hide our object to that different client again, and then destroy it + m_NetSpawnedObject.NetworkHide(otherClient.ClientId); + yield return new WaitForSeconds(0.2f); + m_NetSpawnedObject.Despawn(); + yield return new WaitForSeconds(0.2f); + } + } +} diff --git a/Tests/Runtime/HiddenVariableTests.cs.meta b/Tests/Runtime/HiddenVariableTests.cs.meta new file mode 100644 index 0000000..87cfb81 --- /dev/null +++ b/Tests/Runtime/HiddenVariableTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c64eb7c36fc44eadac730241b23e006 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Messaging.meta b/Tests/Runtime/Messaging.meta new file mode 100644 index 0000000..bb30551 --- /dev/null +++ b/Tests/Runtime/Messaging.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 60efb82fa0154d6caf94fea440f167d4 +timeCreated: 1627407732 \ No newline at end of file diff --git a/Tests/Runtime/Messaging/NamedMessageTests.cs b/Tests/Runtime/Messaging/NamedMessageTests.cs new file mode 100644 index 0000000..9c246e3 --- /dev/null +++ b/Tests/Runtime/Messaging/NamedMessageTests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class NamedMessageTests : BaseMultiInstanceTest + { + protected override int NbClients => 2; + + private NetworkManager FirstClient => m_ClientNetworkManagers[0]; + private NetworkManager SecondClient => m_ClientNetworkManagers[1]; + + [UnityTest] + public IEnumerator NamedMessageIsReceivedOnClientWithContent() + { + var messageName = Guid.NewGuid().ToString(); + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage( + messageName, + FirstClient.LocalClientId, + writer); + } + + ulong receivedMessageSender = 0; + var receivedMessageContent = new Guid(); + FirstClient.CustomMessagingManager.RegisterNamedMessageHandler( + messageName, + (ulong sender, FastBufferReader reader) => + { + receivedMessageSender = sender; + + reader.ReadValueSafe(out receivedMessageContent); + }); + + yield return new WaitForSeconds(0.2f); + + Assert.AreEqual(messageContent, receivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, receivedMessageSender); + } + + [UnityTest] + public IEnumerator NamedMessageIsReceivedOnMultipleClientsWithContent() + { + var messageName = Guid.NewGuid().ToString(); + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage( + messageName, + new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, + writer); + } + + ulong firstReceivedMessageSender = 0; + var firstReceivedMessageContent = new Guid(); + FirstClient.CustomMessagingManager.RegisterNamedMessageHandler( + messageName, + (ulong sender, FastBufferReader reader) => + { + firstReceivedMessageSender = sender; + + reader.ReadValueSafe(out firstReceivedMessageContent); + }); + + ulong secondReceivedMessageSender = 0; + var secondReceivedMessageContent = new Guid(); + SecondClient.CustomMessagingManager.RegisterNamedMessageHandler( + messageName, + (ulong sender, FastBufferReader reader) => + { + secondReceivedMessageSender = sender; + + reader.ReadValueSafe(out secondReceivedMessageContent); + }); + + yield return new WaitForSeconds(0.2f); + + Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); + + Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); + } + + [UnityTest] + public IEnumerator WhenSendingNamedMessageToAll_AllClientsReceiveIt() + { + var messageName = Guid.NewGuid().ToString(); + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessageToAll(messageName, writer); + } + + ulong firstReceivedMessageSender = 0; + var firstReceivedMessageContent = new Guid(); + FirstClient.CustomMessagingManager.RegisterNamedMessageHandler( + messageName, + (ulong sender, FastBufferReader reader) => + { + firstReceivedMessageSender = sender; + + reader.ReadValueSafe(out firstReceivedMessageContent); + }); + + ulong secondReceivedMessageSender = 0; + var secondReceivedMessageContent = new Guid(); + SecondClient.CustomMessagingManager.RegisterNamedMessageHandler( + messageName, + (ulong sender, FastBufferReader reader) => + { + secondReceivedMessageSender = sender; + + reader.ReadValueSafe(out secondReceivedMessageContent); + }); + + yield return new WaitForSeconds(0.2f); + + Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); + + Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); + } + + [Test] + public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrown() + { + var messageName = Guid.NewGuid().ToString(); + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendNamedMessage(messageName, null, writer); + }); + } + } + } +} diff --git a/Tests/Runtime/Messaging/NamedMessageTests.cs.meta b/Tests/Runtime/Messaging/NamedMessageTests.cs.meta new file mode 100644 index 0000000..8b7e64e --- /dev/null +++ b/Tests/Runtime/Messaging/NamedMessageTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50770f69eb9d3604184f918a2d0674e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Messaging/UnnamedMessageTests.cs b/Tests/Runtime/Messaging/UnnamedMessageTests.cs new file mode 100644 index 0000000..45721a0 --- /dev/null +++ b/Tests/Runtime/Messaging/UnnamedMessageTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class UnnamedMessageTests : BaseMultiInstanceTest + { + protected override int NbClients => 2; + + private NetworkManager FirstClient => m_ClientNetworkManagers[0]; + private NetworkManager SecondClient => m_ClientNetworkManagers[1]; + + [UnityTest] + public IEnumerator UnnamedMessageIsReceivedOnClientWithContent() + { + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage( + FirstClient.LocalClientId, + writer); + } + + ulong receivedMessageSender = 0; + var receivedMessageContent = new Guid(); + FirstClient.CustomMessagingManager.OnUnnamedMessage += + (ulong sender, FastBufferReader reader) => + { + receivedMessageSender = sender; + + reader.ReadValueSafe(out receivedMessageContent); + }; + + yield return new WaitForSeconds(0.2f); + + Assert.AreEqual(messageContent, receivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, receivedMessageSender); + } + + [UnityTest] + public IEnumerator UnnamedMessageIsReceivedOnMultipleClientsWithContent() + { + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage( + new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, + writer); + } + + ulong firstReceivedMessageSender = 0; + var firstReceivedMessageContent = new Guid(); + FirstClient.CustomMessagingManager.OnUnnamedMessage += + (ulong sender, FastBufferReader reader) => + { + firstReceivedMessageSender = sender; + + reader.ReadValueSafe(out firstReceivedMessageContent); + }; + + ulong secondReceivedMessageSender = 0; + var secondReceivedMessageContent = new Guid(); + SecondClient.CustomMessagingManager.OnUnnamedMessage += + (ulong sender, FastBufferReader reader) => + { + secondReceivedMessageSender = sender; + + reader.ReadValueSafe(out secondReceivedMessageContent); + }; + + yield return new WaitForSeconds(0.2f); + + Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); + + Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); + } + + [UnityTest] + public IEnumerator WhenSendingUnnamedMessageToAll_AllClientsReceiveIt() + { + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessageToAll(writer); + } + + ulong firstReceivedMessageSender = 0; + var firstReceivedMessageContent = new Guid(); + FirstClient.CustomMessagingManager.OnUnnamedMessage += + (ulong sender, FastBufferReader reader) => + { + firstReceivedMessageSender = sender; + + reader.ReadValueSafe(out firstReceivedMessageContent); + }; + + ulong secondReceivedMessageSender = 0; + var secondReceivedMessageContent = new Guid(); + SecondClient.CustomMessagingManager.OnUnnamedMessage += + (ulong sender, FastBufferReader reader) => + { + secondReceivedMessageSender = sender; + + reader.ReadValueSafe(out secondReceivedMessageContent); + }; + + yield return new WaitForSeconds(0.2f); + + Assert.AreEqual(messageContent, firstReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, firstReceivedMessageSender); + + Assert.AreEqual(messageContent, secondReceivedMessageContent); + Assert.AreEqual(m_ServerNetworkManager.LocalClientId, secondReceivedMessageSender); + } + + [Test] + public void WhenSendingNamedMessageToNullClientList_ArgumentNullExceptionIsThrown() + { + var messageContent = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + using (writer) + { + writer.WriteValueSafe(messageContent); + Assert.Throws( + () => + { + m_ServerNetworkManager.CustomMessagingManager.SendUnnamedMessage(null, writer); + }); + } + } + } +} diff --git a/Tests/Runtime/Messaging/UnnamedMessageTests.cs.meta b/Tests/Runtime/Messaging/UnnamedMessageTests.cs.meta new file mode 100644 index 0000000..81b2ef4 --- /dev/null +++ b/Tests/Runtime/Messaging/UnnamedMessageTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5affb61a1b56d44c80d0e2d55cc04aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics.meta b/Tests/Runtime/Metrics.meta new file mode 100644 index 0000000..11b63da --- /dev/null +++ b/Tests/Runtime/Metrics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4cd7fa97c73f3674b9cce18b1e0a6874 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/MessagingMetricsTests.cs b/Tests/Runtime/Metrics/MessagingMetricsTests.cs new file mode 100644 index 0000000..fe21b45 --- /dev/null +++ b/Tests/Runtime/Metrics/MessagingMetricsTests.cs @@ -0,0 +1,275 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Collections; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.RuntimeTests.Metrics.Utility; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + public class MessagingMetricsTests : DualClientMetricTestBase + { + const uint MessageNameHashSize = 8; + + const uint MessageOverhead = MessageNameHashSize; + + protected override int NbClients => 2; + + [UnityTest] + public IEnumerator TrackNetworkMessageSentMetric() + { + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NetworkMessageSent); + + var messageName = Guid.NewGuid(); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + } + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var networkMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, networkMessageSentMetricValues.Count); + + var networkMessageEvent = networkMessageSentMetricValues.First(); + Assert.AreEqual(nameof(NamedMessage), networkMessageEvent.Name); + Assert.AreEqual(FirstClient.LocalClientId, networkMessageEvent.Connection.Id); + } + + [UnityTest] + public IEnumerator TrackNetworkMessageSentMetricToMultipleClients() + { + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NetworkMessageSent); + var messageName = Guid.NewGuid(); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, writer); + } + + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var networkMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, networkMessageSentMetricValues.Count(x => x.Name.Equals(nameof(NamedMessage)))); + } + + [UnityTest] + public IEnumerator TrackNetworkMessageReceivedMetric() + { + var messageName = Guid.NewGuid(); + + LogAssert.Expect(LogType.Log, $"Received from {Server.LocalClientId}"); + FirstClient.CustomMessagingManager.RegisterNamedMessageHandler(messageName.ToString(), (ulong sender, FastBufferReader payload) => + { + Debug.Log($"Received from {sender}"); + }); + var waitForMetricValues = new WaitForMetricValues(FirstClientMetrics.Dispatcher, NetworkMetricTypes.NetworkMessageReceived); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + } + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var networkMessageReceivedValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, networkMessageReceivedValues.Count(x => x.Name.Equals(nameof(NamedMessage)))); + + var namedMessageReceived = networkMessageReceivedValues.First(); + Assert.AreEqual(Server.LocalClientId, namedMessageReceived.Connection.Id); + } + + [UnityTest] + public IEnumerator TrackNamedMessageSentMetric() + { + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NamedMessageSent); + + var messageName = Guid.NewGuid(); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + } + + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var namedMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, namedMessageSentMetricValues.Count); + + var namedMessageSent = namedMessageSentMetricValues.First(); + Assert.AreEqual(messageName.ToString(), namedMessageSent.Name); + Assert.AreEqual(FirstClient.LocalClientId, namedMessageSent.Connection.Id); + Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + MessageOverhead, namedMessageSent.BytesCount); + } + + [UnityTest] + public IEnumerator TrackNamedMessageSentMetricToMultipleClients() + { + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NamedMessageSent); + var messageName = Guid.NewGuid(); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, writer); + } + + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var namedMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, namedMessageSentMetricValues.Count); + Assert.That(namedMessageSentMetricValues.Select(x => x.Name), Has.All.EqualTo(messageName.ToString())); + Assert.That(namedMessageSentMetricValues.Select(x => x.BytesCount), Has.All.EqualTo(FastBufferWriter.GetWriteSize(messageName) + MessageOverhead)); + } + + [UnityTest] + public IEnumerator TrackNamedMessageSentMetricToSelf() + { + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NamedMessageSent); + var messageName = Guid.NewGuid(); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), Server.LocalClientId, writer); + } + + yield return waitForMetricValues.WaitForMetricsReceived(); + + waitForMetricValues.AssertMetricValuesHaveNotBeenFound(); + } + + [UnityTest] + public IEnumerator TrackNamedMessageReceivedMetric() + { + var waitForMetricValues = new WaitForMetricValues(FirstClientMetrics.Dispatcher, NetworkMetricTypes.NamedMessageReceived); + + var messageName = Guid.NewGuid(); + + LogAssert.Expect(LogType.Log, $"Received from {Server.LocalClientId}"); + FirstClient.CustomMessagingManager.RegisterNamedMessageHandler(messageName.ToString(), (ulong sender, FastBufferReader payload) => + { + Debug.Log($"Received from {sender}"); + }); + + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), FirstClient.LocalClientId, writer); + } + + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var namedMessageReceivedValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, namedMessageReceivedValues.Count); + + var namedMessageReceived = namedMessageReceivedValues.First(); + Assert.AreEqual(messageName.ToString(), namedMessageReceived.Name); + Assert.AreEqual(Server.LocalClientId, namedMessageReceived.Connection.Id); + Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + MessageOverhead, namedMessageReceived.BytesCount); + } + + [UnityTest] + public IEnumerator TrackUnnamedMessageSentMetric() + { + var message = Guid.NewGuid(); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(message); + + Server.CustomMessagingManager.SendUnnamedMessage(FirstClient.LocalClientId, writer); + } + + + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.UnnamedMessageSent); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var unnamedMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, unnamedMessageSentMetricValues.Count); + + var unnamedMessageSent = unnamedMessageSentMetricValues.First(); + Assert.AreEqual(FirstClient.LocalClientId, unnamedMessageSent.Connection.Id); + Assert.AreEqual(FastBufferWriter.GetWriteSize(message), unnamedMessageSent.BytesCount); + } + + [UnityTest] + public IEnumerator TrackUnnamedMessageSentMetricToMultipleClients() + { + var message = Guid.NewGuid(); + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.UnnamedMessageSent); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(message); + + Server.CustomMessagingManager.SendUnnamedMessage(new List { FirstClient.LocalClientId, SecondClient.LocalClientId }, writer); + } + + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var unnamedMessageSentMetricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, unnamedMessageSentMetricValues.Count); + Assert.That(unnamedMessageSentMetricValues.Select(x => x.BytesCount), Has.All.EqualTo(FastBufferWriter.GetWriteSize(message))); + + var clientIds = unnamedMessageSentMetricValues.Select(x => x.Connection.Id).ToList(); + Assert.Contains(FirstClient.LocalClientId, clientIds); + Assert.Contains(SecondClient.LocalClientId, clientIds); + } + + [UnityTest] + public IEnumerator TrackUnnamedMessageSentMetricToSelf() + { + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.UnnamedMessageSent); + var messageName = Guid.NewGuid(); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendUnnamedMessage(Server.LocalClientId, writer); + } + + yield return waitForMetricValues.WaitForMetricsReceived(); + + waitForMetricValues.AssertMetricValuesHaveNotBeenFound(); + } + + [UnityTest] + public IEnumerator TrackUnnamedMessageReceivedMetric() + { + var message = Guid.NewGuid(); + var waitForMetricValues = new WaitForMetricValues(FirstClientMetrics.Dispatcher, NetworkMetricTypes.UnnamedMessageReceived); + using (var writer = new FastBufferWriter(1300, Allocator.Temp)) + { + writer.WriteValueSafe(message); + + Server.CustomMessagingManager.SendUnnamedMessage(FirstClient.LocalClientId, writer); + } + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var unnamedMessageReceivedValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, unnamedMessageReceivedValues.Count); + + var unnamedMessageReceived = unnamedMessageReceivedValues.First(); + Assert.AreEqual(Server.LocalClientId, unnamedMessageReceived.Connection.Id); + Assert.AreEqual(FastBufferWriter.GetWriteSize(message), unnamedMessageReceived.BytesCount); + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/MessagingMetricsTests.cs.meta b/Tests/Runtime/Metrics/MessagingMetricsTests.cs.meta new file mode 100644 index 0000000..7910ff2 --- /dev/null +++ b/Tests/Runtime/Metrics/MessagingMetricsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2490ed51138306e4d92f8e9dcfc34462 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/MetricsDispatchTests.cs b/Tests/Runtime/Metrics/MetricsDispatchTests.cs new file mode 100644 index 0000000..0c8cc09 --- /dev/null +++ b/Tests/Runtime/Metrics/MetricsDispatchTests.cs @@ -0,0 +1,66 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using NUnit.Framework; +using Unity.Multiplayer.Tools.NetStats; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + public class MetricsDispatchTests + { + private int m_NbDispatches; + + private NetworkManager m_NetworkManager; + + [SetUp] + public void SetUp() + { + var networkManagerStarted = NetworkManagerHelper.StartNetworkManager( + out m_NetworkManager, + NetworkManagerHelper.NetworkManagerOperatingMode.Host, + new NetworkConfig + { + TickRate = 1, + }); + Assert.IsTrue(networkManagerStarted); + + var networkMetrics = m_NetworkManager.NetworkMetrics as NetworkMetrics; + networkMetrics.Dispatcher.RegisterObserver(new MockMetricsObserver(() => m_NbDispatches++)); + } + + [TearDown] + public void TearDown() + { + NetworkManagerHelper.ShutdownNetworkManager(); + } + + [UnityTest] + public IEnumerator VerifyNetworkMetricsDispatchesOncePerFrame() + { + var nbDispatchesBeforeFrame = m_NbDispatches; + + yield return null; // Wait one frame so dispatch occurs + + var nbDispatchesAfterFrame = m_NbDispatches; + + Assert.AreEqual(1, nbDispatchesAfterFrame - nbDispatchesBeforeFrame); + } + + private class MockMetricsObserver : IMetricObserver + { + private readonly Action m_OnObserve; + + public MockMetricsObserver(Action onObserve) + { + m_OnObserve = onObserve; + } + + public void Observe(MetricCollection collection) + { + m_OnObserve?.Invoke(); + } + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/MetricsDispatchTests.cs.meta b/Tests/Runtime/Metrics/MetricsDispatchTests.cs.meta new file mode 100644 index 0000000..9a19940 --- /dev/null +++ b/Tests/Runtime/Metrics/MetricsDispatchTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4833f15c8a59407abbb8532ea64b5683 +timeCreated: 1633451646 \ No newline at end of file diff --git a/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs b/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs new file mode 100644 index 0000000..9dd0277 --- /dev/null +++ b/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs @@ -0,0 +1,197 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.RuntimeTests.Metrics.Utility; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + internal class NetworkObjectMetricsTests : SingleClientMetricTestBase + { + private const string k_NewNetworkObjectName = "TestNetworkObjectToSpawn"; + private NetworkObject m_NewNetworkPrefab; + + protected override Action UpdatePlayerPrefab => _ => + { + var gameObject = new GameObject(k_NewNetworkObjectName); + m_NewNetworkPrefab = gameObject.AddComponent(); + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(m_NewNetworkPrefab); + + var networkPrefab = new NetworkPrefab { Prefab = gameObject }; + m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab); + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.NetworkPrefabs.Add(networkPrefab); + } + }; + + private NetworkObject SpawnNetworkObject() + { + // Spawn another network object so we can hide multiple. + var gameObject = UnityEngine.Object.Instantiate(m_NewNetworkPrefab); // new GameObject(NewNetworkObjectName); + var networkObject = gameObject.GetComponent(); + networkObject.NetworkManagerOwner = Server; + networkObject.Spawn(); + + return networkObject; + } + + [UnityTest] + public IEnumerator TrackNetworkObjectSpawnSentMetric() + { + var waitForMetricEvent = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.ObjectSpawnedSent); + + SpawnNetworkObject(); + + yield return waitForMetricEvent.WaitForMetricsReceived(); + + var objectSpawnedSentMetricValues = waitForMetricEvent.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, objectSpawnedSentMetricValues.Count); + + var objectSpawned = objectSpawnedSentMetricValues.Last(); + Assert.AreEqual(Client.LocalClientId, objectSpawned.Connection.Id); + Assert.AreEqual($"{k_NewNetworkObjectName}(Clone)", objectSpawned.NetworkId.Name); + Assert.AreNotEqual(0, objectSpawned.BytesCount); + } + + [UnityTest] + public IEnumerator TrackNetworkObjectSpawnReceivedMetric() + { + var waitForMetricEvent = new WaitForMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.ObjectSpawnedReceived); + + var networkObject = SpawnNetworkObject(); + + yield return waitForMetricEvent.WaitForMetricsReceived(); + + var objectSpawnedReceivedMetricValues = waitForMetricEvent.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, objectSpawnedReceivedMetricValues.Count); + + var objectSpawned = objectSpawnedReceivedMetricValues.First(); + Assert.AreEqual(Server.LocalClientId, objectSpawned.Connection.Id); + Assert.AreEqual(networkObject.NetworkObjectId, objectSpawned.NetworkId.NetworkId); + Assert.AreEqual($"{k_NewNetworkObjectName}(Clone)", objectSpawned.NetworkId.Name); + Assert.AreNotEqual(0, objectSpawned.BytesCount); + } + + [UnityTest] + public IEnumerator TrackNetworkObjectDestroySentMetric() + { + var networkObject = SpawnNetworkObject(); + + yield return new WaitForSeconds(0.2f); + + var waitForMetricEvent = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.ObjectDestroyedSent); + + Server.SpawnManager.OnDespawnObject(networkObject, true); + + yield return waitForMetricEvent.WaitForMetricsReceived(); + + var objectDestroyedSentMetricValues = waitForMetricEvent.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, objectDestroyedSentMetricValues.Count); // As there's a client and server, this event is emitted twice. + + var objectDestroyed = objectDestroyedSentMetricValues.Last(); + Assert.AreEqual(Client.LocalClientId, objectDestroyed.Connection.Id); + Assert.AreEqual($"{k_NewNetworkObjectName}(Clone)", objectDestroyed.NetworkId.Name); + Assert.AreNotEqual(0, objectDestroyed.BytesCount); + } + + [UnityTest] + public IEnumerator TrackNetworkObjectDestroyReceivedMetric() + { + var networkObject = SpawnNetworkObject(); + + yield return new WaitForSeconds(0.2f); + + var waitForMetricEvent = new WaitForMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.ObjectDestroyedReceived); + + Server.SpawnManager.OnDespawnObject(networkObject, true); + + yield return waitForMetricEvent.WaitForMetricsReceived(); + + var objectDestroyedReceivedMetricValues = waitForMetricEvent.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, objectDestroyedReceivedMetricValues.Count); + + var objectDestroyed = objectDestroyedReceivedMetricValues.First(); + Assert.AreEqual(Server.LocalClientId, objectDestroyed.Connection.Id); + Assert.AreEqual(networkObject.NetworkObjectId, objectDestroyed.NetworkId.NetworkId); + Assert.AreEqual($"{k_NewNetworkObjectName}(Clone)", objectDestroyed.NetworkId.Name); + Assert.AreNotEqual(0, objectDestroyed.BytesCount); + } + + [UnityTest] + public IEnumerator TrackMultipleNetworkObjectSpawnSentMetric() + { + var networkObject1 = SpawnNetworkObject(); + var networkObject2 = SpawnNetworkObject(); + + yield return new WaitForSeconds(0.2f); + + NetworkObject.NetworkHide(new List { networkObject1, networkObject2 }, Client.LocalClientId); + + yield return new WaitForSeconds(0.2f); + + var waitForMetricEvent = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.ObjectSpawnedSent); + + NetworkObject.NetworkShow(new List { networkObject1, networkObject2 }, Client.LocalClientId); + + yield return waitForMetricEvent.WaitForMetricsReceived(); + + var objectSpawnedSentMetricValues = waitForMetricEvent.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, objectSpawnedSentMetricValues.Count); // As there's a client and server, this event is emitted twice. + Assert.That( + objectSpawnedSentMetricValues, + Has.Exactly(1).Matches( + x => Client.LocalClientId == x.Connection.Id + && x.NetworkId.NetworkId == networkObject1.NetworkObjectId + && x.NetworkId.Name == networkObject1.name)); + Assert.That( + objectSpawnedSentMetricValues, + Has.Exactly(1).Matches( + x => Client.LocalClientId == x.Connection.Id + && x.NetworkId.NetworkId == networkObject2.NetworkObjectId + && x.NetworkId.Name == networkObject2.name)); + + Assert.AreEqual(1, objectSpawnedSentMetricValues.Select(x => x.BytesCount).Distinct().Count()); + Assert.That(objectSpawnedSentMetricValues.Select(x => x.BytesCount), Has.All.Not.EqualTo(0)); + } + + [UnityTest] + public IEnumerator TrackMultipleNetworkObjectDestroySentMetric() + { + var networkObject1 = SpawnNetworkObject(); + var networkObject2 = SpawnNetworkObject(); + + yield return new WaitForSeconds(0.2f); + + var waitForMetricEvent = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.ObjectDestroyedSent); + + NetworkObject.NetworkHide(new List { networkObject1, networkObject2 }, Client.LocalClientId); + + yield return waitForMetricEvent.WaitForMetricsReceived(); + + var objectDestroyedSentMetricValues = waitForMetricEvent.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, objectDestroyedSentMetricValues.Count); // As there's a client and server, this event is emitted twice. + Assert.That( + objectDestroyedSentMetricValues, + Has.Exactly(1).Matches( + x => Client.LocalClientId == x.Connection.Id + && x.NetworkId.NetworkId == networkObject1.NetworkObjectId + && x.NetworkId.Name == networkObject1.name)); + Assert.That( + objectDestroyedSentMetricValues, + Has.Exactly(1).Matches( + x => Client.LocalClientId == x.Connection.Id + && x.NetworkId.NetworkId == networkObject2.NetworkObjectId + && x.NetworkId.Name == networkObject2.name)); + + Assert.AreEqual(1, objectDestroyedSentMetricValues.Select(x => x.BytesCount).Distinct().Count()); + Assert.That(objectDestroyedSentMetricValues.Select(x => x.BytesCount), Has.All.Not.EqualTo(0)); + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs.meta b/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs.meta new file mode 100644 index 0000000..d09dcac --- /dev/null +++ b/Tests/Runtime/Metrics/NetworkObjectMetricsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4cb56c442bed164da4908e54590dfeb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs b/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs new file mode 100644 index 0000000..29c0f0a --- /dev/null +++ b/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs @@ -0,0 +1,52 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.RuntimeTests.Metrics.Utility; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + internal class NetworkVariableMetricsTests : SingleClientMetricTestBase + { + protected override Action UpdatePlayerPrefab => prefab => prefab.AddComponent(); + + [UnityTest] + public IEnumerator TrackNetworkVariableDeltaSentMetric() + { + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.NetworkVariableDeltaSent); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var metricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + + var networkVariableDeltaSent = metricValues.First(); + Assert.AreEqual(nameof(NetworkVariableComponent.MyNetworkVariable), networkVariableDeltaSent.Name); + Assert.AreEqual(Server.LocalClientId, networkVariableDeltaSent.Connection.Id); + Assert.AreNotEqual(0, networkVariableDeltaSent.BytesCount); + } + + [UnityTest] + public IEnumerator TrackNetworkVariableDeltaReceivedMetric() + { + var waitForMetricValues = new WaitForMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.NetworkVariableDeltaReceived); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var metricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, metricValues.Count); // We have an instance each of the player prefabs + + var first = metricValues.First(); + Assert.AreEqual(nameof(NetworkVariableComponent.MyNetworkVariable), first.Name); + Assert.AreNotEqual(0, first.BytesCount); + + var last = metricValues.Last(); + Assert.AreEqual(nameof(NetworkVariableComponent.MyNetworkVariable), last.Name); + Assert.AreNotEqual(0, last.BytesCount); + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs.meta b/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs.meta new file mode 100644 index 0000000..690106b --- /dev/null +++ b/Tests/Runtime/Metrics/NetworkVariableMetricsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abbea688505c90d4f82bfa5ea3ee1cd9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs new file mode 100644 index 0000000..8d9a096 --- /dev/null +++ b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs @@ -0,0 +1,86 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.RuntimeTests.Metrics.Utility; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + internal class OwnershipChangeMetricsTests : SingleClientMetricTestBase + { + private const string k_NewNetworkObjectName = "TestNetworkObjectToSpawn"; + private NetworkObject m_NewNetworkPrefab; + + protected override Action UpdatePlayerPrefab => _ => + { + var gameObject = new GameObject(k_NewNetworkObjectName); + m_NewNetworkPrefab = gameObject.AddComponent(); + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(m_NewNetworkPrefab); + + var networkPrefab = new NetworkPrefab { Prefab = gameObject }; + m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(networkPrefab); + foreach (var client in m_ClientNetworkManagers) + { + client.NetworkConfig.NetworkPrefabs.Add(networkPrefab); + } + }; + + private NetworkObject SpawnNetworkObject() + { + // Spawn another network object so we can hide multiple. + var gameObject = UnityEngine.Object.Instantiate(m_NewNetworkPrefab); // new GameObject(NewNetworkObjectName); + var networkObject = gameObject.GetComponent(); + networkObject.NetworkManagerOwner = Server; + networkObject.Spawn(); + + return networkObject; + } + + [UnityTest] + public IEnumerator TrackOwnershipChangeSentMetric() + { + var networkObject = SpawnNetworkObject(); + + yield return new WaitForSeconds(0.2f); + + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.OwnershipChangeSent); + + networkObject.ChangeOwnership(1); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var metricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + + var ownershipChangeSent = metricValues.First(); + Assert.AreEqual(networkObject.NetworkObjectId, ownershipChangeSent.NetworkId.NetworkId); + Assert.AreEqual(Server.LocalClientId, ownershipChangeSent.Connection.Id); + Assert.AreEqual(FastBufferWriter.GetWriteSize(), ownershipChangeSent.BytesCount); + } + + [UnityTest] + public IEnumerator TrackOwnershipChangeReceivedMetric() + { + var networkObject = SpawnNetworkObject(); + + yield return new WaitForSeconds(0.2f); + + var waitForMetricValues = new WaitForMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.OwnershipChangeReceived); + + networkObject.ChangeOwnership(1); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var metricValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, metricValues.Count); + + var ownershipChangeReceived = metricValues.First(); + Assert.AreEqual(networkObject.NetworkObjectId, ownershipChangeReceived.NetworkId.NetworkId); + Assert.AreEqual(FastBufferWriter.GetWriteSize(), ownershipChangeReceived.BytesCount); + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs.meta b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs.meta new file mode 100644 index 0000000..a227871 --- /dev/null +++ b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86daf1dbe91c9ad40818743a805d0052 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/RpcMetricsTests.cs b/Tests/Runtime/Metrics/RpcMetricsTests.cs new file mode 100644 index 0000000..e3a1dc0 --- /dev/null +++ b/Tests/Runtime/Metrics/RpcMetricsTests.cs @@ -0,0 +1,106 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.RuntimeTests.Metrics.Utility; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + internal class RpcMetricsTests : SingleClientMetricTestBase + { + protected override Action UpdatePlayerPrefab => prefab => prefab.AddComponent(); + + [UnityTest] + public IEnumerator TrackRpcSentMetricOnServer() + { + var clientPlayer = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == Client.LocalClientId, Server, clientPlayer)); + + var waitForMetricValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.RpcSent); + + clientPlayer.Result.GetComponent().MyClientRpc(); + + yield return waitForMetricValues.WaitForMetricsReceived(); + + var serverRpcSentValues = waitForMetricValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(2, serverRpcSentValues.Count); // Server will receive this, since it's host + + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.Name == nameof(RpcTestComponent.MyClientRpc))); + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.NetworkBehaviourName == nameof(RpcTestComponent))); + Assert.That(serverRpcSentValues, Has.All.Matches(x => x.BytesCount != 0)); + Assert.Contains(Server.LocalClientId, serverRpcSentValues.Select(x => x.Connection.Id).ToArray()); + Assert.Contains(Client.LocalClientId, serverRpcSentValues.Select(x => x.Connection.Id).ToArray()); + } + + [UnityTest] + public IEnumerator TrackRpcSentMetricOnClient() + { + var clientPlayer = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == Client.LocalClientId, Client, clientPlayer)); + + var waitForClientMetricsValues = new WaitForMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.RpcSent); + + clientPlayer.Result.GetComponent().MyServerRpc(); + + yield return waitForClientMetricsValues.WaitForMetricsReceived(); + + var clientRpcSentValues = waitForClientMetricsValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, clientRpcSentValues.Count); + + var rpcSent = clientRpcSentValues.First(); + Assert.AreEqual(Server.LocalClientId, rpcSent.Connection.Id); + Assert.AreEqual(nameof(RpcTestComponent.MyServerRpc), rpcSent.Name); + Assert.AreEqual(nameof(RpcTestComponent), rpcSent.NetworkBehaviourName); + Assert.AreNotEqual(0, rpcSent.BytesCount); + } + + [UnityTest] + public IEnumerator TrackRpcReceivedMetricOnServer() + { + var clientPlayer = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == Client.LocalClientId, Client, clientPlayer)); + + var waitForServerMetricsValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.RpcReceived); + + clientPlayer.Result.GetComponent().MyServerRpc(); + + yield return waitForServerMetricsValues.WaitForMetricsReceived(); + + var serverRpcReceivedValues = waitForServerMetricsValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, serverRpcReceivedValues.Count); + + var rpcReceived = serverRpcReceivedValues.First(); + Assert.AreEqual(Client.LocalClientId, rpcReceived.Connection.Id); + Assert.AreEqual(nameof(RpcTestComponent.MyServerRpc), rpcReceived.Name); + Assert.AreEqual(nameof(RpcTestComponent), rpcReceived.NetworkBehaviourName); + Assert.AreNotEqual(0, rpcReceived.BytesCount); + } + + [UnityTest] + public IEnumerator TrackRpcReceivedMetricOnClient() + { + var clientPlayer = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == Client.LocalClientId, Server, clientPlayer)); + + var waitForServerMetricsValues = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.RpcReceived); + + clientPlayer.Result.GetComponent().MyClientRpc(); + + yield return waitForServerMetricsValues.WaitForMetricsReceived(); + + var clientRpcReceivedValues = waitForServerMetricsValues.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, clientRpcReceivedValues.Count); + + var rpcReceived = clientRpcReceivedValues.First(); + Assert.AreEqual(Server.LocalClientId, rpcReceived.Connection.Id); + Assert.AreEqual(nameof(RpcTestComponent.MyClientRpc), rpcReceived.Name); + Assert.AreEqual(nameof(RpcTestComponent), rpcReceived.NetworkBehaviourName); + Assert.AreNotEqual(0, rpcReceived.BytesCount); + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/RpcMetricsTests.cs.meta b/Tests/Runtime/Metrics/RpcMetricsTests.cs.meta new file mode 100644 index 0000000..38427d5 --- /dev/null +++ b/Tests/Runtime/Metrics/RpcMetricsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02a31cf8be00f8b46b65477d648b297d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/ServerLogsMetricTests.cs b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs new file mode 100644 index 0000000..79a6e4c --- /dev/null +++ b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs @@ -0,0 +1,53 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Netcode.RuntimeTests.Metrics.Utility; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + internal class ServerLogsMetricTests : SingleClientMetricTestBase + { + [UnityTest] + public IEnumerator TrackServerLogSentMetric() + { + var waitForSentMetric = new WaitForMetricValues(ClientMetrics.Dispatcher, NetworkMetricTypes.ServerLogSent); + + var message = Guid.NewGuid().ToString(); + NetworkLog.LogWarningServer(message); + + yield return waitForSentMetric.WaitForMetricsReceived(); + + var sentMetrics = waitForSentMetric.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, sentMetrics.Count); + + var sentMetric = sentMetrics.First(); + Assert.AreEqual(Server.LocalClientId, sentMetric.Connection.Id); + Assert.AreEqual((uint)NetworkLog.LogType.Warning, (uint)sentMetric.LogLevel); + Assert.AreEqual(message.Length + 2, sentMetric.BytesCount); + } + + [UnityTest] + public IEnumerator TrackServerLogReceivedMetric() + { + var waitForReceivedMetric = new WaitForMetricValues(ServerMetrics.Dispatcher, NetworkMetricTypes.ServerLogReceived); + + var message = Guid.NewGuid().ToString(); + NetworkLog.LogWarningServer(message); + + yield return waitForReceivedMetric.WaitForMetricsReceived(); + + var receivedMetrics = waitForReceivedMetric.AssertMetricValuesHaveBeenFound(); + Assert.AreEqual(1, receivedMetrics.Count); + + var receivedMetric = receivedMetrics.First(); + Assert.AreEqual(Client.LocalClientId, receivedMetric.Connection.Id); + Assert.AreEqual((uint)NetworkLog.LogType.Warning, (uint)receivedMetric.LogLevel); + Assert.AreEqual(message.Length + 2, receivedMetric.BytesCount); + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/ServerLogsMetricTests.cs.meta b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs.meta new file mode 100644 index 0000000..8eccb56 --- /dev/null +++ b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5ba082aac78de5d488f621a0cbaf16a6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs new file mode 100644 index 0000000..6ba1029 --- /dev/null +++ b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs @@ -0,0 +1,102 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.IO; +using NUnit.Framework; +using Unity.Collections; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Multiplayer.Tools.NetStats; +using Unity.Netcode.RuntimeTests.Metrics.Utility; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics +{ + internal class TransportBytesMetricsTests : SingleClientMetricTestBase + { + static readonly long MessageOverhead = 8 + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize(); + + [UnityTest] + public IEnumerator TrackTotalNumberOfBytesSent() + { + var messageName = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + var observer = new TotalBytesObserver(ClientMetrics.Dispatcher, NetworkMetricTypes.TotalBytesReceived); + try + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), Client.LocalClientId, writer); + } + finally + { + writer.Dispose(); + } + + var nbFrames = 0; + while (!observer.Found || nbFrames < 10) + { + yield return null; + nbFrames++; + } + + Assert.True(observer.Found); + Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + MessageOverhead, observer.Value); + } + + [UnityTest] + public IEnumerator TrackTotalNumberOfBytesReceived() + { + var messageName = Guid.NewGuid(); + var writer = new FastBufferWriter(1300, Allocator.Temp); + var observer = new TotalBytesObserver(ClientMetrics.Dispatcher, NetworkMetricTypes.TotalBytesReceived); + try + { + writer.WriteValueSafe(messageName); + + Server.CustomMessagingManager.SendNamedMessage(messageName.ToString(), Client.LocalClientId, writer); + } + finally + { + writer.Dispose(); + } + + + + var nbFrames = 0; + while (!observer.Found || nbFrames < 10) + { + yield return null; + nbFrames++; + } + + Assert.True(observer.Found); + Assert.AreEqual(FastBufferWriter.GetWriteSize(messageName) + MessageOverhead, observer.Value); + } + + private class TotalBytesObserver : IMetricObserver + { + private readonly DirectionalMetricInfo m_MetricInfo; + + public TotalBytesObserver(IMetricDispatcher dispatcher, DirectionalMetricInfo metricInfo) + { + m_MetricInfo = metricInfo; + + dispatcher.RegisterObserver(this); + } + + public bool Found { get; private set; } + + public long Value { get; private set; } + + public void Observe(MetricCollection collection) + { + if (collection.TryGetCounter(m_MetricInfo.Id, out var counter) && counter.Value > 0) + { + Found = true; + Value = counter.Value; + } + } + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs.meta b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs.meta new file mode 100644 index 0000000..2cfb1f3 --- /dev/null +++ b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8be98b50d230114f853054c479341ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/Utility.meta b/Tests/Runtime/Metrics/Utility.meta new file mode 100644 index 0000000..8aa9943 --- /dev/null +++ b/Tests/Runtime/Metrics/Utility.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4e60372130aba464f9f9ae4a24bb9fe0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/Utility/MetricTestBase.cs b/Tests/Runtime/Metrics/Utility/MetricTestBase.cs new file mode 100644 index 0000000..14a42d3 --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/MetricTestBase.cs @@ -0,0 +1,71 @@ +#if MULTIPLAYER_TOOLS +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Metrics.Utility +{ + internal abstract class SingleClientMetricTestBase : BaseMultiInstanceTest + { + protected override int NbClients => 1; + + protected virtual Action UpdatePlayerPrefab => _ => { }; + + internal NetworkManager Server { get; private set; } + + internal NetworkMetrics ServerMetrics { get; private set; } + + internal NetworkManager Client { get; private set; } + + internal NetworkMetrics ClientMetrics { get; private set; } + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, UpdatePlayerPrefab); + + Server = m_ServerNetworkManager; + ServerMetrics = Server.NetworkMetrics as NetworkMetrics; + Client = m_ClientNetworkManagers[0]; + ClientMetrics = Client.NetworkMetrics as NetworkMetrics; + } + } + + public abstract class DualClientMetricTestBase : BaseMultiInstanceTest + { + protected override int NbClients => 2; + + protected virtual Action UpdatePlayerPrefab => _ => { }; + + internal NetworkManager Server { get; private set; } + + internal NetworkMetrics ServerMetrics { get; private set; } + + internal NetworkManager FirstClient { get; private set; } + + internal NetworkMetrics FirstClientMetrics { get; private set; } + + internal NetworkManager SecondClient { get; private set; } + + internal NetworkMetrics SecondClientMetrics { get; private set; } + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, UpdatePlayerPrefab); + + Server = m_ServerNetworkManager; + ServerMetrics = Server.NetworkMetrics as NetworkMetrics; + FirstClient = m_ClientNetworkManagers[0]; + FirstClientMetrics = FirstClient.NetworkMetrics as NetworkMetrics; + SecondClient = m_ClientNetworkManagers[0]; + SecondClientMetrics = SecondClient.NetworkMetrics as NetworkMetrics; + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/Utility/MetricTestBase.cs.meta b/Tests/Runtime/Metrics/Utility/MetricTestBase.cs.meta new file mode 100644 index 0000000..fc32437 --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/MetricTestBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c726f5bc421c3874d9c1a26bcac3f091 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/Utility/NetworkVariableComponent.cs b/Tests/Runtime/Metrics/Utility/NetworkVariableComponent.cs new file mode 100644 index 0000000..ba0a8ce --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/NetworkVariableComponent.cs @@ -0,0 +1,19 @@ +#if MULTIPLAYER_TOOLS +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests.Metrics.Utility +{ + public class NetworkVariableComponent : NetworkBehaviour + { + public NetworkVariable MyNetworkVariable { get; } = new NetworkVariable(); + + private void Update() + { + if (IsServer) + { + MyNetworkVariable.Value = Random.Range(100, 999); + } + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/Utility/NetworkVariableComponent.cs.meta b/Tests/Runtime/Metrics/Utility/NetworkVariableComponent.cs.meta new file mode 100644 index 0000000..6694f4a --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/NetworkVariableComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 124489f89ef59d449ab4bed1f5ef2f59 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/Utility/RpcTestComponent.cs b/Tests/Runtime/Metrics/Utility/RpcTestComponent.cs new file mode 100644 index 0000000..36dbb6b --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/RpcTestComponent.cs @@ -0,0 +1,22 @@ +using System; + +namespace Unity.Netcode.RuntimeTests.Metrics.Utility +{ + public class RpcTestComponent : NetworkBehaviour + { + public event Action OnServerRpcAction; + public event Action OnClientRpcAction; + + [ServerRpc] + public void MyServerRpc() + { + OnServerRpcAction?.Invoke(); + } + + [ClientRpc] + public void MyClientRpc() + { + OnClientRpcAction?.Invoke(); + } + } +} diff --git a/Tests/Runtime/Metrics/Utility/RpcTestComponent.cs.meta b/Tests/Runtime/Metrics/Utility/RpcTestComponent.cs.meta new file mode 100644 index 0000000..5df8153 --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/RpcTestComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fdfa28da9866545428083671c445a9ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Metrics/Utility/WaitForMetricValues.cs b/Tests/Runtime/Metrics/Utility/WaitForMetricValues.cs new file mode 100644 index 0000000..62fa831 --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/WaitForMetricValues.cs @@ -0,0 +1,118 @@ +#if MULTIPLAYER_TOOLS +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Multiplayer.Tools.MetricTypes; +using Unity.Multiplayer.Tools.NetStats; + +namespace Unity.Netcode.RuntimeTests.Metrics.Utility +{ + internal class WaitForMetricValues : IMetricObserver + { + readonly string m_MetricName; + bool m_Found; + bool m_HasError; + string m_Error; + uint m_NbFrames = 0; + IReadOnlyCollection m_Values; + + public delegate bool Filter(TMetric metric); + + Filter m_FilterDelegate; + + + public WaitForMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName) + { + m_MetricName = directionalMetricName.Id; + + dispatcher.RegisterObserver(this); + } + + public WaitForMetricValues(IMetricDispatcher dispatcher, DirectionalMetricInfo directionalMetricName, Filter filter) + : this(dispatcher, directionalMetricName) + { + m_FilterDelegate = filter; + } + + public IEnumerator WaitForMetricsReceived() + { + yield return WaitForFrames(60); + } + + public IReadOnlyCollection AssertMetricValuesHaveBeenFound() + { + if (m_HasError) + { + Assert.Fail(m_Error); + } + + if (!m_Found) + { + Assert.Fail($"Found no matching values for metric of type '{typeof(TMetric).Name}', with name '{m_MetricName}' during '{m_NbFrames}' frames."); + } + + return m_Values; + } + + public void AssertMetricValuesHaveNotBeenFound() + { + if (m_HasError) + { + Assert.Fail(m_Error); + } + + if (!m_Found) + { + Assert.Pass(); + } + else + { + Assert.Fail(); + } + } + + public void Observe(MetricCollection collection) + { + if (m_Found || m_HasError) + { + return; + } + + var metric = collection.Metrics.SingleOrDefault(x => x.Name == m_MetricName); + if (metric == default) + { + m_HasError = true; + m_Error = $"Metric collection does not contain metric named '{m_MetricName}'."; + + return; + } + + var typedMetric = metric as IEventMetric; + if (typedMetric == default) + { + m_HasError = true; + m_Error = $"Metric collection contains a metric of type '{metric.GetType().Name}' for name '{m_MetricName}', but was expecting '{typeof(TMetric).Name}'."; + + return; + } + + if (typedMetric.Values.Any()) + { + // Apply filter if one was provided + m_Values = m_FilterDelegate != null ? typedMetric.Values.Where(x => m_FilterDelegate(x)).ToList() : typedMetric.Values.ToList(); + m_Found = m_Values.Count > 0; + } + } + + private IEnumerator WaitForFrames(uint maxNbFrames) + { + while (!m_Found && m_NbFrames < maxNbFrames) + { + m_NbFrames++; + yield return null; + } + } + } +} +#endif diff --git a/Tests/Runtime/Metrics/Utility/WaitForMetricValues.cs.meta b/Tests/Runtime/Metrics/Utility/WaitForMetricValues.cs.meta new file mode 100644 index 0000000..64a440f --- /dev/null +++ b/Tests/Runtime/Metrics/Utility/WaitForMetricValues.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 176888f06e2c5e14db33783fd0299668 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/MultiInstanceHelpers.cs b/Tests/Runtime/MultiInstanceHelpers.cs new file mode 100644 index 0000000..bfcc44d --- /dev/null +++ b/Tests/Runtime/MultiInstanceHelpers.cs @@ -0,0 +1,505 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.SceneManagement; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Provides helpers for running multi instance tests. + /// + public static class MultiInstanceHelpers + { + public const int DefaultMinFrames = 1; + public const int DefaultMaxFrames = 64; + private static List s_NetworkManagerInstances = new List(); + private static bool s_IsStarted; + private static int s_ClientCount; + private static int s_OriginalTargetFrameRate = -1; + + public static List NetworkManagerInstances => s_NetworkManagerInstances; + + /// + /// Creates NetworkingManagers and configures them for use in a multi instance setting. + /// + /// The amount of clients + /// The server NetworkManager + /// The clients NetworkManagers + /// The targetFrameRate of the Unity engine to use while the multi instance helper is running. Will be reset on shutdown. + public static bool Create(int clientCount, out NetworkManager server, out NetworkManager[] clients, int targetFrameRate = 60) + { + s_NetworkManagerInstances = new List(); + CreateNewClients(clientCount, out clients); + + // Create gameObject + var go = new GameObject("NetworkManager - Server"); + + // Create networkManager component + server = go.AddComponent(); + NetworkManagerInstances.Insert(0, server); + + // Set the NetworkConfig + server.NetworkConfig = new NetworkConfig() + { + // Set transport + NetworkTransport = go.AddComponent() + }; + + s_OriginalTargetFrameRate = Application.targetFrameRate; + Application.targetFrameRate = targetFrameRate; + + return true; + } + + /// + /// Used to add a client to the already existing list of clients + /// + /// The amount of clients + /// + /// + public static bool CreateNewClients(int clientCount, out NetworkManager[] clients) + { + clients = new NetworkManager[clientCount]; + var activeSceneName = SceneManager.GetActiveScene().name; + for (int i = 0; i < clientCount; i++) + { + // Create gameObject + var go = new GameObject("NetworkManager - Client - " + i); + // Create networkManager component + clients[i] = go.AddComponent(); + + // Set the NetworkConfig + clients[i].NetworkConfig = new NetworkConfig() + { + // Set transport + NetworkTransport = go.AddComponent() + }; + } + + NetworkManagerInstances.AddRange(clients); + return true; + } + + /// + /// Stops one single client and makes sure to cleanup any static variables in this helper + /// + /// + public static void StopOneClient(NetworkManager clientToStop) + { + clientToStop.Shutdown(); + Object.Destroy(clientToStop.gameObject); + NetworkManagerInstances.Remove(clientToStop); + } + + /// + /// Should always be invoked when finished with a single unit test + /// (i.e. during TearDown) + /// + public static void Destroy() + { + if (s_IsStarted == false) + { + return; + } + + s_IsStarted = false; + + // Shutdown the server which forces clients to disconnect + foreach (var networkManager in NetworkManagerInstances) + { + networkManager.Shutdown(); + } + + // Destroy the network manager instances + foreach (var networkManager in NetworkManagerInstances) + { + Object.DestroyImmediate(networkManager.gameObject); + } + + NetworkManagerInstances.Clear(); + + // Destroy the temporary GameObject used to run co-routines + if (s_CoroutineRunner != null) + { + s_CoroutineRunner.StopAllCoroutines(); + Object.DestroyImmediate(s_CoroutineRunner); + } + + Application.targetFrameRate = s_OriginalTargetFrameRate; + } + + /// + /// Starts NetworkManager instances created by the Create method. + /// + /// Whether or not to create a Host instead of Server + /// The Server NetworkManager + /// The Clients NetworkManager + /// called immediately after server and client(s) are started + /// + public static bool Start(bool host, NetworkManager server, NetworkManager[] clients, Action startInitializationCallback = null) + { + if (s_IsStarted) + { + throw new InvalidOperationException("MultiInstanceHelper already started. Did you forget to Destroy?"); + } + + s_IsStarted = true; + s_ClientCount = clients.Length; + + if (host) + { + server.StartHost(); + } + else + { + server.StartServer(); + } + + // if set, then invoke this for the server + startInitializationCallback?.Invoke(server); + + for (int i = 0; i < clients.Length; i++) + { + clients[i].StartClient(); + + // if set, then invoke this for the client + startInitializationCallback?.Invoke(clients[i]); + } + + return true; + } + + // Empty MonoBehaviour that is a holder of coroutine + private class CoroutineRunner : MonoBehaviour + { + } + + private static CoroutineRunner s_CoroutineRunner; + + /// + /// Runs a IEnumerator as a Coroutine on a dummy GameObject. Used to get exceptions coming from the coroutine + /// + /// The IEnumerator to run + public static Coroutine Run(IEnumerator enumerator) + { + if (s_CoroutineRunner == null) + { + s_CoroutineRunner = new GameObject(nameof(CoroutineRunner)).AddComponent(); + } + + return s_CoroutineRunner.StartCoroutine(enumerator); + } + + public class CoroutineResultWrapper + { + public T Result; + } + + private static uint s_AutoIncrementGlobalObjectIdHashCounter = 111111; + + /// + /// Normally we would only allow player prefabs to be set to a prefab. Not runtime created objects. + /// In order to prevent having a Resource folder full of a TON of prefabs that we have to maintain, + /// MultiInstanceHelper has a helper function that lets you mark a runtime created object to be + /// treated as a prefab by the Netcode. That's how we can get away with creating the player prefab + /// at runtime without it being treated as a SceneObject or causing other conflicts with the Netcode. + /// + /// The networkObject to be treated as Prefab + /// The GlobalObjectId to force + public static void MakeNetworkObjectTestPrefab(NetworkObject networkObject, uint globalObjectIdHash = default) + { + // Override `GlobalObjectIdHash` if `globalObjectIdHash` param is set + if (globalObjectIdHash != default) + { + networkObject.GlobalObjectIdHash = globalObjectIdHash; + } + + // Fallback to auto-increment if `GlobalObjectIdHash` was never set + if (networkObject.GlobalObjectIdHash == default) + { + networkObject.GlobalObjectIdHash = ++s_AutoIncrementGlobalObjectIdHashCounter; + } + + // Prevent object from being snapped up as a scene object + networkObject.IsSceneObject = false; + } + + // We use GameObject instead of SceneObject to be able to keep hierarchy + public static void MarkAsSceneObjectRoot(GameObject networkObjectRoot, NetworkManager server, NetworkManager[] clients) + { + networkObjectRoot.name += " - Server"; + + NetworkObject[] serverNetworkObjects = networkObjectRoot.GetComponentsInChildren(); + + for (int i = 0; i < serverNetworkObjects.Length; i++) + { + serverNetworkObjects[i].NetworkManagerOwner = server; + } + + for (int i = 0; i < clients.Length; i++) + { + GameObject root = Object.Instantiate(networkObjectRoot); + root.name += " - Client - " + i; + + NetworkObject[] clientNetworkObjects = root.GetComponentsInChildren(); + + for (int j = 0; j < clientNetworkObjects.Length; j++) + { + clientNetworkObjects[j].NetworkManagerOwner = clients[i]; + } + } + } + + /// + /// Waits on the client side to be connected. + /// + /// The client + /// The result. If null, it will automatically assert + /// The max frames to wait for + public static IEnumerator WaitForClientConnected(NetworkManager client, CoroutineResultWrapper result = null, int maxFrames = DefaultMaxFrames) + { + yield return WaitForClientsConnected(new NetworkManager[] { client }, result, maxFrames); + } + + /// + /// Similar to WaitForClientConnected, this waits for multiple clients to be connected. + /// + /// The clients to be connected + /// The result. If null, it will automatically assert< + /// The max frames to wait for + /// + public static IEnumerator WaitForClientsConnected(NetworkManager[] clients, CoroutineResultWrapper result = null, int maxFrames = DefaultMaxFrames) + { + // Make sure none are the host client + foreach (var client in clients) + { + if (client.IsServer) + { + throw new InvalidOperationException("Cannot wait for connected as server"); + } + } + + var startFrameNumber = Time.frameCount; + var allConnected = true; + while (Time.frameCount - startFrameNumber <= maxFrames) + { + allConnected = true; + foreach (var client in clients) + { + if (!client.IsConnectedClient) + { + allConnected = false; + break; + } + } + if (allConnected) + { + break; + } + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + + if (result != null) + { + result.Result = allConnected; + } + else + { + for (var i = 0; i < clients.Length; ++i) + { + var client = clients[i]; + // Logging i+1 because that's the local client ID they'll get (0 is server) + // Can't use client.LocalClientId because that doesn't get assigned until IsConnectedClient == true, + Assert.True(client.IsConnectedClient, $"Client {i + 1} never connected"); + } + } + } + + /// + /// Waits on the server side for 1 client to be connected + /// + /// The server + /// The result. If null, it will automatically assert + /// The max frames to wait for + public static IEnumerator WaitForClientConnectedToServer(NetworkManager server, CoroutineResultWrapper result = null, int maxFrames = DefaultMaxFrames) + { + yield return WaitForClientsConnectedToServer(server, server.IsHost ? s_ClientCount + 1 : s_ClientCount, result, maxFrames); + } + + /// + /// Waits on the server side for 1 client to be connected + /// + /// The server + /// The result. If null, it will automatically assert + /// The max frames to wait for + public static IEnumerator WaitForClientsConnectedToServer(NetworkManager server, int clientCount = 1, CoroutineResultWrapper result = null, int maxFrames = DefaultMaxFrames) + { + if (!server.IsServer) + { + throw new InvalidOperationException("Cannot wait for connected as client"); + } + + var startFrameNumber = Time.frameCount; + + while (Time.frameCount - startFrameNumber <= maxFrames && server.ConnectedClients.Count != clientCount) + { + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + + var res = server.ConnectedClients.Count == clientCount; + + if (result != null) + { + result.Result = res; + } + else + { + Assert.True(res, "A client never connected to server"); + } + } + + /// + /// Gets a NetworkObject instance as it's represented by a certain peer. + /// + /// The networkObjectId to get + /// The representation to get the object from + /// The result + /// Whether or not to fail if no object is found and result is null + /// The max frames to wait for + public static IEnumerator GetNetworkObjectByRepresentation(ulong networkObjectId, NetworkManager representation, CoroutineResultWrapper result, bool failIfNull = true, int maxFrames = DefaultMaxFrames) + { + if (result == null) + { + throw new ArgumentNullException("Result cannot be null"); + } + + var startFrameNumber = Time.frameCount; + + while (Time.frameCount - startFrameNumber <= maxFrames && representation.SpawnManager.SpawnedObjects.All(x => x.Value.NetworkObjectId != networkObjectId)) + { + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + + result.Result = representation.SpawnManager.SpawnedObjects.First(x => x.Value.NetworkObjectId == networkObjectId).Value; + + if (failIfNull && result.Result == null) + { + Assert.Fail("NetworkObject could not be found"); + } + } + + /// + /// Gets a NetworkObject instance as it's represented by a certain peer. + /// + /// The predicate used to filter for your target NetworkObject + /// The representation to get the object from + /// The result + /// Whether or not to fail if no object is found and result is null + /// The max frames to wait for + public static IEnumerator GetNetworkObjectByRepresentation(Func predicate, NetworkManager representation, CoroutineResultWrapper result, bool failIfNull = true, int maxFrames = DefaultMaxFrames) + { + if (result == null) + { + throw new ArgumentNullException("Result cannot be null"); + } + + if (predicate == null) + { + throw new ArgumentNullException("Predicate cannot be null"); + } + + var startFrame = Time.frameCount; + + while (Time.frameCount - startFrame <= maxFrames && !representation.SpawnManager.SpawnedObjects.Any(x => predicate(x.Value))) + { + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + + result.Result = representation.SpawnManager.SpawnedObjects.FirstOrDefault(x => predicate(x.Value)).Value; + + if (failIfNull && result.Result == null) + { + Assert.Fail("NetworkObject could not be found"); + } + } + + /// + /// Runs some code, then verifies the condition (combines 'Run' and 'WaitForCondition') + /// + /// Action / code to run + /// The predicate to wait for + /// The max frames to wait for + public static IEnumerator RunAndWaitForCondition(Action workload, Func predicate, int maxFrames = DefaultMaxFrames, int minFrames = DefaultMinFrames) + { + var waitResult = new CoroutineResultWrapper(); + workload(); + + yield return Run(WaitForCondition( + predicate, + waitResult, + maxFrames: maxFrames, + minFrames: minFrames)); + + if (!waitResult.Result) + { + throw new Exception(); + } + } + + /// + /// Waits for a predicate condition to be met + /// + /// The predicate to wait for + /// The result. If null, it will fail if the predicate is not met + /// The min frames to wait for + /// The max frames to wait for + public static IEnumerator WaitForCondition(Func predicate, CoroutineResultWrapper result = null, int maxFrames = DefaultMaxFrames, int minFrames = DefaultMinFrames) + { + if (predicate == null) + { + throw new ArgumentNullException("Predicate cannot be null"); + } + + var startFrameNumber = Time.frameCount; + + if (minFrames > 0) + { + yield return new WaitUntil(() => + { + return Time.frameCount >= minFrames; + }); + } + + while (Time.frameCount - startFrameNumber <= maxFrames && + !predicate()) + { + // Changed to 2 frames to avoid the scenario where it would take 1+ frames to + // see a value change (i.e. discovered in the NetworkTransformTests) + var nextFrameNumber = Time.frameCount + 2; + yield return new WaitUntil(() => + { + return Time.frameCount >= nextFrameNumber; + }); + } + + var res = predicate(); + + if (result != null) + { + result.Result = res; + } + else + { + Assert.True(res, "PREDICATE CONDITION"); + } + } + } +} diff --git a/Tests/Runtime/MultiInstanceHelpers.cs.meta b/Tests/Runtime/MultiInstanceHelpers.cs.meta new file mode 100644 index 0000000..37a6b37 --- /dev/null +++ b/Tests/Runtime/MultiInstanceHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1c62160e3e5b4489b2143fc21b56e55 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkBehaviourUpdaterTests.cs b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs new file mode 100644 index 0000000..0863787 --- /dev/null +++ b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkBehaviourUpdaterTests : BaseMultiInstanceTest + { + protected override int NbClients => throw new NotSupportedException("handled per test"); + + private static Type[] s_TypesToTest = new[] { null, typeof(ZeroNetVar), typeof(OneNetVar), typeof(TwoNetVar) }; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield break; + } + + /// + /// This runs test combinations for the following + /// test with 0, 1, 2 clients + /// test with host and server mode + /// test with 0, 1, 2 spawned objects + /// test with 0, 1, 2 network behaviour per prefab + /// test with 0, 1, 2 network variable per network behaviour + /// for each, update netvar + /// for each check value changed + /// check that all network variables are no longer dirty after update + /// + /// + /// + /// + /// + /// + /// + /// + [UnityTest] + public IEnumerator BehaviourUpdaterAllTests([Values(0, 1, 2)] int nbClients, [Values] bool useHost, [Values(0, 1, 2)] int nbSpawnedObjects, + [ValueSource(nameof(s_TypesToTest))] Type firstNetworkBehaviour, [ValueSource(nameof(s_TypesToTest))] Type secondNetworkBehaviour) + { + // Create multiple NetworkManager instances + if (!MultiInstanceHelpers.Create(nbClients, out NetworkManager server, out NetworkManager[] clients)) + { + Debug.LogError("Failed to create instances"); + Assert.Fail("Failed to create instances"); + } + m_ClientNetworkManagers = clients; + m_ServerNetworkManager = server; + Assert.That(m_ClientNetworkManagers.Length, Is.EqualTo(nbClients)); + Assert.That(m_ServerNetworkManager, Is.Not.Null); + + // setup prefab to spawn + void AddNetworkBehaviour(Type type, GameObject prefab) + { + if (type != null) + { + var info = prefab.AddComponent(type) as INetVarInfo; + } + } + var prefabToSpawn = new GameObject(); + var networkObjectPrefab = prefabToSpawn.AddComponent(); + AddNetworkBehaviour(firstNetworkBehaviour, prefabToSpawn); + AddNetworkBehaviour(secondNetworkBehaviour, prefabToSpawn); + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObjectPrefab); + m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Prefab = prefabToSpawn }); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + clientNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Prefab = prefabToSpawn }); + } + + // Start the instances + if (!MultiInstanceHelpers.Start(useHost, server, clients)) + { + Debug.LogError("Failed to start instances"); + Assert.Fail("Failed to start instances"); + } + // Wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients)); + + // Wait for connection on server side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnectedToServer(server, clientCount: useHost ? nbClients + 1 : nbClients)); + + // gathering netvars to test on + var serverNetVarsToUpdate = new List>(); + for (int i = 0; i < nbSpawnedObjects; i++) + { + var spawnedObject = Object.Instantiate(prefabToSpawn); + var networkSpawnedObject = spawnedObject.GetComponent(); + networkSpawnedObject.NetworkManagerOwner = m_ServerNetworkManager; + networkSpawnedObject.Spawn(); + int nbBehaviours = 0; + foreach (var networkBehaviour in spawnedObject.GetComponents()) + { + serverNetVarsToUpdate.AddRange(((INetVarInfo)networkBehaviour).AllNetVars); + nbBehaviours++; + } + Assert.That(nbBehaviours, Is.EqualTo((firstNetworkBehaviour == null ? 0 : 1) + (secondNetworkBehaviour == null ? 0 : 1))); + } + var serverNetVarCount = serverNetVarsToUpdate.Count; + + yield return new WaitForSeconds(0); // wait a frame to make sure spawn is done + + foreach (var netVar in serverNetVarsToUpdate) + { + Assert.That(netVar.Value, Is.EqualTo(0)); // sanity check + } + + // test updating all netvars + int updatedValue = 1; + foreach (var netVar in serverNetVarsToUpdate) + { + netVar.Value = updatedValue; + Assert.That(netVar.IsDirty, Is.True); + } + + m_ServerNetworkManager.BehaviourUpdater.NetworkBehaviourUpdate(m_ServerNetworkManager); + + // make sure we're not dirty anymore and that clients will receive that new value + foreach (var netVar in serverNetVarsToUpdate) + { + // if we don't have connected clients, netvars remain dirty + Assert.That(netVar.IsDirty, nbClients > 0 || useHost ? Is.Not.True : Is.True); + } + foreach (var client in m_ClientNetworkManagers) + { + var nbVarsCheckedClientSide = 0; + var countSpawnObjectResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.WaitForCondition(() => client.SpawnManager.SpawnedObjects.Count == nbSpawnedObjects, countSpawnObjectResult); + Assert.That(countSpawnObjectResult.Result, Is.True); + + foreach (var spawnedObject in client.SpawnManager.SpawnedObjects) + { + foreach (var behaviour in spawnedObject.Value.GetComponentsInChildren()) + { + foreach (var networkVariable in behaviour.NetworkVariableFields) + { + var varInt = networkVariable as NetworkVariable; + var varUpdateResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.WaitForCondition(() => varInt.Value == updatedValue, varUpdateResult); + Assert.That(varUpdateResult.Result, Is.True); + + nbVarsCheckedClientSide++; + Assert.That(varInt.Value, Is.EqualTo(updatedValue)); + } + } + } + Assert.That(nbVarsCheckedClientSide, Is.EqualTo(m_ClientNetworkManagers.Length > 0 ? serverNetVarCount : 0)); + } + } + } + + public interface INetVarInfo + { + public List> AllNetVars { get; } + } + + public class ZeroNetVar : NetworkBehaviour, INetVarInfo + { + public List> AllNetVars => new List>(); // Needed to be independant from NetworkBehaviour's list of fields. This way, if that changes, we can still do this validation in this test + } + + public class OneNetVar : NetworkBehaviour, INetVarInfo + { + private NetworkVariable m_SomeValue = new NetworkVariable(); + public List> AllNetVars => new List>() { m_SomeValue }; + } + + public class TwoNetVar : NetworkBehaviour, INetVarInfo + { + private NetworkVariable m_SomeValue = new NetworkVariable(); + private NetworkVariable m_SomeOtherValue = new NetworkVariable(); + public List> AllNetVars => new List>() { m_SomeValue, m_SomeOtherValue }; + } +} diff --git a/Tests/Runtime/NetworkBehaviourUpdaterTests.cs.meta b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs.meta new file mode 100644 index 0000000..594d533 --- /dev/null +++ b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef76141a46cb34c44a8ca73bc15d2bd6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkObject.meta b/Tests/Runtime/NetworkObject.meta new file mode 100644 index 0000000..3b96bc2 --- /dev/null +++ b/Tests/Runtime/NetworkObject.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3a1f0974a98eaa1498ac39be872eeed4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs new file mode 100644 index 0000000..2bc2c37 --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs @@ -0,0 +1,86 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Tests calling destroy on spawned / unspawned s. Expected behavior: + /// - Server or client destroy on unspawned => Object gets destroyed, no exceptions + /// - Server destroy spawned => Object gets destroyed and despawned/destroyed on all clients. Server does not run . Client runs it. + /// - Client destroy spawned => throw exception. + /// + public class NetworkObjectDestroyTests : BaseMultiInstanceTest + { + protected override int NbClients => 1; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, playerPrefab => + { + // playerPrefab.AddComponent(); + }); + } + + /// + /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. + /// + /// + [UnityTest] + public IEnumerator TestNetworkObjectServerDestroy() + { + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult)); + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult)); + + Assert.IsNotNull(serverClientPlayerResult.Result.gameObject); + Assert.IsNotNull(clientClientPlayerResult.Result.gameObject); + + // destroy the server player + Object.Destroy(serverClientPlayerResult.Result.gameObject); + + yield return null; + + Assert.IsTrue(serverClientPlayerResult.Result == null); // Assert.IsNull doesn't work here + + yield return null; // wait one frame more until we receive on client + + Assert.IsTrue(clientClientPlayerResult.Result == null); + + // create an unspawned networkobject and destroy it + var go = new GameObject(); + go.AddComponent(); + Object.Destroy(go); + + yield return null; + Assert.IsTrue(go == null); + } + + /// + /// Tests that a client cannot destroy a spawned networkobject. + /// + /// + [UnityTest] + public IEnumerator TestNetworkObjectClientDestroy() + { + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult)); + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult)); + + // destroy the client player, this is not allowed + LogAssert.Expect(LogType.Exception, "NotServerException: Destroy a spawned NetworkObject on a non-host client is not valid. Call Destroy or Despawn on the server/host instead."); + Object.DestroyImmediate(clientClientPlayerResult.Result.gameObject); + } + } +} diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs.meta b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs.meta new file mode 100644 index 0000000..76a6f7f --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectDestroyTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: adef71bb3a6193e498dedf91a2f378b9 +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 new file mode 100644 index 0000000..24dba57 --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs @@ -0,0 +1,77 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkObjectDontDestroyWithOwnerTests + { + [UnityTest] + public IEnumerator DontDestroyWithOwnerTest() + { + // create server and client instances + MultiInstanceHelpers.Create(1, out NetworkManager server, out NetworkManager[] clients); + + // create prefab + var gameObject = new GameObject("ClientOwnedObject"); + var networkObject = gameObject.AddComponent(); + networkObject.DontDestroyWithOwner = true; + MultiInstanceHelpers.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 + }); + } + + // start server and connect clients + MultiInstanceHelpers.Start(false, server, clients); + + // wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients)); + + // wait for connection on server side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientConnectedToServer(server)); + + // network objects + var networkObjects = new List(); + + // 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); + } + + // wait for object spawn on client + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => clients[0].SpawnManager.SpawnedObjects.Count == 32)); + + // disconnect the client that owns all the clients + MultiInstanceHelpers.StopOneClient(clients[0]); + + // wait for disconnect + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => server.ConnectedClients.Count == 0)); + + for (int i = 0; i < networkObjects.Count; i++) + { + // ensure ownership was transferred back + Assert.That(networkObjects[i].OwnerClientId == server.ServerClientId); + } + + // cleanup + MultiInstanceHelpers.Destroy(); + } + } +} diff --git a/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs.meta b/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs.meta new file mode 100644 index 0000000..9e08ce6 --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectDontDestroyWithOwnerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 216add57838067047b8f67fed02d6c71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs new file mode 100644 index 0000000..597563f --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs @@ -0,0 +1,183 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkObjectOnSpawnTests : BaseMultiInstanceTest + { + private GameObject m_TestNetworkObjectPrefab; + private GameObject m_TestNetworkObjectInstance; + + protected override int NbClients => 2; + + + /// + /// Tests that instantiating a and destroying without spawning it + /// does not run or . + /// + /// + [UnityTest] + public IEnumerator InstantiateDestroySpawnNotCalled() + { + m_TestNetworkObjectPrefab = new GameObject("InstantiateDestroySpawnNotCalled_Object"); + var networkObject = m_TestNetworkObjectPrefab.AddComponent(); + var fail = m_TestNetworkObjectPrefab.AddComponent(); + + // instantiate + m_TestNetworkObjectInstance = Object.Instantiate(m_TestNetworkObjectPrefab); + yield return null; + } + + private class FailWhenSpawned : NetworkBehaviour + { + public override void OnNetworkSpawn() + { + Assert.Fail("Spawn should not be called on not spawned object"); + } + + public override void OnNetworkDespawn() + { + Assert.Fail("Depawn should not be called on not spawned object"); + } + } + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, playerPrefab => + { + // add test component + playerPrefab.AddComponent(); + }); + } + + [UnityTearDown] + public override IEnumerator Teardown() + { + + if (m_TestNetworkObjectPrefab != null) + { + Object.Destroy(m_TestNetworkObjectPrefab); + } + + if (m_TestNetworkObjectInstance != null) + { + Object.Destroy(m_TestNetworkObjectInstance); + } + yield return base.Teardown(); + + } + + /// + /// Test that callbacks are run for playerobject spawn, despawn, regular spawn, destroy on server. + /// + /// + [UnityTest] + public IEnumerator TestOnNetworkSpawnCallbacks() + { + // [Host-Side] Get the Host owned instance + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult)); + + var serverInstance = serverClientPlayerResult.Result.GetComponent(); + + var clientInstances = new List(); + foreach (var client in m_ClientNetworkManagers) + { + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), client, clientClientPlayerResult)); + var clientRpcTests = clientClientPlayerResult.Result.GetComponent(); + Assert.IsNotNull(clientRpcTests); + clientInstances.Add(clientRpcTests); + } + + // -------------- step 1 check player spawn despawn + + // check spawned on server + Assert.AreEqual(1, serverInstance.OnNetworkSpawnCalledCount); + + // safety check despawned + Assert.AreEqual(0, serverInstance.OnNetworkDespawnCalledCount); + + // check spawned on client + foreach (var clientInstance in clientInstances) + { + Assert.AreEqual(1, clientInstance.OnNetworkSpawnCalledCount); + + // safety check despawned + Assert.AreEqual(0, clientInstance.OnNetworkDespawnCalledCount); + } + + // despawn on server. However, since we'll be using this object later in the test, don't delete it (false) + serverInstance.GetComponent().Despawn(false); + + // check despawned on server + Assert.AreEqual(1, serverInstance.OnNetworkDespawnCalledCount); + + // wait long enough for player object to be despawned + int nextFrameNumber = Time.frameCount + 2; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + // check despawned on clients + foreach (var clientInstance in clientInstances) + { + Assert.AreEqual(1, clientInstance.OnNetworkDespawnCalledCount); + } + + //----------- step 2 check spawn again and destroy + + serverInstance.GetComponent().Spawn(); + + // wait long enough for player object to be spawned + nextFrameNumber = Time.frameCount + 2; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + // check spawned again on server this is 2 because we are reusing the object which was already spawned once. + Assert.AreEqual(2, serverInstance.OnNetworkSpawnCalledCount); + + // check spawned on client + foreach (var clientInstance in clientInstances) + { + Assert.AreEqual(1, clientInstance.OnNetworkSpawnCalledCount); + } + + // destroy the server object + Object.Destroy(serverInstance.gameObject); + + // wait one frame for destroy to kick in + yield return null; + + // check whether despawned was called again on server instance + Assert.AreEqual(2, serverInstance.OnNetworkDespawnCalledCount); + + // wait long enough for player object to be despawned on client + nextFrameNumber = Time.frameCount + 2; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + // check despawned on clients + foreach (var clientInstance in clientInstances) + { + Assert.AreEqual(1, clientInstance.OnNetworkDespawnCalledCount); + } + } + + private class TrackOnSpawnFunctions : NetworkBehaviour + { + public int OnNetworkSpawnCalledCount { get; private set; } + public int OnNetworkDespawnCalledCount { get; private set; } + + public override void OnNetworkSpawn() + { + OnNetworkSpawnCalledCount++; + } + + public override void OnNetworkDespawn() + { + OnNetworkDespawnCalledCount++; + } + } + } +} diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs.meta b/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs.meta new file mode 100644 index 0000000..407ad63 --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dbb32425abaa21a44aeacf00f37ca4e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs new file mode 100644 index 0000000..26e2486 --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs @@ -0,0 +1,153 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkObjectOwnershipComponent : NetworkBehaviour + { + public bool OnLostOwnershipFired = false; + public ulong CachedOwnerIdOnLostOwnership = 0; + + 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) + { + m_IsHost = isHost; + } + + [UnitySetUp] + public IEnumerator Setup() + { + // we need at least 1 client for tests + Assert.That(k_ClientInstanceCount, Is.GreaterThan(0)); + + // create NetworkManager instances + Assert.That(MultiInstanceHelpers.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(); + MultiInstanceHelpers.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 }); + } + + // start server and client NetworkManager instances + Assert.That(MultiInstanceHelpers.Start(m_IsHost, m_ServerNetworkManager, m_ClientNetworkManagers)); + + // wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(m_ClientNetworkManagers)); + + // wait for connection on server side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientConnectedToServer(m_ServerNetworkManager)); + } + + [TearDown] + public void Teardown() + { + MultiInstanceHelpers.Destroy(); + + if (m_DummyGameObject != null) + { + Object.DestroyImmediate(m_DummyGameObject); + } + + if (m_DummyPrefab != null) + { + Object.DestroyImmediate(m_DummyPrefab); + } + } + + [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)); + + int nextFrameNumber = Time.frameCount + 2; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + Assert.That(m_ServerNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(dummyNetworkObjectId)); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + Assert.That(clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(dummyNetworkObjectId)); + } + + + var serverObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[dummyNetworkObjectId]; + var clientObject = m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[dummyNetworkObjectId]; + Assert.That(serverObject, Is.Not.Null); + Assert.That(clientObject, Is.Not.Null); + + var serverComponent = serverObject.GetComponent(); + var clientComponent = clientObject.GetComponent(); + 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(m_ServerNetworkManager.ConnectedClients.ContainsKey(m_ClientNetworkManagers[0].LocalClientId)); + serverObject.ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); + + nextFrameNumber = Time.frameCount + 2; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + + Assert.That(clientComponent.OnGainedOwnershipFired); + Assert.That(clientComponent.CachedOwnerIdOnGainedOwnership, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); + serverObject.ChangeOwnership(m_ServerNetworkManager.ServerClientId); + + nextFrameNumber = Time.frameCount + 2; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + + Assert.That(serverObject.OwnerClientId, Is.EqualTo(m_ServerNetworkManager.LocalClientId)); + Assert.That(clientComponent.OnLostOwnershipFired); + Assert.That(clientComponent.CachedOwnerIdOnLostOwnership, Is.EqualTo(m_ClientNetworkManagers[0].LocalClientId)); + } + } +} diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs.meta b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs.meta new file mode 100644 index 0000000..d8dea9e --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 08f9390096acb44698be8b3eacf242ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSceneSerializationTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectSceneSerializationTests.cs new file mode 100644 index 0000000..d092ab1 --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectSceneSerializationTests.cs @@ -0,0 +1,232 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; +using NUnit.Framework; +using Unity.Collections; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkObjectSceneSerializationTests + { + + /// + /// The purpose behind this test is to assure that in-scene NetworkObjects + /// that are serialized into a single stream (approval or switch scene this happens) + /// will continue to be processed even if one of the NetworkObjects is invalid. + /// + [Test] + public void NetworkObjectSceneSerializationFailure() + { + var networkObjectsToTest = new List(); + + var writer = new FastBufferWriter(1300, Allocator.Temp, 4096000); + var invalidNetworkObjectOffsets = new List(); + var invalidNetworkObjectIdCount = new List(); + var invalidNetworkObjects = new List(); + var invalidNetworkObjectFrequency = 3; + using (writer) + { + // Construct 50 NetworkObjects + for (int i = 0; i < 50; i++) + { + // Inject an invalid NetworkObject every [invalidNetworkObjectFrequency] entry + if ((i % invalidNetworkObjectFrequency) == 0) + { + // Create the invalid NetworkObject + var gameObject = new GameObject($"InvalidTestObject{i}"); + + Assert.IsNotNull(gameObject); + + var networkObject = gameObject.AddComponent(); + + Assert.IsNotNull(networkObject); + + var networkVariableComponent = gameObject.AddComponent(); + Assert.IsNotNull(networkVariableComponent); + + // Add invalid NetworkObject's starting position before serialization to handle trapping for the Debug.LogError message + // that we know will be thrown + invalidNetworkObjectOffsets.Add(writer.Position); + + networkObject.GlobalObjectIdHash = (uint)(i); + invalidNetworkObjectIdCount.Add(i); + + invalidNetworkObjects.Add(gameObject); + + writer.WriteValueSafe((int)networkObject.gameObject.scene.handle); + // Serialize the invalid NetworkObject + var sceneObject = networkObject.GetMessageSceneObject(0); + var prePosition = writer.Position; + sceneObject.Serialize(writer); + + Debug.Log( + $"Invalid {nameof(NetworkObject)} Size {writer.Position - prePosition}"); + + // Now adjust how frequent we will inject invalid NetworkObjects + invalidNetworkObjectFrequency = Random.Range(2, 5); + + } + else + { + // Create a valid NetworkObject + var gameObject = new GameObject($"TestObject{i}"); + + Assert.IsNotNull(gameObject); + + var networkObject = gameObject.AddComponent(); + + var networkVariableComponent = gameObject.AddComponent(); + Assert.IsNotNull(networkVariableComponent); + + Assert.IsNotNull(networkObject); + + networkObject.GlobalObjectIdHash = (uint)(i + 4096); + + networkObjectsToTest.Add(gameObject); + + writer.WriteValueSafe((int)networkObject.gameObject.scene.handle); + + // Handle populating the scenes loaded list + var scene = networkObject.gameObject.scene; + + if (!NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenesLoaded.ContainsKey( + scene.handle)) + { + NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenesLoaded + .Add(scene.handle, scene); + } + + // Since this is a unit test, we will fake the server to client handle lookup by just adding the same handle key and value + if (!NetworkManagerHelper.NetworkManagerObject.SceneManager.ServerSceneHandleToClientSceneHandle + .ContainsKey(networkObject.gameObject.scene.handle)) + { + NetworkManagerHelper.NetworkManagerObject.SceneManager.ServerSceneHandleToClientSceneHandle + .Add(networkObject.gameObject.scene.handle, networkObject.gameObject.scene.handle); + } + + // Serialize the valid NetworkObject + var sceneObject = networkObject.GetMessageSceneObject(0); + sceneObject.Serialize(writer); + + if (!NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenePlacedObjects.ContainsKey( + networkObject.GlobalObjectIdHash)) + { + NetworkManagerHelper.NetworkManagerObject.SceneManager.ScenePlacedObjects.Add( + networkObject.GlobalObjectIdHash, new Dictionary()); + } + + // Add this valid NetworkObject into the ScenePlacedObjects list + NetworkManagerHelper.NetworkManagerObject.SceneManager + .ScenePlacedObjects[networkObject.GlobalObjectIdHash] + .Add(SceneManager.GetActiveScene().handle, networkObject); + } + } + + var totalBufferSize = writer.Position; + + var reader = new FastBufferReader(writer, Allocator.Temp); + using (reader) + { + + var networkObjectsDeSerialized = new List(); + var currentLogLevel = NetworkManager.Singleton.LogLevel; + var invalidNetworkObjectCount = 0; + while (reader.Position != totalBufferSize) + { + // If we reach the point where we expect it to fail, then make sure we let TestRunner know it should expect this log error message + if (invalidNetworkObjectOffsets.Count > 0 && + reader.Position == invalidNetworkObjectOffsets[0]) + { + invalidNetworkObjectOffsets.RemoveAt(0); + + // Turn off Network Logging to avoid other errors that we know will happen after the below LogAssert.Expect message occurs. + NetworkManager.Singleton.LogLevel = LogLevel.Nothing; + + // Trap for this specific error message so we don't make Test Runner think we failed (it will fail on Debug.LogError) + UnityEngine.TestTools.LogAssert.Expect(LogType.Error, + $"Failed to spawn {nameof(NetworkObject)} for Hash {invalidNetworkObjectIdCount[invalidNetworkObjectCount]}."); + + invalidNetworkObjectCount++; + } + + + reader.ReadValueSafe(out int handle); + NetworkManagerHelper.NetworkManagerObject.SceneManager.SetTheSceneBeingSynchronized(handle); + var sceneObject = new NetworkObject.SceneObject(); + sceneObject.Deserialize(reader); + + var deserializedNetworkObject = NetworkObject.AddSceneObject(sceneObject, reader, + NetworkManagerHelper.NetworkManagerObject); + if (deserializedNetworkObject != null) + { + networkObjectsDeSerialized.Add(deserializedNetworkObject); + } + else + { + // Under this condition, we are expecting null (i.e. no NetworkObject instantiated) + // and will set our log level back to the original value to assure the valid NetworkObjects + // aren't causing any log Errors to occur + NetworkManager.Singleton.LogLevel = currentLogLevel; + } + } + + // Now validate all NetworkObjects returned against the original NetworkObjects we created + // after they validate, destroy the objects + foreach (var entry in networkObjectsToTest) + { + var entryNetworkObject = entry.GetComponent(); + Assert.IsTrue(networkObjectsDeSerialized.Contains(entryNetworkObject)); + Object.Destroy(entry); + } + } + } + + // Destroy the invalid network objects + foreach (var entry in invalidNetworkObjects) + { + Object.Destroy(entry); + } + } + + [SetUp] + public void Setup() + { + // Create, instantiate, and host + NetworkManagerHelper.StartNetworkManager(out NetworkManager networkManager, NetworkManagerHelper.NetworkManagerOperatingMode.None); + networkManager.NetworkConfig.EnableSceneManagement = true; + networkManager.StartHost(); + + } + + [TearDown] + public void TearDown() + { + // Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + } + } + + /// + /// A simple test class that will provide varying NetworkBuffer stream sizes + /// when the NetworkVariable is serialized + /// + public class NetworkBehaviourWithNetworkVariables : NetworkBehaviour + { + private const uint k_MinDataBlocks = 1; + private const uint k_MaxDataBlocks = 64; + + public NetworkList NetworkVariableData; + + private void Awake() + { + var dataBlocksAssigned = new List(); + var numberDataBlocks = Random.Range(k_MinDataBlocks, k_MaxDataBlocks); + for (var i = 0; i < numberDataBlocks; i++) + { + dataBlocksAssigned.Add((ulong)Random.Range(0.0f, float.MaxValue)); + } + + NetworkVariableData = new NetworkList(dataBlocksAssigned); + } + } +} diff --git a/Tests/Runtime/NetworkObject/NetworkObjectSceneSerializationTests.cs.meta b/Tests/Runtime/NetworkObject/NetworkObjectSceneSerializationTests.cs.meta new file mode 100644 index 0000000..39e88d4 --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectSceneSerializationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c8e27bb7c831a8f49884a8af5e59f2f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkPrefabHandlerTests.cs b/Tests/Runtime/NetworkPrefabHandlerTests.cs new file mode 100644 index 0000000..da1b712 --- /dev/null +++ b/Tests/Runtime/NetworkPrefabHandlerTests.cs @@ -0,0 +1,234 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using UnityEngine; +using NUnit.Framework; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// The NetworkPrefabHandler unit tests validates: + /// Registering with GameObject, NetworkObject, or GlobalObjectIdHash + /// Newly assigned rotation or position values for newly spawned NetworkObject instances are valid + /// Destroying a newly spawned NetworkObject instance works + /// Removing a INetworkPrefabInstanceHandler is removed and can be verified (very last check) + /// + public class NetworkPrefabHandlerTests + { + + private const string k_TestPrefabObjectName = "NetworkPrefabTestObject"; + private uint m_ObjectId = 1; + private GameObject MakeValidNetworkPrefab() + { + Guid baseObjectID = NetworkManagerHelper.AddGameNetworkObject(k_TestPrefabObjectName + m_ObjectId.ToString()); + NetworkObject validPrefab = NetworkManagerHelper.InstantiatedNetworkObjects[baseObjectID]; + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(validPrefab); + m_ObjectId++; + return validPrefab.gameObject; + } + + /// + /// Tests the NetwokConfig NetworkPrefabs initialization during NetworkManager's Init method to make sure that + /// it will still initialize but remove the invalid prefabs + /// + [Test] + public void NetworkConfigInvalidNetworkPrefabTest() + { + + // Add null entry + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(null); + + // Add a NetworkPrefab with no prefab + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab()); + + // Add a NetworkPrefab override with an invalid hash + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Override = NetworkPrefabOverride.Hash, SourceHashToOverride = 0 }); + + // Add a NetworkPrefab override with a valid hash but an invalid target prefab + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Override = NetworkPrefabOverride.Hash, SourceHashToOverride = 654321, OverridingTargetPrefab = null }); + + // Add a NetworkPrefab override with a valid hash to override but an invalid target prefab + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Override = NetworkPrefabOverride.Prefab, SourceHashToOverride = 654321, OverridingTargetPrefab = null }); + + // Add a NetworkPrefab override with an invalid source prefab to override + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Override = NetworkPrefabOverride.Prefab, SourcePrefabToOverride = null }); + + // Add a NetworkPrefab override with a valid source prefab to override but an invalid target prefab + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Override = NetworkPrefabOverride.Prefab, SourcePrefabToOverride = MakeValidNetworkPrefab(), OverridingTargetPrefab = null }); + + // Add a valid prefab + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Prefab = MakeValidNetworkPrefab() }); + + // Add a NetworkPrefab override with a valid hash and valid target prefab + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Override = NetworkPrefabOverride.Hash, SourceHashToOverride = 11111111, OverridingTargetPrefab = MakeValidNetworkPrefab() }); + + // Add a NetworkPrefab override with a valid prefab and valid target prefab + NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Override = NetworkPrefabOverride.Prefab, SourcePrefabToOverride = MakeValidNetworkPrefab(), OverridingTargetPrefab = MakeValidNetworkPrefab() }); + + var exceptionOccurred = false; + try + { + NetworkManagerHelper.NetworkManagerObject.StartHost(); + } + catch + { + exceptionOccurred = true; + } + + Assert.False(exceptionOccurred); + + // In the end we should only have 3 valid registered network prefabs + Assert.True(NetworkManagerHelper.NetworkManagerObject.NetworkConfig.NetworkPrefabOverrideLinks.Count == 3); + } + + private const string k_PrefabObjectName = "NetworkPrefabHandlerTestObject"; + + [Test] + public void NetworkPrefabHandlerClass() + { + Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _)); + var testPrefabObjectName = k_PrefabObjectName; + + Guid baseObjectID = NetworkManagerHelper.AddGameNetworkObject(testPrefabObjectName); + NetworkObject baseObject = NetworkManagerHelper.InstantiatedNetworkObjects[baseObjectID]; + + var networkPrefabHandler = new NetworkPrefabHandler(); + var networkPrefaInstanceHandler = new NetworkPrefaInstanceHandler(baseObject); + + var prefabPosition = new Vector3(1.0f, 5.0f, 3.0f); + var prefabRotation = new Quaternion(1.0f, 0.5f, 0.4f, 0.1f); + + //Register via GameObject + var gameObjectRegistered = networkPrefabHandler.AddHandler(baseObject.gameObject, networkPrefaInstanceHandler); + + //Test result of registering via GameObject reference + Assert.True(gameObjectRegistered); + + var spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation); + + //Test that something was instantiated + Assert.NotNull(spawnedObject); + + //Test that this is indeed an instance of our original object + Assert.True(spawnedObject.name.Contains(testPrefabObjectName)); + + //Test for position and rotation + Assert.True(prefabPosition == spawnedObject.transform.position); + Assert.True(prefabRotation == spawnedObject.transform.rotation); + + networkPrefabHandler.HandleNetworkPrefabDestroy(spawnedObject); //Destroy our prefab instance + networkPrefabHandler.RemoveHandler(baseObject); //Remove our handler + + //Register via NetworkObject + gameObjectRegistered = networkPrefabHandler.AddHandler(baseObject, networkPrefaInstanceHandler); + + //Test result of registering via NetworkObject reference + Assert.True(gameObjectRegistered); + + //Change it up + prefabPosition = new Vector3(2.0f, 1.0f, 5.0f); + prefabRotation = new Quaternion(4.0f, 1.5f, 5.4f, 5.1f); + + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation); + + //Test that something was instantiated + Assert.NotNull(spawnedObject); + + //Test that this is indeed an instance of our original object + Assert.True(spawnedObject.name.Contains(testPrefabObjectName)); + + //Test for position and rotation + Assert.True(prefabPosition == spawnedObject.transform.position); + Assert.True(prefabRotation == spawnedObject.transform.rotation); + + networkPrefabHandler.HandleNetworkPrefabDestroy(spawnedObject); //Destroy our prefab instance + networkPrefabHandler.RemoveHandler(baseObject); //Remove our handler + + //Register via GlobalObjectIdHash + gameObjectRegistered = networkPrefabHandler.AddHandler(baseObject.GlobalObjectIdHash, networkPrefaInstanceHandler); + + //Test result of registering via GlobalObjectIdHash reference + Assert.True(gameObjectRegistered); + + //Change it up + prefabPosition = new Vector3(6.0f, 4.0f, 1.0f); + prefabRotation = new Quaternion(3f, 2f, 4f, 1f); + + spawnedObject = networkPrefabHandler.HandleNetworkPrefabSpawn(baseObject.GlobalObjectIdHash, 0, prefabPosition, prefabRotation); + + //Test that something was instantiated + Assert.NotNull(spawnedObject); + + //Test that this is indeed an instance of our original object + Assert.True(spawnedObject.name.Contains(testPrefabObjectName)); + + //Test for position and rotation + Assert.True(prefabPosition == spawnedObject.transform.position); + Assert.True(prefabRotation == spawnedObject.transform.rotation); + + networkPrefabHandler.HandleNetworkPrefabDestroy(spawnedObject); //Destroy our prefab instance + networkPrefabHandler.RemoveHandler(baseObject); //Remove our handler + + Assert.False(networkPrefaInstanceHandler.StillHasInstances()); + } + + [SetUp] + public void Setup() + { + //Create, instantiate, and host + NetworkManagerHelper.StartNetworkManager(out _, NetworkManagerHelper.NetworkManagerOperatingMode.None); + } + + [TearDown] + public void TearDown() + { + //Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + + var networkObjects = UnityEngine.Object.FindObjectsOfType().ToList(); + var networkObjectsList = networkObjects.Where(c => c.name.Contains(k_PrefabObjectName)); + foreach (var networkObject in networkObjectsList) + { + UnityEngine.Object.DestroyImmediate(networkObject); + } + } + } + + /// + /// The Prefab instance handler to use for this test + /// + public class NetworkPrefaInstanceHandler : INetworkPrefabInstanceHandler + { + private NetworkObject m_NetworkObject; + + private List m_Instances; + + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) + { + var networkObjectInstance = UnityEngine.Object.Instantiate(m_NetworkObject.gameObject).GetComponent(); + networkObjectInstance.transform.position = position; + networkObjectInstance.transform.rotation = rotation; + m_Instances.Add(networkObjectInstance); + return networkObjectInstance; + } + + public void Destroy(NetworkObject networkObject) + { + var instancesContainsNetworkObject = m_Instances.Contains(networkObject); + Assert.True(instancesContainsNetworkObject); + m_Instances.Remove(networkObject); + UnityEngine.Object.Destroy(networkObject.gameObject); + } + + public bool StillHasInstances() + { + return (m_Instances.Count > 0); + } + + public NetworkPrefaInstanceHandler(NetworkObject networkObject) + { + m_NetworkObject = networkObject; + m_Instances = new List(); + } + } +} diff --git a/Tests/Runtime/NetworkPrefabHandlerTests.cs.meta b/Tests/Runtime/NetworkPrefabHandlerTests.cs.meta new file mode 100644 index 0000000..334e3eb --- /dev/null +++ b/Tests/Runtime/NetworkPrefabHandlerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 590750036c5d65c43bbc696071aa3541 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkShowHideTests.cs b/Tests/Runtime/NetworkShowHideTests.cs new file mode 100644 index 0000000..2df915b --- /dev/null +++ b/Tests/Runtime/NetworkShowHideTests.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkShowHideTest : NetworkBehaviour + { + + } + + public class ShowHideObject : NetworkBehaviour + { + public NetworkVariable MyNetworkVariable; + + private void Start() + { + MyNetworkVariable = new NetworkVariable(); + MyNetworkVariable.OnValueChanged += Changed; + } + + public void Changed(int before, int after) + { + Debug.Log($"Value changed from {before} to {after}"); + } + + } + + public class NetworkShowHideTests : BaseMultiInstanceTest + { + protected override int NbClients => 2; + + private ulong m_ClientId0; + private GameObject m_PrefabToSpawn; + + private NetworkObject m_NetSpawnedObject1; + private NetworkObject m_NetSpawnedObject2; + private NetworkObject m_NetSpawnedObject3; + private NetworkObject m_Object1OnClient0; + private NetworkObject m_Object2OnClient0; + private NetworkObject m_Object3OnClient0; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(useHost: true, nbClients: NbClients, + updatePlayerPrefab: playerPrefab => + { + var networkTransform = playerPrefab.AddComponent(); + m_PrefabToSpawn = PreparePrefab(typeof(ShowHideObject)); + }); + } + + public GameObject PreparePrefab(Type type) + { + var prefabToSpawn = new GameObject(); + prefabToSpawn.AddComponent(type); + var networkObjectPrefab = prefabToSpawn.AddComponent(); + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObjectPrefab); + m_ServerNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Prefab = prefabToSpawn }); + foreach (var clientNetworkManager in m_ClientNetworkManagers) + { + clientNetworkManager.NetworkConfig.NetworkPrefabs.Add(new NetworkPrefab() { Prefab = prefabToSpawn }); + } + return prefabToSpawn; + } + + // Check that the first client see them, or not, as expected + private IEnumerator CheckVisible(bool target) + { + int count = 0; + do + { + yield return new WaitForSeconds(0.1f); + count++; + + if (count > 20) + { + // timeout waiting for object to reach the expect visibility + Assert.Fail("timeout waiting for object to reach the expect visibility"); + break; + } + } while (m_NetSpawnedObject1.IsNetworkVisibleTo(m_ClientId0) != target || + m_NetSpawnedObject2.IsNetworkVisibleTo(m_ClientId0) != target || + m_NetSpawnedObject3.IsNetworkVisibleTo(m_ClientId0) != target || + m_Object1OnClient0.IsSpawned != target || + m_Object2OnClient0.IsSpawned != target || + m_Object3OnClient0.IsSpawned != target + ); + + Debug.Assert(m_NetSpawnedObject1.IsNetworkVisibleTo(m_ClientId0) == target); + Debug.Assert(m_NetSpawnedObject2.IsNetworkVisibleTo(m_ClientId0) == target); + Debug.Assert(m_NetSpawnedObject3.IsNetworkVisibleTo(m_ClientId0) == target); + + Debug.Assert(m_Object1OnClient0.IsSpawned == target); + Debug.Assert(m_Object2OnClient0.IsSpawned == target); + Debug.Assert(m_Object3OnClient0.IsSpawned == target); + } + + // Set the 3 objects visibility + private void Show(bool individually, bool visibility) + { + if (individually) + { + if (!visibility) + { + m_NetSpawnedObject1.NetworkHide(m_ClientId0); + m_NetSpawnedObject2.NetworkHide(m_ClientId0); + m_NetSpawnedObject3.NetworkHide(m_ClientId0); + } + else + { + m_NetSpawnedObject1.NetworkShow(m_ClientId0); + m_NetSpawnedObject2.NetworkShow(m_ClientId0); + m_NetSpawnedObject3.NetworkShow(m_ClientId0); + } + } + else + { + var list = new List(); + list.Add(m_NetSpawnedObject1); + list.Add(m_NetSpawnedObject2); + list.Add(m_NetSpawnedObject3); + + if (!visibility) + { + NetworkObject.NetworkHide(list, m_ClientId0); + } + else + { + NetworkObject.NetworkShow(list, m_ClientId0); + } + } + } + + private IEnumerator RefreshNetworkObjects() + { + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run( + MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.NetworkObjectId == m_NetSpawnedObject1.NetworkObjectId, + m_ClientNetworkManagers[0], + serverClientPlayerResult)); + m_Object1OnClient0 = serverClientPlayerResult.Result; + yield return MultiInstanceHelpers.Run( + MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.NetworkObjectId == m_NetSpawnedObject2.NetworkObjectId, + m_ClientNetworkManagers[0], + serverClientPlayerResult)); + m_Object2OnClient0 = serverClientPlayerResult.Result; + serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run( + MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.NetworkObjectId == m_NetSpawnedObject3.NetworkObjectId, + m_ClientNetworkManagers[0], + serverClientPlayerResult)); + m_Object3OnClient0 = serverClientPlayerResult.Result; + + // make sure the objects are set with the right network manager + m_Object1OnClient0.NetworkManagerOwner = m_ClientNetworkManagers[0]; + m_Object2OnClient0.NetworkManagerOwner = m_ClientNetworkManagers[0]; + m_Object3OnClient0.NetworkManagerOwner = m_ClientNetworkManagers[0]; + } + + + [UnityTest] + public IEnumerator NetworkShowHideTest() + { + m_ClientId0 = m_ClientNetworkManagers[0].LocalClientId; + + // create 3 objects + + + var spawnedObject1 = UnityEngine.Object.Instantiate(m_PrefabToSpawn); + var spawnedObject2 = UnityEngine.Object.Instantiate(m_PrefabToSpawn); + var spawnedObject3 = UnityEngine.Object.Instantiate(m_PrefabToSpawn); + m_NetSpawnedObject1 = spawnedObject1.GetComponent(); + m_NetSpawnedObject2 = spawnedObject2.GetComponent(); + m_NetSpawnedObject3 = spawnedObject3.GetComponent(); + m_NetSpawnedObject1.NetworkManagerOwner = m_ServerNetworkManager; + m_NetSpawnedObject2.NetworkManagerOwner = m_ServerNetworkManager; + m_NetSpawnedObject3.NetworkManagerOwner = m_ServerNetworkManager; + m_NetSpawnedObject1.Spawn(); + m_NetSpawnedObject2.Spawn(); + m_NetSpawnedObject3.Spawn(); + + + for (int mode = 0; mode < 2; mode++) + { + // get the NetworkObject on a client instance + yield return RefreshNetworkObjects(); + + // check object start visible + yield return CheckVisible(true); + + // hide them on one client + Show(mode == 0, false); + + yield return new WaitForSeconds(1.0f); + + m_NetSpawnedObject1.GetComponent().MyNetworkVariable.Value = 3; + + yield return new WaitForSeconds(1.0f); + + // verify they got hidden + yield return CheckVisible(false); + + // show them to that client + Show(mode == 0, true); + yield return RefreshNetworkObjects(); + + // verify they become visible + yield return CheckVisible(true); + } + } + } +} diff --git a/Tests/Runtime/NetworkShowHideTests.cs.meta b/Tests/Runtime/NetworkShowHideTests.cs.meta new file mode 100644 index 0000000..8550a0f --- /dev/null +++ b/Tests/Runtime/NetworkShowHideTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bad53d2f164c74c46a55fa2ccb1f9b57 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkSpawnManagerTests.cs b/Tests/Runtime/NetworkSpawnManagerTests.cs new file mode 100644 index 0000000..621ba68 --- /dev/null +++ b/Tests/Runtime/NetworkSpawnManagerTests.cs @@ -0,0 +1,129 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkSpawnManagerTests : BaseMultiInstanceTest + { + private ulong serverSideClientId => m_ServerNetworkManager.ServerClientId; + private ulong clientSideClientId => m_ClientNetworkManagers[0].LocalClientId; + private ulong otherClientSideClientId => m_ClientNetworkManagers[1].LocalClientId; + + protected override int NbClients => 2; + + [Test] + public void TestServerCanAccessItsOwnPlayer() + { + // server can access its own player + var serverSideServerPlayerObject = m_ServerNetworkManager.SpawnManager.GetPlayerNetworkObject(serverSideClientId); + Assert.NotNull(serverSideServerPlayerObject); + Assert.AreEqual(serverSideClientId, serverSideServerPlayerObject.OwnerClientId); + } + + [Test] + public void TestServerCanAccessOtherPlayers() + { + // server can access other players + var serverSideClientPlayerObject = m_ServerNetworkManager.SpawnManager.GetPlayerNetworkObject(clientSideClientId); + Assert.NotNull(serverSideClientPlayerObject); + Assert.AreEqual(clientSideClientId, serverSideClientPlayerObject.OwnerClientId); + + var serverSideOtherClientPlayerObject = m_ServerNetworkManager.SpawnManager.GetPlayerNetworkObject(otherClientSideClientId); + Assert.NotNull(serverSideOtherClientPlayerObject); + Assert.AreEqual(otherClientSideClientId, serverSideOtherClientPlayerObject.OwnerClientId); + } + + [Test] + public void TestClientCantAccessServerPlayer() + { + // client can't access server player + Assert.Throws(() => + { + m_ClientNetworkManagers[0].SpawnManager.GetPlayerNetworkObject(serverSideClientId); + }); + } + + [Test] + public void TestClientCanAccessOwnPlayer() + { + // client can access own player + var clientSideClientPlayerObject = m_ClientNetworkManagers[0].SpawnManager.GetPlayerNetworkObject(clientSideClientId); + Assert.NotNull(clientSideClientPlayerObject); + Assert.AreEqual(clientSideClientId, clientSideClientPlayerObject.OwnerClientId); + } + + [Test] + public void TestClientCantAccessOtherPlayer() + { + // client can't access other player + Assert.Throws(() => + { + m_ClientNetworkManagers[0].SpawnManager.GetPlayerNetworkObject(otherClientSideClientId); + }); + } + + [Test] + public void TestServerGetsNullValueIfInvalidId() + { + // server gets null value if invalid id + var nullPlayer = m_ServerNetworkManager.SpawnManager.GetPlayerNetworkObject(9999); + Assert.Null(nullPlayer); + } + + [Test] + public void TestServerCanUseGetLocalPlayerObject() + { + // test server can use GetLocalPlayerObject + var serverSideServerPlayerObject = m_ServerNetworkManager.SpawnManager.GetLocalPlayerObject(); + Assert.NotNull(serverSideServerPlayerObject); + Assert.AreEqual(serverSideClientId, serverSideServerPlayerObject.OwnerClientId); + } + + [Test] + public void TestClientCanUseGetLocalPlayerObject() + { + // test client can use GetLocalPlayerObject + var clientSideClientPlayerObject = m_ClientNetworkManagers[0].SpawnManager.GetLocalPlayerObject(); + Assert.NotNull(clientSideClientPlayerObject); + Assert.AreEqual(clientSideClientId, clientSideClientPlayerObject.OwnerClientId); + } + + [UnityTest] + public IEnumerator TestConnectAndDisconnect() + { + // test when client connects, player object is now available + + // connect new client + if (!MultiInstanceHelpers.CreateNewClients(1, out NetworkManager[] clients)) + { + Debug.LogError("Failed to create instances"); + Assert.Fail("Failed to create instances"); + } + var newClientNetworkManager = clients[0]; + newClientNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab; + newClientNetworkManager.StartClient(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientConnected(newClientNetworkManager)); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => m_ServerNetworkManager.ConnectedClients.ContainsKey(newClientNetworkManager.LocalClientId))); + var newClientLocalClientId = newClientNetworkManager.LocalClientId; + + // test new client can get that itself locally + var newPlayerObject = newClientNetworkManager.SpawnManager.GetLocalPlayerObject(); + Assert.NotNull(newPlayerObject); + Assert.AreEqual(newClientLocalClientId, newPlayerObject.OwnerClientId); + // test server can get that new client locally + var serverSideNewClientPlayer = m_ServerNetworkManager.SpawnManager.GetPlayerNetworkObject(newClientLocalClientId); + Assert.NotNull(serverSideNewClientPlayer); + Assert.AreEqual(newClientLocalClientId, serverSideNewClientPlayer.OwnerClientId); + + // test when client disconnects, player object no longer available. + var nbConnectedClients = m_ServerNetworkManager.ConnectedClients.Count; + MultiInstanceHelpers.StopOneClient(newClientNetworkManager); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => m_ServerNetworkManager.ConnectedClients.Count == nbConnectedClients - 1)); + + serverSideNewClientPlayer = m_ServerNetworkManager.SpawnManager.GetPlayerNetworkObject(newClientLocalClientId); + Assert.Null(serverSideNewClientPlayer); + } + } +} diff --git a/Tests/Runtime/NetworkSpawnManagerTests.cs.meta b/Tests/Runtime/NetworkSpawnManagerTests.cs.meta new file mode 100644 index 0000000..88c0a11 --- /dev/null +++ b/Tests/Runtime/NetworkSpawnManagerTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 173bed02aed54db4a4f056c245a67393 +timeCreated: 1621449221 \ No newline at end of file diff --git a/Tests/Runtime/NetworkTransform.meta b/Tests/Runtime/NetworkTransform.meta new file mode 100644 index 0000000..5d61983 --- /dev/null +++ b/Tests/Runtime/NetworkTransform.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7b88072d3918a44268b5ed6910f6324e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs new file mode 100644 index 0000000..572b04a --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs @@ -0,0 +1,349 @@ +using NUnit.Framework; +using Unity.Netcode.Components; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkTransformStateTests + { + [Test] + public void TestSyncAxes( + [Values] bool inLocalSpace, + [Values] bool syncPosX, [Values] bool syncPosY, [Values] bool syncPosZ, + [Values] bool syncRotX, [Values] bool syncRotY, [Values] bool syncRotZ, + [Values] bool syncScaX, [Values] bool syncScaY, [Values] bool syncScaZ) + { + var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestSyncAxes)}"); + var networkObject = gameObject.AddComponent(); + var networkTransform = gameObject.AddComponent(); + networkTransform.enabled = false; // do not tick `FixedUpdate()` or `Update()` + + var initialPosition = Vector3.zero; + var initialRotAngles = Vector3.zero; + var initialScale = Vector3.one; + + networkTransform.transform.position = initialPosition; + networkTransform.transform.eulerAngles = initialRotAngles; + networkTransform.transform.localScale = initialScale; + networkTransform.SyncPositionX = syncPosX; + networkTransform.SyncPositionY = syncPosY; + networkTransform.SyncPositionZ = syncPosZ; + networkTransform.SyncRotAngleX = syncRotX; + networkTransform.SyncRotAngleY = syncRotY; + networkTransform.SyncRotAngleZ = syncRotZ; + networkTransform.SyncScaleX = syncScaX; + networkTransform.SyncScaleY = syncScaY; + networkTransform.SyncScaleZ = syncScaZ; + networkTransform.InLocalSpace = inLocalSpace; + + var networkTransformState = new NetworkTransform.NetworkTransformState + { + PositionX = initialPosition.x, + PositionY = initialPosition.y, + PositionZ = initialPosition.z, + RotAngleX = initialRotAngles.x, + RotAngleY = initialRotAngles.y, + RotAngleZ = initialRotAngles.z, + ScaleX = initialScale.x, + ScaleY = initialScale.y, + ScaleZ = initialScale.z, + HasPositionX = syncPosX, + HasPositionY = syncPosY, + HasPositionZ = syncPosZ, + HasRotAngleX = syncRotX, + HasRotAngleY = syncRotY, + HasRotAngleZ = syncRotZ, + HasScaleX = syncScaX, + HasScaleY = syncScaY, + HasScaleZ = syncScaZ, + InLocalSpace = inLocalSpace + }; + + // Step 1: change properties, expect state to be dirty + { + networkTransform.transform.position = new Vector3(3, 4, 5); + networkTransform.transform.eulerAngles = new Vector3(30, 45, 90); + networkTransform.transform.localScale = new Vector3(1.1f, 0.5f, 2.5f); + + if (syncPosX || syncPosY || syncPosZ || syncRotX || syncRotY || syncRotZ || syncScaX || syncScaY || syncScaZ) + { + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + } + + // Step 2: disable a particular sync flag, expect state to be not dirty + { + var position = networkTransform.transform.position; + var rotAngles = networkTransform.transform.eulerAngles; + var scale = networkTransform.transform.localScale; + + // SyncPositionX + { + networkTransform.SyncPositionX = false; + + position.x++; + networkTransform.transform.position = position; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + // SyncPositionY + { + networkTransform.SyncPositionY = false; + + position.y++; + networkTransform.transform.position = position; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + // SyncPositionZ + { + networkTransform.SyncPositionZ = false; + + position.z++; + networkTransform.transform.position = position; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // SyncRotAngleX + { + networkTransform.SyncRotAngleX = false; + + rotAngles.x++; + networkTransform.transform.eulerAngles = rotAngles; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + // SyncRotAngleY + { + networkTransform.SyncRotAngleY = false; + + rotAngles.y++; + networkTransform.transform.eulerAngles = rotAngles; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + // SyncRotAngleZ + { + networkTransform.SyncRotAngleZ = false; + + rotAngles.z++; + networkTransform.transform.eulerAngles = rotAngles; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // SyncScaleX + { + networkTransform.SyncScaleX = false; + + scale.x++; + networkTransform.transform.localScale = scale; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + // SyncScaleY + { + networkTransform.SyncScaleY = false; + + scale.y++; + networkTransform.transform.localScale = scale; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + // SyncScaleZ + { + networkTransform.SyncScaleZ = false; + + scale.z++; + networkTransform.transform.localScale = scale; + + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + } + + Object.DestroyImmediate(gameObject); + } + + + [Test] + public void TestThresholds( + [Values] bool inLocalSpace, + [Values(NetworkTransform.PositionThresholdDefault, 1.0f)] float positionThreshold, + [Values(NetworkTransform.RotAngleThresholdDefault, 1.0f)] float rotAngleThreshold, + [Values(NetworkTransform.ScaleThresholdDefault, 0.5f)] float scaleThreshold) + { + var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestThresholds)}"); + var networkTransform = gameObject.AddComponent(); + networkTransform.enabled = false; // do not tick `FixedUpdate()` or `Update()` + + var initialPosition = Vector3.zero; + var initialRotAngles = Vector3.zero; + var initialScale = Vector3.one; + + networkTransform.transform.position = initialPosition; + networkTransform.transform.eulerAngles = initialRotAngles; + networkTransform.transform.localScale = initialScale; + networkTransform.SyncPositionX = true; + networkTransform.SyncPositionY = true; + networkTransform.SyncPositionZ = true; + networkTransform.SyncRotAngleX = true; + networkTransform.SyncRotAngleY = true; + networkTransform.SyncRotAngleZ = true; + networkTransform.SyncScaleX = true; + networkTransform.SyncScaleY = true; + networkTransform.SyncScaleZ = true; + networkTransform.InLocalSpace = inLocalSpace; + networkTransform.PositionThreshold = positionThreshold; + networkTransform.RotAngleThreshold = rotAngleThreshold; + networkTransform.ScaleThreshold = scaleThreshold; + + var networkTransformState = new NetworkTransform.NetworkTransformState + { + PositionX = initialPosition.x, + PositionY = initialPosition.y, + PositionZ = initialPosition.z, + RotAngleX = initialRotAngles.x, + RotAngleY = initialRotAngles.y, + RotAngleZ = initialRotAngles.z, + ScaleX = initialScale.x, + ScaleY = initialScale.y, + ScaleZ = initialScale.z, + InLocalSpace = inLocalSpace + }; + + // Step 1: change properties, expect state to be dirty + { + networkTransform.transform.position = new Vector3(3, 4, 5); + networkTransform.transform.eulerAngles = new Vector3(30, 45, 90); + networkTransform.transform.localScale = new Vector3(1.1f, 0.5f, 2.5f); + + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // Step 2: make changes below and above thresholds + // changes below the threshold should not make `NetworkState` dirty + // changes above the threshold should make `NetworkState` dirty + { + // Position + if (!Mathf.Approximately(positionThreshold, 0.0f)) + { + var position = networkTransform.transform.position; + + // PositionX + { + position.x += positionThreshold / 2; + networkTransform.transform.position = position; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + position.x += positionThreshold * 2; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // PositionY + { + position.y += positionThreshold / 2; + networkTransform.transform.position = position; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + position.y += positionThreshold * 2; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // PositionZ + { + position.z += positionThreshold / 2; + networkTransform.transform.position = position; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + position.z += positionThreshold * 2; + networkTransform.transform.position = position; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + } + + // RotAngles + if (!Mathf.Approximately(rotAngleThreshold, 0.0f)) + { + var rotAngles = networkTransform.transform.eulerAngles; + + // RotAngleX + { + rotAngles.x += rotAngleThreshold / 2; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + rotAngles.x += rotAngleThreshold * 2; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // RotAngleY + { + rotAngles.y += rotAngleThreshold / 2; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + rotAngles.y += rotAngleThreshold * 2; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // RotAngleZ + { + rotAngles.z += rotAngleThreshold / 2; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + rotAngles.z += rotAngleThreshold * 2; + networkTransform.transform.eulerAngles = rotAngles; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + } + + // Scale + if (!Mathf.Approximately(scaleThreshold, 0.0f) && inLocalSpace) + { + var scale = networkTransform.transform.localScale; + + // ScaleX + { + scale.x += scaleThreshold / 2; + networkTransform.transform.localScale = scale; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + scale.x += scaleThreshold * 2; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // ScaleY + { + scale.y += scaleThreshold / 2; + networkTransform.transform.localScale = scale; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + scale.y += scaleThreshold * 2; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + + // ScaleZ + { + scale.z += scaleThreshold / 2; + networkTransform.transform.localScale = scale; + Assert.IsFalse(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + + scale.z += scaleThreshold * 2; + networkTransform.transform.localScale = scale; + Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform)); + } + } + } + + Object.DestroyImmediate(gameObject); + } + } +} diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs.meta b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs.meta new file mode 100644 index 0000000..b9ad199 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 89acf93a968324f208e359c47bbf9f9b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs new file mode 100644 index 0000000..dd61209 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections; +#if NGO_TRANSFORM_DEBUG +using System.Text.RegularExpressions; +#endif +using Unity.Netcode.Components; +using NUnit.Framework; +// using Unity.Netcode.Samples; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + // [TestFixture(true, true)] + [TestFixture(true, false)] + // [TestFixture(false, true)] + [TestFixture(false, false)] + public class NetworkTransformTests : BaseMultiInstanceTest + { + private NetworkObject m_ClientSideClientPlayer; + private NetworkObject m_ServerSideClientPlayer; + + private readonly bool m_TestWithClientNetworkTransform; + + private readonly bool m_TestWithHost; + + public NetworkTransformTests(bool testWithHost, bool testWithClientNetworkTransform) + { + m_TestWithHost = testWithHost; // from test fixture + m_TestWithClientNetworkTransform = testWithClientNetworkTransform; + } + + protected override int NbClients => 1; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(useHost: m_TestWithHost, nbClients: NbClients, updatePlayerPrefab: playerPrefab => + { + if (m_TestWithClientNetworkTransform) + { + // playerPrefab.AddComponent(); + } + else + { + playerPrefab.AddComponent(); + } + }); + +#if NGO_TRANSFORM_DEBUG + // Log assert for writing without authority is a developer log... + // TODO: This is why monolithic test base classes and test helpers are an anti-pattern - this is part of an individual test case setup but is separated from the code verifying it! + m_ServerNetworkManager.LogLevel = LogLevel.Developer; + m_ClientNetworkManagers[0].LogLevel = LogLevel.Developer; +#endif + + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ServerNetworkManager, serverClientPlayerResult)); + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ClientNetworkManagers[0], clientClientPlayerResult)); + + m_ServerSideClientPlayer = serverClientPlayerResult.Result; + m_ClientSideClientPlayer = clientClientPlayerResult.Result; + } + + // TODO: rewrite after perms & authority changes + [UnityTest] + public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] bool testLocalTransform) + { + var waitResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + + NetworkTransform authoritativeNetworkTransform; + NetworkTransform otherSideNetworkTransform; + // if (m_TestWithClientNetworkTransform) + // { + // // client auth net transform can write from client, not from server + // otherSideNetworkTransform = m_ServerSideClientPlayer.GetComponent(); + // authoritativeNetworkTransform = m_ClientSideClientPlayer.GetComponent(); + // } + // else + { + // server auth net transform can't write from client, not from client + authoritativeNetworkTransform = m_ServerSideClientPlayer.GetComponent(); + otherSideNetworkTransform = m_ClientSideClientPlayer.GetComponent(); + } + Assert.That(!otherSideNetworkTransform.CanCommitToTransform); + Assert.That(authoritativeNetworkTransform.CanCommitToTransform); + + authoritativeNetworkTransform.Interpolate = false; + otherSideNetworkTransform.Interpolate = false; + + if (authoritativeNetworkTransform.CanCommitToTransform) + { + authoritativeNetworkTransform.InLocalSpace = testLocalTransform; + } + + if (otherSideNetworkTransform.CanCommitToTransform) + { + otherSideNetworkTransform.InLocalSpace = testLocalTransform; + } + + float approximation = 0.05f; + + // test position + var authPlayerTransform = authoritativeNetworkTransform.transform; + authPlayerTransform.position = new Vector3(10, 20, 30); + Assert.AreEqual(Vector3.zero, otherSideNetworkTransform.transform.position, "server side pos should be zero at first"); // sanity check + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => otherSideNetworkTransform.transform.position.x > approximation, waitResult, maxFrames: 120)); + if (!waitResult.Result) + { + throw new Exception("timeout while waiting for position change"); + } + Assert.True(new Vector3(10, 20, 30) == otherSideNetworkTransform.transform.position, $"wrong position on ghost, {otherSideNetworkTransform.transform.position}"); // Vector3 already does float approximation with == + + // test rotation + authPlayerTransform.rotation = Quaternion.Euler(45, 40, 35); // using euler angles instead of quaternions directly to really see issues users might encounter + Assert.AreEqual(Quaternion.identity, otherSideNetworkTransform.transform.rotation, "wrong initial value for rotation"); // sanity check + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => otherSideNetworkTransform.transform.rotation.eulerAngles.x > approximation, waitResult, maxFrames: 120)); + if (!waitResult.Result) + { + throw new Exception("timeout while waiting for rotation change"); + } + // approximation needed here since eulerAngles isn't super precise. + Assert.LessOrEqual(Math.Abs(45 - otherSideNetworkTransform.transform.rotation.eulerAngles.x), approximation, $"wrong rotation on ghost on x, got {otherSideNetworkTransform.transform.rotation.eulerAngles.x}"); + Assert.LessOrEqual(Math.Abs(40 - otherSideNetworkTransform.transform.rotation.eulerAngles.y), approximation, $"wrong rotation on ghost on y, got {otherSideNetworkTransform.transform.rotation.eulerAngles.y}"); + Assert.LessOrEqual(Math.Abs(35 - otherSideNetworkTransform.transform.rotation.eulerAngles.z), approximation, $"wrong rotation on ghost on z, got {otherSideNetworkTransform.transform.rotation.eulerAngles.z}"); + + // test scale + UnityEngine.Assertions.Assert.AreApproximatelyEqual(1f, otherSideNetworkTransform.transform.lossyScale.x, "wrong initial value for scale"); // sanity check + UnityEngine.Assertions.Assert.AreApproximatelyEqual(1f, otherSideNetworkTransform.transform.lossyScale.y, "wrong initial value for scale"); // sanity check + UnityEngine.Assertions.Assert.AreApproximatelyEqual(1f, otherSideNetworkTransform.transform.lossyScale.z, "wrong initial value for scale"); // sanity check + authPlayerTransform.localScale = new Vector3(2, 3, 4); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => otherSideNetworkTransform.transform.lossyScale.x > 1f + approximation, waitResult, maxFrames: 120)); + if (!waitResult.Result) + { + throw new Exception("timeout while waiting for scale change"); + } + UnityEngine.Assertions.Assert.AreApproximatelyEqual(2f, otherSideNetworkTransform.transform.lossyScale.x, "wrong scale on ghost"); + UnityEngine.Assertions.Assert.AreApproximatelyEqual(3f, otherSideNetworkTransform.transform.lossyScale.y, "wrong scale on ghost"); + UnityEngine.Assertions.Assert.AreApproximatelyEqual(4f, otherSideNetworkTransform.transform.lossyScale.z, "wrong scale on ghost"); + + // todo reparent and test + // todo test all public API + } + + [UnityTest] + // [Ignore("skipping for now, still need to figure weird multiinstance issue with hosts")] + public IEnumerator TestCantChangeTransformFromOtherSideAuthority([Values] bool testClientAuthority) + { + // test server can't change client authoritative transform + NetworkTransform authoritativeNetworkTransform; + NetworkTransform otherSideNetworkTransform; + + // if (m_TestWithClientNetworkTransform) + // { + // // client auth net transform can write from client, not from server + // otherSideNetworkTransform = m_ServerSideClientPlayer.GetComponent(); + // authoritativeNetworkTransform = m_ClientSideClientPlayer.GetComponent(); + // } + // else + { + // server auth net transform can't write from client, not from client + authoritativeNetworkTransform = m_ServerSideClientPlayer.GetComponent(); + otherSideNetworkTransform = m_ClientSideClientPlayer.GetComponent(); + } + + authoritativeNetworkTransform.Interpolate = false; + otherSideNetworkTransform.Interpolate = false; + + Assert.AreEqual(Vector3.zero, otherSideNetworkTransform.transform.position, "other side pos should be zero at first"); // sanity check + otherSideNetworkTransform.transform.position = new Vector3(4, 5, 6); + + yield return null; // one frame + + Assert.AreEqual(Vector3.zero, otherSideNetworkTransform.transform.position, "got authority error, but other side still moved!"); +#if NGO_TRANSFORM_DEBUG + // We are no longer emitting this warning, and we are banishing tests that rely on console output, so + // needs re-implementation + // TODO: This should be a separate test - verify 1 behavior per test + LogAssert.Expect(LogType.Warning, new Regex(".*without authority detected.*")); +#endif + } + + /* + * ownership change + * test teleport with interpolation + * test teleport without interpolation + * test dynamic spawning + */ + + [UnityTearDown] + public override IEnumerator Teardown() + { + yield return base.Teardown(); + UnityEngine.Object.DestroyImmediate(m_PlayerPrefab); + } + } +} diff --git a/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs.meta b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs.meta new file mode 100644 index 0000000..d5b2726 --- /dev/null +++ b/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cf4ff0d6357bb4474a404b9ce52b22ad +timeCreated: 1620872927 \ No newline at end of file diff --git a/Tests/Runtime/NetworkUpdateLoopTests.cs b/Tests/Runtime/NetworkUpdateLoopTests.cs new file mode 100644 index 0000000..bf93176 --- /dev/null +++ b/Tests/Runtime/NetworkUpdateLoopTests.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections; +using System.Linq; +using UnityEngine; +using UnityEngine.TestTools; +using NUnit.Framework; +using UnityEngine.LowLevel; +using UnityEngine.PlayerLoop; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkUpdateLoopTests + { + [Test] + public void RegisterCustomLoopInTheMiddle() + { + // caching the current PlayerLoop (to prevent side-effects on other tests) + var cachedPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + { + // since current PlayerLoop already took NetworkUpdateLoop systems inside, + // we are going to swap it with the default PlayerLoop temporarily for testing + PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop()); + + NetworkUpdateLoop.RegisterLoopSystems(); + + var curPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + int initSubsystemCount = curPlayerLoop.subSystemList[0].subSystemList.Length; + var newInitSubsystems = new PlayerLoopSystem[initSubsystemCount + 1]; + Array.Copy(curPlayerLoop.subSystemList[0].subSystemList, newInitSubsystems, initSubsystemCount); + newInitSubsystems[initSubsystemCount] = new PlayerLoopSystem { type = typeof(NetworkUpdateLoopTests) }; + curPlayerLoop.subSystemList[0].subSystemList = newInitSubsystems; + PlayerLoop.SetPlayerLoop(curPlayerLoop); + + NetworkUpdateLoop.UnregisterLoopSystems(); + + // our custom `PlayerLoopSystem` with the type of `NetworkUpdateLoopTests` should still exist + Assert.AreEqual(typeof(NetworkUpdateLoopTests), PlayerLoop.GetCurrentPlayerLoop().subSystemList[0].subSystemList.Last().type); + } + // replace the current PlayerLoop with the cached PlayerLoop after the test + PlayerLoop.SetPlayerLoop(cachedPlayerLoop); + } + + [UnityTest] + public IEnumerator RegisterAndUnregisterSystems() + { + // caching the current PlayerLoop (it will have NetworkUpdateLoop systems registered) + var cachedPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + { + // since current PlayerLoop already took NetworkUpdateLoop systems inside, + // we are going to swap it with the default PlayerLoop temporarily for testing + PlayerLoop.SetPlayerLoop(PlayerLoop.GetDefaultPlayerLoop()); + + var oldPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + NetworkUpdateLoop.RegisterLoopSystems(); + + int nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + NetworkUpdateLoop.UnregisterLoopSystems(); + + var newPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + + // recursively compare old and new PlayerLoop systems and their subsystems + AssertAreEqualPlayerLoopSystems(newPlayerLoop, oldPlayerLoop); + } + // replace the current PlayerLoop with the cached PlayerLoop after the test + PlayerLoop.SetPlayerLoop(cachedPlayerLoop); + } + + private void AssertAreEqualPlayerLoopSystems(PlayerLoopSystem leftPlayerLoop, PlayerLoopSystem rightPlayerLoop) + { + Assert.AreEqual(leftPlayerLoop.type, rightPlayerLoop.type); + Assert.AreEqual(leftPlayerLoop.subSystemList?.Length ?? 0, rightPlayerLoop.subSystemList?.Length ?? 0); + for (int i = 0; i < (leftPlayerLoop.subSystemList?.Length ?? 0); i++) + { + AssertAreEqualPlayerLoopSystems(leftPlayerLoop.subSystemList[i], rightPlayerLoop.subSystemList[i]); + } + } + + [Test] + public void UpdateStageSystems() + { + var currentPlayerLoop = PlayerLoop.GetCurrentPlayerLoop(); + for (int i = 0; i < currentPlayerLoop.subSystemList.Length; i++) + { + var playerLoopSystem = currentPlayerLoop.subSystemList[i]; + var subsystems = playerLoopSystem.subSystemList.ToList(); + + if (playerLoopSystem.type == typeof(Initialization)) + { + Assert.True( + subsystems.Exists(s => s.type == typeof(NetworkUpdateLoop.NetworkInitialization)), + nameof(NetworkUpdateLoop.NetworkInitialization)); + } + else if (playerLoopSystem.type == typeof(EarlyUpdate)) + { + Assert.True( + subsystems.Exists(s => s.type == typeof(NetworkUpdateLoop.NetworkEarlyUpdate)), + nameof(NetworkUpdateLoop.NetworkEarlyUpdate)); + } + else if (playerLoopSystem.type == typeof(FixedUpdate)) + { + Assert.True( + subsystems.Exists(s => s.type == typeof(NetworkUpdateLoop.NetworkFixedUpdate)), + nameof(NetworkUpdateLoop.NetworkFixedUpdate)); + } + else if (playerLoopSystem.type == typeof(PreUpdate)) + { + Assert.True( + subsystems.Exists(s => s.type == typeof(NetworkUpdateLoop.NetworkPreUpdate)), + nameof(NetworkUpdateLoop.NetworkPreUpdate)); + } + else if (playerLoopSystem.type == typeof(Update)) + { + Assert.True( + subsystems.Exists(s => s.type == typeof(NetworkUpdateLoop.NetworkUpdate)), + nameof(NetworkUpdateLoop.NetworkUpdate)); + } + else if (playerLoopSystem.type == typeof(PreLateUpdate)) + { + Assert.True( + subsystems.Exists(s => s.type == typeof(NetworkUpdateLoop.NetworkPreLateUpdate)), + nameof(NetworkUpdateLoop.NetworkPreLateUpdate)); + } + else if (playerLoopSystem.type == typeof(PostLateUpdate)) + { + Assert.True( + subsystems.Exists(s => s.type == typeof(NetworkUpdateLoop.NetworkPostLateUpdate)), + nameof(NetworkUpdateLoop.NetworkPostLateUpdate)); + } + } + } + + private struct NetworkUpdateCallbacks + { + public Action OnInitialization; + public Action OnEarlyUpdate; + public Action OnFixedUpdate; + public Action OnPreUpdate; + public Action OnUpdate; + public Action OnPreLateUpdate; + public Action OnPostLateUpdate; + } + + private class MyPlainScript : IDisposable, INetworkUpdateSystem + { + public NetworkUpdateCallbacks UpdateCallbacks; + + public void Initialize() + { + this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); + this.RegisterNetworkUpdate(NetworkUpdateStage.PreLateUpdate); + } + + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + switch (updateStage) + { + case NetworkUpdateStage.Initialization: + UpdateCallbacks.OnInitialization(); + break; + case NetworkUpdateStage.EarlyUpdate: + UpdateCallbacks.OnEarlyUpdate(); + break; + case NetworkUpdateStage.FixedUpdate: + UpdateCallbacks.OnFixedUpdate(); + break; + case NetworkUpdateStage.PreUpdate: + UpdateCallbacks.OnPreUpdate(); + break; + case NetworkUpdateStage.Update: + UpdateCallbacks.OnUpdate(); + break; + case NetworkUpdateStage.PreLateUpdate: + UpdateCallbacks.OnPreLateUpdate(); + break; + case NetworkUpdateStage.PostLateUpdate: + UpdateCallbacks.OnPostLateUpdate(); + break; + } + } + + public void Dispose() + { + this.UnregisterAllNetworkUpdates(); + } + } + + [UnityTest] + public IEnumerator UpdateStagesPlain() + { + const int kNetInitializationIndex = 0; + const int kNetEarlyUpdateIndex = 1; + const int kNetFixedUpdateIndex = 2; + const int kNetPreUpdateIndex = 3; + const int kNetUpdateIndex = 4; + const int kNetPreLateUpdateIndex = 5; + const int kNetPostLateUpdateIndex = 6; + int[] netUpdates = new int[7]; + + bool isTesting = false; + using var plainScript = new MyPlainScript(); + plainScript.UpdateCallbacks = new NetworkUpdateCallbacks + { + OnInitialization = () => + { + if (isTesting) + { + netUpdates[kNetInitializationIndex]++; + } + }, + OnEarlyUpdate = () => + { + if (isTesting) + { + netUpdates[kNetEarlyUpdateIndex]++; + } + }, + OnFixedUpdate = () => + { + if (isTesting) + { + netUpdates[kNetFixedUpdateIndex]++; + } + }, + OnPreUpdate = () => + { + if (isTesting) + { + netUpdates[kNetPreUpdateIndex]++; + } + }, + OnUpdate = () => + { + if (isTesting) + { + netUpdates[kNetUpdateIndex]++; + } + }, + OnPreLateUpdate = () => + { + if (isTesting) + { + netUpdates[kNetPreLateUpdateIndex]++; + } + }, + OnPostLateUpdate = () => + { + if (isTesting) + { + netUpdates[kNetPostLateUpdateIndex]++; + } + } + }; + + plainScript.Initialize(); + int nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + isTesting = true; + + const int kRunTotalFrames = 16; + int waitFrameNumber = Time.frameCount + kRunTotalFrames; + yield return new WaitUntil(() => Time.frameCount >= waitFrameNumber); + + Assert.AreEqual(0, netUpdates[kNetInitializationIndex]); + Assert.AreEqual(kRunTotalFrames, netUpdates[kNetEarlyUpdateIndex]); + Assert.AreEqual(0, netUpdates[kNetFixedUpdateIndex]); + Assert.AreEqual(0, netUpdates[kNetPreUpdateIndex]); + Assert.AreEqual(0, netUpdates[kNetUpdateIndex]); + Assert.AreEqual(kRunTotalFrames, netUpdates[kNetPreLateUpdateIndex]); + Assert.AreEqual(0, netUpdates[kNetPostLateUpdateIndex]); + } + + private struct MonoBehaviourCallbacks + { + public Action OnFixedUpdate; + public Action OnUpdate; + public Action OnLateUpdate; + } + + private class MyGameScript : MonoBehaviour, INetworkUpdateSystem + { + public NetworkUpdateCallbacks UpdateCallbacks; + public MonoBehaviourCallbacks BehaviourCallbacks; + + private void Awake() + { + this.RegisterNetworkUpdate(NetworkUpdateStage.FixedUpdate); + this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); + this.RegisterNetworkUpdate(NetworkUpdateStage.PreLateUpdate); + this.RegisterNetworkUpdate(NetworkUpdateStage.PostLateUpdate); + + // intentionally try to register for 'PreUpdate' stage twice + // it should be ignored and the instance should not be registered twice + // otherwise test would fail because it would call 'OnPreUpdate()' twice + // which would ultimately increment 'netUpdates[idx]' integer twice + // and cause 'Assert.AreEqual()' to fail the test + this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); + } + + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + switch (updateStage) + { + case NetworkUpdateStage.FixedUpdate: + UpdateCallbacks.OnFixedUpdate(); + break; + case NetworkUpdateStage.PreUpdate: + UpdateCallbacks.OnPreUpdate(); + break; + case NetworkUpdateStage.PreLateUpdate: + UpdateCallbacks.OnPreLateUpdate(); + break; + case NetworkUpdateStage.PostLateUpdate: + UpdateCallbacks.OnPostLateUpdate(); + break; + } + } + + private void FixedUpdate() + { + BehaviourCallbacks.OnFixedUpdate(); + } + + private void Update() + { + BehaviourCallbacks.OnUpdate(); + } + + private void LateUpdate() + { + BehaviourCallbacks.OnLateUpdate(); + } + + private void OnDestroy() + { + this.UnregisterAllNetworkUpdates(); + } + } + + [UnityTest] + public IEnumerator UpdateStagesMixed() + { + const int kNetFixedUpdateIndex = 0; + const int kNetPreUpdateIndex = 1; + const int kNetPreLateUpdateIndex = 2; + const int kNetPostLateUpdateIndex = 3; + int[] netUpdates = new int[4]; + const int kMonoFixedUpdateIndex = 0; + const int kMonoUpdateIndex = 1; + const int kMonoLateUpdateIndex = 2; + int[] monoUpdates = new int[3]; + + bool isTesting = false; + { + var gameObject = new GameObject($"{nameof(NetworkUpdateLoopTests)}.{nameof(UpdateStagesMixed)} (Dummy)"); + var gameScript = gameObject.AddComponent(); + gameScript.UpdateCallbacks = new NetworkUpdateCallbacks + { + OnFixedUpdate = () => + { + if (isTesting) + { + netUpdates[kNetFixedUpdateIndex]++; + Assert.AreEqual(monoUpdates[kMonoFixedUpdateIndex] + 1, netUpdates[kNetFixedUpdateIndex]); + } + }, + OnPreUpdate = () => + { + if (isTesting) + { + netUpdates[kNetPreUpdateIndex]++; + Assert.AreEqual(monoUpdates[kMonoUpdateIndex] + 1, netUpdates[kNetPreUpdateIndex]); + } + }, + OnPreLateUpdate = () => + { + if (isTesting) + { + netUpdates[kNetPreLateUpdateIndex]++; + Assert.AreEqual(monoUpdates[kMonoLateUpdateIndex] + 1, netUpdates[kNetPreLateUpdateIndex]); + } + }, + OnPostLateUpdate = () => + { + if (isTesting) + { + netUpdates[kNetPostLateUpdateIndex]++; + Assert.AreEqual(netUpdates[kNetPostLateUpdateIndex], netUpdates[kNetPreLateUpdateIndex]); + } + } + }; + gameScript.BehaviourCallbacks = new MonoBehaviourCallbacks + { + OnFixedUpdate = () => + { + if (isTesting) + { + monoUpdates[kMonoFixedUpdateIndex]++; + Assert.AreEqual(netUpdates[kNetFixedUpdateIndex], monoUpdates[kMonoFixedUpdateIndex]); + } + }, + OnUpdate = () => + { + if (isTesting) + { + monoUpdates[kMonoUpdateIndex]++; + Assert.AreEqual(netUpdates[kNetPreUpdateIndex], monoUpdates[kMonoUpdateIndex]); + } + }, + OnLateUpdate = () => + { + if (isTesting) + { + monoUpdates[kMonoLateUpdateIndex]++; + Assert.AreEqual(netUpdates[kNetPreLateUpdateIndex], monoUpdates[kMonoLateUpdateIndex]); + } + } + }; + + int nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + isTesting = true; + + const int kRunTotalFrames = 16; + int waitFrameNumber = Time.frameCount + kRunTotalFrames; + yield return new WaitUntil(() => Time.frameCount >= waitFrameNumber); + + Assert.AreEqual(kRunTotalFrames, netUpdates[kNetPreUpdateIndex]); + Assert.AreEqual(netUpdates[kNetPreUpdateIndex], monoUpdates[kMonoUpdateIndex]); + + UnityEngine.Object.DestroyImmediate(gameObject); + } + } + } +} diff --git a/Tests/Runtime/NetworkUpdateLoopTests.cs.meta b/Tests/Runtime/NetworkUpdateLoopTests.cs.meta new file mode 100644 index 0000000..849c0f4 --- /dev/null +++ b/Tests/Runtime/NetworkUpdateLoopTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ffdb76de4852b3447b8cd673f0ca3ade +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/NetworkVarBufferCopyTest.cs b/Tests/Runtime/NetworkVarBufferCopyTest.cs new file mode 100644 index 0000000..8097d66 --- /dev/null +++ b/Tests/Runtime/NetworkVarBufferCopyTest.cs @@ -0,0 +1,135 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class NetworkVarBufferCopyTest : BaseMultiInstanceTest + { + public class DummyNetVar : NetworkVariableBase + { + private const int k_DummyValue = 0x13579BDF; + public bool DeltaWritten; + public bool FieldWritten; + public bool DeltaRead; + public bool FieldRead; + public bool Dirty = true; + + public override void ResetDirty() + { + Dirty = false; + } + + public override bool IsDirty() + { + return Dirty; + } + + public override void WriteDelta(FastBufferWriter writer) + { + writer.TryBeginWrite(FastBufferWriter.GetWriteSize(k_DummyValue) + 1); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits((byte)1, 1); + } + writer.WriteValue(k_DummyValue); + + DeltaWritten = true; + } + + public override void WriteField(FastBufferWriter writer) + { + writer.TryBeginWrite(FastBufferWriter.GetWriteSize(k_DummyValue) + 1); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits((byte)1, 1); + } + writer.WriteValue(k_DummyValue); + + FieldWritten = true; + } + + public override void ReadField(FastBufferReader reader) + { + reader.TryBeginRead(FastBufferWriter.GetWriteSize(k_DummyValue) + 1); + using (var bitReader = reader.EnterBitwiseContext()) + { + bitReader.ReadBits(out byte b, 1); + } + + reader.ReadValue(out int i); + Assert.AreEqual(k_DummyValue, i); + + FieldRead = true; + } + + public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) + { + reader.TryBeginRead(FastBufferWriter.GetWriteSize(k_DummyValue) + 1); + using (var bitReader = reader.EnterBitwiseContext()) + { + bitReader.ReadBits(out byte b, 1); + } + + reader.ReadValue(out int i); + Assert.AreEqual(k_DummyValue, i); + + DeltaRead = true; + } + } + + public class DummyNetBehaviour : NetworkBehaviour + { + public DummyNetVar NetVar; + } + protected override int NbClients => 1; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(useHost: true, nbClients: NbClients, + updatePlayerPrefab: playerPrefab => + { + var dummyNetBehaviour = playerPrefab.AddComponent(); + }); + } + + [UnityTest] + public IEnumerator TestEntireBufferIsCopiedOnNetworkVariableDelta() + { + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, + m_ServerNetworkManager, serverClientPlayerResult)); + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, + m_ClientNetworkManagers[0], clientClientPlayerResult)); + + var serverSideClientPlayer = serverClientPlayerResult.Result; + var clientSideClientPlayer = clientClientPlayerResult.Result; + + var serverComponent = (serverSideClientPlayer).GetComponent(); + var clientComponent = (clientSideClientPlayer).GetComponent(); + + var waitResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition( + () => clientComponent.NetVar.DeltaRead == true, + waitResult, + maxFrames: 120)); + + if (!waitResult.Result) + { + Assert.Fail("Failed to send a delta within 120 frames"); + } + Assert.True(serverComponent.NetVar.FieldWritten); + Assert.True(serverComponent.NetVar.DeltaWritten); + Assert.True(clientComponent.NetVar.FieldRead); + Assert.True(clientComponent.NetVar.DeltaRead); + } + } +} diff --git a/Tests/Runtime/NetworkVarBufferCopyTest.cs.meta b/Tests/Runtime/NetworkVarBufferCopyTest.cs.meta new file mode 100644 index 0000000..88e49e3 --- /dev/null +++ b/Tests/Runtime/NetworkVarBufferCopyTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a7d44caa76a64b02978f5aca0e7b576a +timeCreated: 1627926008 \ No newline at end of file diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs new file mode 100644 index 0000000..600883b --- /dev/null +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -0,0 +1,418 @@ +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using NUnit.Framework; +using Unity.Collections; + +namespace Unity.Netcode.RuntimeTests +{ + public struct TestStruct : INetworkSerializable + { + public uint SomeInt; + public bool SomeBool; + + public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter + { + serializer.SerializeValue(ref SomeInt); + serializer.SerializeValue(ref SomeBool); + } + } + + public class NetworkVariableTest : NetworkBehaviour + { + public readonly NetworkVariable TheScalar = new NetworkVariable(); + public readonly NetworkList TheList = new NetworkList(); + + public readonly NetworkVariable FixedString32 = new NetworkVariable(); + + private void ListChanged(NetworkListEvent e) + { + ListDelegateTriggered = true; + } + + public void Awake() + { + TheList.OnListChanged += ListChanged; + } + + public readonly NetworkVariable TheStruct = new NetworkVariable(); + + public bool ListDelegateTriggered; + } + + public class NetworkVariableTests : BaseMultiInstanceTest + { + private const string k_FixedStringTestValue = "abcdefghijklmnopqrstuvwxyz"; + protected override int NbClients => 2; + + private const uint k_TestUInt = 0x12345678; + + private const int k_TestVal1 = 111; + private const int k_TestVal2 = 222; + private const int k_TestVal3 = 333; + + private const int k_TestKey1 = 0x0f0f; + + // Player1 component on the server + private NetworkVariableTest m_Player1OnServer; + + // Player1 component on client1 + private NetworkVariableTest m_Player1OnClient1; + + private bool m_TestWithHost; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(useHost: m_TestWithHost, nbClients: NbClients, + updatePlayerPrefab: playerPrefab => + { + playerPrefab.AddComponent(); + }); + + // These are the *SERVER VERSIONS* of the *CLIENT PLAYER 1 & 2* + var result = new MultiInstanceHelpers.CoroutineResultWrapper(); + + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, + m_ServerNetworkManager, result)); + m_Player1OnServer = result.Result.GetComponent(); + + // This is client1's view of itself + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation( + x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, + m_ClientNetworkManagers[0], result)); + + m_Player1OnClient1 = result.Result.GetComponent(); + + m_Player1OnServer.TheList.Clear(); + + if (m_Player1OnServer.TheList.Count > 0) + { + throw new Exception("at least one server network container not empty at start"); + } + if (m_Player1OnClient1.TheList.Count > 0) + { + throw new Exception("at least one client network container not empty at start"); + } + } + + /// + /// Runs generalized tests on all predefined NetworkVariable types + /// + [UnityTest] + public IEnumerator AllNetworkVariableTypes([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + + // Create, instantiate, and host + // This would normally go in Setup, but since every other test but this one + // uses MultiInstanceHelper, and it does its own NetworkManager setup / teardown, + // for now we put this within this one test until we migrate it to MIH + Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _)); + + Guid gameObjectId = NetworkManagerHelper.AddGameNetworkObject("NetworkVariableTestComponent"); + + var networkVariableTestComponent = NetworkManagerHelper.AddComponentToObject(gameObjectId); + + NetworkManagerHelper.SpawnNetworkObject(gameObjectId); + + // Start Testing + networkVariableTestComponent.EnableTesting = true; + + var testsAreComplete = networkVariableTestComponent.IsTestComplete(); + + // Wait for the NetworkVariable tests to complete + while (!testsAreComplete) + { + yield return new WaitForSeconds(0.003f); + testsAreComplete = networkVariableTestComponent.IsTestComplete(); + } + + // Stop Testing + networkVariableTestComponent.EnableTesting = false; + + Assert.IsTrue(networkVariableTestComponent.DidAllValuesChange()); + + // Disable this once we are done. + networkVariableTestComponent.gameObject.SetActive(false); + + Assert.IsTrue(testsAreComplete); + + // This would normally go in Teardown, but since every other test but this one + // uses MultiInstanceHelper, and it does its own NetworkManager setup / teardown, + // for now we put this within this one test until we migrate it to MIH + NetworkManagerHelper.ShutdownNetworkManager(); + } + + [Test] + public void ClientWritePermissionTest([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + + // client must not be allowed to write to a server auth variable + Assert.Throws(() => m_Player1OnClient1.TheScalar.Value = k_TestVal1); + } + + [UnityTest] + public IEnumerator FixedString32Test([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.FixedString32.Value = k_FixedStringTestValue; + + // we are writing to the private and public variables on player 1's object... + }, + () => + { + + // ...and we should see the writes to the private var only on the server & the owner, + // but the public variable everywhere + return + m_Player1OnClient1.FixedString32.Value == k_FixedStringTestValue; + } + ); + } + + [UnityTest] + public IEnumerator NetworkListAdd([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheList.Add(k_TestVal1); + m_Player1OnServer.TheList.Add(k_TestVal2); + }, + () => + { + return m_Player1OnServer.TheList.Count == 2 && + m_Player1OnClient1.TheList.Count == 2 && + m_Player1OnServer.ListDelegateTriggered && + m_Player1OnClient1.ListDelegateTriggered && + m_Player1OnServer.TheList[0] == k_TestVal1 && + m_Player1OnClient1.TheList[0] == k_TestVal1 && + m_Player1OnServer.TheList[1] == k_TestVal2 && + m_Player1OnClient1.TheList[1] == k_TestVal2; + } + ); + } + + [UnityTest] + public IEnumerator NetworkListContains([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheList.Add(k_TestVal1); + }, + () => + { + return m_Player1OnServer.TheList.Count == 1 && + m_Player1OnClient1.TheList.Count == 1 && + m_Player1OnServer.TheList.Contains(k_TestKey1) && + m_Player1OnClient1.TheList.Contains(k_TestKey1); + } + ); + } + + [UnityTest] + public IEnumerator NetworkListRemoveValue([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheList.Add(k_TestVal1); + m_Player1OnServer.TheList.Add(k_TestVal2); + m_Player1OnServer.TheList.Add(k_TestVal3); + m_Player1OnServer.TheList.Remove(k_TestVal2); + }, + () => + { + return m_Player1OnServer.TheList.Count == 2 && + m_Player1OnClient1.TheList.Count == 2 && + m_Player1OnServer.TheList[0] == k_TestVal1 && + m_Player1OnClient1.TheList[0] == k_TestVal1 && + m_Player1OnServer.TheList[1] == k_TestVal3 && + m_Player1OnClient1.TheList[1] == k_TestVal3; + } + ); + } + + [UnityTest] + public IEnumerator NetworkListInsert([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheList.Add(k_TestVal1); + m_Player1OnServer.TheList.Add(k_TestVal2); + m_Player1OnServer.TheList.Insert(1, k_TestVal3); + }, + () => + { + return m_Player1OnServer.TheList.Count == 3 && + m_Player1OnClient1.TheList.Count == 3 && + m_Player1OnServer.ListDelegateTriggered && + m_Player1OnClient1.ListDelegateTriggered && + m_Player1OnServer.TheList[0] == k_TestVal1 && + m_Player1OnClient1.TheList[0] == k_TestVal1 && + m_Player1OnServer.TheList[1] == k_TestVal3 && + m_Player1OnClient1.TheList[1] == k_TestVal3 && + m_Player1OnServer.TheList[2] == k_TestVal2 && + m_Player1OnClient1.TheList[2] == k_TestVal2; + } + ); + } + + [UnityTest] + public IEnumerator NetworkListIndexOf([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheList.Add(k_TestVal1); + m_Player1OnServer.TheList.Add(k_TestVal2); + m_Player1OnServer.TheList.Add(k_TestVal3); + }, + () => + { + return m_Player1OnServer.TheList.IndexOf(k_TestVal1) == 0 && + m_Player1OnClient1.TheList.IndexOf(k_TestVal1) == 0 && + m_Player1OnServer.TheList.IndexOf(k_TestVal2) == 1 && + m_Player1OnClient1.TheList.IndexOf(k_TestVal2) == 1 && + m_Player1OnServer.TheList.IndexOf(k_TestVal3) == 2 && + m_Player1OnClient1.TheList.IndexOf(k_TestVal3) == 2; + } + ); + } + + [UnityTest] + public IEnumerator NetworkListArrayOperator([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheList.Add(k_TestVal3); + m_Player1OnServer.TheList.Add(k_TestVal3); + m_Player1OnServer.TheList[0] = k_TestVal1; + m_Player1OnServer.TheList[1] = k_TestVal2; + }, + () => + { + return m_Player1OnServer.TheList.Count == 2 && + m_Player1OnClient1.TheList.Count == 2 && + m_Player1OnServer.TheList[0] == k_TestVal1 && + m_Player1OnClient1.TheList[0] == k_TestVal1 && + m_Player1OnServer.TheList[1] == k_TestVal2 && + m_Player1OnClient1.TheList[1] == k_TestVal2; + } + ); + } + + [Test] + public void NetworkListIEnumerator([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + var correctVals = new int[3]; + correctVals[0] = k_TestVal1; + correctVals[1] = k_TestVal2; + correctVals[2] = k_TestVal3; + + m_Player1OnServer.TheList.Add(correctVals[0]); + m_Player1OnServer.TheList.Add(correctVals[1]); + m_Player1OnServer.TheList.Add(correctVals[2]); + + Assert.IsTrue(m_Player1OnServer.TheList.Count == 3); + + int index = 0; + foreach (var val in m_Player1OnServer.TheList) + { + if (val != correctVals[index++]) + { + Assert.Fail(); + } + } + } + + [UnityTest] + public IEnumerator NetworkListRemoveAt([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheList.Add(k_TestVal1); + m_Player1OnServer.TheList.Add(k_TestVal2); + m_Player1OnServer.TheList.Add(k_TestVal3); + m_Player1OnServer.TheList.RemoveAt(1); + }, + () => + { + return m_Player1OnServer.TheList.Count == 2 && + m_Player1OnClient1.TheList.Count == 2 && + m_Player1OnServer.TheList[0] == k_TestVal1 && + m_Player1OnClient1.TheList[0] == k_TestVal1 && + m_Player1OnServer.TheList[1] == k_TestVal3 && + m_Player1OnClient1.TheList[1] == k_TestVal3; + } + ); + } + + [UnityTest] + public IEnumerator NetworkListClear([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + + // first put some stuff in; re-use the add test + yield return NetworkListAdd(useHost); + + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => m_Player1OnServer.TheList.Clear(), + () => + { + return + m_Player1OnServer.ListDelegateTriggered && + m_Player1OnClient1.ListDelegateTriggered && + m_Player1OnServer.TheList.Count == 0 && + m_Player1OnClient1.TheList.Count == 0; + } + ); + } + + [UnityTest] + public IEnumerator TestNetworkVariableStruct([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + m_Player1OnServer.TheStruct.Value = + new TestStruct() { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.TheStruct.SetDirty(true); + }, + () => + { + return + m_Player1OnClient1.TheStruct.Value.SomeBool == false && + m_Player1OnClient1.TheStruct.Value.SomeInt == k_TestUInt; + } + ); + } + + [UnityTearDown] + public override IEnumerator Teardown() + { + yield return base.Teardown(); + } + } +} diff --git a/Tests/Runtime/NetworkVariableTests.cs.meta b/Tests/Runtime/NetworkVariableTests.cs.meta new file mode 100644 index 0000000..5073e66 --- /dev/null +++ b/Tests/Runtime/NetworkVariableTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d271d8738dbbb5e4aa0cae91b663f183 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Physics.meta b/Tests/Runtime/Physics.meta new file mode 100644 index 0000000..18693af --- /dev/null +++ b/Tests/Runtime/Physics.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 58745956b2a1e8346a48c5179cb247eb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs new file mode 100644 index 0000000..1a9f2a8 --- /dev/null +++ b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs @@ -0,0 +1,81 @@ +using System.Collections; +using NUnit.Framework; +using Unity.Netcode.Components; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Physics +{ + public class NetworkRigidbody2DDynamicTest : NetworkRigidbody2DTestBase + { + public override bool Kinematic => false; + } + + public class NetworkRigidbody2DKinematicTest : NetworkRigidbody2DTestBase + { + public override bool Kinematic => true; + } + + public abstract class NetworkRigidbody2DTestBase : BaseMultiInstanceTest + { + protected override int NbClients => 1; + + public abstract bool Kinematic { get; } + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, playerPrefab => + { + playerPrefab.AddComponent(); + playerPrefab.AddComponent(); + playerPrefab.AddComponent(); + playerPrefab.GetComponent().interpolation = RigidbodyInterpolation2D.Interpolate; + playerPrefab.GetComponent().isKinematic = Kinematic; + }); + } + + /// + /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. + /// + /// + [UnityTest] + public IEnumerator TestRigidbodyKinematicEnableDisable() + { + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult)); + var serverPlayer = serverClientPlayerResult.Result.gameObject; + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult)); + var clientPlayer = clientClientPlayerResult.Result.gameObject; + + Assert.IsNotNull(serverPlayer); + Assert.IsNotNull(clientPlayer); + + yield return NetworkRigidbodyTestBase.WaitForFrames(5); + + // server rigidbody has authority and should have a kinematic mode of false + Assert.True(serverPlayer.GetComponent().isKinematic == Kinematic); + Assert.AreEqual(RigidbodyInterpolation2D.Interpolate, serverPlayer.GetComponent().interpolation); + + // client rigidbody has no authority and should have a kinematic mode of true + Assert.True(clientPlayer.GetComponent().isKinematic); + Assert.AreEqual(RigidbodyInterpolation2D.None, clientPlayer.GetComponent().interpolation); + + // despawn the server player, (but keep it around on the server) + serverPlayer.GetComponent().Despawn(false); + + yield return NetworkRigidbodyTestBase.WaitForFrames(5); + + Assert.IsTrue(serverPlayer.GetComponent().isKinematic == Kinematic); + + yield return NetworkRigidbodyTestBase.WaitForFrames(5); + + Assert.IsTrue(clientPlayer == null); // safety check that object is actually despawned. + } + + } +} diff --git a/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs.meta b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs.meta new file mode 100644 index 0000000..48faa51 --- /dev/null +++ b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f33b2f298cbb7b248b2a76ba48ee1d53 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs new file mode 100644 index 0000000..bb42349 --- /dev/null +++ b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs @@ -0,0 +1,86 @@ +using System.Collections; +using NUnit.Framework; +using Unity.Netcode.Components; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Physics +{ + public class NetworkRigidbodyDynamicTest : NetworkRigidbodyTestBase + { + public override bool Kinematic => false; + } + + public class NetworkRigidbodyKinematicTest : NetworkRigidbodyTestBase + { + public override bool Kinematic => true; + } + + public abstract class NetworkRigidbodyTestBase : BaseMultiInstanceTest + { + protected override int NbClients => 1; + + public abstract bool Kinematic { get; } + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, playerPrefab => + { + playerPrefab.AddComponent(); + playerPrefab.AddComponent(); + playerPrefab.AddComponent(); + playerPrefab.GetComponent().interpolation = RigidbodyInterpolation.Interpolate; + playerPrefab.GetComponent().isKinematic = Kinematic; + }); + } + + /// + /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. + /// + /// + [UnityTest] + public IEnumerator TestRigidbodyKinematicEnableDisable() + { + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult)); + var serverPlayer = serverClientPlayerResult.Result.gameObject; + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult)); + var clientPlayer = clientClientPlayerResult.Result.gameObject; + + Assert.IsNotNull(serverPlayer); + Assert.IsNotNull(clientPlayer); + + yield return WaitForFrames(5); + + // server rigidbody has authority and should have a kinematic mode of false + Assert.True(serverPlayer.GetComponent().isKinematic == Kinematic); + Assert.AreEqual(RigidbodyInterpolation.Interpolate, serverPlayer.GetComponent().interpolation); + + // client rigidbody has no authority and should have a kinematic mode of true + Assert.True(clientPlayer.GetComponent().isKinematic); + Assert.AreEqual(RigidbodyInterpolation.None, clientPlayer.GetComponent().interpolation); + + // despawn the server player (but keep it around on the server) + serverPlayer.GetComponent().Despawn(false); + + yield return WaitForFrames(5); + + Assert.IsTrue(serverPlayer.GetComponent().isKinematic == Kinematic); + + yield return WaitForFrames(5); + + Assert.IsTrue(clientPlayer == null); // safety check that object is actually despawned. + } + + public static IEnumerator WaitForFrames(int count) + { + int nextFrameNumber = Time.frameCount + count; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + } +} diff --git a/Tests/Runtime/Physics/NetworkRigidbodyTest.cs.meta b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs.meta new file mode 100644 index 0000000..777618f --- /dev/null +++ b/Tests/Runtime/Physics/NetworkRigidbodyTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 344ba3f81f24b6d40afb926333fd118d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Profiling.meta b/Tests/Runtime/Profiling.meta new file mode 100644 index 0000000..617845c --- /dev/null +++ b/Tests/Runtime/Profiling.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b613c8dd4ba7d046b62971a3cb631ed +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Profiling/NetworkVariableNameTests.cs b/Tests/Runtime/Profiling/NetworkVariableNameTests.cs new file mode 100644 index 0000000..e9d005a --- /dev/null +++ b/Tests/Runtime/Profiling/NetworkVariableNameTests.cs @@ -0,0 +1,38 @@ +using System; +using NUnit.Framework; + +namespace Unity.Netcode.RuntimeTests +{ + public sealed class NetworkVariableNameTests + { + private NetworkVariableNameComponent m_NetworkVariableNameComponent; + + [SetUp] + public void SetUp() + { + NetworkManagerHelper.StartNetworkManager(out _); + + var gameObjectId = NetworkManagerHelper.AddGameNetworkObject(Guid.NewGuid().ToString()); + m_NetworkVariableNameComponent = NetworkManagerHelper.AddComponentToObject(gameObjectId); + NetworkManagerHelper.SpawnNetworkObject(gameObjectId); + } + + [TearDown] + public void TearDown() + { + NetworkManagerHelper.ShutdownNetworkManager(); + } + + [Test] + public void VerifyNetworkVariableNameInitialization() + { + // Fields have regular naming + Assert.AreEqual(nameof(NetworkVariableNameComponent.NetworkVarList), m_NetworkVariableNameComponent.NetworkVarList.Name); + } + + private class NetworkVariableNameComponent : NetworkBehaviour + { + public NetworkList NetworkVarList = new NetworkList(); + } + } +} diff --git a/Tests/Runtime/Profiling/NetworkVariableNameTests.cs.meta b/Tests/Runtime/Profiling/NetworkVariableNameTests.cs.meta new file mode 100644 index 0000000..6ddaf9d --- /dev/null +++ b/Tests/Runtime/Profiling/NetworkVariableNameTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e10a16550007020409234a1efb1fa383 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/RpcPipelineTestComponent.cs b/Tests/Runtime/RpcPipelineTestComponent.cs new file mode 100644 index 0000000..07e7fee --- /dev/null +++ b/Tests/Runtime/RpcPipelineTestComponent.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Used in conjunction with the RpcQueueTest to validate: + /// - Sending and Receiving pipeline to validate that both sending and receiving pipelines are functioning properly. + /// - Usage of the ServerRpcParams.Send.UpdateStage and ClientRpcParams.Send.UpdateStage functionality. + /// - Rpcs receive will be invoked at the appropriate NetworkUpdateStage. + /// + public class RpcPipelineTestComponent : NetworkBehaviour + { + /// + /// Allows the external RPCQueueTest to begin testing or stop it + /// + public bool PingSelfEnabled; + + /// + /// How many times will we iterate through the various NetworkUpdateStage values? + /// (defaults to 2) + /// + public int MaxIterations = 2; + + // Start is called before the first frame update + private void Start() + { + m_MaxStagesSent = Enum.GetValues(typeof(NetworkUpdateStage)).Length * MaxIterations; + + //Start out with this being true (for first sequence) + m_ClientReceivedRpc = true; + } + + /// + /// Determine if we have iterated over more than our maximum stages we want to test + /// + /// true or false (did we exceed the max iterations or not?) + public bool ExceededMaxIterations() + { + if (m_StagesSent.Count > m_MaxStagesSent && m_MaxStagesSent > 0) + { + return true; + } + + return false; + } + + /// + /// Returns back whether the test has completed the total number of iterations + /// + /// + public bool IsTestComplete() + { + if (m_Counter >= MaxIterations) + { + return true; + } + + return false; + } + + private bool m_ClientReceivedRpc; + private int m_Counter = 0; + private int m_MaxStagesSent = 0; + private ServerRpcParams m_ServerParams; + private ClientRpcParams m_ClientParams; + private NetworkUpdateStage m_LastUpdateStage; + + // Update is called once per frame + private void Update() + { + if (NetworkManager.Singleton.IsListening && PingSelfEnabled && m_ClientReceivedRpc) + { + //Reset this for the next sequence of rpcs + m_ClientReceivedRpc = false; + + //As long as testing isn't completed, keep testing + if (!IsTestComplete()) + { + PingMySelfServerRpc(m_StagesSent.Count, m_ServerParams); + } + } + } + + + private readonly List m_ServerStagesReceived = new List(); + private readonly List m_ClientStagesReceived = new List(); + private readonly List m_StagesSent = new List(); + + /// + /// Assures all update stages were in alginment with one another + /// + /// true or false + public bool ValidateUpdateStages() + { + var validated = false; + if (m_ServerStagesReceived.Count == m_ClientStagesReceived.Count && m_ClientStagesReceived.Count == m_StagesSent.Count) + { + for (int i = 0; i < m_StagesSent.Count; i++) + { + var currentStage = m_StagesSent[i]; + if (m_ServerStagesReceived[i] != currentStage) + { + Debug.Log($"ServerRpc Update Stage ({m_ServerStagesReceived[i]}) is not equal to the current update stage ({currentStage})"); + + return validated; + } + + if (m_ClientStagesReceived[i] != currentStage) + { + Debug.Log($"ClientRpc Update Stage ({m_ClientStagesReceived[i]}) is not equal to the current update stage ({currentStage})"); + + return validated; + } + } + + validated = true; + } + + return validated; + } + + /// + /// Server side RPC for testing + /// + /// server rpc parameters + [ServerRpc] + private void PingMySelfServerRpc(int currentCount, ServerRpcParams parameters = default) + { + Debug.Log($"{nameof(PingMySelfServerRpc)}: [HostClient][ServerRpc][{currentCount}] invoked."); + + PingMySelfClientRpc(currentCount, m_ClientParams); + } + + /// + /// Client Side RPC called by PingMySelfServerRPC to validate both Client->Server and Server-Client pipeline is working + /// + /// client rpc parameters + [ClientRpc] + private void PingMySelfClientRpc(int currentCount, ClientRpcParams parameters = default) + { + Debug.Log($"{nameof(PingMySelfClientRpc)}: [HostServer][ClientRpc][{currentCount}] invoked. (previous output line should confirm this)"); + + m_ClientReceivedRpc = true; + } + } +} diff --git a/Tests/Runtime/RpcPipelineTestComponent.cs.meta b/Tests/Runtime/RpcPipelineTestComponent.cs.meta new file mode 100644 index 0000000..2c945db --- /dev/null +++ b/Tests/Runtime/RpcPipelineTestComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e946a6fdcfcb9dd48b76b38871c0a77b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/RpcQueueTests.cs b/Tests/Runtime/RpcQueueTests.cs new file mode 100644 index 0000000..ae0e7d4 --- /dev/null +++ b/Tests/Runtime/RpcQueueTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using NUnit.Framework; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// The RpcQueue unit tests validate: + /// - Maximum buffer size that can be sent (currently 1MB is the default maximum `MessageQueueHistoryFrame` size) + /// - That all RPCs invoke at the appropriate `NetworkUpdateStage` (Client and Server) + /// - A lower level `MessageQueueContainer` test that validates `MessageQueueFrameItems` after they have been put into the queue + /// + public class RpcQueueTests + { + [SetUp] + public void Setup() + { + // Create, instantiate, and host + Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _)); + } + + /// + /// This tests the RPC Queue outbound and inbound buffer capabilities. + /// + /// IEnumerator + [UnityTest, Order(2)] + public IEnumerator BufferDataValidation() + { + Guid gameObjectId = NetworkManagerHelper.AddGameNetworkObject("GrowingBufferObject"); + + var growingRpcBufferSizeComponent = NetworkManagerHelper.AddComponentToObject(gameObjectId); + + NetworkManagerHelper.SpawnNetworkObject(gameObjectId); + + // Start Testing + growingRpcBufferSizeComponent.EnableTesting = true; + + var testsAreComplete = growingRpcBufferSizeComponent.IsTestComplete(); + + // Wait for the RPC pipeline test to complete or if we exceeded the maximum iterations bail + while (!testsAreComplete) + { + yield return new WaitForSeconds(0.003f); + + testsAreComplete = growingRpcBufferSizeComponent.IsTestComplete(); + } + + // Stop Testing + growingRpcBufferSizeComponent.EnableTesting = false; + + // Just disable this once we are done. + growingRpcBufferSizeComponent.gameObject.SetActive(false); + + Assert.IsTrue(testsAreComplete); + } + + [TearDown] + public void TearDown() + { + // Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + } + } +} diff --git a/Tests/Runtime/RpcQueueTests.cs.meta b/Tests/Runtime/RpcQueueTests.cs.meta new file mode 100644 index 0000000..4309f8f --- /dev/null +++ b/Tests/Runtime/RpcQueueTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a96d73d8acaa2a4aadc4e6be8b10c7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/RpcTests.cs b/Tests/Runtime/RpcTests.cs new file mode 100644 index 0000000..50ca4f6 --- /dev/null +++ b/Tests/Runtime/RpcTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine.TestTools; +using Debug = UnityEngine.Debug; + +namespace Unity.Netcode.RuntimeTests +{ + public class RpcTests : BaseMultiInstanceTest + { + public class RpcTestNB : NetworkBehaviour + { + public event Action OnServer_Rpc; + public event Action OnClient_Rpc; + + [ServerRpc] + public void MyServerRpc() + { + OnServer_Rpc(); + } + + [ClientRpc] + public void MyClientRpc() + { + OnClient_Rpc(); + } + } + + protected override int NbClients => 1; + + [UnitySetUp] + public override IEnumerator Setup() + { + yield return StartSomeClientsAndServerWithPlayers(true, NbClients, playerPrefab => + { + playerPrefab.AddComponent(); + }); + } + + [UnityTest] + public IEnumerator TestRpcs() + { + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult)); + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult)); + + // Setup state + bool hasReceivedServerRpc = false; + bool hasReceivedClientRpcRemotely = false; + bool hasReceivedClientRpcLocally = false; + + clientClientPlayerResult.Result.GetComponent().OnClient_Rpc += () => + { + Debug.Log("ClientRpc received on client object"); + hasReceivedClientRpcRemotely = true; + }; + + clientClientPlayerResult.Result.GetComponent().OnServer_Rpc += () => + { + // The RPC invoked locally. (Weaver failure?) + Assert.Fail("ServerRpc invoked locally. Weaver failure?"); + }; + + serverClientPlayerResult.Result.GetComponent().OnServer_Rpc += () => + { + Debug.Log("ServerRpc received on server object"); + hasReceivedServerRpc = true; + }; + + serverClientPlayerResult.Result.GetComponent().OnClient_Rpc += () => + { + // The RPC invoked locally. (Weaver failure?) + Debug.Log("ClientRpc received on server object"); + hasReceivedClientRpcLocally = true; + }; + + // Send ServerRpc + clientClientPlayerResult.Result.GetComponent().MyServerRpc(); + + // Send ClientRpc + serverClientPlayerResult.Result.GetComponent().MyClientRpc(); + + // Wait for RPCs to be received + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForCondition(() => hasReceivedServerRpc && hasReceivedClientRpcLocally && hasReceivedClientRpcRemotely)); + + Assert.True(hasReceivedServerRpc, "ServerRpc was not received"); + Assert.True(hasReceivedClientRpcLocally, "ClientRpc was not locally received on the server"); + Assert.True(hasReceivedClientRpcRemotely, "ClientRpc was not remotely received on the client"); + } + } +} diff --git a/Tests/Runtime/RpcTests.cs.meta b/Tests/Runtime/RpcTests.cs.meta new file mode 100644 index 0000000..02e7960 --- /dev/null +++ b/Tests/Runtime/RpcTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46681ff78c6154f87acf5e1cccbfc0e1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Serialization.meta b/Tests/Runtime/Serialization.meta new file mode 100644 index 0000000..1f1dc0f --- /dev/null +++ b/Tests/Runtime/Serialization.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 90d51da7691e302498265bba08c43636 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs new file mode 100644 index 0000000..bcc7119 --- /dev/null +++ b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Serialization +{ + /// + /// Unit tests to test: + /// - Serializing NetworkObject to NetworkObjectReference + /// - Deserializing NetworkObjectReference to NetworkObject + /// - Implicit operators of NetworkObjectReference + /// + public class NetworkBehaviourReferenceTests : IDisposable + { + private class TestNetworkBehaviour : NetworkBehaviour + { + public NetworkVariable TestVariable = new NetworkVariable(); + + public TestNetworkBehaviour RpcReceivedBehaviour; + + [ServerRpc] + public void SendReferenceServerRpc(NetworkBehaviourReference value) + { + RpcReceivedBehaviour = (TestNetworkBehaviour)value; + } + } + + [UnityTest] + public IEnumerator TestRpc() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + + testNetworkBehaviour.SendReferenceServerRpc(new NetworkBehaviourReference(testNetworkBehaviour)); + + // wait for rpc completion + float t = 0; + while (testNetworkBehaviour.RpcReceivedBehaviour == null) + { + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour); + } + + [UnityTest] + public IEnumerator TestRpcImplicitNetworkBehaviour() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + + testNetworkBehaviour.SendReferenceServerRpc(testNetworkBehaviour); + + // wait for rpc completion + float t = 0; + while (testNetworkBehaviour.RpcReceivedBehaviour == null) + { + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour); + } + + [Test] + public void TestNetworkVariable() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + + // check default value is null + Assert.IsNull((NetworkBehaviour)testNetworkBehaviour.TestVariable.Value); + + testNetworkBehaviour.TestVariable.Value = testNetworkBehaviour; + + Assert.AreEqual((NetworkBehaviour)testNetworkBehaviour.TestVariable.Value, testNetworkBehaviour); + } + + [Test] + public void FailSerializeNonSpawnedNetworkObject() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var component = networkObjectContext.Object.gameObject.AddComponent(); + + Assert.Throws(() => + { + NetworkBehaviourReference outReference = component; + }); + } + + [Test] + public void FailSerializeGameObjectWithoutNetworkObject() + { + using var gameObjectContext = UnityObjectContext.CreateGameObject(); + var component = gameObjectContext.Object.gameObject.AddComponent(); + + Assert.Throws(() => + { + NetworkBehaviourReference outReference = component; + }); + } + + [Test] + public void FailSerializeNullBehaviour() + { + Assert.Throws(() => + { + NetworkBehaviourReference outReference = null; + }); + } + + public void Dispose() + { + //Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + } + + public NetworkBehaviourReferenceTests() + { + //Create, instantiate, and host + NetworkManagerHelper.StartNetworkManager(out _); + } + } +} diff --git a/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs.meta b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs.meta new file mode 100644 index 0000000..6ae11f3 --- /dev/null +++ b/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0fca807d195f9fc49a400cfce86b085d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs new file mode 100644 index 0000000..3cd24eb --- /dev/null +++ b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests.Serialization +{ + /// + /// Unit tests to test: + /// - Serializing NetworkObject to NetworkObjectReference + /// - Deserializing NetworkObjectReference to NetworkObject + /// - Implicit operators of NetworkObjectReference + /// + public class NetworkObjectReferenceTests : IDisposable + { + private class TestNetworkBehaviour : NetworkBehaviour + { + public NetworkVariable TestVariable = new NetworkVariable(); + + public NetworkObject RpcReceivedNetworkObject; + + public GameObject RpcReceivedGameObject; + + [ServerRpc] + public void SendReferenceServerRpc(NetworkObjectReference value) + { + RpcReceivedGameObject = value; + RpcReceivedNetworkObject = value; + } + } + + [Test] + public void TestSerializeNetworkObject() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + networkObjectContext.Object.Spawn(); + var outWriter = new FastBufferWriter(1300, Allocator.Temp); + try + { + // serialize + var outSerializer = new BufferSerializer(new BufferSerializerWriter(outWriter)); + NetworkObjectReference outReference = networkObjectContext.Object; + outReference.NetworkSerialize(outSerializer); + + // deserialize + NetworkObjectReference inReference = default; + var inReader = new FastBufferReader(outWriter, Allocator.Temp); + try + { + var inSerializer = + new BufferSerializer(new BufferSerializerReader(inReader)); + inReference.NetworkSerialize(inSerializer); + } + finally + { + inReader.Dispose(); + } + + // validate + Assert.NotNull((NetworkObject)inReference); + Assert.AreEqual(inReference.NetworkObjectId, networkObjectContext.Object.NetworkObjectId); + Assert.AreEqual(outReference, inReference); + Assert.AreEqual(networkObjectContext.Object, (NetworkObject)inReference); + } + finally + { + outWriter.Dispose(); + } + } + + [Test] + public void TestSerializeGameObject() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + networkObjectContext.Object.Spawn(); + var outWriter = new FastBufferWriter(1300, Allocator.Temp); + try + { + // serialize + var outSerializer = new BufferSerializer(new BufferSerializerWriter(outWriter)); + NetworkObjectReference outReference = networkObjectContext.Object.gameObject; + outReference.NetworkSerialize(outSerializer); + + // deserialize + NetworkObjectReference inReference = default; + var inReader = new FastBufferReader(outWriter, Allocator.Temp); + try + { + var inSerializer = + new BufferSerializer(new BufferSerializerReader(inReader)); + inReference.NetworkSerialize(inSerializer); + } + finally + { + inReader.Dispose(); + } + GameObject gameObject = inReference; + + // validate + Assert.AreEqual(outReference, inReference); + Assert.AreEqual(networkObjectContext.Object.gameObject, gameObject); + } + finally + { + outWriter.Dispose(); + } + } + + [Test] + public void TestTryGet() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + networkObjectContext.Object.Spawn(); + + NetworkObjectReference networkObjectReference = networkObjectContext.Object; + + Assert.True(networkObjectReference.TryGet(out NetworkObject networkObject)); + Assert.NotNull(networkObject); + networkObjectReference.TryGet(out NetworkObject result); + Assert.AreEqual(networkObject, result); + } + + [UnityTest] + public IEnumerator TestRpc() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + + testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference(otherObjectContext.Object)); + + // wait for rpc completion + float t = 0; + while (testNetworkBehaviour.RpcReceivedGameObject == null) + { + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(otherObjectContext.Object, testNetworkBehaviour.RpcReceivedNetworkObject); + Assert.AreEqual(otherObjectContext.Object.gameObject, testNetworkBehaviour.RpcReceivedGameObject); + } + + [UnityTest] + public IEnumerator TestRpcImplicitNetworkObject() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + + testNetworkBehaviour.SendReferenceServerRpc(otherObjectContext.Object); + + // wait for rpc completion + float t = 0; + while (testNetworkBehaviour.RpcReceivedGameObject == null) + { + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(otherObjectContext.Object, testNetworkBehaviour.RpcReceivedNetworkObject); + Assert.AreEqual(otherObjectContext.Object.gameObject, testNetworkBehaviour.RpcReceivedGameObject); + } + + [UnityTest] + public IEnumerator TestRpcImplicitGameObject() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + + testNetworkBehaviour.SendReferenceServerRpc(otherObjectContext.Object.gameObject); + + // wait for rpc completion + float t = 0; + while (testNetworkBehaviour.RpcReceivedGameObject == null) + { + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(otherObjectContext.Object, testNetworkBehaviour.RpcReceivedNetworkObject); + Assert.AreEqual(otherObjectContext.Object.gameObject, testNetworkBehaviour.RpcReceivedGameObject); + } + + [Test] + public void TestNetworkVariable() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + + // check default value is null + Assert.IsNull((NetworkObject)testNetworkBehaviour.TestVariable.Value); + + testNetworkBehaviour.TestVariable.Value = networkObjectContext.Object; + + Assert.AreEqual((GameObject)testNetworkBehaviour.TestVariable.Value, networkObjectContext.Object.gameObject); + Assert.AreEqual((NetworkObject)testNetworkBehaviour.TestVariable.Value, networkObjectContext.Object); + } + + [Test] + public void TestDespawn() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + networkObjectContext.Object.Spawn(); + var originalId = networkObjectContext.Object.NetworkObjectId; + + NetworkObjectReference networkObjectReference = networkObjectContext.Object; + Assert.AreEqual(networkObjectContext.Object, (NetworkObject)networkObjectReference); + + networkObjectContext.Object.Despawn(); + Assert.IsFalse(networkObjectReference.TryGet(out NetworkObject _)); + + networkObjectContext.Object.Spawn(); + + // After spawning again the reference will still no longer work as it still points to the old object + Assert.AreNotEqual(originalId, networkObjectContext.Object.NetworkObjectId); + Assert.IsFalse(networkObjectReference.TryGet(out NetworkObject _)); + + // creating a new reference will make it work again + networkObjectReference = networkObjectContext.Object; + Assert.AreEqual(networkObjectContext.Object, (NetworkObject)networkObjectReference); + } + + [Test] + public void FailSerializeNonSpawnedNetworkObject() + { + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + + Assert.Throws(() => + { + NetworkObjectReference outReference = networkObjectContext.Object; + }); + } + + [Test] + public void FailSerializeGameObjectWithoutNetworkObject() + { + using var gameObjectContext = UnityObjectContext.CreateGameObject(); + + Assert.Throws(() => + { + NetworkObjectReference outReference = gameObjectContext.Object; + }); + } + + [Test] + public void FailSerializeNullNetworkObject() + { + Assert.Throws(() => + { + NetworkObjectReference outReference = (NetworkObject)null; + }); + } + + [Test] + public void FailSerializeNullGameObject() + { + Assert.Throws(() => + { + NetworkObjectReference outReference = (GameObject)null; + }); + } + + public void Dispose() + { + //Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + } + + public NetworkObjectReferenceTests() + { + //Create, instantiate, and host + NetworkManagerHelper.StartNetworkManager(out _); + } + } + + /// + /// Helper method for tests to create and destroy Unity Objects. + /// + /// The type of Object this context incorporates. + public class UnityObjectContext : UnityObjectContext where T : Object + { + private T m_Object; + + internal UnityObjectContext(T unityObject, Object root) + : base(root) + { + m_Object = unityObject; + } + + public T Object => m_Object; + } + + public class UnityObjectContext : IDisposable + { + private Object m_Root; + + protected UnityObjectContext(Object root) + { + m_Root = root; + } + + public static UnityObjectContext CreateGameObject(string name = "") + { + var gameObject = new GameObject(name); + return new UnityObjectContext(gameObject, gameObject); + } + + public static UnityObjectContext CreateNetworkObject(string name = "") + { + var gameObject = new GameObject(name); + var networkObject = gameObject.AddComponent(); + return new UnityObjectContext(networkObject, gameObject); + } + + public void Dispose() + { + Object.DestroyImmediate(m_Root); + } + } +} diff --git a/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs.meta b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs.meta new file mode 100644 index 0000000..bcaad14 --- /dev/null +++ b/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 182c2000b73248b4bbdd79f70ec90cd2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Timing.meta b/Tests/Runtime/Timing.meta new file mode 100644 index 0000000..fa4107a --- /dev/null +++ b/Tests/Runtime/Timing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: db225cab5067aae4e8b7771af8a4183c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Timing/NetworkTimeSystemTests.cs b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs new file mode 100644 index 0000000..caee733 --- /dev/null +++ b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs @@ -0,0 +1,137 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Runtime tests to test the network time system with the Unity player loop. + /// + public class NetworkTimeSystemTests + { + private MonoBehaviourTest m_MonoBehaviourTest; // cache for teardown + + [SetUp] + public void Setup() + { + // Create, instantiate, and host + Assert.IsTrue(NetworkManagerHelper.StartNetworkManager(out _)); + } + + /// + /// Tests whether time is accessible and has correct values inside Update/FixedUpdate. + /// + /// + [UnityTest] + public IEnumerator PlayerLoopTimeTest() + { + m_MonoBehaviourTest = new MonoBehaviourTest(); + + yield return m_MonoBehaviourTest; + } + + /// + /// Tests whether the time system invokes the correct amount of ticks over a period of time. + /// Note we cannot test against Time.Time directly because of floating point precision. Our time is more precise leading to different results. + /// + /// + [UnityTest] + public IEnumerator CorrectAmountTicksTest() + { + var tickSystem = NetworkManager.Singleton.NetworkTickSystem; + var delta = tickSystem.LocalTime.FixedDeltaTime; + + while (tickSystem.LocalTime.Time < 3f) + { + yield return null; + Assert.AreEqual(Mathf.FloorToInt((tickSystem.LocalTime.TimeAsFloat / delta)), NetworkManager.Singleton.LocalTime.Tick); + Assert.AreEqual(Mathf.FloorToInt((tickSystem.ServerTime.TimeAsFloat / delta)), NetworkManager.Singleton.ServerTime.Tick); + Assert.True(Mathf.Approximately((float)NetworkManager.Singleton.LocalTime.Time, (float)NetworkManager.Singleton.ServerTime.Time)); + } + } + + [TearDown] + public void TearDown() + { + // Stop, shutdown, and destroy + NetworkManagerHelper.ShutdownNetworkManager(); + + if (m_MonoBehaviourTest != null) + { + Object.DestroyImmediate(m_MonoBehaviourTest.gameObject); + } + } + + } + + public class PlayerLoopTimeTestComponent : MonoBehaviour, IMonoBehaviourTest + { + public const int Passes = 100; + + private int m_UpdatePasses = 0; + + private int m_LastFixedUpdateTick = 0; + private int m_TickOffset = -1; + + private NetworkTime m_LocalTimePreviousUpdate; + private NetworkTime m_ServerTimePreviousUpdate; + private NetworkTime m_LocalTimePreviousFixedUpdate; + + public void Start() + { + // Run fixed update at same rate as network tick + Time.fixedDeltaTime = NetworkManager.Singleton.LocalTime.FixedDeltaTime; + + // Uncap fixed time else we might skip fixed updates + Time.maximumDeltaTime = float.MaxValue; + } + + public void Update() + { + // This must run first else it wont run if there is an exception + m_UpdatePasses++; + + var localTime = NetworkManager.Singleton.LocalTime; + var serverTime = NetworkManager.Singleton.ServerTime; + + // time should have advanced on the host/server + Assert.True(m_LocalTimePreviousUpdate.Time < localTime.Time); + Assert.True(m_ServerTimePreviousUpdate.Time < serverTime.Time); + + // time should be further then last fixed step in update + Assert.True(m_LocalTimePreviousFixedUpdate.FixedTime < localTime.Time); + + // we should be in same or further tick then fixed update + Assert.True(m_LocalTimePreviousFixedUpdate.Tick <= localTime.Tick); + + // fixed update should result in same amounts of tick as network time + if (m_TickOffset == -1) + { + m_TickOffset = serverTime.Tick - m_LastFixedUpdateTick; + } + else + { + // offset of 1 is ok, this happens due to different tick duration offsets + Assert.True(Mathf.Abs(serverTime.Tick - m_TickOffset - m_LastFixedUpdateTick) <= 1); + } + + m_LocalTimePreviousUpdate = localTime; + } + + public void FixedUpdate() + { + var time = NetworkManager.Singleton.LocalTime; + + m_LocalTimePreviousFixedUpdate = time; + + Assert.AreEqual(Time.fixedDeltaTime, time.FixedDeltaTime); + Assert.True(Mathf.Approximately((float)NetworkManager.Singleton.LocalTime.Time, (float)NetworkManager.Singleton.ServerTime.Time)); + + m_LastFixedUpdateTick++; + } + + public bool IsTestFinished => m_UpdatePasses >= Passes; + } + +} diff --git a/Tests/Runtime/Timing/NetworkTimeSystemTests.cs.meta b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs.meta new file mode 100644 index 0000000..d87b284 --- /dev/null +++ b/Tests/Runtime/Timing/NetworkTimeSystemTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c9dcad9faf35434d8b997e769ece4d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Timing/TimeInitializationTest.cs b/Tests/Runtime/Timing/TimeInitializationTest.cs new file mode 100644 index 0000000..d6cbfc5 --- /dev/null +++ b/Tests/Runtime/Timing/TimeInitializationTest.cs @@ -0,0 +1,102 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Tests that the time and tick system are initialized properly + /// + public class TimeInitializationTest + { + private int m_ClientTickCounter; + private int m_ConnectedTick; + private NetworkManager m_Client; + + [UnityTest] + public IEnumerator TestClientTimeInitializationOnConnect([Values(0, 1f)] float serverStartDelay, [Values(0, 1f)] float clientStartDelay, [Values(true, false)] bool isHost) + { + // Create multiple NetworkManager instances + if (!MultiInstanceHelpers.Create(1, out NetworkManager server, out NetworkManager[] clients, 30)) + { + Debug.LogError("Failed to create instances"); + Assert.Fail("Failed to create instances"); + } + + yield return new WaitForSeconds(serverStartDelay); + MultiInstanceHelpers.Start(false, server, new NetworkManager[] { }, BaseMultiInstanceTest.SceneManagerValidationAndTestRunnerInitialization); // passing no clients on purpose to start them manually later + + // 0 ticks should have passed + var serverTick = server.NetworkTickSystem.ServerTime.Tick; + Assert.AreEqual(0, serverTick); + + // server time should be 0 + Assert.AreEqual(0, server.NetworkTickSystem.ServerTime.Time); + + // wait 2 frames to ensure network tick is run + yield return null; + yield return null; + + var serverTimePassed = server.NetworkTickSystem.ServerTime.Time; + var expectedServerTickCount = Mathf.FloorToInt((float)(serverTimePassed * 30)); + + var ticksPassed = server.NetworkTickSystem.ServerTime.Tick - serverTick; + Assert.AreEqual(expectedServerTickCount, ticksPassed); + + yield return new WaitForSeconds(clientStartDelay); + + Assert.AreEqual(1, clients.Length); + m_Client = clients[0]; + + Assert.Null(m_Client.NetworkTickSystem); + + m_Client.OnClientConnectedCallback += ClientOnOnClientConnectedCallback; + + var clientStartRealTime = Time.time; + + m_Client.StartClient(); + BaseMultiInstanceTest.SceneManagerValidationAndTestRunnerInitialization(clients[0]); + + m_Client.NetworkTickSystem.Tick += NetworkTickSystemOnTick; + m_ClientTickCounter = 0; + + + // don't check for anything here and assume non-async connection. + + // Wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients)); + + var clientStartRealTimeDuration = Time.time - clientStartRealTime; + var clientStartRealTickDuration = Mathf.FloorToInt(clientStartRealTimeDuration * 30); + + // check tick is initialized with server value + Assert.AreNotEqual(0, m_ConnectedTick); + + Assert.True(m_ClientTickCounter <= clientStartRealTickDuration); + + MultiInstanceHelpers.Destroy(); + yield return null; + } + + private void NetworkTickSystemOnTick() + { + Debug.Log(m_Client.NetworkTickSystem.ServerTime.Tick); + m_ClientTickCounter++; + } + + private void ClientOnOnClientConnectedCallback(ulong id) + { + // client connected to server + m_ConnectedTick = m_Client.NetworkTickSystem.ServerTime.Tick; + Debug.Log($"Connected tick: {m_ConnectedTick}"); + } + + [UnityTearDown] + public virtual IEnumerator Teardown() + { + MultiInstanceHelpers.Destroy(); + yield return null; + } + } +} diff --git a/Tests/Runtime/Timing/TimeInitializationTest.cs.meta b/Tests/Runtime/Timing/TimeInitializationTest.cs.meta new file mode 100644 index 0000000..f5ab60f --- /dev/null +++ b/Tests/Runtime/Timing/TimeInitializationTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d1a3e8bb2e1d204abc82173fddc8977 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs new file mode 100644 index 0000000..c182b53 --- /dev/null +++ b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections; +using System.Linq; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Tests the times of two clients connecting to a server using the SIPTransport (returns 50ms RTT but has no latency simulation) + /// + public class TimeMultiInstanceTest : BaseMultiInstanceTest + { + private const double k_AdditionalTimeTolerance = 0.3d; // magic number and in theory not needed but without this mac os test fail in Yamato because it looks like we get random framerate drops during unit test. + + private NetworkTimeState m_ServerState; + private NetworkTimeState m_Client1State; + private NetworkTimeState m_Client2State; + + protected override int NbClients => 2; + + // we need to change frame rate which is done in startup so removing this from here and moving into the test. + public override IEnumerator Setup() + { + yield break; + } + + private void UpdateTimeStates(NetworkManager[] networkManagers) + { + var server = networkManagers.First(t => t.IsServer); + var firstClient = networkManagers.First(t => t.IsClient); + var secondClient = networkManagers.Last(t => t.IsClient); + + Assert.AreNotEqual(firstClient, secondClient); + + m_ServerState = new NetworkTimeState(server); + m_Client1State = new NetworkTimeState(firstClient); + m_Client2State = new NetworkTimeState(secondClient); + } + + [UnityTest] + [TestCase(60, 30u, ExpectedResult = null)] + [TestCase(30, 30u, ExpectedResult = null)] + [TestCase(40, 30u, ExpectedResult = null)] + [TestCase(10, 30u, ExpectedResult = null)] + [TestCase(60, 60u, ExpectedResult = null)] + [TestCase(60, 10u, ExpectedResult = null)] + public IEnumerator TestTimeMultiInstance(int targetFrameRate, uint tickRate) + { + yield return StartSomeClientsAndServerWithPlayersCustom(true, NbClients, targetFrameRate, tickRate); + + double frameInterval = 1d / targetFrameRate; + double tickInterval = 1d / tickRate; + + var networkManagers = MultiInstanceHelpers.NetworkManagerInstances.ToArray(); + + var server = networkManagers.First(t => t.IsServer); + var firstClient = networkManagers.First(t => t.IsClient); + var secondClient = networkManagers.Last(t => t.IsClient); + + Assert.AreNotEqual(firstClient, secondClient); + + // increase the buffer time of client 2 // the values for client 1 are 0.0333/0.0333 here + secondClient.NetworkTimeSystem.LocalBufferSec = 0.2; + secondClient.NetworkTimeSystem.ServerBufferSec = 0.1; + + UpdateTimeStates(networkManagers); + + + // wait for at least one tick to pass + yield return new WaitUntil(() => m_ServerState.LocalTime.Tick != server.NetworkTickSystem.LocalTime.Tick); + yield return new WaitUntil(() => m_Client1State.LocalTime.Tick != firstClient.NetworkTickSystem.LocalTime.Tick); + yield return new WaitUntil(() => m_Client2State.LocalTime.Tick != secondClient.NetworkTickSystem.LocalTime.Tick); + + + var framesToRun = 3d / frameInterval; + + for (int i = 0; i < framesToRun; i++) + { + yield return null; + + UpdateTimeStates(networkManagers); + + // compares whether client times have the correct offset to server + m_ServerState.AssertCheckDifference(m_Client1State, tickInterval, tickInterval, tickInterval * 2 + frameInterval * 2 + k_AdditionalTimeTolerance); + m_ServerState.AssertCheckDifference(m_Client2State, 0.2, 0.1, tickInterval * 2 + frameInterval * 2 + k_AdditionalTimeTolerance); + + // compares the two client times, only difference should be based on buffering. + m_Client1State.AssertCheckDifference(m_Client2State, 0.2 - tickInterval, (0.1 - tickInterval), tickInterval * 2 + frameInterval * 2 + k_AdditionalTimeTolerance); + } + } + + // This is from BaseMultiInstanceTest but we need a custom version of this to modifiy the config + private IEnumerator StartSomeClientsAndServerWithPlayersCustom(bool useHost, int nbClients, int targetFrameRate, uint tickRate) + { + // Create multiple NetworkManager instances + if (!MultiInstanceHelpers.Create(nbClients, out NetworkManager server, out NetworkManager[] clients, targetFrameRate)) + { + Debug.LogError("Failed to create instances"); + Assert.Fail("Failed to create instances"); + } + + m_ClientNetworkManagers = clients; + m_ServerNetworkManager = server; + + // Create playerPrefab + m_PlayerPrefab = new GameObject("Player"); + NetworkObject networkObject = m_PlayerPrefab.AddComponent(); + + /* + * Normally we would only allow player prefabs to be set to a prefab. Not runtime created objects. + * In order to prevent having a Resource folder full of a TON of prefabs that we have to maintain, + * MultiInstanceHelper has a helper function that lets you mark a runtime created object to be + * treated as a prefab by the Netcode. That's how we can get away with creating the player prefab + * at runtime without it being treated as a SceneObject or causing other conflicts with the Netcode. + */ + // Make it a prefab + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObject); + + // Set the player prefab + server.NetworkConfig.PlayerPrefab = m_PlayerPrefab; + + for (int i = 0; i < clients.Length; i++) + { + clients[i].NetworkConfig.PlayerPrefab = m_PlayerPrefab; + clients[i].NetworkConfig.TickRate = tickRate; + } + + server.NetworkConfig.TickRate = tickRate; + + // Start the instances + if (!MultiInstanceHelpers.Start(useHost, server, clients)) + { + Debug.LogError("Failed to start instances"); + Assert.Fail("Failed to start instances"); + } + + // Wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients)); + + // Wait for connection on server side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnectedToServer(server, useHost ? nbClients + 1 : nbClients)); + } + + private IEnumerator WaitForFrames(int count) + { + int nextFrameNumber = Time.frameCount + count; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + } + + private readonly struct NetworkTimeState : IEquatable + { + public NetworkTime LocalTime { get; } + public NetworkTime ServerTime { get; } + + public NetworkTimeState(NetworkManager manager) + { + LocalTime = manager.NetworkTickSystem.LocalTime; + ServerTime = manager.NetworkTickSystem.ServerTime; + } + + public void AssertCheckDifference(NetworkTimeState clientState, double localBufferDifference, double serverBufferDifference, double tolerance) + { + var difLocalAbs = Math.Abs(clientState.LocalTime.Time - LocalTime.Time - localBufferDifference); + var difServerAbs = Math.Abs(ServerTime.Time - clientState.ServerTime.Time - serverBufferDifference); + + Assert.True(difLocalAbs < tolerance, $"localtime difference: {difLocalAbs} bigger than tolerance: {tolerance}"); + Assert.True(difServerAbs < tolerance, $"servertime difference: {difServerAbs} bigger than tolerance: {tolerance}"); + } + + public bool Equals(NetworkTimeState other) + { + return LocalTime.Time.Equals(other.LocalTime.Time) && ServerTime.Time.Equals(other.ServerTime.Time); + } + + public override bool Equals(object obj) + { + return obj is NetworkTimeState other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (LocalTime.Time.GetHashCode() * 397) ^ ServerTime.Time.GetHashCode(); + } + } + } + } +} diff --git a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs.meta b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs.meta new file mode 100644 index 0000000..4cf2ed5 --- /dev/null +++ b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 71180f84c488966489fe44d57bf085d1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Transport.meta b/Tests/Runtime/Transport.meta new file mode 100644 index 0000000..b909388 --- /dev/null +++ b/Tests/Runtime/Transport.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d764f651f0e54e8281952933cc49be97 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/Transport/SIPTransport.cs b/Tests/Runtime/Transport/SIPTransport.cs new file mode 100644 index 0000000..9ea8816 --- /dev/null +++ b/Tests/Runtime/Transport/SIPTransport.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +#if UNITY_EDITOR +using Unity.Netcode.Editor; +#endif +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// SIPTransport (SIngleProcessTransport) + /// is a NetworkTransport designed to be used with multiple network instances in a single process + /// it's designed for the netcode in a way where no networking stack has to be available + /// it's designed for testing purposes and it's not designed with speed in mind + /// +#if UNITY_EDITOR + [DontShowInTransportDropdown] +#endif + public class SIPTransport : NetworkTransport + { + private struct Event + { + public NetworkEvent Type; + public ulong ConnectionId; + public ArraySegment Data; + } + + private class Peer + { + public ulong ConnectionId; + public SIPTransport Transport; + public Queue IncomingBuffer = new Queue(); + } + + private readonly Dictionary m_Peers = new Dictionary(); + private ulong m_ClientsCounter = 1; + + private static Peer s_Server; + private Peer m_LocalConnection; + + public override ulong ServerClientId => 0; + public ulong LocalClientId; + + public override void DisconnectLocalClient() + { + if (m_LocalConnection != null) + { + // Inject local disconnect + m_LocalConnection.IncomingBuffer.Enqueue(new Event + { + Type = NetworkEvent.Disconnect, + ConnectionId = m_LocalConnection.ConnectionId, + Data = new ArraySegment() + }); + + if (s_Server != null && m_LocalConnection != null) + { + // Remove the connection + s_Server.Transport.m_Peers.Remove(m_LocalConnection.ConnectionId); + } + + if (m_LocalConnection.ConnectionId == ServerClientId) + { + StopServer(); + } + + // Remove the local connection + m_LocalConnection = null; + } + } + + // Called by server + public override void DisconnectRemoteClient(ulong clientId) + { + if (m_Peers.ContainsKey(clientId)) + { + // Inject disconnect into remote + m_Peers[clientId].IncomingBuffer.Enqueue(new Event + { + Type = NetworkEvent.Disconnect, + ConnectionId = clientId, + Data = new ArraySegment() + }); + + // Inject local disconnect + m_LocalConnection.IncomingBuffer.Enqueue(new Event + { + Type = NetworkEvent.Disconnect, + ConnectionId = clientId, + Data = new ArraySegment() + }); + + // Remove the local connection on remote + m_Peers[clientId].Transport.m_LocalConnection = null; + + // Remove connection on server + m_Peers.Remove(clientId); + } + } + + public override ulong GetCurrentRtt(ulong clientId) + { + // Always returns 50ms + return 50; + } + + public override void Initialize() + { + } + + private void StopServer() + { + s_Server = null; + m_Peers.Remove(ServerClientId); + } + + public override void Shutdown() + { + // Inject disconnects to all the remotes + foreach (KeyValuePair onePeer in m_Peers) + { + onePeer.Value.IncomingBuffer.Enqueue(new Event + { + Type = NetworkEvent.Disconnect, + ConnectionId = LocalClientId, + Data = new ArraySegment() + }); + } + + if (m_LocalConnection != null && m_LocalConnection.ConnectionId == ServerClientId) + { + StopServer(); + } + + + // TODO: Cleanup + } + + public override bool StartClient() + { + if (s_Server == null) + { + // No server + Debug.LogError("No server"); + return false; + } + + if (m_LocalConnection != null) + { + // Already connected + Debug.LogError("Already connected"); + return false; + } + + // Generate an Id for the server that represents this client + ulong serverConnectionId = ++s_Server.Transport.m_ClientsCounter; + LocalClientId = serverConnectionId; + + // Create local connection + m_LocalConnection = new Peer() + { + ConnectionId = serverConnectionId, + Transport = this, + IncomingBuffer = new Queue() + }; + + // Add the server as a local connection + m_Peers.Add(ServerClientId, s_Server); + + // Add local connection as a connection on the server + s_Server.Transport.m_Peers.Add(serverConnectionId, m_LocalConnection); + + // Sends a connect message to the server + s_Server.Transport.m_LocalConnection.IncomingBuffer.Enqueue(new Event() + { + Type = NetworkEvent.Connect, + ConnectionId = serverConnectionId, + Data = new ArraySegment() + }); + + // Send a local connect message + m_LocalConnection.IncomingBuffer.Enqueue(new Event + { + Type = NetworkEvent.Connect, + ConnectionId = ServerClientId, + Data = new ArraySegment() + }); + + return true; + } + + public override bool StartServer() + { + if (s_Server != null) + { + // Can only have one server + Debug.LogError("Server already started"); + return false; + } + + if (m_LocalConnection != null) + { + // Already connected + Debug.LogError("Already connected"); + return false; + } + + // Create local connection + m_LocalConnection = new Peer() + { + ConnectionId = ServerClientId, + Transport = this, + IncomingBuffer = new Queue() + }; + + // Set the local connection as the server + s_Server = m_LocalConnection; + + m_Peers.Add(ServerClientId, s_Server); + + return true; + } + + public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) + { + if (m_LocalConnection != null) + { + // Create copy since netcode wants the byte array back straight after the method call. + // Hard on GC. + byte[] copy = new byte[payload.Count]; + Buffer.BlockCopy(payload.Array, payload.Offset, copy, 0, payload.Count); + + if (m_Peers.ContainsKey(clientId)) + { + m_Peers[clientId].IncomingBuffer.Enqueue(new Event + { + Type = NetworkEvent.Data, + ConnectionId = m_LocalConnection.ConnectionId, + Data = new ArraySegment(copy) + }); + } + } + } + + public override NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime) + { + if (m_LocalConnection != null) + { + if (m_LocalConnection.IncomingBuffer.Count == 0) + { + clientId = 0; + payload = new ArraySegment(); + receiveTime = 0; + return NetworkEvent.Nothing; + } + + var peerEvent = m_LocalConnection.IncomingBuffer.Dequeue(); + + clientId = peerEvent.ConnectionId; + payload = peerEvent.Data; + receiveTime = 0; + + return peerEvent.Type; + } + + clientId = 0; + payload = new ArraySegment(); + receiveTime = 0; + return NetworkEvent.Nothing; + } + } +} diff --git a/Tests/Runtime/Transport/SIPTransport.cs.meta b/Tests/Runtime/Transport/SIPTransport.cs.meta new file mode 100644 index 0000000..8c84114 --- /dev/null +++ b/Tests/Runtime/Transport/SIPTransport.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1fd1b14eba874a189f13f12d343c331c +timeCreated: 1620145176 \ No newline at end of file diff --git a/Tests/Runtime/Transport/SIPTransportTests.cs b/Tests/Runtime/Transport/SIPTransportTests.cs new file mode 100644 index 0000000..c438d18 --- /dev/null +++ b/Tests/Runtime/Transport/SIPTransportTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Text; +using NUnit.Framework; +using UnityEngine; + +namespace Unity.Netcode.RuntimeTests.Transport +{ + public class SIPTransportTests + { + [Test] + public void SendReceiveData() + { + SIPTransport server = new GameObject("Server").AddComponent(); + SIPTransport client = new GameObject("Client").AddComponent(); + + server.Initialize(); + server.StartServer(); + + client.Initialize(); + client.StartClient(); + + NetworkEvent serverEvent = server.PollEvent(out ulong clientId, out _, out _); + NetworkEvent clientEvent = client.PollEvent(out ulong serverId, out _, out _); + + // Make sure both connected + Assert.True(serverEvent == NetworkEvent.Connect); + Assert.True(clientEvent == NetworkEvent.Connect); + + // Send data + server.Send(clientId, new ArraySegment(Encoding.ASCII.GetBytes("Hello Client")), NetworkDelivery.ReliableSequenced); + client.Send(serverId, new ArraySegment(Encoding.ASCII.GetBytes("Hello Server")), NetworkDelivery.ReliableSequenced); + + serverEvent = server.PollEvent(out ulong newClientId, out ArraySegment serverPayload, out _); + clientEvent = client.PollEvent(out ulong newServerId, out ArraySegment clientPayload, out _); + + // Make sure we got data + Assert.True(serverEvent == NetworkEvent.Data); + Assert.True(clientEvent == NetworkEvent.Data); + + // Make sure the ID is correct + Assert.True(newClientId == clientId); + Assert.True(newServerId == serverId); + + // Make sure the payload was correct + Assert.That(serverPayload, Is.EquivalentTo(Encoding.ASCII.GetBytes("Hello Server"))); + Assert.That(clientPayload, Is.EquivalentTo(Encoding.ASCII.GetBytes("Hello Client"))); + + server.Shutdown(); + client.Shutdown(); + } + } +} diff --git a/Tests/Runtime/Transport/SIPTransportTests.cs.meta b/Tests/Runtime/Transport/SIPTransportTests.cs.meta new file mode 100644 index 0000000..5730f2c --- /dev/null +++ b/Tests/Runtime/Transport/SIPTransportTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e3fe3777ca6a4f4392d6281d148d0d3c +timeCreated: 1620385694 \ No newline at end of file diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef new file mode 100644 index 0000000..295cdb3 --- /dev/null +++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef @@ -0,0 +1,34 @@ +{ + "name": "Unity.Netcode.RuntimeTests", + "rootNamespace": "Unity.Netcode.RuntimeTests", + "references": [ + "Unity.Netcode.Runtime", + "Unity.Netcode.Editor", + "Unity.Netcode.Components", + "Unity.Collections", + "UnityEngine.TestRunner", + "Unity.Multiplayer.MetricTypes", + "Unity.Multiplayer.NetStats", + "Unity.Netcode.Adapter.UTP", + "ClientNetworkTransform" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [ + { + "name": "com.unity.multiplayer.tools", + "expression": "", + "define": "MULTIPLAYER_TOOLS" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Runtime/com.unity.netcode.runtimetests.asmdef.meta b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef.meta new file mode 100644 index 0000000..1a5d075 --- /dev/null +++ b/Tests/Runtime/com.unity.netcode.runtimetests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0d0243c226d5dab4fb5d34006a1ad53b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Third Party Notices.md b/Third Party Notices.md new file mode 100644 index 0000000..5d05eaa --- /dev/null +++ b/Third Party Notices.md @@ -0,0 +1,32 @@ +This package contains third-party software components governed by the license(s) indicated below: +--------- + +## Package: Editor/CodeGen/XXHash + +--------- + +Component Name: xxHash + +License Type: MIT + +Copyright (c) 2015, 2016 Sedat Kapanoglu + +http://www.xxhash.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Third Party Notices.md.meta b/Third Party Notices.md.meta new file mode 100644 index 0000000..68346b1 --- /dev/null +++ b/Third Party Notices.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 43e3f545298d44899b42e94aa7d17f07 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..646ab45 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "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.2", + "unity": "2020.3", + "dependencies": { + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.nuget.mono-cecil": "1.10.1", + "com.unity.collections": "1.0.0-pre.5" + }, + "upmCi": { + "footprint": "f3acafb35c17cf3cb48042bf9655c4ada00c34ae" + }, + "repository": { + "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", + "type": "git", + "revision": "bcef5b992c5414707ff48c95a48a113fd0e09ad3" + }, + "samples": [ + { + "displayName": "Bootstrap", + "description": "A lightweight sample to get started", + "path": "Samples~/Bootstrap" + }, + { + "displayName": "ClientNetworkTransform", + "description": "A sample to demonstrate how client-driven NetworkTransform can be implemented", + "path": "Samples~/ClientNetworkTransform" + } + ] +} diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..0433924 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1d54835385782194a91c301198674dd8 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: