From 1f728d4d44355d17292f5419dedc44d351a259cd Mon Sep 17 00:00:00 2001 From: MaxIsJoe <34368774+MaxIsJoe@users.noreply.github.com> Date: Sat, 22 Mar 2025 05:21:56 +0200 Subject: [PATCH 1/2] code errors --- Assets/Mirror/Core/NetworkIdentity.cs | 47 +++++++++++++-------------- Assets/Mirror/Core/NetworkServer.cs | 6 ++-- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/Assets/Mirror/Core/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs index ec1421de70..47d3d1354e 100644 --- a/Assets/Mirror/Core/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -1503,33 +1503,30 @@ internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick, boo // (otherwise [SyncVar] changes would never be serialized in tests) // // NOTE: != instead of < because int.max+1 overflows at some point. - lock (lastSerialization.observersWriter) - { - if (lastSerialization.tick != tick + if (lastSerialization.tick != tick #if UNITY_EDITOR - || NetworkServer.ApplicationIsPlayingCash == false + || NetworkServer.ApplicationIsPlayingCash == false #endif - ) - { - // reset - lastSerialization.ResetWriters(); - - // serialize both Reliable and Unreliable components in one iteration. - // doing each in their own iteration would be too costly. - SerializeServer_Broadcast( - lastSerialization.ownerWriterReliable, - lastSerialization.observersWriterReliable, - lastSerialization.ownerWriterUnreliableBaseline, - lastSerialization.observersWriterUnreliableBaseline, - lastSerialization.ownerWriterUnreliableDelta, - lastSerialization.observersWriterUnreliableDelta, - unreliableBaselineElapsed - ); - - // set tick - lastSerialization.tick = tick; - //Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}"); - } + ) + { + // reset + lastSerialization.ResetWriters(); + + // serialize both Reliable and Unreliable components in one iteration. + // doing each in their own iteration would be too costly. + SerializeServer_Broadcast( + lastSerialization.ownerWriterReliable, + lastSerialization.observersWriterReliable, + lastSerialization.ownerWriterUnreliableBaseline, + lastSerialization.observersWriterUnreliableBaseline, + lastSerialization.ownerWriterUnreliableDelta, + lastSerialization.observersWriterUnreliableDelta, + unreliableBaselineElapsed + ); + + // set tick + lastSerialization.tick = tick; + //Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}"); } // return it diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index f625e954e2..acd71a0754 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -2239,16 +2239,14 @@ static void BroadcastToConnection(NetworkConnectionToClient connection, bool unr // GameObject.Destroy instead of NetworkServer.Destroy. else { - hasNull = true; Debug.LogWarning($"Found 'null' entry in observing list for connectionId={connection.connectionId}. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy."); } } catch (Exception ex) { - Debug.LogError(" dirty observing had null "); + Debug.LogError($" dirty observing had null: {ex}"); } } - connection.DirtyObserving[i] = null; } // helper function to check a connection for inactivity and disconnect if necessary @@ -2322,7 +2320,7 @@ public static void SubConnectionBroadcast(NetworkConnectionToClient connection) connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); // broadcast world state to this connection - BroadcastToConnection(connection, unreliableBaselineElapsed); + BroadcastToConnection(connection, true); } // update connection to flush out batched messages From 47c63a97f68ddb429b9778ff2b133b2c1fe9a5e2 Mon Sep 17 00:00:00 2001 From: MaxIsJoe <34368774+MaxIsJoe@users.noreply.github.com> Date: Sat, 22 Mar 2025 05:33:34 +0200 Subject: [PATCH 2/2] Revert "Merge branch 'updatingv2' into upstream-only" This reverts commit 7047968391c43fc18f6cfe4bcbe02aa4ab4fc5f0, reversing changes made to 1f728d4d44355d17292f5419dedc44d351a259cd. --- .github/workflows/RunUnityTests.yml | 4 +- .../CompilerSymbols/PreprocessorDefine.cs | 9 +- .../Discovery/NetworkDiscoveryHUD.cs | 2 + Assets/Mirror/Components/GUIConsole.cs | 2 + .../Distance/DistanceInterestManagement.cs | 3 +- .../Scene/SceneInterestManagement.cs | 9 +- .../SceneDistanceInterestManagement.cs | 11 +- .../SpatialHashing/HexGrid2D.cs | 170 + .../SpatialHashing/HexGrid2D.cs.meta | 11 + .../SpatialHashing/HexGrid3D.cs | 243 ++ .../SpatialHashing/HexGrid3D.cs.meta | 11 + .../HexSpatialHash2DInterestManagement.cs | 345 ++ ...HexSpatialHash2DInterestManagement.cs.meta | 11 + .../HexSpatialHash3DInterestManagement.cs | 336 ++ ...exSpatialHash3DInterestManagement.cs.meta} | 2 +- .../SpatialHashing3DInterestManagement.cs | 4 +- ...SpatialHashing3DInterestManagement.cs.meta | 10 +- .../SpatialHashingInterestManagement.cs | 4 +- Assets/Mirror/Components/NetworkAnimator.cs | 10 +- .../Mirror/Components/NetworkPingDisplay.cs | 2 + .../NetworkRigidbodyUnreliableCompressed.cs | 115 + ...tworkRigidbodyUnreliableCompressed.cs.meta | 11 + Assets/Mirror/Components/NetworkRoomPlayer.cs | 2 + Assets/Mirror/Components/NetworkStatistics.cs | 2 + .../NetworkTransform/NetworkTransformBase.cs | 12 +- .../NetworkTransformHybrid.cs | 939 ++--- .../NetworkTransformReliable.cs | 65 +- .../NetworkTransformUnreliable.cs | 50 +- Assets/Mirror/Components/RemoteStatistics.cs | 2 + Assets/Mirror/Core/Messages.cs | 89 +- Assets/Mirror/Core/NetworkBehaviour.cs | 46 +- Assets/Mirror/Core/NetworkBehaviourHybrid.cs | 483 --- Assets/Mirror/Core/NetworkClient.cs | 199 +- Assets/Mirror/Core/NetworkConnection.cs | 12 - .../Mirror/Core/NetworkConnectionToClient.cs | 13 +- Assets/Mirror/Core/NetworkIdentity.cs | 408 ++- Assets/Mirror/Core/NetworkManager.cs | 19 +- Assets/Mirror/Core/NetworkManagerHUD.cs | 2 + Assets/Mirror/Core/NetworkServer.cs | 234 +- Assets/Mirror/Editor/AndroidManifestHelper.cs | 159 +- .../Editor/NetworkBehaviourInspector.cs | 10 + Assets/Mirror/Examples/Editor.meta | 8 + .../Editor/MirrorRenderPipelineConverter.cs | 291 ++ .../MirrorRenderPipelineConverter.cs.meta | 11 + Assets/Mirror/Examples/HexSpatialHash.meta | 8 + .../HexSpatialHash/Hex2DSpatialHash.unity | 510 +++ .../Hex2DSpatialHash.unity.meta | 7 + .../HexSpatialHash/Hex3DSpatialHash.unity | 509 +++ .../Hex3DSpatialHash.unity.meta | 7 + .../Examples/HexSpatialHash/Mateirals.meta | 8 + .../HexSpatialHash/Mateirals/RandomColor.mat | 77 + .../Mateirals/RandomColor.mat.meta | 8 + .../Examples/HexSpatialHash/Prefabs.meta | 8 + .../HexSpatialHash/Prefabs/Hex2DPlayer.prefab | 170 + .../Prefabs/Hex2DPlayer.prefab.meta | 7 + .../HexSpatialHash/Prefabs/Hex3DPlayer.prefab | 182 + .../Prefabs/Hex3DPlayer.prefab.meta | 7 + .../HexSpatialHash/Prefabs/SpawnPrefab.prefab | 118 + .../Prefabs/SpawnPrefab.prefab.meta | 7 + .../Examples/HexSpatialHash/Scripts.meta | 8 + .../Scripts/Hex2DNetworkManager.cs | 86 + .../Scripts/Hex2DNetworkManager.cs.meta | 11 + .../HexSpatialHash/Scripts/Hex2DPlayer.cs | 51 + .../Scripts/Hex2DPlayer.cs.meta | 11 + .../Scripts/Hex2DPlayerCamera.cs | 95 + .../Scripts/Hex2DPlayerCamera.cs.meta | 11 + .../Scripts/Hex3DNetworkManager.cs | 70 + .../Scripts/Hex3DNetworkManager.cs.meta | 11 + .../HexSpatialHash/Scripts/Hex3DPlayer.cs | 49 + .../Scripts/Hex3DPlayer.cs.meta | 11 + .../Scripts/Enumerations.cs | 10 + .../Scripts/Enumerations.cs.meta | 11 + .../Scripts/Interfaces.meta | 8 + .../Scripts/Interfaces/EquippedBall.cs | 67 + .../Scripts/Interfaces/EquippedBall.cs.meta | 11 + .../Scripts/Interfaces/EquippedBat.cs | 67 + .../Scripts/Interfaces/EquippedBat.cs.meta | 11 + .../Scripts/Interfaces/EquippedBox.cs | 67 + .../Scripts/Interfaces/EquippedBox.cs.meta | 11 + .../Scripts/Interfaces/IEquipped.cs | 74 + .../Scripts/Interfaces/IEquipped.cs.meta | 11 + Assets/Mirror/Examples/TanksHybrid.meta | 8 + .../Mirror/Examples/TanksHybrid/Prefabs.meta | 8 + .../TanksHybrid/Prefabs/Projectile.prefab | 187 + .../Prefabs/Projectile.prefab.meta | 7 + .../Examples/TanksHybrid/Prefabs/Tank.prefab | 377 ++ .../TanksHybrid/Prefabs/Tank.prefab.meta | 8 + Assets/Mirror/Examples/TanksHybrid/Readme.txt | 6 + .../Examples/TanksHybrid/Readme.txt.meta | 3 + .../Mirror/Examples/TanksHybrid/Scenes.meta | 8 + .../TanksHybrid/Scenes/MirrorTanksHybrid.meta | 8 + .../Scenes/MirrorTanksHybrid.unity | 730 ++++ .../Scenes/MirrorTanksHybrid.unity.meta | 7 + .../Scenes/MirrorTanksHybrid/NavMesh.asset | Bin 0 -> 6564 bytes .../MirrorTanksHybrid/NavMesh.asset.meta | 8 + .../Mirror/Examples/TanksHybrid/Scripts.meta | 8 + .../TanksHybrid/Scripts/Projectile.cs | 46 + .../TanksHybrid/Scripts/Projectile.cs.meta | 11 + .../Examples/TanksHybrid/Scripts/Tank.cs | 128 + .../Examples/TanksHybrid/Scripts/Tank.cs.meta | 11 + .../Examples/_Common/Textures/Wall01.jpg | Bin 0 -> 60894 bytes .../Examples/_Common/Textures/Wall01_n.jpg | Bin 0 -> 33321 bytes Assets/Mirror/Hosting/Edgegap/Edgegap.asmdef | 4 +- .../Edgegap/Editor/Api/EdgegapApiBase.cs | 62 +- .../Edgegap/Editor/Api/EdgegapAppApi.cs | 44 +- .../Editor/Api/EdgegapDeploymentsApi.cs | 113 +- .../Edgegap/Editor/Api/EdgegapIpApi.cs | 21 +- .../Requests/CreateDeploymentRequest.cs | 29 +- .../Api/Models/Results/EdgegapErrorResult.cs | 2 +- .../Api/Models/Results/EdgegapHttpResult.cs | 52 +- .../Models/Results/GetAppVersionsResult.cs | 15 + .../Results/GetAppVersionsResult.cs.meta | 11 + .../Api/Models/Results/GetAppsResult.cs | 15 + .../Api/Models/Results/GetAppsResult.cs.meta | 11 + .../Api/Models/Results/GetDeploymentResult.cs | 20 + .../Results/GetDeploymentResult.cs.meta | 11 + .../Models/Results/GetDeploymentsResult.cs | 14 + .../Results/GetDeploymentsResult.cs.meta | 11 + .../Results/StopActiveDeploymentResult.cs | 53 +- .../Edgegap/Editor/Api/Models/VersionData.cs | 13 + .../Editor/Api/Models/VersionData.cs.meta | 11 + .../Edgegap/Editor/CustomPopupContent.cs | 70 + .../Edgegap/Editor/CustomPopupContent.cs.meta | 11 + .../Edgegap/Editor/EdgegapBuildUtils.cs | 232 +- .../Hosting/Edgegap/Editor/EdgegapWindow.uss | 115 +- .../Hosting/Edgegap/Editor/EdgegapWindow.uxml | 213 +- .../Edgegap/Editor/EdgegapWindowMetadata.cs | 160 +- .../Hosting/Edgegap/Editor/EdgegapWindowV2.cs | 3247 ++++++++++------- .../Images/discord-brands-solid-64px.png | Bin 0 -> 1607 bytes .../Images/discord-brands-solid-64px.png.meta | 140 + .../Editor/Images/discord-brands-solid.svg | 1 + .../Images/discord-brands-solid.svg.meta | 54 + Assets/Mirror/Hosting/Edgegap/README.md | 31 + .../Mirror/Hosting/Edgegap/_MIRROR_README.md | 5 +- Assets/Mirror/Hosting/Edgegap/package.json | 2 +- .../Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll | Bin 27648 -> 26624 bytes .../Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll | Bin 340992 -> 408576 bytes .../LobbyServiceCreateDialogue.cs | 3 + .../EdgegapRelay/EdgegapKcpTransport.cs | 4 +- .../Transports/Encryption/PubKeyInfo.cs | 11 +- Assets/Mirror/Transports/KCP/KcpTransport.cs | 4 +- .../Transports/KCP/ThreadedKcpTransport.cs | 4 +- .../SimpleWeb/SimpleWeb/Common/Log.cs | 56 +- Assets/Mirror/package.json | 4 +- 144 files changed, 10273 insertions(+), 2951 deletions(-) create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs rename Assets/Mirror/{Core/NetworkBehaviourHybrid.cs.meta => Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta} (86%) create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta delete mode 100644 Assets/Mirror/Core/NetworkBehaviourHybrid.cs create mode 100644 Assets/Mirror/Examples/Editor.meta create mode 100644 Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs create mode 100644 Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Mateirals.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Prefabs.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs.meta create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs create mode 100644 Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs.meta create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs.meta create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces.meta create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs.meta create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs.meta create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs.meta create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs create mode 100644 Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Prefabs.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab create mode 100644 Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab create mode 100644 Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Readme.txt create mode 100644 Assets/Mirror/Examples/TanksHybrid/Readme.txt.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scenes.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid/NavMesh.asset create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid/NavMesh.asset.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scripts.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scripts/Projectile.cs create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scripts/Projectile.cs.meta create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scripts/Tank.cs create mode 100644 Assets/Mirror/Examples/TanksHybrid/Scripts/Tank.cs.meta create mode 100644 Assets/Mirror/Examples/_Common/Textures/Wall01.jpg create mode 100644 Assets/Mirror/Examples/_Common/Textures/Wall01_n.jpg create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs.meta create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs.meta create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs.meta create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs.meta create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs.meta create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs.meta create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Images/discord-brands-solid-64px.png create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Images/discord-brands-solid-64px.png.meta create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Images/discord-brands-solid.svg create mode 100644 Assets/Mirror/Hosting/Edgegap/Editor/Images/discord-brands-solid.svg.meta diff --git a/.github/workflows/RunUnityTests.yml b/.github/workflows/RunUnityTests.yml index 4bc3151cfc..2a2f2e2073 100644 --- a/.github/workflows/RunUnityTests.yml +++ b/.github/workflows/RunUnityTests.yml @@ -14,9 +14,9 @@ jobs: # - 2019.4.40f1 - 2020.3.48f1 - 2021.3.45f1 - - 2022.3.55f1 + - 2022.3.60f1 - 2023.2.20f1 - - 6000.0.32f1 + - 6000.0.43f1 steps: - name: Checkout repository diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs index 1d363b823b..26aff41902 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs @@ -22,15 +22,10 @@ public static void AddDefineSymbols() HashSet defines = new HashSet(currentDefines.Split(';')) { "MIRROR", - "MIRROR_81_OR_NEWER", - "MIRROR_82_OR_NEWER", - "MIRROR_83_OR_NEWER", - "MIRROR_84_OR_NEWER", - "MIRROR_85_OR_NEWER", - "MIRROR_86_OR_NEWER", "MIRROR_89_OR_NEWER", "MIRROR_90_OR_NEWER", - "MIRROR_93_OR_NEWER" + "MIRROR_93_OR_NEWER", + "MIRROR_96_OR_NEWER" }; // only touch PlayerSettings if we actually modified it, diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs index a34bbe0947..92d2bf7982 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs @@ -36,6 +36,7 @@ void Reset() } #endif +#if !UNITY_SERVER void OnGUI() { if (NetworkManager.singleton == null) @@ -126,6 +127,7 @@ void StopButtons() GUILayout.EndArea(); } +#endif void Connect(ServerResponse info) { diff --git a/Assets/Mirror/Components/GUIConsole.cs b/Assets/Mirror/Components/GUIConsole.cs index 7055fe1ba4..18e400511c 100644 --- a/Assets/Mirror/Components/GUIConsole.cs +++ b/Assets/Mirror/Components/GUIConsole.cs @@ -105,6 +105,7 @@ void Update() visible = !visible; } +#if !UNITY_SERVER void OnGUI() { if (!visible) return; @@ -129,5 +130,6 @@ void OnGUI() GUILayout.EndArea(); } +#endif } } diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs index 654996191b..e4f56cadc5 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs @@ -75,9 +75,8 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet= lastRebuildTime + rebuildInterval) diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs index 28e5ebabb3..c688dfdc5e 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs @@ -35,10 +35,10 @@ public override void OnSpawned(NetworkIdentity identity) [ServerCallback] public override void OnDestroyed(NetworkIdentity identity) { - // Don't RebuildSceneObservers here - that will happen in Update. + // Don't RebuildSceneObservers here - that will happen in LateUpdate. // Multiple objects could be destroyed in same frame and we don't - // want to rebuild for each one...let Update do it once. - // We must add the current scene to dirtyScenes for Update to rebuild it. + // want to rebuild for each one...let LateUpdate do it once. + // We must add the current scene to dirtyScenes for LateUpdate to rebuild it. if (lastObjectScene.TryGetValue(identity, out Scene currentScene)) { lastObjectScene.Remove(identity); @@ -47,9 +47,8 @@ public override void OnDestroyed(NetworkIdentity identity) } } - // internal so we can update from tests [ServerCallback] - internal void Update() + void LateUpdate() { // for each spawned: // if scene changed: diff --git a/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs index 2ecfa99464..cdd5f16cd0 100644 --- a/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs @@ -64,10 +64,10 @@ public override void OnDestroyed(NetworkIdentity identity) { CustomRanges.Remove(identity); - // Don't RebuildSceneObservers here - that will happen in Update. + // Don't RebuildSceneObservers here - that will happen in LateUpdate. // Multiple objects could be destroyed in same frame and we don't - // want to rebuild for each one...let Update do it once. - // We must add the current scene to dirtyScenes for Update to rebuild it. + // want to rebuild for each one...let LateUpdate do it once. + // We must add the current scene to dirtyScenes for LateUpdate to rebuild it. if (lastObjectScene.TryGetValue(identity, out Scene currentScene)) { lastObjectScene.Remove(identity); @@ -76,9 +76,8 @@ public override void OnDestroyed(NetworkIdentity identity) } } - // internal so we can update from tests [ServerCallback] - internal void Update() + void LateUpdate() { // for each spawned: // if scene changed: @@ -101,7 +100,7 @@ internal void Update() lastRebuildTime = NetworkTime.localTime; } - // no scehe change, so we're done here + // no scene change, so we're done here continue; } diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs new file mode 100644 index 0000000000..29bd6ce617 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs @@ -0,0 +1,170 @@ +using UnityEngine; + +namespace Mirror +{ + internal class HexGrid2D + { + // Radius of each hexagonal cell (half the width) + internal float cellRadius; + + // Offset applied to align the grid with the world origin + Vector2 originOffset; + + // Precomputed constants for hexagon math to improve performance + readonly float sqrt3Div3; // sqrt(3) / 3, used in coordinate conversions + readonly float oneDiv3; // 1 / 3, used in coordinate conversions + readonly float twoDiv3; // 2 / 3, used in coordinate conversions + readonly float sqrt3; // sqrt(3), used in world coordinate calculations + readonly float sqrt3Div2; // sqrt(3) / 2, used in world coordinate calculations + + internal HexGrid2D(ushort visRange) + { + // Set cell radius as half the visibility range + cellRadius = visRange / 2f; + + // Offset to center the grid at world origin (2D XZ plane) + originOffset = Vector2.zero; + + // Precompute mathematical constants for efficiency + sqrt3Div3 = Mathf.Sqrt(3) / 3f; + oneDiv3 = 1f / 3f; + twoDiv3 = 2f / 3f; + sqrt3 = Mathf.Sqrt(3); + sqrt3Div2 = Mathf.Sqrt(3) / 2f; + } + + // Precomputed array of neighbor offsets as Cell2D structs (center + 6 neighbors) + static readonly Cell2D[] neighborCellsBase = new Cell2D[] + { + new Cell2D(0, 0), // Center + new Cell2D(1, -1), // Top-right + new Cell2D(1, 0), // Right + new Cell2D(0, 1), // Bottom-right + new Cell2D(-1, 1), // Bottom-left + new Cell2D(-1, 0), // Left + new Cell2D(0, -1) // Top-left + }; + + // Converts a grid cell (q, r) to a world position (x, z) + internal Vector2 CellToWorld(Cell2D cell) + { + // Calculate X and Z using hexagonal coordinate formulas + float x = cellRadius * (sqrt3 * cell.q + sqrt3Div2 * cell.r); + float z = cellRadius * (1.5f * cell.r); + + // Subtract the origin offset to align with world space and return the position + return new Vector2(x, z) - originOffset; + } + + // Converts a world position (x, z) to a grid cell (q, r) + internal Cell2D WorldToCell(Vector2 position) + { + // Apply the origin offset to adjust the position before conversion + position += originOffset; + + // Convert world X, Z to axial q, r coordinates using inverse hexagonal formulas + float q = (sqrt3Div3 * position.x - oneDiv3 * position.y) / cellRadius; + float r = (twoDiv3 * position.y) / cellRadius; + + // Round to the nearest valid cell and return + return RoundToCell(q, r); + } + + // Rounds floating-point axial coordinates (q, r) to the nearest integer cell coordinates + Cell2D RoundToCell(float q, float r) + { + // Calculate the third hexagonal coordinate (s) for consistency + float s = -q - r; + int qInt = Mathf.RoundToInt(q); // Round q to nearest integer + int rInt = Mathf.RoundToInt(r); // Round r to nearest integer + int sInt = Mathf.RoundToInt(s); // Round s to nearest integer + + // Calculate differences to determine which coordinate needs adjustment + float qDiff = Mathf.Abs(q - qInt); + float rDiff = Mathf.Abs(r - rInt); + float sDiff = Mathf.Abs(s - sInt); + + // Adjust q or r based on which has the largest rounding error (ensures q + r + s = 0) + if (qDiff > rDiff && qDiff > sDiff) + qInt = -rInt - sInt; // Adjust q if it has the largest error + else if (rDiff > sDiff) + rInt = -qInt - sInt; // Adjust r if it has the largest error + + return new Cell2D(qInt, rInt); + } + + // Populates the provided array with neighboring cells around a given center cell + internal void GetNeighborCells(Cell2D center, Cell2D[] neighbors) + { + // Ensure the array has the correct size (7: center + 6 neighbors) + if (neighbors.Length != 7) + throw new System.ArgumentException("Neighbor array must have exactly 7 elements"); + + // Populate the array by adjusting precomputed offsets with the center cell's coordinates + for (int i = 0; i < neighborCellsBase.Length; i++) + { + neighbors[i] = new Cell2D( + center.q + neighborCellsBase[i].q, + center.r + neighborCellsBase[i].r + ); + } + } + +#if UNITY_EDITOR + // Draws a 2D hexagonal gizmo in the Unity Editor for visualization + internal void DrawHexGizmo(Vector3 center, float radius, HexSpatialHash2DInterestManagement.CheckMethod checkMethod) + { + // Hexagon has 6 sides + const int segments = 6; + + // Array to store the 6 corner points in 3D + Vector3[] corners = new Vector3[segments]; + + // Calculate the corner positions based on the plane (XZ or XY) + for (int i = 0; i < segments; i++) + { + // Angle for each corner, offset by 90 degrees + float angle = 2 * Mathf.PI / segments * i + Mathf.PI / 2; + + if (checkMethod == HexSpatialHash2DInterestManagement.CheckMethod.XZ_FOR_3D) + { + // XZ plane: flat hexagon, Y=0 + corners[i] = center + new Vector3(radius * Mathf.Cos(angle), 0, radius * Mathf.Sin(angle)); + } + else // XY_FOR_2D + { + // XY plane: vertical hexagon, Z=0 + corners[i] = center + new Vector3(radius * Mathf.Cos(angle), radius * Mathf.Sin(angle), 0); + } + } + + // Draw each side of the hexagon + for (int i = 0; i < segments; i++) + { + Vector3 cornerA = corners[i]; + Vector3 cornerB = corners[(i + 1) % segments]; + Gizmos.DrawLine(cornerA, cornerB); + } + } +#endif + } + + // Struct representing a single cell in the 2D hexagonal grid + internal struct Cell2D + { + internal readonly int q; // Axial q coordinate (horizontal axis) + internal readonly int r; // Axial r coordinate (diagonal axis) + + internal Cell2D(int q, int r) + { + this.q = q; + this.r = r; + } + + public override bool Equals(object obj) => + obj is Cell2D other && q == other.q && r == other.r; + + // Generate a unique hash code for the cell + public override int GetHashCode() => (q << 16) ^ r; + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta new file mode 100644 index 0000000000..0f19a983a3 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9b8dc0273250624c91b6681065741ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs new file mode 100644 index 0000000000..b7a0fd3624 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs @@ -0,0 +1,243 @@ +using UnityEngine; + +namespace Mirror +{ + internal class HexGrid3D + { + // Radius of each hexagonal cell (half the width) + internal float cellRadius; + + // Height of each cell along the Y-axis + internal float cellHeight; + + // Offset applied to align the grid with the world origin + Vector3 originOffset; + + // Precomputed constants for hexagon math to improve performance + readonly float sqrt3Div3; // sqrt(3) / 3, used in coordinate conversions + readonly float oneDiv3; // 1 / 3, used in coordinate conversions + readonly float twoDiv3; // 2 / 3, used in coordinate conversions + readonly float sqrt3; // sqrt(3), used in world coordinate calculations + readonly float sqrt3Div2; // sqrt(3) / 2, used in world coordinate calculations + + internal HexGrid3D(ushort visRange, ushort height) + { + // Set cell radius as half the visibility range + cellRadius = visRange / 2f; + + // Cell3D height is absolute...don't double it + cellHeight = height; + + // Offset to center the grid at world origin + // Cell3D height must be divided by 2 for vertical centering + originOffset = new Vector3(0, -cellHeight / 2, 0); + + // Precompute mathematical constants for efficiency + sqrt3Div3 = Mathf.Sqrt(3) / 3f; + oneDiv3 = 1f / 3f; + twoDiv3 = 2f / 3f; + sqrt3 = Mathf.Sqrt(3); + sqrt3Div2 = Mathf.Sqrt(3) / 2f; + } + + // Precomputed array of neighbor offsets as Cell3D structs (center + 6 per layer x 3 layers) + static readonly Cell3D[] neighborCellsBase = new Cell3D[] + { + // Center + new Cell3D(0, 0, 0), + // Upper layer (1) and its 6 neighbors + new Cell3D(0, 0, 1), + new Cell3D(1, -1, 1), new Cell3D(1, 0, 1), new Cell3D(0, 1, 1), + new Cell3D(-1, 1, 1), new Cell3D(-1, 0, 1), new Cell3D(0, -1, 1), + // Same layer (0) - 6 neighbors + new Cell3D(1, -1, 0), new Cell3D(1, 0, 0), new Cell3D(0, 1, 0), + new Cell3D(-1, 1, 0), new Cell3D(-1, 0, 0), new Cell3D(0, -1, 0), + // Lower layer (-1) and its 6 neighbors + new Cell3D(0, 0, -1), + new Cell3D(1, -1, -1), new Cell3D(1, 0, -1), new Cell3D(0, 1, -1), + new Cell3D(-1, 1, -1), new Cell3D(-1, 0, -1), new Cell3D(0, -1, -1) + }; + + // Converts a grid cell (q, r, layer) to a world position (x, y, z) + internal Vector3 CellToWorld(Cell3D cell) + { + // Calculate X and Z using hexagonal coordinate formulas + float x = cellRadius * (sqrt3 * cell.q + sqrt3Div2 * cell.r); + float z = cellRadius * (1.5f * cell.r); + + // Calculate Y based on layer and cell height + float y = cell.layer * cellHeight + cellHeight / 2; + + // Subtract the origin offset to align with world space and return the position + return new Vector3(x, y, z) - originOffset; + } + + // Converts a world position (x, y, z) to a grid cell (q, r, layer) + internal Cell3D WorldToCell(Vector3 position) + { + // Apply the origin offset to adjust the position before conversion + position += originOffset; + + // Calculate the vertical layer based on Y position + int layer = Mathf.FloorToInt(position.y / cellHeight); + + // Convert world X, Z to axial q, r coordinates using inverse hexagonal formulas + float q = (sqrt3Div3 * position.x - oneDiv3 * position.z) / cellRadius; + float r = (twoDiv3 * position.z) / cellRadius; + + // Round to the nearest valid cell and return + return RoundToCell(q, r, layer); + } + + // Rounds floating-point axial coordinates (q, r) to the nearest integer cell coordinates + Cell3D RoundToCell(float q, float r, int layer) + { + // Calculate the third hexagonal coordinate (s) for consistency + float s = -q - r; + int qInt = Mathf.RoundToInt(q); // Round q to nearest integer + int rInt = Mathf.RoundToInt(r); // Round r to nearest integer + int sInt = Mathf.RoundToInt(s); // Round s to nearest integer + + // Calculate differences to determine which coordinate needs adjustment + float qDiff = Mathf.Abs(q - qInt); + float rDiff = Mathf.Abs(r - rInt); + float sDiff = Mathf.Abs(s - sInt); + + // Adjust q or r based on which has the largest rounding error (ensures q + r + s = 0) + if (qDiff > rDiff && qDiff > sDiff) + qInt = -rInt - sInt; // Adjust q if it has the largest error + else if (rDiff > sDiff) + rInt = -qInt - sInt; // Adjust r if it has the largest error + + return new Cell3D(qInt, rInt, layer); + } + + // Populates the provided array with neighboring cells around a given center cell + internal void GetNeighborCells(Cell3D center, Cell3D[] neighbors) + { + // Ensure the array has the correct size + if (neighbors.Length != 21) + throw new System.ArgumentException("Neighbor array must have exactly 21 elements"); + + // Populate the array by adjusting precomputed offsets with the center cell's coordinates + for (int i = 0; i < neighborCellsBase.Length; i++) + { + neighbors[i] = new Cell3D( + center.q + neighborCellsBase[i].q, + center.r + neighborCellsBase[i].r, + center.layer + neighborCellsBase[i].layer + ); + } + } + +#if UNITY_EDITOR + + // Draws a hexagonal gizmo in the Unity Editor for visualization + internal void DrawHexGizmo(Vector3 center, float radius, float height, int relativeLayer) + { + // Hexagon has 6 sides + const int segments = 6; + + // Array to store the 6 corner points + Vector3[] corners = new Vector3[segments]; + + // Calculate the corner positions of the hexagon in the XZ plane + for (int i = 0; i < segments; i++) + { + // Angle for each corner, offset by 90 degrees + float angle = 2 * Mathf.PI / segments * i + Mathf.PI / 2; + + // Calculate the corner position based on the angle and radius + corners[i] = center + new Vector3(radius * Mathf.Cos(angle), 0, radius * Mathf.Sin(angle)); + } + + // Set gizmo color based on the relative layer for easy identification + Color gizmoColor; + switch (relativeLayer) + { + case 1: + gizmoColor = Color.green; // Upper layer (positive Y) + break; + case 0: + gizmoColor = Color.cyan; // Same layer as the reference point + break; + case -1: + gizmoColor = Color.yellow; // Lower layer (negative Y) + break; + default: + gizmoColor = Color.red; // Fallback for unexpected layers + break; + } + + // Store the current Gizmos color to restore later + Color previousColor = Gizmos.color; + + // Apply the chosen color + Gizmos.color = gizmoColor; + + // Draw each side of the hexagon as a 3D quad (wall) + for (int i = 0; i < segments; i++) + { + // Current corner + Vector3 cornerA = corners[i]; + + // Next corner (wraps around at 6) + Vector3 cornerB = corners[(i + 1) % segments]; + + // Calculate top and bottom corners to form a vertical quad + Vector3 cornerATop = cornerA + Vector3.up * (height / 2); + Vector3 cornerBTop = cornerB + Vector3.up * (height / 2); + Vector3 cornerABottom = cornerA - Vector3.up * (height / 2); + Vector3 cornerBBottom = cornerB - Vector3.up * (height / 2); + + // Draw the four lines of the quad to visualize the wall + Gizmos.DrawLine(cornerATop, cornerBTop); + Gizmos.DrawLine(cornerBTop, cornerBBottom); + Gizmos.DrawLine(cornerBBottom, cornerABottom); + Gizmos.DrawLine(cornerABottom, cornerATop); + } + + // Restore the original Gizmos color + Gizmos.color = previousColor; + } + +#endif + } + + // Custom struct for neighbor offsets (reduced memory usage) + internal struct HexOffset + { + internal int qOffset; // Offset in the q (axial) coordinate + internal int rOffset; // Offset in the r (axial) coordinate + + internal HexOffset(int q, int r) + { + qOffset = q; + rOffset = r; + } + } + + // Struct representing a single cell in the 3D hexagonal grid + internal struct Cell3D + { + internal readonly int q; // Axial q coordinate (horizontal axis) + internal readonly int r; // Axial r coordinate (diagonal axis) + internal readonly int layer; // Vertical layer index (Y-axis stacking) + + internal Cell3D(int q, int r, int layer) + { + this.q = q; + this.r = r; + this.layer = layer; + } + + public override bool Equals(object obj) => + obj is Cell3D other + && q == other.q + && r == other.r + && layer == other.layer; + + // Generate a unique hash code for the cell + public override int GetHashCode() => (q << 16) ^ (r << 8) ^ layer; + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta new file mode 100644 index 0000000000..d8d2674a39 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c4fe05752c9a85458b8e44611fe832b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs new file mode 100644 index 0000000000..a394ce0f8c --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs @@ -0,0 +1,345 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Hex Spatial Hash (2D)")] + public class HexSpatialHash2DInterestManagement : InterestManagement + { + [Range(1, 60), Tooltip("Time interval in seconds between observer rebuilds")] + public byte rebuildInterval = 1; + + [Range(1, 60), Tooltip("Time interval in seconds between static object rebuilds")] + public byte staticRebuildInterval = 10; + + [Range(10, 5000), Tooltip("Radius of super hex.\nSet to 10% larger than camera far clip plane.")] + public ushort visRange = 1100; + + [Range(1, 100), Tooltip("Distance an object must move for updating cell positions")] + public ushort minMoveDistance = 1; + + [Tooltip("Spatial Hashing supports XZ for 3D games or XY for 2D games.")] + public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D; + + double lastRebuildTime; + + // Counter for batching static object updates + byte rebuildCounter = 0; + + HexGrid2D grid; + + // Sparse array mapping cell indices to sets of NetworkIdentities + readonly List> cells = new List>(); + + // Tracks the last known cell position and world position of each NetworkIdentity + readonly Dictionary lastIdentityPositions = new Dictionary(); + + // Tracks the last known cell position and world position of each player's connection (observer) + readonly Dictionary lastConnectionPositions = new Dictionary(); + + // Pre-allocated array for storing neighbor cells (center + 6 neighbors) + readonly Cell2D[] neighborCells = new Cell2D[7]; + + // Maps each connection to the set of NetworkIdentities it can observe, precomputed for rebuilds + readonly Dictionary> connectionObservers = new Dictionary>(); + + // Reusable list for safe iteration over NetworkIdentities, avoiding ToList() allocations + readonly List identityKeys = new List(); + + // Pool of reusable HashSet instances to reduce allocations + readonly Stack> cellPool = new Stack>(); + + // Set of static NetworkIdentities that don't move, updated less frequently + readonly HashSet staticObjects = new HashSet(); + + // Scene bounds: ±9 km (18 km total) in each dimension + const int MAX_Q = 19; // Covers -9 to 9 (~18 km) + const int MAX_R = 23; // Covers -11 to 11 (~18 km) + const ushort MAX_AREA = 9000; // Maximum area in meters + + public enum CheckMethod + { + XZ_FOR_3D, + XY_FOR_2D + } + + void Awake() + { + grid = new HexGrid2D(visRange); + // Initialize cells list with null entries up to max size (±9 km bounds) + int maxSize = MAX_Q * MAX_R; + for (int i = 0; i < maxSize; i++) + cells.Add(null); + } + + // Project 3D world position to 2D grid position based on checkMethod + Vector2 ProjectToGrid(Vector3 position) => + checkMethod == CheckMethod.XZ_FOR_3D + ? new Vector2(position.x, position.z) + : new Vector2(position.x, position.y); + + void LateUpdate() + { + if (NetworkTime.time - lastRebuildTime >= rebuildInterval) + { + // Update positions of all active connections (players) in the network + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + if (conn?.identity != null) // Ensure connection and its identity exist + { + Vector2 position = ProjectToGrid(conn.identity.transform.position); + // Only update if the position has changed significantly + if (!lastConnectionPositions.TryGetValue(conn, out (Cell2D cell, Vector2 worldPos) last) || + Vector2.Distance(position, last.worldPos) >= minMoveDistance) + { + Cell2D cell = grid.WorldToCell(position); // Convert world position to grid cell + lastConnectionPositions[conn] = (cell, position); // Store the player's cell and position + } + } + + // Populate the reusable list with current keys for safe iteration + identityKeys.Clear(); + identityKeys.AddRange(lastIdentityPositions.Keys); + + // Update dynamic objects every rebuild, static objects every staticRebuildInterval + bool updateStatic = rebuildCounter >= staticRebuildInterval; + foreach (NetworkIdentity identity in identityKeys) + if (updateStatic || !staticObjects.Contains(identity)) + UpdateIdentityPosition(identity); // Refresh cell position for dynamic or scheduled static objects + + if (updateStatic) + rebuildCounter = 0; // Reset the counter after updating static objects + else + rebuildCounter++; + + // Precompute observer sets for each connection before rebuilding + connectionObservers.Clear(); + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + if (conn?.identity == null || !lastConnectionPositions.TryGetValue(conn, out (Cell2D cell, Vector2 worldPos) connPos)) + continue; + + // Get cells visible from the player's position + grid.GetNeighborCells(connPos.cell, neighborCells); + + // Initialize the observer set for this connection + HashSet observers = new HashSet(); + connectionObservers[conn] = observers; + + // Add all identities in visible cells to the observer set + for (int i = 0; i < neighborCells.Length; i++) + { + int index = GetCellIndex(neighborCells[i]); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + foreach (NetworkIdentity identity in cells[index]) + observers.Add(identity); + } + } + } + + // RebuildAll invokes NetworkServer.RebuildObservers on all spawned objects + base.RebuildAll(); + + // Update the last rebuild time + lastRebuildTime = NetworkTime.time; + } + } + + // Called when a new networked object is spawned on the server + public override void OnSpawned(NetworkIdentity identity) + { + // Register the new object's position in the grid system + UpdateIdentityPosition(identity); + + // Check if the object is statically batched (indicating it won't move) + Renderer[] renderers = identity.gameObject.GetComponentsInChildren(); + if (renderers.Any(r => r.isPartOfStaticBatch)) + staticObjects.Add(identity); + } + + // Updates the grid cell position of a NetworkIdentity when it moves or spawns + void UpdateIdentityPosition(NetworkIdentity identity) + { + // Get the current world position of the object + Vector2 position = ProjectToGrid(identity.transform.position); + + // Convert position to grid cell coordinates + Cell2D newCell = grid.WorldToCell(position); + + // Check if the object is within ±9 km bounds + if (Mathf.Abs(position.x) > MAX_AREA || Mathf.Abs(position.y) > MAX_AREA) + return; // Ignore objects outside bounds + + // Check if the object was previously tracked + if (lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) previous)) + { + // Only update if the position has changed significantly or the cell has changed + if (Vector2.Distance(position, previous.worldPos) >= minMoveDistance || !newCell.Equals(previous.cell)) + { + if (!newCell.Equals(previous.cell)) + { + // Object moved to a new cell + // Remove it from the old cell's set and add it to the new cell's set + int oldIndex = GetCellIndex(previous.cell); + if (oldIndex >= 0 && oldIndex < cells.Count && cells[oldIndex] != null) + cells[oldIndex].Remove(identity); + AddToCell(newCell, identity); + } + // Update the stored position and cell + lastIdentityPositions[identity] = (newCell, position); + } + } + else + { + // New object - add it to the grid and track its position + AddToCell(newCell, identity); + lastIdentityPositions[identity] = (newCell, position); + } + } + + // Adds a NetworkIdentity to a specific cell's set of objects + void AddToCell(Cell2D cell, NetworkIdentity identity) + { + int index = GetCellIndex(cell); + if (index < 0 || index >= cells.Count) + return; // Out of bounds, ignore + + // If the cell doesn't exist in the array yet, fetch or create a new set from the pool + if (cells[index] == null) + { + cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet(); + } + cells[index].Add(identity); + } + + // Determines if a new observer can see a given NetworkIdentity + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Check if we have position data for both the object and the observer + if (!lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) identityPos) || + !lastConnectionPositions.TryGetValue(newObserver, out (Cell2D cell, Vector2 worldPos) observerPos)) + return false; // If not, assume no visibility + + // Populate the pre-allocated array with visible cells from the observer's position + grid.GetNeighborCells(observerPos.cell, neighborCells); + + // Check if the object's cell is among the visible ones + for (int i = 0; i < neighborCells.Length; i++) + if (neighborCells[i].Equals(identityPos.cell)) + return true; + + return false; + } + + // Rebuilds the set of observers for a specific NetworkIdentity + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // If the object's position isn't tracked, skip rebuilding + if (!lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) identityPos)) + return; + + // Use the precomputed observer sets to determine visibility + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + // Skip if the connection or its identity is null + if (conn?.identity == null) + continue; + + // Check if this connection can observe the identity + if (connectionObservers.TryGetValue(conn, out HashSet observers) && observers.Contains(identity)) + newObservers.Add(conn); + } + } + + public override void ResetState() + { + lastRebuildTime = 0; + // Clear and return all cell sets to the pool + for (int i = 0; i < cells.Count; i++) + { + if (cells[i] != null) + { + cells[i].Clear(); + cellPool.Push(cells[i]); + cells[i] = null; + } + } + lastIdentityPositions.Clear(); + lastConnectionPositions.Clear(); + connectionObservers.Clear(); + identityKeys.Clear(); + staticObjects.Clear(); + rebuildCounter = 0; + } + + public override void OnDestroyed(NetworkIdentity identity) + { + // If the object was tracked, remove it from its cell and position records + if (lastIdentityPositions.TryGetValue(identity, out (Cell2D cell, Vector2 worldPos) pos)) + { + int index = GetCellIndex(pos.cell); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + cells[index].Remove(identity); // Remove from the cell's set + // If the cell's set is now empty, return it to the pool + if (cells[index].Count == 0) + { + cellPool.Push(cells[index]); + cells[index] = null; + } + } + lastIdentityPositions.Remove(identity); // Remove from position tracking + staticObjects.Remove(identity); // Ensure it's removed from static set if present + } + } + + // Computes a unique index for a cell in the sparse array, supporting ±9 km bounds + int GetCellIndex(Cell2D cell) + { + int qOffset = cell.q + MAX_Q / 2; // Shift -9 to 9 -> 0 to 18 + int rOffset = cell.r + MAX_R / 2; // Shift -11 to 11 -> 0 to 22 + return qOffset + rOffset * MAX_Q; + } + +#if UNITY_EDITOR + // Draws debug gizmos in the Unity Editor to visualize the 2D grid + void OnDrawGizmos() + { + // Initialize the grid if it hasn’t been created yet (e.g., before Awake) + if (grid == null) + grid = new HexGrid2D(visRange); + + // Only draw if there’s a local player to base the visualization on + if (NetworkClient.localPlayer != null) + { + Vector3 playerPosition = NetworkClient.localPlayer.transform.position; + + // Convert to grid cell using the full Vector3 for proper plane projection + Vector2 projectedPos = ProjectToGrid(playerPosition); + Cell2D playerCell = grid.WorldToCell(projectedPos); + + // Get all visible cells around the player into the pre-allocated array + grid.GetNeighborCells(playerCell, neighborCells); + + // Set gizmo color for visibility + Gizmos.color = Color.cyan; + + // Draw each visible cell as a 2D hexagon, oriented based on checkMethod + for (int i = 0; i < neighborCells.Length; i++) + { + // Convert cell to world coordinates (2D) + Vector2 worldPos2D = grid.CellToWorld(neighborCells[i]); + + // Convert to 3D position based on checkMethod + Vector3 worldPos = checkMethod == CheckMethod.XZ_FOR_3D + ? new Vector3(worldPos2D.x, 0, worldPos2D.y) // XZ plane, flat + : new Vector3(worldPos2D.x, worldPos2D.y, 0); // XY plane, vertical + + grid.DrawHexGizmo(worldPos, grid.cellRadius, checkMethod); + } + } + } +#endif + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta new file mode 100644 index 0000000000..9a3dc1ac65 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b8b055f11f85ff428da471a0e625dd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs new file mode 100644 index 0000000000..64415da74c --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs @@ -0,0 +1,336 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Hex Spatial Hash (3D)")] + public class HexSpatialHash3DInterestManagement : InterestManagement + { + [Range(1, 60), Tooltip("Time interval in seconds between observer rebuilds")] + public byte rebuildInterval = 1; + + [Range(1, 60), Tooltip("Time interval in seconds between static object rebuilds")] + public byte staticRebuildInterval = 10; + + [Range(10, 5000), Tooltip("Radius of super hex.\nSet to 10% larger than camera far clip plane.")] + public ushort visRange = 1100; + + [Range(10, 5000), Tooltip("Cell3D height effects all 3 layers")] + public ushort cellHeight = 500; + + [Range(1, 100), Tooltip("Distance an object must move for updating cell positions")] + public ushort minMoveDistance = 1; + + double lastRebuildTime; + + // Counter for batching static object updates + byte rebuildCounter = 0; + + HexGrid3D grid; + + // Sparse array mapping cell indices to sets of NetworkIdentities + readonly List> cells = new List>(); + + // Tracks the last known cell position and world position of each NetworkIdentity for efficient updates + readonly Dictionary lastIdentityPositions = new Dictionary(); + + // Tracks the last known cell position and world position of each player's connection (observer) + readonly Dictionary lastConnectionPositions = new Dictionary(); + + // Pre-allocated array for storing neighbor cells (center + 6 neighbors per layer x 3 layers) + readonly Cell3D[] neighborCells = new Cell3D[21]; + + // Maps each connection to the set of NetworkIdentities it can observe, precomputed for rebuilds + readonly Dictionary> connectionObservers = new Dictionary>(); + + // Reusable list for safe iteration over NetworkIdentities, avoiding ToList() allocations + readonly List identityKeys = new List(); + + // Pool of reusable HashSet instances to reduce allocations + readonly Stack> cellPool = new Stack>(); + + // Set of static NetworkIdentities that don't move, updated less frequently + readonly HashSet staticObjects = new HashSet(); + + // Scene bounds: ±9 km (18 km total) in each dimension + const int MAX_Q = 19; // Covers -9 to 9 (~18 km) + const int MAX_R = 23; // Covers -11 to 11 (~18 km) + const int LAYER_OFFSET = 18; // Offset for -18 to 17 layers + const int MAX_LAYERS = 36; // Total layers for ±9 km (18 km) + const ushort MAX_AREA = 9000; // Maximum area in meters + + void Awake() + { + grid = new HexGrid3D(visRange, cellHeight); + // Initialize cells list with null entries up to max size (±9 km bounds) + int maxSize = MAX_Q * MAX_R * MAX_LAYERS; + for (int i = 0; i < maxSize; i++) + cells.Add(null); + } + + void LateUpdate() + { + if (NetworkTime.time - lastRebuildTime >= rebuildInterval) + { + // Update positions of all active connections (players) in the network + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + if (conn?.identity != null) // Ensure connection and its identity exist + { + Vector3 position = conn.identity.transform.position; + // Only update if the position has changed significantly + if (!lastConnectionPositions.TryGetValue(conn, out (Cell3D cell, Vector3 worldPos) last) || + Vector3.Distance(position, last.worldPos) >= minMoveDistance) + { + Cell3D cell = grid.WorldToCell(position); // Convert world position to grid cell + lastConnectionPositions[conn] = (cell, position); // Store the player's cell and position + } + } + + // Populate the reusable list with current keys for safe iteration + identityKeys.Clear(); + identityKeys.AddRange(lastIdentityPositions.Keys); + + // Update dynamic objects every rebuild, static objects every staticRebuildInterval + bool updateStatic = rebuildCounter >= staticRebuildInterval; + foreach (NetworkIdentity identity in identityKeys) + if (updateStatic || !staticObjects.Contains(identity)) + UpdateIdentityPosition(identity); // Refresh cell position for dynamic or scheduled static objects + + if (updateStatic) + rebuildCounter = 0; // Reset the counter after updating static objects + else + rebuildCounter++; + + // Precompute observer sets for each connection before rebuilding + connectionObservers.Clear(); + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + if (conn?.identity == null || !lastConnectionPositions.TryGetValue(conn, out (Cell3D cell, Vector3 worldPos) connPos)) + continue; + + // Get cells visible from the player's position + grid.GetNeighborCells(connPos.cell, neighborCells); + + // Initialize the observer set for this connection + HashSet observers = new HashSet(); + connectionObservers[conn] = observers; + + // Add all identities in visible cells to the observer set + for (int i = 0; i < neighborCells.Length; i++) + { + int index = GetCellIndex(neighborCells[i]); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + foreach (NetworkIdentity identity in cells[index]) + observers.Add(identity); + } + } + } + + // RebuildAll invokes NetworkServer.RebuildObservers on all spawned objects + base.RebuildAll(); + + // Update the last rebuild time + lastRebuildTime = NetworkTime.time; + } + } + + // Called when a new networked object is spawned on the server + public override void OnSpawned(NetworkIdentity identity) + { + // Register the new object's position in the grid system + UpdateIdentityPosition(identity); + + // Check if the object is statically batched (indicating it won't move) + Renderer[] renderers = identity.gameObject.GetComponentsInChildren(); + if (renderers.Any(r => r.isPartOfStaticBatch)) + staticObjects.Add(identity); + } + + // Updates the grid cell position of a NetworkIdentity when it moves or spawns + void UpdateIdentityPosition(NetworkIdentity identity) + { + // Get the current world position of the object + Vector3 position = identity.transform.position; + + // Convert player position to grid cell coordinates + Cell3D newCell = grid.WorldToCell(position); + + // Check if the object is within ±9 km bounds + if (Mathf.Abs(position.x) > MAX_AREA || Mathf.Abs(position.y) > MAX_AREA || Mathf.Abs(position.z) > MAX_AREA) + return; // Ignore objects outside bounds + + // Check if the object was previously tracked + if (lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) previous)) + { + // Only update if the position has changed significantly or the cell has changed + if (Vector3.Distance(position, previous.worldPos) >= minMoveDistance || !newCell.Equals(previous.cell)) + { + if (!newCell.Equals(previous.cell)) + { + // Object moved to a new cell + // Remove it from the old cell's set and add it to the new cell's set + int oldIndex = GetCellIndex(previous.cell); + if (oldIndex >= 0 && oldIndex < cells.Count && cells[oldIndex] != null) + cells[oldIndex].Remove(identity); + AddToCell(newCell, identity); + } + // Update the stored position and cell + lastIdentityPositions[identity] = (newCell, position); + } + } + else + { + // New object - add it to the grid and track its position + AddToCell(newCell, identity); + lastIdentityPositions[identity] = (newCell, position); + } + } + + // Adds a NetworkIdentity to a specific cell's set of objects + void AddToCell(Cell3D cell, NetworkIdentity identity) + { + int index = GetCellIndex(cell); + if (index < 0 || index >= cells.Count) + return; // Out of bounds, ignore + + // If the cell doesn't exist in the array yet, fetch or create a new set from the pool + if (cells[index] == null) + { + cells[index] = cellPool.Count > 0 ? cellPool.Pop() : new HashSet(); + } + cells[index].Add(identity); + } + + // Determines if a new observer can see a given NetworkIdentity + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Check if we have position data for both the object and the observer + if (!lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) identityPos) || + !lastConnectionPositions.TryGetValue(newObserver, out (Cell3D cell, Vector3 worldPos) observerPos)) + return false; // If not, assume no visibility + + // Populate the pre-allocated array with visible cells from the observer's position + grid.GetNeighborCells(observerPos.cell, neighborCells); + + // Check if the object's cell is among the visible ones + for (int i = 0; i < neighborCells.Length; i++) + if (neighborCells[i].Equals(identityPos.cell)) + return true; + + return false; + } + + // Rebuilds the set of observers for a specific NetworkIdentity + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // If the object's position isn't tracked, skip rebuilding + if (!lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) identityPos)) + return; + + // Use the precomputed observer sets to determine visibility + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + { + // Skip if the connection or its identity is null + if (conn?.identity == null) + continue; + + // Check if this connection can observe the identity + if (connectionObservers.TryGetValue(conn, out HashSet observers) && observers.Contains(identity)) + newObservers.Add(conn); + } + } + + public override void ResetState() + { + lastRebuildTime = 0; + // Clear and return all cell sets to the pool + for (int i = 0; i < cells.Count; i++) + { + if (cells[i] != null) + { + cells[i].Clear(); + cellPool.Push(cells[i]); + cells[i] = null; + } + } + lastIdentityPositions.Clear(); + lastConnectionPositions.Clear(); + connectionObservers.Clear(); + identityKeys.Clear(); + staticObjects.Clear(); + rebuildCounter = 0; + } + + public override void OnDestroyed(NetworkIdentity identity) + { + // If the object was tracked, remove it from its cell and position records + if (lastIdentityPositions.TryGetValue(identity, out (Cell3D cell, Vector3 worldPos) pos)) + { + int index = GetCellIndex(pos.cell); + if (index >= 0 && index < cells.Count && cells[index] != null) + { + cells[index].Remove(identity); // Remove from the cell's set + // If the cell's set is now empty, return it to the pool + if (cells[index].Count == 0) + { + cellPool.Push(cells[index]); + cells[index] = null; + } + } + lastIdentityPositions.Remove(identity); // Remove from position tracking + staticObjects.Remove(identity); // Ensure it's removed from static set if present + } + } + + // Computes a unique index for a cell in the sparse array, supporting ±9 km bounds + int GetCellIndex(Cell3D cell) + { + int qOffset = cell.q + MAX_Q / 2; // Shift -9 to 9 -> 0 to 18 + int rOffset = cell.r + MAX_R / 2; // Shift -11 to 11 -> 0 to 22 + int layerOffset = cell.layer + LAYER_OFFSET; // Shift -18 to 17 -> 0 to 35 + return qOffset + rOffset * MAX_Q + layerOffset * MAX_Q * MAX_R; + } + +#if UNITY_EDITOR + + // Draws debug gizmos in the Unity Editor to visualize the grid + void OnDrawGizmos() + { + // Initialize the grid if it hasn't been created yet (e.g., before Awake) + if (grid == null) + grid = new HexGrid3D(visRange, cellHeight); + + // Only draw if there's a local player to base the visualization on + if (NetworkClient.localPlayer != null) + { + Vector3 playerPosition = NetworkClient.localPlayer.transform.position; + + // Convert to grid cell + Cell3D playerCell = grid.WorldToCell(playerPosition); + + // Get all visible cells around the player into the pre-allocated array + grid.GetNeighborCells(playerCell, neighborCells); + + // Set default gizmo color (though overridden per cell) + Gizmos.color = Color.cyan; + + // Draw each visible cell as a hexagonal prism + for (int i = 0; i < neighborCells.Length; i++) + { + // Convert cell to world coordinates + Vector3 worldPos = grid.CellToWorld(neighborCells[i]); + + // Determine the layer relative to the player's cell for color coding + int relativeLayer = neighborCells[i].layer - playerCell.layer; + + // Draw the hexagonal cell with appropriate color based on layer + grid.DrawHexGizmo(worldPos, grid.cellRadius, grid.cellHeight, relativeLayer); + } + } + } + +#endif + } +} diff --git a/Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta similarity index 86% rename from Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta rename to Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta index 63547db418..6194878a33 100644 --- a/Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 657535a722c74173bdaa18a4394ce016 +guid: 58e492e77a2a1a3488412ceed5c2aa2d MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs index 1953de7e2f..49b1e68771 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs @@ -120,8 +120,8 @@ internal void Update() } } -// OnGUI allocates even if it does nothing. avoid in release. -#if UNITY_EDITOR || DEVELOPMENT_BUILD +#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) + // OnGUI allocates even if it does nothing. avoid in release. // slider from dotsnet. it's nice to play around with in the benchmark // demo. void OnGUI() diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta index b6a218b163..806c583d66 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta @@ -1,3 +1,11 @@ fileFormatVersion: 2 guid: 120b4d6121d94e0280cd2ec536b0ea8f -timeCreated: 1713534045 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs index 0cb5e23139..0930112ca3 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -130,8 +130,8 @@ internal void Update() } } -// OnGUI allocates even if it does nothing. avoid in release. -#if UNITY_EDITOR || DEVELOPMENT_BUILD +#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) + // OnGUI allocates even if it does nothing. avoid in release. // slider from dotsnet. it's nice to play around with in the benchmark // demo. void OnGUI() diff --git a/Assets/Mirror/Components/NetworkAnimator.cs b/Assets/Mirror/Components/NetworkAnimator.cs index 657c28fa21..b573ad7b27 100644 --- a/Assets/Mirror/Components/NetworkAnimator.cs +++ b/Assets/Mirror/Components/NetworkAnimator.cs @@ -642,13 +642,19 @@ void RpcOnAnimationParametersClientMessage(byte[] parameters) [ClientRpc(includeOwner = false)] void RpcOnAnimationTriggerClientMessage(int hash) { - HandleAnimTriggerMsg(hash); + // already handled on server in SetTrigger + // or CmdOnAnimationTriggerServerMessage + if (!isServer) + HandleAnimTriggerMsg(hash); } [ClientRpc(includeOwner = false)] void RpcOnAnimationResetTriggerClientMessage(int hash) { - HandleAnimResetTriggerMsg(hash); + // already handled on server in ResetTrigger + // or CmdOnAnimationResetTriggerServerMessage + if (!isServer) + HandleAnimResetTriggerMsg(hash); } #endregion diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs b/Assets/Mirror/Components/NetworkPingDisplay.cs index 255e62dcce..7fa18c003e 100644 --- a/Assets/Mirror/Components/NetworkPingDisplay.cs +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs @@ -16,6 +16,7 @@ public class NetworkPingDisplay : MonoBehaviour public int width = 150; public int height = 25; +#if !UNITY_SERVER void OnGUI() { // only while client is active @@ -35,5 +36,6 @@ void OnGUI() GUILayout.EndArea(); GUI.color = Color.white; } +#endif } } diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs new file mode 100644 index 0000000000..d8a6095060 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody Hybrid (CORE)")] + public class NetworkRigidbodyHybridCORE : NetworkTransformHybrid + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody rb; + bool wasKinematic; + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + + // we can't overwrite .target to be a Rigidbody. + // but we can ensure that .target has a Rigidbody, and use it. + if (target.GetComponent() == null) + { + Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + } + } + + // cach Rigidbody and original isKinematic setting + protected override void Awake() + { + // we can't overwrite .target to be a Rigidbody. + // but we can use its Rigidbody component. + rb = target.GetComponent(); + if (rb == null) + { + Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this); + return; + } + wasKinematic = rb.isKinematic; + base.Awake(); + } + + // reset forced isKinematic flag to original. + // otherwise the overwritten value would remain between sessions forever. + // for example, a game may run as client, set rigidbody.iskinematic=true, + // then run as server, where .iskinematic isn't touched and remains at + // the overwritten=true, even though the user set it to false originally. + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; + + // overwriting Construct() and Apply() to set Rigidbody.MovePosition + // would give more jittery movement. + + // FixedUpdate for physics + void FixedUpdate() + { + // who ever has authority moves the Rigidbody with physics. + // everyone else simply sets it to kinematic. + // so that only the Transform component is synced. + + // host mode + if (isServer && isClient) + { + // in host mode, we own it it if: + // clientAuthority is disabled (hence server / we own it) + // clientAuthority is enabled and we have authority over this object. + bool owned = !clientAuthority || IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // client only + else if (isClient) + { + // on the client, we own it only if clientAuthority is enabled, + // and we have authority over this object. + bool owned = IsClientWithAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + // server only + else if (isServer) + { + // on the server, we always own it if clientAuthority is disabled. + bool owned = !clientAuthority; + + // only set to kinematic if we don't own it + // otherwise don't touch isKinematic. + // the authority owner might use it either way. + if (!owned) rb.isKinematic = true; + } + } + + protected override void OnTeleport(Vector3 destination) + { + base.OnTeleport(destination); + + rb.position = transform.position; + } + + protected override void OnTeleport(Vector3 destination, Quaternion rotation) + { + base.OnTeleport(destination, rotation); + + rb.position = transform.position; + rb.rotation = transform.rotation; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta new file mode 100644 index 0000000000..c1fb167c3b --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliableCompressed.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f830b261ed7644a4b1cc262cf36fc96 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs b/Assets/Mirror/Components/NetworkRoomPlayer.cs index 401118b652..109e4831d4 100644 --- a/Assets/Mirror/Components/NetworkRoomPlayer.cs +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs @@ -123,6 +123,7 @@ public virtual void OnClientExitRoom() {} #endregion +#if !UNITY_SERVER #region Optional UI /// @@ -192,5 +193,6 @@ void DrawPlayerReadyButton() } #endregion +#endif } } diff --git a/Assets/Mirror/Components/NetworkStatistics.cs b/Assets/Mirror/Components/NetworkStatistics.cs index c00bb19381..bbc275085a 100644 --- a/Assets/Mirror/Components/NetworkStatistics.cs +++ b/Assets/Mirror/Components/NetworkStatistics.cs @@ -141,6 +141,7 @@ void UpdateServer() serverIntervalSentBytes = 0; } +#if !UNITY_SERVER void OnGUI() { // only show if either server or client active @@ -190,5 +191,6 @@ void OnServerGUI() // end background GUILayout.EndVertical(); } +#endif } } diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs index 035c25f874..65da8b7a3a 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs @@ -119,7 +119,7 @@ public uint sendIntervalMultiplier protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1); protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0; - // velocity for covenience (animators etc.) + // velocity for convenience (animators etc.) // this isn't technically NetworkTransforms job, but it's needed by so many projects that we just provide it anyway. public Vector3 velocity { get; private set; } public Vector3 angularVelocity { get; private set; } @@ -272,8 +272,8 @@ protected virtual void Apply(TransformSnapshot interpolated, TransformSnapshot e // these can be used to drive animations or other behaviours if (!isOwned && Time.deltaTime > 0) { - velocity = (transform.position - interpolated.position) / Time.deltaTime; - angularVelocity = (transform.rotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime; + velocity = (transform.localPosition - interpolated.position) / Time.deltaTime; + angularVelocity = (transform.localRotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime; } // interpolate parts @@ -427,6 +427,10 @@ public virtual void ResetState() // so let's clear the buffers. serverSnapshots.Clear(); clientSnapshots.Clear(); + + // Prevent resistance from CharacterController + // or non-knematic Rigidbodies when teleporting. + Physics.SyncTransforms(); } public virtual void Reset() @@ -469,8 +473,8 @@ void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity id } } +#if !UNITY_SERVER && (UNITY_EDITOR || DEVELOPMENT_BUILD) // OnGUI allocates even if it does nothing. avoid in release. -#if UNITY_EDITOR || DEVELOPMENT_BUILD // debug /////////////////////////////////////////////////////////////// protected virtual void OnGUI() { diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs index f0016f96cf..09b4c6afcc 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs @@ -1,13 +1,5 @@ -// Quake NetworkTransform based on 2022 NetworkTransformUnreliable. -// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ -// Quake: https://www.jfedor.org/quake3/ -// -// Base class for NetworkTransform and NetworkTransformChild. -// => simple unreliable sync without any interpolation for now. -// => which means we don't need teleport detection either -// -// several functions are virtual in case someone needs to modify a part. -using System; +// NetworkTransform V3 based on NetworkTransformUnreliable, using Mirror's new +// Unreliable quake style networking model with delta compression. using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; @@ -15,311 +7,103 @@ namespace Mirror { [AddComponentMenu("Network/Network Transform Hybrid")] - public class NetworkTransformHybrid : NetworkBehaviourHybrid + public class NetworkTransformHybrid : NetworkTransformBase { - // target transform to sync. can be on a child. - [Header("Target")] - [Tooltip("The Transform component to sync. May be on this GameObject, or on a child.")] - public Transform target; - - [Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")] - public int bufferSizeLimit = 64; - internal SortedList clientSnapshots = new SortedList(); - internal SortedList serverSnapshots = new SortedList(); - - // CUSTOM CHANGE: bring back sendRate. this will probably be ported to Mirror. - // TODO but use built in syncInterval instead of the extra field here! - [Header("Synchronization")] - [Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")] - public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333' - public float sendInterval => 1f / sendRate; - // END CUSTOM CHANGE + // FixedUpdate support to fix: https://github.com/MirrorNetworking/Mirror/pull/3989 + public bool useFixedUpdate; + TransformSnapshot? pendingSnapshot; - // delta compression needs to remember 'last' to compress against. - // this is from reliable full state serializations, not from last - // unreliable delta since that isn't guaranteed to be delivered. - Vector3 lastSerializedBaselinePosition = Vector3.zero; - Quaternion lastSerializedBaselineRotation = Quaternion.identity; - Vector3 lastSerializedBaselineScale = Vector3.one; - - // save last deserialized baseline to delta decompress against - Vector3 lastDeserializedBaselinePosition = Vector3.zero; // unused, but keep for delta - Quaternion lastDeserializedBaselineRotation = Quaternion.identity; // unused, but keep for delta - Vector3 lastDeserializedBaselineScale = Vector3.one; // unused, but keep for delta - - // sensitivity is for changed-detection, - // this is != precision, which is for quantization and delta compression. - [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] - public float positionSensitivity = 0.01f; + [Header("Additional Settings")] + [Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")] + public float onlySyncOnChangeCorrectionMultiplier = 2; + + [Header("Rotation")] + [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] public float rotationSensitivity = 0.01f; - public float scaleSensitivity = 0.01f; - // selective sync ////////////////////////////////////////////////////// - [Header("Selective Sync & interpolation")] - public bool syncPosition = true; - public bool syncRotation = true; - public bool syncScale = false; + // delta compression is capable of detecting byte-level changes. + // if we scale float position to bytes, + // then small movements will only change one byte. + // this gives optimal bandwidth. + // benchmark with 0.01 precision: 130 KB/s => 60 KB/s + // benchmark with 0.1 precision: 130 KB/s => 30 KB/s + [Header("Precision")] + [Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")] + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float positionPrecision = 0.01f; // 1 cm + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float rotationPrecision = 0.001f; // this is for the quaternion's components, needs to be small + [Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range. + public float scalePrecision = 0.01f; // 1 cm - // debugging /////////////////////////////////////////////////////////// [Header("Debug")] - public bool debugDraw; - public bool showGizmos; - public bool showOverlay; - public Color overlayColor = new Color(0, 0, 0, 0.5f); - - // initialization ////////////////////////////////////////////////////// - // make sure to call this when inheriting too! - protected virtual void Awake() {} + public bool debugDraw = false; - protected override void OnValidate() - { - base.OnValidate(); + // delta compression needs to remember 'last' to compress against. + // this is from reliable full state serializations, not from last + // unreliable delta since that isn't guaranteed to be delivered. + protected Vector3Long lastSerializedPosition = Vector3Long.zero; + protected Vector3Long lastDeserializedPosition = Vector3Long.zero; - // set target to self if none yet - if (target == null) target = transform; + protected Vector4Long lastSerializedRotation = Vector4Long.zero; + protected Vector4Long lastDeserializedRotation = Vector4Long.zero; - // we use sendRate for convenience. - // but project it to syncInterval for NetworkTransformHybrid to work properly. - syncInterval = sendInterval; - } + protected Vector3Long lastSerializedScale = Vector3Long.zero; + protected Vector3Long lastDeserializedScale = Vector3Long.zero; - // apply a snapshot to the Transform. - // -> start, end, interpolated are all passed in caes they are needed - // -> a regular game would apply the 'interpolated' snapshot - // -> a board game might want to jump to 'goal' directly - // (it's easier to always interpolate and then apply selectively, - // instead of manually interpolating x, y, z, ... depending on flags) - // => internal for testing - // - // NOTE: stuck detection is unnecessary here. - // we always set transform.position anyway, we can't get stuck. - protected virtual void ApplySnapshot(TransformSnapshot interpolated) - { - // local position/rotation for VR support - // - // if syncPosition/Rotation/Scale is disabled then we received nulls - // -> current position/rotation/scale would've been added as snapshot - // -> we still interpolated - // -> but simply don't apply it. if the user doesn't want to sync - // scale, then we should not touch scale etc. - if (syncPosition) target.localPosition = interpolated.position; - if (syncRotation) target.localRotation = interpolated.rotation; - if (syncScale) target.localScale = interpolated.scale; - } + // Used to store last sent snapshots + protected TransformSnapshot last; - // store state after baseline sync - protected override void StoreState() + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() { - target.GetLocalPositionAndRotation(out lastSerializedBaselinePosition, out lastSerializedBaselineRotation); - lastSerializedBaselineScale = target.localScale; - } - - // check if position / rotation / scale changed since last _full reliable_ sync. - // squared comparisons for performance - protected override bool StateChanged() - { - target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); - Vector3 scale = target.localScale; - - if (syncPosition) - { - float positionDelta = Vector3.Distance(position, lastSerializedBaselinePosition); - if (positionDelta >= positionSensitivity) - { - return true; - } - } - - if (syncRotation) - { - float rotationDelta = Quaternion.Angle(lastSerializedBaselineRotation, rotation); - if (rotationDelta >= rotationSensitivity) - { - return true; - } - } + base.Configure(); - if (syncScale) - { - float scaleDelta = Vector3.Distance(scale, lastSerializedBaselineScale); - if (scaleDelta >= scaleSensitivity) - { - return true; - } - } + // force syncMethod to unreliable + syncMethod = SyncMethod.Hybrid; - return false; + // Unreliable ignores syncInterval. don't need to force anymore: + // sendIntervalMultiplier = 1; } - // serialization /////////////////////////////////////////////////////// - // called on server and on client, depending on SyncDirection - protected override void OnSerializeBaseline(NetworkWriter writer) + // update ////////////////////////////////////////////////////////////// + void Update() { - // perf: get position/rotation directly. TransformSnapshot is too expensive. - // TransformSnapshot snapshot = ConstructSnapshot(); - target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); - Vector3 scale = target.localScale; - - if (syncPosition) writer.WriteVector3(position); - if (syncRotation) writer.WriteQuaternion(rotation); - if (syncScale) writer.WriteVector3(scale); + // if server then always sync to others. + if (isServer) UpdateServer(); + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient) UpdateClient(); } - // called on server and on client, depending on SyncDirection - protected override void OnDeserializeBaseline(NetworkReader reader, byte baselineTick) + void FixedUpdate() { - // deserialize - Vector3? position = null; - Quaternion? rotation = null; - Vector3? scale = null; + if (!useFixedUpdate) return; - if (syncPosition) - { - position = reader.ReadVector3(); - lastDeserializedBaselinePosition = position.Value; - } - if (syncRotation) - { - rotation = reader.ReadQuaternion(); - lastDeserializedBaselineRotation = rotation.Value; - } - if (syncScale) + if (pendingSnapshot.HasValue && !IsClientWithAuthority) { - scale = reader.ReadVector3(); - lastDeserializedBaselineScale = scale.Value; + // Apply via base method, but in FixedUpdate + Apply(pendingSnapshot.Value, pendingSnapshot.Value); + pendingSnapshot = null; } - - // debug draw: baseline = yellow - if (debugDraw && position.HasValue) Debug.DrawLine(position.Value, position.Value + Vector3.up, Color.yellow, 10f); - - // if baseline counts as delta, insert it into snapshot buffer too - if (baselineIsDelta) - { - if (isServer) - { - OnClientToServerDeltaSync(position, rotation, scale); - } - else if (isClient) - { - OnServerToClientDeltaSync(position, rotation, scale); - } - } - } - - // called on server and on client, depending on SyncDirection - protected override void OnSerializeDelta(NetworkWriter writer) - { - // perf: get position/rotation directly. TransformSnapshot is too expensive. - // TransformSnapshot snapshot = ConstructSnapshot(); - target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); - Vector3 scale = target.localScale; - - if (syncPosition) writer.WriteVector3(position); - if (syncRotation) writer.WriteQuaternion(rotation); - if (syncScale) writer.WriteVector3(scale); } - // called on server and on client, depending on SyncDirection - protected override void OnDeserializeDelta(NetworkReader reader, byte baselineTick) + void LateUpdate() { - Vector3? position = null; - Quaternion? rotation = null; - Vector3? scale = null; - - if (syncPosition) position = reader.ReadVector3(); - if (syncRotation) rotation = reader.ReadQuaternion(); - if (syncScale) scale = reader.ReadVector3(); - - // debug draw: delta = white - if (debugDraw && position.HasValue) Debug.DrawLine(position.Value, position.Value + Vector3.up, Color.white, 10f); - - if (isServer) + // set dirty to trigger OnSerialize. either always, or only if changed. + // It has to be checked in LateUpdate() for onlySyncOnChange to avoid + // the possibility of Update() running first before the object's movement + // script's Update(), which then causes NT to send every alternate frame + // instead. + if (isServer || (IsClientWithAuthority && NetworkClient.ready)) { - OnClientToServerDeltaSync(position, rotation, scale); - } - else if (isClient) - { - OnServerToClientDeltaSync(position, rotation, scale); + if (!onlySyncOnChange || Changed(Construct())) + SetDirty(); } } - // processing ////////////////////////////////////////////////////////// - // local authority client sends sync message to server for broadcasting - protected virtual void OnClientToServerDeltaSync(Vector3? position, Quaternion? rotation, Vector3? scale) - { - // only apply if in client authority mode - if (syncDirection != SyncDirection.ClientToServer) return; - - // protect against ever-growing buffer size attacks - if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; - - // only player owned objects (with a connection) can send to - // server. we can get the timestamp from the connection. - double timestamp = connectionToClient.remoteTimeStamp; - - // insert transform snapshot - SnapshotInterpolation.InsertIfNotExists( - serverSnapshots, - bufferSizeLimit, - new TransformSnapshot( - timestamp, // arrival remote timestamp. NOT remote time. - NetworkTime.localTime, // Unity 2019 doesn't have Time.timeAsDouble yet - position.HasValue ? position.Value : Vector3.zero, - rotation.HasValue ? rotation.Value : Quaternion.identity, - scale.HasValue ? scale.Value : Vector3.one - )); - } - - // server broadcasts sync message to all clients - protected virtual void OnServerToClientDeltaSync(Vector3? position, Quaternion? rotation, Vector3? scale) - { - // in host mode, the server sends rpcs to all clients. - // the host client itself will receive them too. - // -> host server is always the source of truth - // -> we can ignore any rpc on the host client - // => otherwise host objects would have ever growing clientBuffers - // (rpc goes to clients. if isServer is true too then we are host) - if (isServer) return; - - // don't apply for local player with authority - if (IsClientWithAuthority) return; - - // Debug.Log($"[{name}] Client: received delta for baseline #{baselineTick}"); - - // on the client, we receive rpcs for all entities. - // not all of them have a connectionToServer. - // but all of them go through NetworkClient.connection. - // we can get the timestamp from there. - double timestamp = NetworkClient.connection.remoteTimeStamp; - - // position, rotation, scale can have no value if same as last time. - // saves bandwidth. - // but we still need to feed it to snapshot interpolation. we can't - // just have gaps in there if nothing has changed. for example, if - // client sends snapshot at t=0 - // client sends nothing for 10s because not moved - // client sends snapshot at t=10 - // then the server would assume that it's one super slow move and - // replay it for 10 seconds. - // if (!syncPosition) position = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].position : target.localPosition; - // if (!syncRotation) rotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : target.localRotation; - // if (!syncScale) scale = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].scale : target.localScale; - - // insert snapshot - SnapshotInterpolation.InsertIfNotExists( - clientSnapshots, - bufferSizeLimit, - new TransformSnapshot( - timestamp, // arrival remote timestamp. NOT remote time. - NetworkTime.localTime, // Unity 2019 doesn't have Time.timeAsDouble yet - position.HasValue ? position.Value : Vector3.zero, - rotation.HasValue ? rotation.Value : Quaternion.identity, - scale.HasValue ? scale.Value : Vector3.one - )); - } - - // update server /////////////////////////////////////////////////////// - void UpdateServerInterpolation() + protected virtual void UpdateServer() { // apply buffered snapshots IF client authority // -> in server authority, server moves the object @@ -327,7 +111,11 @@ void UpdateServerInterpolation() // -> don't apply for host mode player objects either, even if in // client authority mode. if it doesn't go over the network, // then we don't need to do anything. - if (syncDirection == SyncDirection.ClientToServer && !isOwned) + // -> connectionToClient is briefly null after scene changes: + // https://github.com/MirrorNetworking/Mirror/issues/3329 + if (syncDirection == SyncDirection.ClientToServer && + connectionToClient != null && + !isOwned) { if (serverSnapshots.Count > 0) { @@ -335,339 +123,382 @@ void UpdateServerInterpolation() // NetworkClient is responsible for time globally. SnapshotInterpolation.StepInterpolation( serverSnapshots, - // CUSTOM CHANGE: allow for custom sendRate+sendInterval again. - // for example, if the object is moving @ 1 Hz, always put it back by 1s. - // that's how we still get smooth movement even with a global timeline. - connectionToClient.remoteTimeline - sendInterval, - // END CUSTOM CHANGE + connectionToClient.remoteTimeline, out TransformSnapshot from, out TransformSnapshot to, out double t); // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - ApplySnapshot(computed); - } - } - } - // update client /////////////////////////////////////////////////////// - void UpdateClientInterpolation() - { - // only while we have snapshots - if (clientSnapshots.Count > 0) - { - // step the interpolation without touching time. - // NetworkClient is responsible for time globally. - SnapshotInterpolation.StepInterpolation( - clientSnapshots, - // CUSTOM CHANGE: allow for custom sendRate+sendInterval again. - // for example, if the object is moving @ 1 Hz, always put it back by 1s. - // that's how we still get smooth movement even with a global timeline. - NetworkTime.time - sendInterval, // == NetworkClient.localTimeline from snapshot interpolation - // END CUSTOM CHANGE - out TransformSnapshot from, - out TransformSnapshot to, - out double t); - - // interpolate & apply - TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - ApplySnapshot(computed); + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); + } } } - // Update() without LateUpdate() split: otherwise perf. is cut in half! - protected override void Update() + protected virtual void UpdateClient() { - base.Update(); // NetworkBehaviourHybrid - - if (isServer) - { - // interpolate remote clients - UpdateServerInterpolation(); - } - // 'else if' because host mode shouldn't update both. - else if (isClient) + // client authority, and local player (= allowed to move myself)? + if (!IsClientWithAuthority) { - // interpolate remote client (and local player if no authority) - if (!IsClientWithAuthority) UpdateClientInterpolation(); - } - } - - // common Teleport code for client->server and server->client - protected virtual void OnTeleport(Vector3 destination) - { - // reset any in-progress interpolation & buffers - Reset(); - - // set the new position. - // interpolation will automatically continue. - target.position = destination; - - // TODO - // what if we still receive a snapshot from before the interpolation? - // it could easily happen over unreliable. - // -> maybe add destination as first entry? - } - - // common Teleport code for client->server and server->client - protected virtual void OnTeleport(Vector3 destination, Quaternion rotation) - { - // reset any in-progress interpolation & buffers - Reset(); - - // set the new position. - // interpolation will automatically continue. - target.position = destination; - target.rotation = rotation; - - // TODO - // what if we still receive a snapshot from before the interpolation? - // it could easily happen over unreliable. - // -> maybe add destination as first entry? - } - - // server->client teleport to force position without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [ClientRpc] - public void RpcTeleport(Vector3 destination) - { - // NOTE: even in client authority mode, the server is always allowed - // to teleport the player. for example: - // * CmdEnterPortal() might teleport the player - // * Some people use client authority with server sided checks - // so the server should be able to reset position if needed. - - // TODO what about host mode? - OnTeleport(destination); - } - - // server->client teleport to force position and rotation without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [ClientRpc] - public void RpcTeleport(Vector3 destination, Quaternion rotation) - { - // NOTE: even in client authority mode, the server is always allowed - // to teleport the player. for example: - // * CmdEnterPortal() might teleport the player - // * Some people use client authority with server sided checks - // so the server should be able to reset position if needed. - - // TODO what about host mode? - OnTeleport(destination, rotation); - } - - // client->server teleport to force position without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [Command] - public void CmdTeleport(Vector3 destination) - { - // client can only teleport objects that it has authority over. - if (syncDirection != SyncDirection.ClientToServer) return; + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); - // TODO what about host mode? - OnTeleport(destination); - - // if a client teleports, we need to broadcast to everyone else too - // TODO the teleported client should ignore the rpc though. - // otherwise if it already moved again after teleporting, - // the rpc would come a little bit later and reset it once. - // TODO or not? if client ONLY calls Teleport(pos), the position - // would only be set after the rpc. unless the client calls - // BOTH Teleport(pos) and target.position=pos - RpcTeleport(destination); - } + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - // client->server teleport to force position and rotation without interpolation. - // otherwise it would interpolate to a (far away) new position. - // => manually calling Teleport is the only 100% reliable solution. - [Command] - public void CmdTeleport(Vector3 destination, Quaternion rotation) - { - // client can only teleport objects that it has authority over. - if (syncDirection != SyncDirection.ClientToServer) return; + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); - // TODO what about host mode? - OnTeleport(destination, rotation); - - // if a client teleports, we need to broadcast to everyone else too - // TODO the teleported client should ignore the rpc though. - // otherwise if it already moved again after teleporting, - // the rpc would come a little bit later and reset it once. - // TODO or not? if client ONLY calls Teleport(pos), the position - // would only be set after the rpc. unless the client calls - // BOTH Teleport(pos) and target.position=pos - RpcTeleport(destination, rotation); + if (debugDraw) + { + Debug.DrawLine(from.position, to.position, Color.white, 10f); + Debug.DrawLine(computed.position, computed.position + Vector3.up, Color.white, 10f); + } + } + } } - [Server] - public void ServerTeleport(Vector3 destination, Quaternion rotation) + // check if position / rotation / scale changed since last _full reliable_ sync. + protected virtual bool Changed(TransformSnapshot current) => + // position is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.position, current.position, positionPrecision) || + // rotation isn't quantized / delta compressed. + // check with sensitivity. + Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity || + // scale is quantized and delta compressed. + // only consider it changed if the quantized representation is changed. + // careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc. + QuantizedChanged(last.scale, current.scale, scalePrecision); + + // helper function to compare quantized representations of a Vector3 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision) { - OnTeleport(destination, rotation); - RpcTeleport(destination, rotation); + Compression.ScaleToLong(u, precision, out Vector3Long uQuantized); + Compression.ScaleToLong(v, precision, out Vector3Long vQuantized); + return uQuantized != vQuantized; } - public override void Reset() + // Unreliable OnSerialize: + // - initial=true sends reliable full state + // - initial=false sends unreliable delta states + public override void OnSerialize(NetworkWriter writer, bool initialState) { - base.Reset(); // NetworkBehaviourHybrid + // get current snapshot for broadcasting. + TransformSnapshot snapshot = Construct(); - // default to ClientToServer so this works immediately for users - syncDirection = SyncDirection.ClientToServer; + // ClientToServer optimization: + // for interpolated client owned identities, + // always broadcast the latest known snapshot so other clients can + // interpolate immediately instead of catching up too - // disabled objects aren't updated anymore. - // so let's clear the buffers. - serverSnapshots.Clear(); - clientSnapshots.Clear(); + // TODO dirty mask? [compression is very good w/o it already] + // each vector's component is delta compressed. + // an unchanged component would still require 1 byte. + // let's use a dirty bit mask to filter those out as well. - // reset baseline - lastSerializedBaselinePosition = Vector3.zero; - lastSerializedBaselineRotation = Quaternion.identity; - lastSerializedBaselineScale = Vector3.one; + // Debug.Log($"NT OnSerialize: initial={initialState} method={syncMethod}"); - lastDeserializedBaselinePosition = Vector3.zero; - lastDeserializedBaselineRotation = Quaternion.identity; - lastDeserializedBaselineScale = Vector3.one; - - // Debug.Log($"[{name}] Reset to baselineTick=0"); - } - - protected virtual void OnDisable() => Reset(); - protected virtual void OnEnable() => Reset(); - - public override void OnSerialize(NetworkWriter writer, bool initialState) - { - // OnSerialize(initial) is called every time when a player starts observing us. - // note this is _not_ called just once on spawn. + // reliable full state + if (initialState) + { + // TODO initialState is now sent multiple times. find a new fix for this: + // If there is a last serialized snapshot, we use it. + // This prevents the new client getting a snapshot that is different + // from what the older clients last got. If this happens, and on the next + // regular serialisation the delta compression will get wrong values. + // Notes: + // 1. Interestingly only the older clients have it wrong, because at the end + // of this function, last = snapshot which is the initial state's snapshot + // 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate + // snapshot constructed would have been the same as the last anyway. + // if (last.remoteTime > 0) snapshot = last; + + int startPosition = writer.Position; + + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) + { + // if smallest-three quaternion compression is enabled, + // then we don't need baseline rotation since delta always + // sends an absolute value. + if (!compressRotation) + { + writer.WriteQuaternion(snapshot.rotation); + } + } + if (syncScale) writer.WriteVector3(snapshot.scale); - base.OnSerialize(writer, initialState); // NetworkBehaviourHybrid + // save serialized as 'last' for next delta compression. + // only for reliable full sync, since unreliable isn't guaranteed to arrive. + if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); + if (syncRotation && !compressRotation) Compression.ScaleToLong(snapshot.rotation, rotationPrecision, out lastSerializedRotation); + if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); - // sync target component's position on spawn. - // fixes https://github.com/vis2k/Mirror/pull/3051/ - // (Spawn message wouldn't sync NTChild positions either) - if (initialState) + // set 'last' + last = snapshot; + } + // unreliable delta: compress against last full reliable state + else { - // spawn message is used as first baseline. - // perf: get position/rotation directly. TransformSnapshot is too expensive. - // TransformSnapshot snapshot = ConstructSnapshot(); - target.GetLocalPositionAndRotation(out Vector3 position, out Quaternion rotation); - Vector3 scale = target.localScale; - - if (syncPosition) writer.WriteVector3(position); - if (syncRotation) writer.WriteQuaternion(rotation); - if (syncScale) writer.WriteVector3(scale); + int startPosition = writer.Position; + + if (syncPosition) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedPosition, quantized); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + { + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + } + else + { + // quantize -> delta -> varint + // this works for quaternions too, where xyzw are [-1,1] + // and gradually change as rotation changes. + Compression.ScaleToLong(snapshot.rotation, rotationPrecision, out Vector4Long quantized); + DeltaCompression.Compress(writer, lastSerializedRotation, quantized); + } + } + if (syncScale) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedScale, quantized); + } } } + // Unreliable OnDeserialize: + // - initial=true sends reliable full state + // - initial=false sends unreliable delta states public override void OnDeserialize(NetworkReader reader, bool initialState) { - base.OnDeserialize(reader, initialState); // NetworkBehaviourHybrid + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; - // sync target component's position on spawn. - // fixes https://github.com/vis2k/Mirror/pull/3051/ - // (Spawn message wouldn't sync NTChild positions either) + // reliable full state if (initialState) { - // save last deserialized baseline tick number to compare deltas against - Vector3 position = Vector3.zero; - Quaternion rotation = Quaternion.identity; - Vector3 scale = Vector3.one; - if (syncPosition) { position = reader.ReadVector3(); - lastDeserializedBaselinePosition = position; + + if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.green, 10.0f); } if (syncRotation) { - rotation = reader.ReadQuaternion(); - lastDeserializedBaselineRotation = rotation; + // if smallest-three quaternion compression is enabled, + // then we don't need baseline rotation since delta always + // sends an absolute value. + if (!compressRotation) + { + rotation = reader.ReadQuaternion(); + } + } + if (syncScale) scale = reader.ReadVector3(); + + // save deserialized as 'last' for next delta compression. + // only for reliable full sync, since unreliable isn't guaranteed to arrive. + if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition); + if (syncRotation && !compressRotation) Compression.ScaleToLong(rotation.Value, rotationPrecision, out lastDeserializedRotation); + if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale); + } + // unreliable delta: decompress against last full reliable state + else + { + // varint -> delta -> quantize + if (syncPosition) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition); + position = Compression.ScaleToFloat(quantized, positionPrecision); + + if (debugDraw) Debug.DrawLine(position.Value, position.Value + Vector3.up , Color.yellow, 10.0f); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + { + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + } + else + { + // varint -> delta -> quantize + // this works for quaternions too, where xyzw are [-1,1] + // and gradually change as rotation changes. + Vector4Long quantized = DeltaCompression.Decompress(reader, lastDeserializedRotation); + rotation = Compression.ScaleToFloat(quantized, rotationPrecision); + } } if (syncScale) { - scale = reader.ReadVector3(); - lastDeserializedBaselineScale = scale; + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale); + scale = Compression.ScaleToFloat(quantized, scalePrecision); } - // if baseline counts as delta, insert it into snapshot buffer too - if (baselineIsDelta) - OnServerToClientDeltaSync(position, rotation, scale); + // handle depending on server / client / host. + // server has priority for host mode. + // + // only do this for the unreliable delta states! + // processing the reliable baselines shows noticeable jitter + // around baseline syncs (e.g. tanks demo @ 4 Hz sendRate). + // unreliable deltas are always within the same time delta, + // so this gives perfectly smooth results. + if (isServer) OnClientToServerSync(position, rotation, scale); + else if (isClient) OnServerToClientSync(position, rotation, scale); } } - // CUSTOM CHANGE /////////////////////////////////////////////////////////// - // Don't run OnGUI or draw gizmos in debug builds. - // OnGUI allocates even if it does nothing. avoid in release. - //#if UNITY_EDITOR || DEVELOPMENT_BUILD -#if UNITY_EDITOR - // debug /////////////////////////////////////////////////////////////// - // END CUSTOM CHANGE /////////////////////////////////////////////////////// - protected virtual void OnGUI() - { - if (!showOverlay) return; - - // show data next to player for easier debugging. this is very useful! - // IMPORTANT: this is basically an ESP hack for shooter games. - // DO NOT make this available with a hotkey in release builds - if (!Debug.isDebugBuild) return; - // project position to screen - Vector3 point = Camera.main.WorldToScreenPoint(target.position); + // sync //////////////////////////////////////////////////////////////// - // enough alpha, in front of camera and in screen? - if (point.z >= 0 && Utils.IsPointInScreen(point)) - { - GUI.color = overlayColor; - GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100)); + // local authority client sends sync message to server for broadcasting + protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // only apply if in client authority mode + if (syncDirection != SyncDirection.ClientToServer) return; - // always show both client & server buffers so it's super - // obvious if we accidentally populate both. - GUILayout.Label($"Server Buffer:{serverSnapshots.Count}"); - GUILayout.Label($"Client Buffer:{clientSnapshots.Count}"); + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; - GUILayout.EndArea(); - GUI.color = Color.white; + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier)) + { + RewriteHistory( + serverSnapshots, + connectionToClient.remoteTimeStamp, + NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline. + NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet + GetPosition(), + GetRotation(), + GetScale()); } + + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); } - protected virtual void DrawGizmos(SortedList buffer) + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) { - // only draw if we have at least two entries - if (buffer.Count < 2) return; - - // calculate threshold for 'old enough' snapshots - double threshold = NetworkTime.localTime - NetworkClient.bufferTime; - Color oldEnoughColor = new Color(0, 1, 0, 0.5f); - Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); + // don't apply for local player with authority + if (IsClientWithAuthority) return; - // draw the whole buffer for easier debugging. - // it's worth seeing how much we have buffered ahead already - for (int i = 0; i < buffer.Count; ++i) + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier)) { - // color depends on if old enough or not - TransformSnapshot entry = buffer.Values[i]; - bool oldEnough = entry.localTime <= threshold; - Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; - Gizmos.DrawCube(entry.position, Vector3.one); + RewriteHistory( + clientSnapshots, + NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline. + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + NetworkClient.sendInterval * sendIntervalMultiplier, + GetPosition(), + GetRotation(), + GetScale()); } - // extra: lines between start<->position<->goal - Gizmos.color = Color.green; - Gizmos.DrawLine(buffer.Values[0].position, target.position); - Gizmos.color = Color.white; - Gizmos.DrawLine(target.position, buffer.Values[1].position); + // add a small timeline offset to account for decoupled arrival of + // NetworkTime and NetworkTransform snapshots. + // needs to be sendInterval. half sendInterval doesn't solve it. + // https://github.com/MirrorNetworking/Mirror/issues/3427 + // remove this after LocalWorldState. + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale); } - protected virtual void OnDrawGizmos() + // only sync on change ///////////////////////////////////////////////// + // snap interp. needs a continous flow of packets. + // 'only sync on change' interrupts it while not changed. + // once it restarts, snap interp. will interp from the last old position. + // this will cause very noticeable stutter for the first move each time. + // the fix is quite simple. + + // 1. detect if the remaining snapshot is too old from a past move. + static bool NeedsCorrection( + SortedList snapshots, + double remoteTimestamp, + double bufferTime, + double toleranceMultiplier) => + snapshots.Count == 1 && + remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier; + + // 2. insert a fake snapshot at current position, + // exactly one 'sendInterval' behind the newly received one. + static void RewriteHistory( + SortedList snapshots, + // timestamp of packet arrival, not interpolated remote time! + double remoteTimeStamp, + double localTime, + double sendInterval, + Vector3 position, + Quaternion rotation, + Vector3 scale) { - // This fires in edit mode but that spams NRE's so check isPlaying - if (!Application.isPlaying) return; - if (!showGizmos) return; + // clear the previous snapshot + snapshots.Clear(); + + // insert a fake one at where we used to be, + // 'sendInterval' behind the new one. + SnapshotInterpolation.InsertIfNotExists( + snapshots, + NetworkClient.snapshotSettings.bufferLimit, + new TransformSnapshot( + remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time. + localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet + position, + rotation, + scale + ) + ); + } + + // reset state for next session. + // do not ever call this during a session (i.e. after teleport). + // calling this will break delta compression. + public override void ResetState() + { + base.ResetState(); + + // reset delta + lastSerializedPosition = Vector3Long.zero; + lastDeserializedPosition = Vector3Long.zero; + + lastSerializedRotation = Vector4Long.zero; + lastDeserializedRotation = Vector4Long.zero; + + lastSerializedScale = Vector3Long.zero; + lastDeserializedScale = Vector3Long.zero; - if (isServer) DrawGizmos(serverSnapshots); - if (isClient) DrawGizmos(clientSnapshots); + // reset 'last' for delta too + last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero); } -#endif } } diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs index b814337731..87eac32cfa 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs @@ -10,10 +10,12 @@ public class NetworkTransformReliable : NetworkTransformBase { uint sendIntervalCounter = 0; double lastSendIntervalTime = double.MinValue; + TransformSnapshot? pendingSnapshot; [Header("Additional Settings")] [Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")] public float onlySyncOnChangeCorrectionMultiplier = 2; + public bool useFixedUpdate; [Header("Rotation")] [Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] @@ -42,6 +44,16 @@ public class NetworkTransformReliable : NetworkTransformBase // Used to store last sent snapshots protected TransformSnapshot last; + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() + { + base.Configure(); + + // force syncMethod to reliable + syncMethod = SyncMethod.Reliable; + } + // update ////////////////////////////////////////////////////////////// void Update() { @@ -52,6 +64,18 @@ void Update() else if (isClient) UpdateClient(); } + void FixedUpdate() + { + if (!useFixedUpdate) return; + + if (pendingSnapshot.HasValue && !IsClientWithAuthority) + { + // Apply via base method, but in FixedUpdate + Apply(pendingSnapshot.Value, pendingSnapshot.Value); + pendingSnapshot = null; + } + } + void LateUpdate() { // set dirty to trigger OnSerialize. either always, or only if changed. @@ -102,24 +126,41 @@ protected virtual void UpdateServer() protected virtual void UpdateClient() { - // client authority, and local player (= allowed to move myself)? - if (!IsClientWithAuthority) + if (useFixedUpdate) { - // only while we have snapshots - if (clientSnapshots.Count > 0) + if (!IsClientWithAuthority && clientSnapshots.Count > 0) { - // step the interpolation without touching time. - // NetworkClient is responsible for time globally. SnapshotInterpolation.StepInterpolation( clientSnapshots, - NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + NetworkTime.time, out TransformSnapshot from, out TransformSnapshot to, - out double t); - - // interpolate & apply - TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - Apply(computed, to); + out double t + ); + pendingSnapshot = TransformSnapshot.Interpolate(from, to, t); + } + } + else + { + // client authority, and local player (= allowed to move myself)? + if (!IsClientWithAuthority) + { + // only while we have snapshots + if (clientSnapshots.Count > 0) + { + // step the interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + } } } } diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs index 18d38865b5..c3c84f8871 100644 --- a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -9,11 +9,13 @@ public class NetworkTransformUnreliable : NetworkTransformBase { uint sendIntervalCounter = 0; double lastSendIntervalTime = double.MinValue; + TransformSnapshot? pendingSnapshot; [Header("Additional Settings")] // Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover. [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results.")] public float bufferResetMultiplier = 3; + public bool useFixedUpdate; [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] public float positionSensitivity = 0.01f; @@ -25,6 +27,16 @@ public class NetworkTransformUnreliable : NetworkTransformBase protected Changed cachedChangedComparison; protected bool hasSentUnchangedPosition; + // validation ////////////////////////////////////////////////////////// + // Configure is called from OnValidate and Awake + protected override void Configure() + { + base.Configure(); + + // force syncMethod to unreliable + syncMethod = SyncMethod.Hybrid; + } + // update ////////////////////////////////////////////////////////////// // Update applies interpolation void Update() @@ -36,6 +48,17 @@ void Update() else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation(); } + void FixedUpdate() + { + if (!useFixedUpdate) return; + + if (pendingSnapshot.HasValue) + { + Apply(pendingSnapshot.Value, pendingSnapshot.Value); + pendingSnapshot = null; + } + } + // LateUpdate broadcasts. // movement scripts may change positions in Update. // use LateUpdate to ensure changes are detected in the same frame. @@ -157,7 +180,10 @@ void UpdateServerInterpolation() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - Apply(computed, to); + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); } } @@ -229,7 +255,10 @@ void UpdateClientInterpolation() // interpolate & apply TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); - Apply(computed, to); + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); } public override void OnSerialize(NetworkWriter writer, bool initialState) @@ -368,7 +397,6 @@ protected virtual void OnClientToServerSync(SyncData syncData) AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale); } - [ClientRpc(channel = Channels.Unreliable)] void RpcServerToClientSync(SyncData syncData) => OnServerToClientSync(syncData); @@ -440,5 +468,21 @@ protected virtual void UpdateSyncData(ref SyncData syncData, SortedList 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale()); } } + + // This is to extract position/rotation/scale data from payload. Override + // Construct and Deconstruct if you are implementing a different SyncData logic. + // Note however that snapshot interpolation still requires the basic 3 data + // position, rotation and scale, which are computed from here. + protected virtual void DeconstructSyncData(System.ArraySegment receivedPayload, out byte? changedFlagData, out Vector3? position, out Quaternion? rotation, out Vector3? scale) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(receivedPayload)) + { + SyncData syncData = reader.Read(); + changedFlagData = (byte)syncData.changedDataByte; + position = syncData.position; + rotation = syncData.quatRotation; + scale = syncData.scale; + } + } } } diff --git a/Assets/Mirror/Components/RemoteStatistics.cs b/Assets/Mirror/Components/RemoteStatistics.cs index 5b3ede9cd7..8ea0df4f6b 100644 --- a/Assets/Mirror/Components/RemoteStatistics.cs +++ b/Assets/Mirror/Components/RemoteStatistics.cs @@ -223,6 +223,7 @@ void Update() if (isLocalPlayer) UpdateClient(); } +#if !UNITY_SERVER void OnGUI() { if (!isLocalPlayer) return; @@ -437,5 +438,6 @@ void OnWindow(int windowID) // dragable window in any case GUI.DragWindow(new Rect(0, 0, 10000, 10000)); } +#endif } } diff --git a/Assets/Mirror/Core/Messages.cs b/Assets/Mirror/Core/Messages.cs index 62888c07e6..2455f0469b 100644 --- a/Assets/Mirror/Core/Messages.cs +++ b/Assets/Mirror/Core/Messages.cs @@ -52,13 +52,19 @@ public struct RpcMessage : NetworkMessage public ArraySegment payload; } + [Flags] public enum SpawnFlags : byte + { + None = 0, + isOwner = 1 << 0, + isLocalPlayer = 1 << 1 + } + public struct SpawnMessage : NetworkMessage { // netId of new or existing object public uint netId; - public bool isLocalPlayer; - // Sets hasAuthority on the spawned object - public bool isOwner; + // isOwner and isLocalPlayer are merged into one byte via bitwise op + public SpawnFlags spawnFlags; public ulong sceneId; // If sceneId != 0 then it is used instead of assetId public uint assetId; @@ -71,13 +77,53 @@ public struct SpawnMessage : NetworkMessage // serialized component data // ArraySegment to avoid unnecessary allocations public ArraySegment payload; + + // Backwards compatibility after implementing spawnFlags + public bool isOwner + { + get => spawnFlags.HasFlag(SpawnFlags.isOwner); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isOwner + : spawnFlags & ~SpawnFlags.isOwner; + } + + // Backwards compatibility after implementing spawnFlags + public bool isLocalPlayer + { + get => spawnFlags.HasFlag(SpawnFlags.isLocalPlayer); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isLocalPlayer + : spawnFlags & ~SpawnFlags.isLocalPlayer; + } } public struct ChangeOwnerMessage : NetworkMessage { public uint netId; - public bool isOwner; - public bool isLocalPlayer; + // isOwner and isLocalPlayer are merged into one byte via bitwise op + public SpawnFlags spawnFlags; + + // Backwards compatibility after implementing spawnFlags + public bool isOwner + { + get => spawnFlags.HasFlag(SpawnFlags.isOwner); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isOwner + : spawnFlags & ~SpawnFlags.isOwner; + } + + // Backwards compatibility after implementing spawnFlags + public bool isLocalPlayer + { + get => spawnFlags.HasFlag(SpawnFlags.isLocalPlayer); + set => spawnFlags = + value + ? spawnFlags | SpawnFlags.isLocalPlayer + : spawnFlags & ~SpawnFlags.isLocalPlayer; + } } public struct ObjectSpawnStartedMessage : NetworkMessage {} @@ -94,6 +140,7 @@ public struct ObjectHideMessage : NetworkMessage public uint netId; } + // state update for reliable sync public struct EntityStateMessage : NetworkMessage { public uint netId; @@ -102,6 +149,38 @@ public struct EntityStateMessage : NetworkMessage public ArraySegment payload; } + // state update for unreliable sync. + // baseline is always sent over Reliable channel. + public struct EntityStateMessageUnreliableBaseline : NetworkMessage + { + // baseline messages send their tick number as byte. + // delta messages are checked against that tick to avoid applying a + // delta on top of the wrong baseline. + // (byte is enough, we just need something small to compare against) + public byte baselineTick; + + public uint netId; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + // state update for unreliable sync + // delta is always sent over Unreliable channel. + public struct EntityStateMessageUnreliableDelta : NetworkMessage + { + // baseline messages send their tick number as byte. + // delta messages are checked against that tick to avoid applying a + // delta on top of the wrong baseline. + // (byte is enough, we just need something small to compare against) + public byte baselineTick; + + public uint netId; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + // whoever wants to measure rtt, sends this to the other end. public struct NetworkPingMessage : NetworkMessage { diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index 4b24d269de..79204743f2 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -6,6 +6,11 @@ namespace Mirror { + // SyncMethod to choose between: + // * Reliable: oldschool reliable sync every syncInterval. If nothing changes, nothing is sent. + // * Hybrid: quake style unreliable sync ('hybrid' to make it scale). + public enum SyncMethod { Reliable, Hybrid } + // SyncMode decides if a component is synced to all observers, or only owner public enum SyncMode { Observers, Owner } @@ -24,6 +29,9 @@ public enum SyncDirection { ServerToClient, ClientToServer } [HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")] public abstract class NetworkBehaviour : MonoBehaviour { + [Tooltip("Choose between:\n- Reliable: only sends when changed. Recommended for most games!\n- Unreliable: immediately sends at the expense of bandwidth. Only for hardcore competitive games.\nClick the Help icon for full details.")] + [HideInInspector] public SyncMethod syncMethod = SyncMethod.Reliable; + /// Sync direction for OnSerialize. ServerToClient by default. ClientToServer for client authority. [Tooltip("Server Authority calls OnSerialize on the server and syncs it to clients.\n\nClient Authority calls OnSerialize on the owning client, syncs it to server, which then broadcasts it to all other clients.\n\nUse server authority for cheat safety.")] public SyncDirection syncDirection = SyncDirection.ServerToClient; @@ -166,14 +174,14 @@ protected virtual void OnValidate() if (GetComponent() == null && GetComponentInParent(true) == null) { - Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or it's parents.", this); + Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or its parents.", this); } #elif UNITY_2020_3_OR_NEWER // 2020 only has GetComponentsInParent(bool includeInactive = false), we can use this too NetworkIdentity[] parentsIds = GetComponentsInParent(true); int parentIdsCount = parentsIds != null ? parentsIds.Length : 0; if (GetComponent() == null && parentIdsCount == 0) { - Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or it's parents.", this); + Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or its parents.", this); } #endif } @@ -236,18 +244,24 @@ public bool IsDirty() => //TODO Investigate putting is dirty on individual network behaviours, To reduce network usage //Even down to the individual syncVar? vs Performance true; - // check bits first. this is basically free. - //(syncVarDirtyBits | syncObjectDirtyBits) != 0UL && - // only check time if bits were dirty. this is more expensive. - //NetworkTime.localTime - lastSyncTime >= syncInterval; + // check bits first. this is basically free. + //(syncVarDirtyBits | syncObjectDirtyBits) != 0UL && + // only check time if bits were dirty. this is more expensive. + //NetworkTime.localTime - lastSyncTime >= syncInterval; + + // true if any SyncVar or SyncObject is dirty + // OR both bitmasks. != 0 if either was dirty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty_BitsOnly() => (syncVarDirtyBits | syncObjectDirtyBits) != 0UL; /// Clears all the dirty bits that were set by SetSyncVarDirtyBit() (formally SetDirtyBits) // automatically invoked when an update is sent for this object, but can // be called manually as well. - public void ClearAllDirtyBits() + public void ClearAllDirtyBits(bool clearSyncTime = true) { /// UNITYSTATION CODE /// \/ Saves a tiny bit of performance //lastSyncTime = NetworkTime.localTime; + if (clearSyncTime) lastSyncTime = NetworkTime.localTime; syncVarDirtyBits = 0L; syncObjectDirtyBits = 0L; @@ -1118,7 +1132,7 @@ protected T GetSyncVarNetworkBehaviour(NetworkBehaviourSyncVar syncNetBehavio { return null; } - + // ensure componentIndex is in range. // show explicit errors if something went wrong, instead of IndexOutOfRangeException. // removing components at runtime isn't allowed, yet this happened in a project so we need to check for it. @@ -1399,28 +1413,28 @@ internal void ResetSyncObjects() } /// Like Start(), but only called on server and host. - public virtual void OnStartServer() {} + public virtual void OnStartServer() { } /// Stop event, only called on server and host. - public virtual void OnStopServer() {} + public virtual void OnStopServer() { } /// Like Start(), but only called on client and host. - public virtual void OnStartClient() {} + public virtual void OnStartClient() { } /// Stop event, only called on client and host. - public virtual void OnStopClient() {} + public virtual void OnStopClient() { } /// Like Start(), but only called on client and host for the local player object. - public virtual void OnStartLocalPlayer() {} + public virtual void OnStartLocalPlayer() { } /// Stop event, but only called on client and host for the local player object. - public virtual void OnStopLocalPlayer() {} + public virtual void OnStopLocalPlayer() { } /// Like Start(), but only called for objects the client has authority over. - public virtual void OnStartAuthority() {} + public virtual void OnStartAuthority() { } /// Stop event, only called for objects the client has authority over. - public virtual void OnStopAuthority() {} + public virtual void OnStopAuthority() { } // Weaver injects this into inheriting classes to return true. // allows runtime & tests to check if a type was weaved. diff --git a/Assets/Mirror/Core/NetworkBehaviourHybrid.cs b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs deleted file mode 100644 index dfdd72b8fa..0000000000 --- a/Assets/Mirror/Core/NetworkBehaviourHybrid.cs +++ /dev/null @@ -1,483 +0,0 @@ -// base class for "Hybrid" sync components. -// inspired by the Quake networking model, but made to scale. -// https://www.jfedor.org/quake3/ -using System; -using UnityEngine; - -namespace Mirror -{ - public abstract class NetworkBehaviourHybrid : NetworkBehaviour - { - // Is this a client with authority over this transform? - // This component could be on the player object or any object that has been assigned authority to this client. - protected bool IsClientWithAuthority => isClient && authority; - - [Tooltip("Occasionally send a full reliable state to delta compress against. This only applies to Components with SyncMethod=Unreliable.")] - public int baselineRate = 1; - public float baselineInterval => baselineRate < int.MaxValue ? 1f / baselineRate : 0; // for 1 Hz, that's 1000ms - protected double lastBaselineTime; - protected double lastDeltaTime; - - // delta compression needs to remember 'last' to compress against. - byte lastSerializedBaselineTick = 0; - byte lastDeserializedBaselineTick = 0; - - [Tooltip("Enable to send all unreliable messages twice. Only useful for extremely fast-paced games since it doubles bandwidth costs.")] - public bool unreliableRedundancy = false; - - [Tooltip("When sending a reliable baseline, should we also send an unreliable delta or rely on the reliable baseline to arrive in a similar time?")] - public bool baselineIsDelta = true; - - // change detection: we need to do this carefully in order to get it right. - // - // DONT just check changes in UpdateBaseline(). this would introduce MrG's grid issue: - // server start in A1, reliable baseline sent to client - // server moves to A2, unreliabe delta sent to client - // server moves to A1, nothing is sent to client becuase last baseline position == position - // => client wouldn't know we moved back to A1 - // - // INSTEAD: every update() check for changes since baseline: - // UpdateDelta() keeps sending only if changed since _baseline_ - // UpdateBaseline() resends if there was any change in the period since last baseline. - // => this avoids the A1->A2->A1 grid issue above - bool changedSinceBaseline = false; - - [Header("Debug")] - public bool debugLog = false; - - public virtual void Reset() - { - lastSerializedBaselineTick = 0; - lastDeserializedBaselineTick = 0; - changedSinceBaseline = false; - } - - // user callbacks ////////////////////////////////////////////////////// - protected abstract void OnSerializeBaseline(NetworkWriter writer); - protected abstract void OnDeserializeBaseline(NetworkReader reader, byte baselineTick); - - protected abstract void OnSerializeDelta(NetworkWriter writer); - protected abstract void OnDeserializeDelta(NetworkReader reader, byte baselineTick); - - // implementations must store the current baseline state when requested: - // - implementations can use this to compress deltas against - // - implementations can use this to detect changes since baseline - // this is called whenever a baseline was sent. - protected abstract void StoreState(); - - // implementations may compare current state to the last stored state. - // this way we only need to send another reliable baseline if changed since last. - // this is called every syncInterval, not every baseline sync interval. - // (see comments where this is called). - protected abstract bool StateChanged(); - - // user callback in case drops due to baseline mismatch need to be logged/visualized/debugged. - protected virtual void OnDrop(byte lastBaselineTick, byte baselineTick, NetworkReader reader) {} - - // rpcs / cmds ///////////////////////////////////////////////////////// - // reliable baseline. - // include owner in case of server authority. - [ClientRpc(channel = Channels.Reliable)] - void RpcServerToClientBaseline(ArraySegment data) - { - // baseline is broadcast to all clients. - // ignore if this object is owned by this client. - if (IsClientWithAuthority) return; - - // host mode: baseline Rpc is also sent through host's local connection and applied. - // applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter. - // in other words: never apply the rpcs in host mode. - if (isServer) return; - - using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) - { - // deserialize - // save last deserialized baseline tick number to compare deltas against - lastDeserializedBaselineTick = reader.ReadByte(); - OnDeserializeBaseline(reader, lastDeserializedBaselineTick); - } - } - - // unreliable delta. - // include owner in case of server authority. - [ClientRpc(channel = Channels.Unreliable)] - void RpcServerToClientDelta(ArraySegment data) - { - // delta is broadcast to all clients. - // ignore if this object is owned by this client. - if (IsClientWithAuthority) return; - - // host mode: baseline Rpc is also sent through host's local connection and applied. - // applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter. - // in other words: never apply the rpcs in host mode. - if (isServer) return; - - // deserialize - using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) - { - // deserialize - byte baselineTick = reader.ReadByte(); - - // ensure this delta is for our last known baseline. - // we should never apply a delta on top of a wrong baseline. - if (baselineTick != lastDeserializedBaselineTick) - { - OnDrop(lastDeserializedBaselineTick, baselineTick, reader); - - // this can happen if unreliable arrives before reliable etc. - // no need to log this except when debugging. - if (debugLog) Debug.Log($"[{name}] Client: received delta for wrong baseline #{baselineTick}. Last was {lastDeserializedBaselineTick}. Ignoring."); - return; - } - - OnDeserializeDelta(reader, baselineTick); - } - } - - [Command(channel = Channels.Reliable)] // reliable baseline - void CmdClientToServerBaseline(ArraySegment data) - { - // deserialize - using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) - { - // deserialize - lastDeserializedBaselineTick = reader.ReadByte(); - OnDeserializeBaseline(reader, lastDeserializedBaselineTick); - } - } - - [Command(channel = Channels.Unreliable)] // unreliable delta - void CmdClientToServerDelta(ArraySegment data) - { - using (NetworkReaderPooled reader = NetworkReaderPool.Get(data)) - { - // deserialize - byte baselineTick = reader.ReadByte(); - - // ensure this delta is for our last known baseline. - // we should never apply a delta on top of a wrong baseline. - if (baselineTick != lastDeserializedBaselineTick) - { - OnDrop(lastDeserializedBaselineTick, baselineTick, reader); - - // this can happen if unreliable arrives before reliable etc. - // no need to log this except when debugging. - if (debugLog) Debug.Log($"[{name}] Server: received delta for wrong baseline #{baselineTick} from: {connectionToClient}. Last was {lastDeserializedBaselineTick}. Ignoring."); - return; - } - - OnDeserializeDelta(reader, baselineTick); - } - } - - // update server /////////////////////////////////////////////////////// - protected virtual void UpdateServerBaseline(double localTime) - { - // send a reliable baseline every 1 Hz - if (localTime < lastBaselineTime + baselineInterval) return; - - // only sync if changed since last reliable baseline - if (!changedSinceBaseline) return; - - // save bandwidth by only transmitting what is needed. - // -> ArraySegment with random data is slower since byte[] copying - // -> Vector3? and Quaternion? nullables takes more bandwidth - byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! - - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // serialize - writer.WriteByte(frameCount); - OnSerializeBaseline(writer); - - // send (no need for redundancy since baseline is reliable) - RpcServerToClientBaseline(writer); - } - - // save the last baseline's tick number. - // included in baseline to identify which one it was on client - // included in deltas to ensure they are on top of the correct baseline - lastSerializedBaselineTick = frameCount; - lastBaselineTime = NetworkTime.localTime; - - // perf. & bandwidth optimization: - // send a delta right after baseline to avoid potential head of - // line blocking, or skip the delta whenever we sent reliable? - // for example: - // 1 Hz baseline - // 10 Hz delta - // => 11 Hz total if we still send delta after reliable - // => 10 Hz total if we skip delta after reliable - // in that case, skip next delta by simply resetting last delta sync's time. - if (baselineIsDelta) lastDeltaTime = localTime; - - // request to store last baseline state (i.e. position) for change detection. - StoreState(); - - // baseline was just sent after a change. reset change detection. - changedSinceBaseline = false; - - if (debugLog) Debug.Log($"[{name}] Server: sent baseline #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}"); - } - - protected virtual void UpdateServerDelta(double localTime) - { - // broadcast to all clients each 'sendInterval' - // (client with authority will drop the rpc) - // NetworkTime.localTime for double precision until Unity has it too - // - // IMPORTANT: - // snapshot interpolation requires constant sending. - // DO NOT only send if position changed. for example: - // --- - // * client sends first position at t=0 - // * ... 10s later ... - // * client moves again, sends second position at t=10 - // --- - // * server gets first position at t=0 - // * server gets second position at t=10 - // * server moves from first to second within a time of 10s - // => would be a super slow move, instead of a wait & move. - // - // IMPORTANT: - // DO NOT send nulls if not changed 'since last send' either. we - // send unreliable and don't know which 'last send' the other end - // received successfully. - // - // Checks to ensure server only sends snapshots if object is - // on server authority(!clientAuthority) mode because on client - // authority mode snapshots are broadcasted right after the authoritative - // client updates server in the command function(see above), OR, - // since host does not send anything to update the server, any client - // authoritative movement done by the host will have to be broadcasted - // here by checking IsClientWithAuthority. - // TODO send same time that NetworkServer sends time snapshot? - - if (localTime < lastDeltaTime + syncInterval) return; - - // look for changes every unreliable sendInterval! - // every reliable interval isn't enough, this would cause MrG's grid issue: - // server start in A1, reliable baseline sent to client - // server moves to A2, unreliabe delta sent to client - // server moves to A1, nothing is sent to client becuase last baseline position == position - // => client wouldn't know we moved back to A1 - // every update works, but it's unnecessary overhead since sends only happen every sendInterval - // every unreliable sendInterval is the perfect place to look for changes. - if (StateChanged()) changedSinceBaseline = true; - - // only sync on change: - // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. - if (!changedSinceBaseline) return; - - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // serialize - writer.WriteByte(lastSerializedBaselineTick); - OnSerializeDelta(writer); - - // send (with optional redundancy to make up for message drops) - RpcServerToClientDelta(writer); - if (unreliableRedundancy) - RpcServerToClientDelta(writer); - } - - lastDeltaTime = localTime; - - if (debugLog) Debug.Log($"[{name}] Server: sent delta for #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}"); - } - - protected virtual void UpdateServerSync() - { - // server broadcasts all objects all the time. - // -> not just ServerToClient: ClientToServer need to be broadcast to others too - - // perf: only grab NetworkTime.localTime property once. - double localTime = NetworkTime.localTime; - - // broadcast - UpdateServerBaseline(localTime); - UpdateServerDelta(localTime); - } - - // update client /////////////////////////////////////////////////////// - protected virtual void UpdateClientBaseline(double localTime) - { - // send a reliable baseline every 1 Hz - if (localTime < lastBaselineTime + baselineInterval) return; - - // only sync if changed since last reliable baseline - if (!changedSinceBaseline) return; - - // save bandwidth by only transmitting what is needed. - // -> ArraySegment with random data is slower since byte[] copying - // -> Vector3? and Quaternion? nullables takes more bandwidth - byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! - - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // serialize - writer.WriteByte(frameCount); - OnSerializeBaseline(writer); - - // send (no need for redundancy since baseline is reliable) - CmdClientToServerBaseline(writer); - } - - // save the last baseline's tick number. - // included in baseline to identify which one it was on client - // included in deltas to ensure they are on top of the correct baseline - lastSerializedBaselineTick = frameCount; - lastBaselineTime = NetworkTime.localTime; - - // perf. & bandwidth optimization: - // send a delta right after baseline to avoid potential head of - // line blocking, or skip the delta whenever we sent reliable? - // for example: - // 1 Hz baseline - // 10 Hz delta - // => 11 Hz total if we still send delta after reliable - // => 10 Hz total if we skip delta after reliable - // in that case, skip next delta by simply resetting last delta sync's time. - if (baselineIsDelta) lastDeltaTime = localTime; - - // request to store last baseline state (i.e. position) for change detection. - // IMPORTANT - // OnSerialize(initial) is called for the spawn payload whenever - // someone starts observing this object. we always must make - // this the new baseline, otherwise this happens: - // - server broadcasts baseline @ t=1 - // - server broadcasts delta for baseline @ t=1 - // - ... time passes ... - // - new observer -> OnSerialize sends current position @ t=2 - // - server broadcasts delta for baseline @ t=1 - // => client's baseline is t=2 but receives delta for t=1 _!_ - StoreState(); - - // baseline was just sent after a change. reset change detection. - changedSinceBaseline = false; - - if (debugLog) Debug.Log($"[{name}] Client: sent baseline #{lastSerializedBaselineTick} at time: {localTime}"); - } - - protected virtual void UpdateClientDelta(double localTime) - { - // send to server each 'sendInterval' - // NetworkTime.localTime for double precision until Unity has it too - // - // IMPORTANT: - // snapshot interpolation requires constant sending. - // DO NOT only send if position changed. for example: - // --- - // * client sends first position at t=0 - // * ... 10s later ... - // * client moves again, sends second position at t=10 - // --- - // * server gets first position at t=0 - // * server gets second position at t=10 - // * server moves from first to second within a time of 10s - // => would be a super slow move, instead of a wait & move. - // - // IMPORTANT: - // DO NOT send nulls if not changed 'since last send' either. we - // send unreliable and don't know which 'last send' the other end - // received successfully. - - if (localTime < lastDeltaTime + syncInterval) return; - - // look for changes every unreliable sendInterval! - // every reliable interval isn't enough, this would cause MrG's grid issue: - // server start in A1, reliable baseline sent to client - // server moves to A2, unreliabe delta sent to client - // server moves to A1, nothing is sent to client becuase last baseline position == position - // => client wouldn't know we moved back to A1 - // every update works, but it's unnecessary overhead since sends only happen every sendInterval - // every unreliable sendInterval is the perfect place to look for changes. - if (StateChanged()) changedSinceBaseline = true; - - // only sync on change: - // unreliable isn't guaranteed to be delivered so this depends on reliable baseline. - if (!changedSinceBaseline) return; - - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // serialize - writer.WriteByte(lastSerializedBaselineTick); - OnSerializeDelta(writer); - - // send (with optional redundancy to make up for message drops) - CmdClientToServerDelta(writer); - if (unreliableRedundancy) - CmdClientToServerDelta(writer); - } - - lastDeltaTime = localTime; - - if (debugLog) Debug.Log($"[{name}] Client: sent delta for #{lastSerializedBaselineTick} at time: {localTime}"); - } - - protected virtual void UpdateClientSync() - { - // client authority, and local player (= allowed to move myself)? - if (IsClientWithAuthority) - { - // https://github.com/vis2k/Mirror/pull/2992/ - if (!NetworkClient.ready) return; - - // perf: only grab NetworkTime.localTime property once. - double localTime = NetworkTime.localTime; - - UpdateClientBaseline(localTime); - UpdateClientDelta(localTime); - } - } - - // Update() without LateUpdate() split: otherwise perf. is cut in half! - protected virtual void Update() - { - // if server then always sync to others. - if (isServer) UpdateServerSync(); - // 'else if' because host mode shouldn't send anything to server. - // it is the server. don't overwrite anything there. - else if (isClient) UpdateClientSync(); - } - - // OnSerialize(initial) is called every time when a player starts observing us. - // note this is _not_ called just once on spawn. - // call this from inheriting classes immediately in OnSerialize(). - public override void OnSerialize(NetworkWriter writer, bool initialState) - { - if (initialState) - { - // always include the tick for deltas to compare against. - byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once! - writer.WriteByte(frameCount); - - // IMPORTANT - // OnSerialize(initial) is called for the spawn payload whenever - // someone starts observing this object. we always must make - // this the new baseline, otherwise this happens: - // - server broadcasts baseline @ t=1 - // - server broadcasts delta for baseline @ t=1 - // - ... time passes ... - // - new observer -> OnSerialize sends current position @ t=2 - // - server broadcasts delta for baseline @ t=1 - // => client's baseline is t=2 but receives delta for t=1 _!_ - lastSerializedBaselineTick = (byte)Time.frameCount; - lastBaselineTime = NetworkTime.localTime; - - // request to store last baseline state (i.e. position) for change detection. - StoreState(); - } - } - - // call this from inheriting classes immediately in OnDeserialize(). - public override void OnDeserialize(NetworkReader reader, bool initialState) - { - if (initialState) - { - // save last deserialized baseline tick number to compare deltas against - lastDeserializedBaselineTick = reader.ReadByte(); - } - } - } -} diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index fc8562620f..c35e5b55f0 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Mirror.RemoteCalls; @@ -33,6 +33,17 @@ public static partial class NetworkClient public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms static double lastSendTime; + // ocassionally send a full reliable state for unreliable components to delta compress against. + // this only applies to Components with SyncMethod=Unreliable. + public static int unreliableBaselineRate => NetworkServer.unreliableBaselineRate; + public static float unreliableBaselineInterval => NetworkServer.unreliableBaselineInterval; + static double lastUnreliableBaselineTime; + + // quake sends unreliable messages twice to make up for message drops. + // this double bandwidth, but allows for smaller buffer time / faster sync. + // best to turn this off unless the game is extremely fast paced. + public static bool unreliableRedundancy => NetworkServer.unreliableRedundancy; + // For security, it is recommended to disconnect a player if a networked // action triggers an exception\nThis could prevent components being // accessed in an undefined state, which may be an attack vector for @@ -520,6 +531,8 @@ internal static void RegisterMessageHandlers(bool hostMode) RegisterHandler(_ => { }); // host mode doesn't need state updates RegisterHandler(_ => { }); + RegisterHandler(_ => { }); + RegisterHandler(_ => { }); } else { @@ -531,6 +544,8 @@ internal static void RegisterMessageHandlers(bool hostMode) RegisterHandler(OnObjectSpawnStarted); RegisterHandler(OnObjectSpawnFinished); RegisterHandler(OnEntityStateMessage); + RegisterHandler(OnEntityStateMessageUnreliableBaseline); + RegisterHandler(OnEntityStateMessageUnreliableDelta); } // These handlers are the same for host and remote clients @@ -1475,6 +1490,90 @@ static void OnEntityStateMessage(EntityStateMessage message) else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); } + static void OnEntityStateMessageUnreliableBaseline(EntityStateMessageUnreliableBaseline message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Reliable) + { + Debug.LogError($"Client OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!"); + return; + } + + // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // set the last received reliable baseline tick number. + identity.lastUnreliableBaselineReceived = message.baselineTick; + + // iniital is always 'true' because unreliable state sync alwasy serializes full + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // full state updates (initial=true) arrive over reliable. + identity.DeserializeClient(reader, true); + } + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + + static void OnEntityStateMessageUnreliableDelta(EntityStateMessageUnreliableDelta message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Unreliable) + { + Debug.LogError($"Client OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!"); + return; + } + + // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // unreliable state sync messages may arrive out of order. + // only ever apply state that's newer than the last received state. + // note that we send one EntityStateMessage per Entity, + // so there will be multiple with the same == timestamp. + // + // note that a reliable baseline may arrive before/after a delta. + // that is fine. + if (connection.remoteTimeStamp < identity.lastUnreliableStateTime) + { + // debug log to show that it's working. + // can be tested via LatencySimulation scramble easily. + Debug.Log($"Client caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + // UDP messages may accidentally arrive twice. + // or even intentionally, if unreliableRedundancy is turned on. + else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime) + { + // only log this if unreliableRedundancy is disabled. + // otherwise it's expected and will happen a lot. + if (!unreliableRedundancy) Debug.Log($"Client caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + + // make sure this delta is for the correct baseline. + // we don't want to apply an old delta on top of a new baseline. + if (message.baselineTick != identity.lastUnreliableBaselineReceived) + { + Debug.Log($"Client caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine."); + return; + } + + // set the new last received time for unreliable + identity.lastUnreliableStateTime = connection.remoteTimeStamp; + + // iniital is always 'true' because unreliable state sync alwasy serializes full + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // delta state updates (initial=false) arrive over unreliable. + identity.DeserializeClient(reader, false); + } + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + static void OnRPCMessage(RpcMessage message) { // Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.netId}"); @@ -1504,8 +1603,34 @@ internal static void OnSpawn(SpawnMessage message) // Defer ApplySpawnPayload until OnObjectSpawnFinished // add to spawned because later when we ApplySpawnPayload // there may be SyncVars that cross-reference other objects + + // When deferring ApplySpawnPayload via pendingSpawns until OnObjectSpawnFinished, + // simply copying the SpawnMessage struct isn't sufficient. The payload is an + // ArraySegment referencing the original buffer received from the server, + // managed by the client's NetworkReaderPooled. This buffer may be recycled or + // reused after OnSpawn but before ApplySpawnPayload, leading to corruption + // (e.g., EndOfStreamException in NetworkReader.ReadBlittable when reading past + // available bytes, as seen with 20+ objects in Benchmark). Deep copying payload + // ensures the data remains intact and independent of the reader's pooled buffer + // lifecycle, preventing corruption during deferred application. + // Note that payload.Count is 0 if there are no components to deserialize, which + // means payload.Array is null, so we must skip the deep copy in such cases. + byte[] payloadCopy = new byte[message.payload.Count]; + if (message.payload.Count > 0) + Array.Copy(message.payload.Array, message.payload.Offset, payloadCopy, 0, message.payload.Count); + SpawnMessage messageCopy = new SpawnMessage + { + netId = message.netId, + spawnFlags = message.spawnFlags, // Preserves isOwner and isLocalPlayer via flags + sceneId = message.sceneId, + assetId = message.assetId, + position = message.position, + rotation = message.rotation, + scale = message.scale, + payload = new ArraySegment(payloadCopy) + }; spawned[message.netId] = identity; - pendingSpawns[identity] = message; + pendingSpawns[identity] = messageCopy; } } } @@ -1599,9 +1724,10 @@ internal static void NetworkLateUpdate() // // Unity 2019 doesn't have Time.timeAsDouble yet bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime); if (!Application.isPlaying || sendIntervalElapsed) { - Broadcast(); + Broadcast(unreliableBaselineElapsed); } UpdateConnectionQuality(); @@ -1665,7 +1791,9 @@ void UpdateConnectionQuality() // broadcast /////////////////////////////////////////////////////////// // make sure Broadcast() is only called every sendInterval. // calling it every update() would require too much bandwidth. - static void Broadcast() + // + // unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + static void Broadcast(bool unreliableBaselineElapsed) { // joined the world yet? if (!connection.isReady) return; @@ -1677,12 +1805,14 @@ static void Broadcast() Send(new TimeSnapshotMessage(), Channels.Unreliable); // broadcast client state to server - BroadcastToServer(); + BroadcastToServer(unreliableBaselineElapsed); } // NetworkServer has BroadcastToConnection. // NetworkClient has BroadcastToServer. - static void BroadcastToServer() + // + // unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + static void BroadcastToServer(bool unreliableBaselineElapsed) { // for each entity that the client owns foreach (NetworkIdentity identity in connection.owned) @@ -1693,21 +1823,64 @@ static void BroadcastToServer() // NetworkServer.Destroy) if (identity != null) { - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + // 'Reliable' sync: send Reliable components over reliable. + using (NetworkWriterPooled writerReliable = NetworkWriterPool.Get(), + writerUnreliableDelta = NetworkWriterPool.Get(), + writerUnreliableBaseline = NetworkWriterPool.Get()) { - // get serialization for this entity viewed by this connection - // (if anything was serialized this time) - identity.SerializeClient(writer); - if (writer.Position > 0) + // serialize reliable and unreliable components in only one iteration. + // serializing reliable and unreliable separately in two iterations would be too costly. + identity.SerializeClient(writerReliable, writerUnreliableBaseline, writerUnreliableDelta, unreliableBaselineElapsed); + + // any reliable components serialization? + if (writerReliable.Position > 0) { // send state update message EntityStateMessage message = new EntityStateMessage { netId = identity.netId, - payload = writer.ToArraySegment() + payload = writerReliable.ToArraySegment() }; Send(message); } + + // any unreliable components serialization? + // we always send unreliable deltas to ensure interpolation always has a data point that arrives immediately. + if (writerUnreliableDelta.Position > 0) + { + EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta + { + // baselineTick: the last unreliable baseline to compare against + baselineTick = identity.lastUnreliableBaselineSent, + netId = identity.netId, + payload = writerUnreliableDelta.ToArraySegment() + }; + Send(message, Channels.Unreliable); + } + + // time for unreliable baseline sync? + // we always send this after the unreliable delta, + // so there's a higher chance that it arrives after the delta. + // in other words: so that the delta can still be used against previous baseline. + if (unreliableBaselineElapsed) + { + if (writerUnreliableBaseline.Position > 0) + { + // remember last sent baseline tick for this entity. + // (byte) to minimize bandwidth. we don't need the full tick, + // just something small to compare against. + identity.lastUnreliableBaselineSent = (byte)Time.frameCount; + + // send state update message + EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline + { + baselineTick = identity.lastUnreliableBaselineSent, + netId = identity.netId, + payload = writerUnreliableBaseline.ToArraySegment() + }; + Send(message, Channels.Reliable); + } + } } } // spawned list should have no null entries because we @@ -1882,6 +2055,7 @@ public static void Shutdown() OnTransportExceptionEvent = null; } +#if !UNITY_SERVER // GUI ///////////////////////////////////////////////////////////////// // called from NetworkManager to display timeline interpolation status. // useful to indicate catchup / slowdown / dynamic adjustment etc. @@ -1911,5 +2085,6 @@ public static void OnGUI() GUILayout.EndArea(); } +#endif } } diff --git a/Assets/Mirror/Core/NetworkConnection.cs b/Assets/Mirror/Core/NetworkConnection.cs index 851beaea09..1268ca2159 100644 --- a/Assets/Mirror/Core/NetworkConnection.cs +++ b/Assets/Mirror/Core/NetworkConnection.cs @@ -10,11 +10,6 @@ public abstract class NetworkConnection { public const int LocalConnectionId = 0; - /// Unique identifier for this connection that is assigned by the transport layer. - // assigned by transport, this id is unique for every connection on server. - // clients don't know their own id and they don't know other client's ids. - public readonly int connectionId; - /// Flag that indicates the client has been authenticated. public bool isAuthenticated; @@ -68,11 +63,6 @@ internal NetworkConnection() lastMessageTime = Time.time; } - internal NetworkConnection(int networkConnectionId) : this() - { - connectionId = networkConnectionId; - } - // TODO if we only have Reliable/Unreliable, then we could initialize // two batches and avoid this code protected Batcher GetBatchForChannelId(int channelId) @@ -213,7 +203,5 @@ public virtual void Cleanup() batcher.Clear(); } } - - public override string ToString() => $"connection({connectionId})"; } } diff --git a/Assets/Mirror/Core/NetworkConnectionToClient.cs b/Assets/Mirror/Core/NetworkConnectionToClient.cs index 9872e862c0..fdd634d038 100644 --- a/Assets/Mirror/Core/NetworkConnectionToClient.cs +++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs @@ -16,6 +16,11 @@ public class NetworkConnectionToClient : NetworkConnection public virtual string address { get; private set; } + /// Unique identifier for this connection that is assigned by the transport layer. + // assigned by transport, this id is unique for every connection on server. + // clients don't know their own id and they don't know other client's ids. + public readonly int connectionId; + /// NetworkIdentities that this connection can see // TODO move to server's NetworkConnectionToClient? public readonly HashSet observing = new HashSet(); @@ -54,9 +59,11 @@ public class NetworkConnectionToClient : NetworkConnection /// Round trip time (in seconds) that it takes a message to go server->client->server. public double rtt => _rtt.Value; - public NetworkConnectionToClient(int networkConnectionId, string clientAddress = "localhost") - : base(networkConnectionId) + internal NetworkConnectionToClient() : base() { } + + public NetworkConnectionToClient(int networkConnectionId, string clientAddress = "localhost") : base() { + connectionId = networkConnectionId; address = clientAddress; // initialize EMA with 'emaDuration' seconds worth of history. @@ -112,6 +119,8 @@ public void RemoveDirty(NetworkIdentity RemovingDirty) } + public override string ToString() => $"connection({connectionId})"; + public void OnTimeSnapshot(TimeSnapshot snapshot) { // protect against ever growing buffer size attacks diff --git a/Assets/Mirror/Core/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs index cde005b970..47d3d1354e 100644 --- a/Assets/Mirror/Core/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -27,13 +27,26 @@ public struct NetworkIdentitySerialization { // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks public int tick; - public NetworkWriter ownerWriter; - public NetworkWriter observersWriter; + + // reliable sync + public NetworkWriter ownerWriterReliable; + public NetworkWriter observersWriterReliable; + + // unreliable sync + public NetworkWriter ownerWriterUnreliableBaseline; + public NetworkWriter observersWriterUnreliableBaseline; + + public NetworkWriter ownerWriterUnreliableDelta; + public NetworkWriter observersWriterUnreliableDelta; public void ResetWriters() { - ownerWriter.Position = 0; - observersWriter.Position = 0; + ownerWriterReliable.Position = 0; + observersWriterReliable.Position = 0; + ownerWriterUnreliableBaseline.Position = 0; + observersWriterUnreliableBaseline.Position = 0; + ownerWriterUnreliableDelta.Position = 0; + observersWriterUnreliableDelta.Position = 0; } } @@ -60,7 +73,7 @@ public bool isDirty if (observers == null || observers.Count == 0) { - return; + return; } foreach (var Observer in observers) @@ -249,10 +262,23 @@ internal set // => way easier to store them per object NetworkIdentitySerialization lastSerialization = new NetworkIdentitySerialization { - ownerWriter = new NetworkWriter(), - observersWriter = new NetworkWriter() + ownerWriterReliable = new NetworkWriter(), + observersWriterReliable = new NetworkWriter(), + ownerWriterUnreliableBaseline = new NetworkWriter(), + observersWriterUnreliableBaseline = new NetworkWriter(), + ownerWriterUnreliableDelta = new NetworkWriter(), + observersWriterUnreliableDelta = new NetworkWriter(), }; + // unreliable state sync messages may arrive out of order, or duplicated. + // keep latest received timestamp so we don't apply older messages. + internal double lastUnreliableStateTime; + + // the last baseline we received for this object. + // deltas are based on the baseline, need to make sure we don't apply on an old one. + internal byte lastUnreliableBaselineSent; + internal byte lastUnreliableBaselineReceived; + // Keep track of all sceneIds to detect scene duplicates public static readonly Dictionary sceneIds = new Dictionary(); @@ -916,7 +942,7 @@ internal void OnStopLocalPlayer() // build dirty mask for server owner & observers (= all dirty components). // faster to do it in one iteration instead of iterating separately. - (ulong, ulong) ServerDirtyMasks(bool initialState) + (ulong, ulong) ServerDirtyMasks_Spawn() { ulong ownerMask = 0; ulong observerMask = 0; @@ -927,38 +953,140 @@ internal void OnStopLocalPlayer() NetworkBehaviour component = components[i]; ulong nthBit = 1ul << i; - bool dirty = component.IsDirty(); - // owner needs to be considered for both SyncModes, because // Observers mode always includes the Owner. // - // for initial, it should always sync owner. - // for delta, only for ServerToClient and only if dirty. - // ClientToServer comes from the owner client. - if (initialState || (component.syncDirection == SyncDirection.ServerToClient && dirty)) - ownerMask |= nthBit; + // for spawn message, it should always sync owner. + ownerMask |= nthBit; // observers need to be considered only in Observers mode, // otherwise they receive no sync data of this component ever. if (component.syncMode == SyncMode.Observers) { - // for initial, it should always sync to observers. - // for delta, only if dirty. + // for spawn message, it should always sync to observers. // SyncDirection is irrelevant, as both are broadcast to // observers which aren't the owner. - if (initialState || dirty) - observerMask |= nthBit; + observerMask |= nthBit; } } return (ownerMask, observerMask); } - // build dirty mask for client. + // build dirty mask for server owner & observers (= all dirty components). + // faster to do it in one iteration instead of iterating separately. + // -> build Reliable and Unreliable masks in one iteration. + // running two loops would be too costly. + void ServerDirtyMasks_Broadcast( + out ulong ownerMaskReliable, out ulong observerMaskReliable, + out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline, + out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta) + { + // clear + ownerMaskReliable = 0; + observerMaskReliable = 0; + ownerMaskUnreliableBaseline = 0; + observerMaskUnreliableBaseline = 0; + ownerMaskUnreliableDelta = 0; + observerMaskUnreliableDelta = 0; + + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour component = components[i]; + ulong nthBit = 1ul << i; + + // RELIABLE COMPONENTS ///////////////////////////////////////// + if (component.syncMethod == SyncMethod.Reliable) + { + // check if this component is dirty + bool dirty = component.IsDirty(); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for broadcast, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (component.syncDirection == SyncDirection.ServerToClient && dirty) + ownerMaskReliable |= nthBit; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) + { + // for broadcast, only sync to observers if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (dirty) observerMaskReliable |= nthBit; + } + } + // UNRELIABLE COMPONENTS /////////////////////////////////////// + else if (component.syncMethod == SyncMethod.Hybrid) + { + // UNRELIABLE DELTAS /////////////////////////////////////// + { + // check if this component is dirty. + // delta sync runs @ syncInterval. + // this allows for significant bandwidth savings. + bool dirty = component.IsDirty(); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for broadcast, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (component.syncDirection == SyncDirection.ServerToClient && dirty) + ownerMaskUnreliableDelta |= nthBit; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) + { + // for broadcast, only sync to observers if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (dirty) observerMaskUnreliableDelta |= nthBit; + } + } + // UNRELIABLE BASELINE ///////////////////////////////////// + { + // check if this component is dirty. + // baseline sync runs @ 1 Hz (netmanager configurable). + // only consider dirty bits, ignore syncinterval. + bool dirty = component.IsDirty_BitsOnly(); + + // owner needs to be considered for both SyncModes, because + // Observers mode always includes the Owner. + // + // for broadcast, only for ServerToClient and only if dirty. + // ClientToServer comes from the owner client. + if (component.syncDirection == SyncDirection.ServerToClient && dirty) + ownerMaskUnreliableBaseline |= nthBit; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) + { + // for broadcast, only sync to observers if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (dirty) observerMaskUnreliableBaseline |= nthBit; + } + } + //////////////////////////////////////////////////////////// + } + } + } + + // build dirty mask for client components. // server always knows initialState, so we don't need it here. - ulong ClientDirtyMask() + // -> build Reliable and Unreliable masks in one iteration. + // running two loops would be too costly. + void ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta) { - ulong mask = 0; + dirtyMaskReliable = 0; + dirtyMaskUnreliableBaseline = 0; + dirtyMaskUnreliableDelta = 0; NetworkBehaviour[] components = NetworkBehaviours; for (int i = 0; i < components.Length; ++i) @@ -976,13 +1104,29 @@ ulong ClientDirtyMask() if (isOwned && component.syncDirection == SyncDirection.ClientToServer) { - // set the n-th bit if dirty - // shifting from small to large numbers is varint-efficient. - if (component.IsDirty()) mask |= nthBit; + // RELIABLE COMPONENTS ///////////////////////////////////////// + if (component.syncMethod == SyncMethod.Reliable) + { + // set the n-th bit if dirty + // shifting from small to large numbers is varint-efficient. + if (component.IsDirty()) dirtyMaskReliable |= nthBit; + } + // UNRELIABLE COMPONENTS /////////////////////////////////////// + else if (component.syncMethod == SyncMethod.Hybrid) + { + // set the n-th bit if dirty + // shifting from small to large numbers is varint-efficient. + + // baseline sync runs @ 1 Hz (netmanager configurable). + // only consider dirty bits, ignore syncinterval. + if (component.IsDirty_BitsOnly()) dirtyMaskUnreliableBaseline |= nthBit; + + // delta sync runs @ syncInterval. + // this allows for significant bandwidth savings. + if (component.IsDirty()) dirtyMaskUnreliableDelta |= nthBit; + } } } - - return mask; } // check if n-th component is dirty. @@ -994,9 +1138,9 @@ internal static bool IsDirty(ulong mask, int index) return (mask & nthBit) != 0; } - // serialize components into writer on the server. + // serialize server components, with full state for spawn message. // check ownerWritten/observersWritten to know if anything was written - internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) + internal void SerializeServer_Spawn(NetworkWriter ownerWriter, NetworkWriter observersWriter) { // ensure NetworkBehaviours are valid before usage ValidateComponents(); @@ -1012,7 +1156,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw // instead of writing a 1 byte index per component, // we limit components to 64 bits and write one ulong instead. // the ulong is also varint compressed for minimum bandwidth. - (ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState); + (ulong ownerMask, ulong observerMask) = ServerDirtyMasks_Spawn(); // if nothing dirty, then don't even write the mask. // otherwise, every unchanged object would send a 1 byte dirty mask! @@ -1046,7 +1190,7 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw // serialize into helper writer using (NetworkWriterPooled temp = NetworkWriterPool.Get()) { - comp.Serialize(temp, initialState); + comp.Serialize(temp, true); ArraySegment segment = temp.ToArraySegment(); // copy to owner / observers as needed @@ -1054,25 +1198,146 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); } - // clear dirty bits for the components that we serialized. - // do not clear for _all_ components, only the ones that - // were dirty and had their syncInterval elapsed. - // - // we don't want to clear bits before the syncInterval - // was elapsed, as then they wouldn't be synced. - // - // only clear for delta, not for full (spawn messages). - // otherwise if a player joins, we serialize monster, - // and shouldn't clear dirty bits not yet synced to - // other players. - if (!initialState) comp.ClearAllDirtyBits(); + // dirty bits indicate 'changed since last delta sync'. + // don't clear then on full sync here, since full sync + // is called whenever a new player spawns and needs the + // full state! + //comp.ClearAllDirtyBits(); } } } } - // serialize components into writer on the client. - internal void SerializeClient(NetworkWriter writer) + // serialize server components, with delta state for broadcast messages. + // check ownerWritten/observersWritten to know if anything was written + // + // serialize Reliable and Unreliable components in one iteration. + // having two separate functions doing two iterations would be too costly. + internal void SerializeServer_Broadcast( + NetworkWriter ownerWriterReliable, NetworkWriter observersWriterReliable, + NetworkWriter ownerWriterUnreliableBaseline, NetworkWriter observersWriterUnreliableBaseline, + NetworkWriter ownerWriterUnreliableDelta, NetworkWriter observersWriterUnreliableDelta, + bool unreliableBaseline) + { + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // check which components are dirty for owner / observers. + // this is quite complicated with SyncMode + SyncDirection. + // see the function for explanation. + // + // instead of writing a 1 byte index per component, + // we limit components to 64 bits and write one ulong instead. + // the ulong is also varint compressed for minimum bandwidth. + ServerDirtyMasks_Broadcast( + out ulong ownerMaskReliable, out ulong observerMaskReliable, + out ulong ownerMaskUnreliableBaseline, out ulong observerMaskUnreliableBaseline, + out ulong ownerMaskUnreliableDelta, out ulong observerMaskUnreliableDelta + ); + + // if nothing dirty, then don't even write the mask. + // otherwise, every unchanged object would send a 1 byte dirty mask! + if (ownerMaskReliable != 0) Compression.CompressVarUInt(ownerWriterReliable, ownerMaskReliable); + if (observerMaskReliable != 0) Compression.CompressVarUInt(observersWriterReliable, observerMaskReliable); + + if (ownerMaskUnreliableDelta != 0) Compression.CompressVarUInt(ownerWriterUnreliableDelta, ownerMaskUnreliableDelta); + if (observerMaskUnreliableDelta != 0) Compression.CompressVarUInt(observersWriterUnreliableDelta, observerMaskUnreliableDelta); + + if (ownerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(ownerWriterUnreliableBaseline, ownerMaskUnreliableBaseline); + if (observerMaskUnreliableBaseline != 0) Compression.CompressVarUInt(observersWriterUnreliableBaseline, observerMaskUnreliableBaseline); + + // serialize all components + // perf: only iterate if either dirty mask has dirty bits. + if ((ownerMaskReliable | observerMaskReliable | + ownerMaskUnreliableBaseline | observerMaskUnreliableBaseline | + ownerMaskUnreliableDelta | observerMaskUnreliableDelta) + != 0) + { + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour comp = components[i]; + + // is the component dirty for anyone (owner or observers)? + // may be serialized to owner, observer, both, or neither. + // + // OnSerialize should only be called once. + // this is faster, and it cleaner because it may set + // internal state, counters, logs, etc. + // + // previously we always serialized to owner and then copied + // the serialization to observers. however, since + // SyncDirection it's not guaranteed to be in owner anymore. + // so we need to serialize to temporary writer first. + // and then copy as needed. + bool ownerDirtyReliable = IsDirty(ownerMaskReliable, i); + bool observersDirtyReliable = IsDirty(observerMaskReliable, i); + bool ownerDirtyUnreliableBaseline = IsDirty(ownerMaskUnreliableBaseline, i); + bool observersDirtyUnreliableBaseline = IsDirty(observerMaskUnreliableBaseline, i); + bool ownerDirtyUnreliableDelta = IsDirty(ownerMaskUnreliableDelta, i); + bool observersDirtyUnreliableDelta = IsDirty(observerMaskUnreliableDelta, i); + + // RELIABLE COMPONENTS ///////////////////////////////////// + if (ownerDirtyReliable || observersDirtyReliable) + { + // serialize into helper writer + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, false); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirtyReliable) ownerWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirtyReliable) observersWriterReliable.WriteBytes(segment.Array, segment.Offset, segment.Count); + } + + // dirty bits indicate 'changed since last delta sync'. + // clear them after a delta sync here. + comp.ClearAllDirtyBits(); + } + // UNRELIABLE DELTA //////////////////////////////////////// + // we always send the unreliable delta no matter what + if (ownerDirtyUnreliableDelta || observersDirtyUnreliableDelta) + { + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, false); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirtyUnreliableDelta) ownerWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirtyUnreliableDelta) observersWriterUnreliableDelta.WriteBytes(segment.Array, segment.Offset, segment.Count); + + // clear sync time to only send delta again after syncInterval. + comp.lastSyncTime = NetworkTime.localTime; + } + } + // UNRELIABLE BASELINE ///////////////////////////////////// + // sometimes we need the unreliable baseline + // (we always sync deltas, so no 'else if' here) + if (unreliableBaseline && (ownerDirtyUnreliableBaseline || observersDirtyUnreliableBaseline)) + { + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, true); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirtyUnreliableBaseline) ownerWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count); + if (observersDirtyUnreliableBaseline) observersWriterUnreliableBaseline.WriteBytes(segment.Array, segment.Offset, segment.Count); + } + + // for unreliable components, only clear dirty bits after the reliable baseline. + // -> don't clear sync time: that's for delta syncs. + comp.ClearAllDirtyBits(false); + } + } + } + } + + // serialize Reliable and Unreliable components in one iteration. + // having two separate functions doing two iterations would be too costly. + internal void SerializeClient(NetworkWriter writerReliable, NetworkWriter writerUnreliableBaseline, NetworkWriter writerUnreliableDelta, bool unreliableBaseline) { // ensure NetworkBehaviours are valid before usage ValidateComponents(); @@ -1085,7 +1350,7 @@ internal void SerializeClient(NetworkWriter writer) // instead of writing a 1 byte index per component, // we limit components to 64 bits and write one ulong instead. // the ulong is also varint compressed for minimum bandwidth. - ulong dirtyMask = ClientDirtyMask(); + ClientDirtyMasks(out ulong dirtyMaskReliable, out ulong dirtyMaskUnreliableBaseline, out ulong dirtyMaskUnreliableDelta); // varint compresses the mask to 1 byte in most cases. // instead of writing an 8 byte ulong. @@ -1096,25 +1361,28 @@ internal void SerializeClient(NetworkWriter writer) // if nothing dirty, then don't even write the mask. // otherwise, every unchanged object would send a 1 byte dirty mask! - if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask); + if (dirtyMaskReliable != 0) Compression.CompressVarUInt(writerReliable, dirtyMaskReliable); + if (dirtyMaskUnreliableDelta != 0) Compression.CompressVarUInt(writerUnreliableDelta, dirtyMaskUnreliableDelta); + if (dirtyMaskUnreliableBaseline != 0) Compression.CompressVarUInt(writerUnreliableBaseline, dirtyMaskUnreliableBaseline); // serialize all components // perf: only iterate if dirty mask has dirty bits. - if (dirtyMask != 0) + if (dirtyMaskReliable != 0 || dirtyMaskUnreliableDelta != 0 || dirtyMaskUnreliableBaseline != 0) { // serialize all components for (int i = 0; i < components.Length; ++i) { NetworkBehaviour comp = components[i]; + // RELIABLE SERIALIZATION ////////////////////////////////// // is this component dirty? // reuse the mask instead of calling comp.IsDirty() again here. - if (IsDirty(dirtyMask, i)) + if (IsDirty(dirtyMaskReliable, i)) // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) { // serialize into writer. // server always knows initialState, we never need to send it - comp.Serialize(writer, false); + comp.Serialize(writerReliable, false); // clear dirty bits for the components that we serialized. // do not clear for _all_ components, only the ones that @@ -1124,13 +1392,39 @@ internal void SerializeClient(NetworkWriter writer) // was elapsed, as then they wouldn't be synced. comp.ClearAllDirtyBits(); } + // UNRELIABLE DELTA //////////////////////////////////////// + // we always send the unreliable delta no matter what + if (IsDirty(dirtyMaskUnreliableDelta, i)) + // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + comp.Serialize(writerUnreliableDelta, false); + + // clear sync time to only send delta again after syncInterval. + comp.lastSyncTime = NetworkTime.localTime; + } + // UNRELIABLE BASELINE ///////////////////////////////////// + // sometimes we need the unreliable baseline + // (we always sync deltas, so no 'else if' here) + if (unreliableBaseline && IsDirty(dirtyMaskUnreliableBaseline, i)) + // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + comp.Serialize(writerUnreliableBaseline, true); + + // for unreliable components, only clear dirty bits after the reliable baseline. + // unreliable deltas aren't guaranteed to be delivered, no point in clearing bits. + // -> don't clear sync time: that's for delta syncs. + comp.ClearAllDirtyBits(false); + } + + //////////////////////////////////////////////////////////// } } } // deserialize components from the client on the server. - // there's no 'initialState'. server always knows the initial state. - internal bool DeserializeServer(NetworkReader reader) + // for reliable state sync, server always knows the initial state. + // for unreliable, we always sync full state so we still need the parameter. + internal bool DeserializeServer(NetworkReader reader, bool initialState) { // ensure NetworkBehaviours are valid before usage ValidateComponents(); @@ -1154,7 +1448,7 @@ internal bool DeserializeServer(NetworkReader reader) // deserialize this component // server always knows the initial state (initial=false) // disconnect if failed, to prevent exploits etc. - if (!comp.Deserialize(reader, false)) return false; + if (!comp.Deserialize(reader, initialState)) return false; // server received state from the owner client. // set dirty so it's broadcast to other clients too. @@ -1198,8 +1492,10 @@ internal void DeserializeClient(NetworkReader reader, bool initialState) // get cached serialization for this tick (or serialize if none yet). // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks. // calls SerializeServer, so this function is to be called on server. - /// UNITYSTATION CODE /// needs lock - internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick) + // + // unreliableBaselineElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + // for reliable components, it just means sync as usual. + internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick, bool unreliableBaselineElapsed) { // only rebuild serialization once per tick. reuse otherwise. // except for tests, where Time.frameCount never increases. @@ -1295,7 +1591,7 @@ internal void ClearAllComponentsDirtyBits() } // this is used when a connection is destroyed, since the "observers" property is read-only - internal void RemoveObserver(NetworkConnection conn) + internal void RemoveObserver(NetworkConnectionToClient conn) { observers.Remove(conn.connectionId); } diff --git a/Assets/Mirror/Core/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs index 114578b55d..dbbe7d98dc 100644 --- a/Assets/Mirror/Core/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -42,6 +42,16 @@ public class NetworkManager : MonoBehaviour [FormerlySerializedAs("serverTickRate")] public int sendRate = 60; + /// + [Tooltip("Ocassionally send a full reliable state for unreliable components to delta compress against. This only applies to Components with SyncMethod=Unreliable.")] + public int unreliableBaselineRate = 1; + + // quake sends unreliable messages twice to make up for message drops. + // this double bandwidth, but allows for smaller buffer time / faster sync. + // best to turn this off unless the game is extremely fast paced. + [Tooltip("Send unreliable messages twice to make up for message drops. This doubles bandwidth, but allows for smaller buffer time / faster sync.\nBest to turn this off unless your game is extremely fast paced.")] + public bool unreliableRedundancy = false; + // client send rate follows server send rate to avoid errors for now /// Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. // [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] @@ -165,6 +175,11 @@ public class NetworkManager : MonoBehaviour // virtual so that inheriting classes' OnValidate() can call base.OnValidate() too public virtual void OnValidate() { + // unreliable full send rate needs to be >= 0. + // we need to have something to delta compress against. + // it should also be <= sendRate otherwise there's no point. + unreliableBaselineRate = Mathf.Clamp(unreliableBaselineRate, 1, sendRate); + // always >= 0 maxConnections = Mathf.Max(maxConnections, 0); @@ -273,6 +288,8 @@ bool IsServerOnlineSceneChangeNeeded() => void ApplyConfiguration() { NetworkServer.tickRate = sendRate; + NetworkServer.unreliableBaselineRate = unreliableBaselineRate; + NetworkServer.unreliableRedundancy = unreliableRedundancy; NetworkClient.snapshotSettings = snapshotSettings; NetworkClient.connectionQualityInterval = evaluationInterval; NetworkClient.connectionQualityMethod = evaluationMethod; @@ -1439,7 +1456,7 @@ public virtual void OnStopClient() { } /// This is called when a host is stopped. public virtual void OnStopHost() { } -#if DEBUG +#if !UNITY_SERVER && DEBUG // keep OnGUI even in builds. useful to debug snap interp. void OnGUI() { diff --git a/Assets/Mirror/Core/NetworkManagerHUD.cs b/Assets/Mirror/Core/NetworkManagerHUD.cs index f78eedefaa..8d7519d5a6 100644 --- a/Assets/Mirror/Core/NetworkManagerHUD.cs +++ b/Assets/Mirror/Core/NetworkManagerHUD.cs @@ -19,6 +19,7 @@ void Awake() manager = GetComponent(); } +#if !UNITY_SERVER void OnGUI() { // If this width is changed, also change offsetX in GUIConsole::OnGUI @@ -158,5 +159,6 @@ void StopButtons() manager.StopServer(); } } +#endif } } diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 43647895f9..acd71a0754 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -68,6 +68,17 @@ public static partial class NetworkServer public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms static double lastSendTime; + // ocassionally send a full reliable state for unreliable components to delta compress against. + // this only applies to Components with SyncMethod=Unreliable. + public static int unreliableBaselineRate = 1; + public static float unreliableBaselineInterval => unreliableBaselineRate < int.MaxValue ? 1f / unreliableBaselineRate : 0; // for 1 Hz, that's 1000ms + static double lastUnreliableBaselineTime; + + // quake sends unreliable messages twice to make up for message drops. + // this double bandwidth, but allows for smaller buffer time / faster sync. + // best to turn this off unless the game is extremely fast paced. + public static bool unreliableRedundancy = false; + /// Connection to host mode client (if any) public static LocalConnectionToClient localConnection { get; private set; } @@ -337,6 +348,8 @@ internal static void RegisterMessageHandlers() RegisterHandler(NetworkTime.OnServerPing, false); RegisterHandler(NetworkTime.OnServerPong, false); RegisterHandler(OnEntityStateMessage, true); + RegisterHandler(OnEntityStateMessageUnreliableBaseline, true); + RegisterHandler(OnEntityStateMessageUnreliableDelta, true); RegisterHandler(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through } @@ -431,7 +444,10 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta { // DeserializeServer checks permissions internally. // failure to deserialize disconnects to prevent exploits. - if (!identity.DeserializeServer(reader)) + // -> initialState=false because for Reliable messages, + // initial always comes from server and broadcast + // updates are always deltas. + if (!identity.DeserializeServer(reader, false)) { if (exceptionsDisconnect) { @@ -454,6 +470,137 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); } + // for client's owned ClientToServer components. + static void OnEntityStateMessageUnreliableBaseline(NetworkConnectionToClient connection, EntityStateMessageUnreliableBaseline message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Reliable) + { + Debug.LogError($"Server OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!"); + return; + } + + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. + + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + // set the last received reliable baseline tick number. + identity.lastUnreliableBaselineReceived = message.baselineTick; + + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + // + // full state updates (initial=true) arrive over reliable. + if (!identity.DeserializeServer(reader, true)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}."); + } + } + } + // An attacker may attempt to modify another connection's entity + // This could also be a race condition of message in flight when + // RemoveClientAuthority is called, so not malicious. + // Don't disconnect, just log the warning. + else + Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority."); + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + + // for client's owned ClientToServer components. + static void OnEntityStateMessageUnreliableDelta(NetworkConnectionToClient connection, EntityStateMessageUnreliableDelta message, int channelId) + { + // safety check: baseline should always arrive over Reliable channel. + if (channelId != Channels.Unreliable) + { + Debug.LogError($"Server OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!"); + return; + } + + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. + + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + // unreliable state sync messages may arrive out of order. + // only ever apply state that's newer than the last received state. + // note that we send one EntityStateMessage per Entity, + // so there will be multiple with the same == timestamp. + if (connection.remoteTimeStamp < identity.lastUnreliableStateTime) + { + // debug log to show that it's working. + // can be tested via LatencySimulation scramble easily. + Debug.Log($"Server caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + // UDP messages may accidentally arrive twice. + // or even intentionally, if unreliableRedundancy is turned on. + else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime) + { + // only log this if unreliableRedundancy is disabled. + // otherwise it's expected and will happen a lot. + if (!unreliableRedundancy) Debug.Log($"Server caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}"); + return; + } + + // make sure this delta is for the correct baseline. + // we don't want to apply an old delta on top of a new baseline. + if (message.baselineTick != identity.lastUnreliableBaselineReceived) + { + Debug.Log($"Server caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine."); + return; + } + + // set the new last received time for unreliable + identity.lastUnreliableStateTime = connection.remoteTimeStamp; + + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + // + // delta state updates (initial=false) arrive over unreliable. + if (!identity.DeserializeServer(reader, false)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}."); + } + } + } + // An attacker may attempt to modify another connection's entity + // This could also be a race condition of message in flight when + // RemoveClientAuthority is called, so not malicious. + // Don't disconnect, just log the warning. + else + Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority."); + } + // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + // client sends TimeSnapshotMessage every sendInterval. // batching already includes the remoteTimestamp. // we simply insert it on-message here. @@ -1223,7 +1370,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga switch (replacePlayerOptions) { - case ReplacePlayerOptions.KeepAuthority: + case ReplacePlayerOptions.KeepAuthority: // This needs to be sent to clear isLocalPlayer on // client while keeping hasAuthority true SendChangeOwnerMessage(previousPlayer, conn); @@ -1389,13 +1536,13 @@ public static void SetAllClientsNotReady() } // show / hide for connection ////////////////////////////////////////// - internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn) + internal static void ShowForConnection(NetworkIdentity identity, NetworkConnectionToClient conn) { if (conn.isReady) SendSpawnMessage(identity, conn); } - internal static void HideForConnection(NetworkIdentity identity, NetworkConnection conn) + internal static void HideForConnection(NetworkIdentity identity, NetworkConnectionToClient conn) { ObjectHideMessage msg = new ObjectHideMessage { @@ -1405,7 +1552,7 @@ internal static void HideForConnection(NetworkIdentity identity, NetworkConnecti } // spawning //////////////////////////////////////////////////////////// - internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn) + internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectionToClient conn) { if (identity.serverOnly) return; @@ -1443,7 +1590,7 @@ static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentit // serialize all components with initialState = true // (can be null if has none) - identity.SerializeServer(true, ownerWriter, observersWriter); + identity.SerializeServer_Spawn(ownerWriter, observersWriter); // convert to ArraySegment to avoid reader allocations // if nothing was written, .ToArraySegment returns an empty segment. @@ -1576,14 +1723,14 @@ static void Respawn(NetworkIdentity identity) /// Spawn the given game object on all clients which are ready. // This will cause a new object to be instantiated from the registered // prefab, or from a custom spawn function. - public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) + public static void Spawn(GameObject obj, NetworkConnectionToClient ownerConnection = null) { SpawnObject(obj, ownerConnection); } /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerConnection = null) + public static void Spawn(GameObject obj, uint assetId, NetworkConnectionToClient ownerConnection = null) { if (GetNetworkIdentity(obj, out NetworkIdentity identity)) { @@ -1592,7 +1739,7 @@ public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerCo SpawnObject(obj, ownerConnection); } - static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) + static void SpawnObject(GameObject obj, NetworkConnectionToClient ownerConnection) { // verify if we can spawn this if (Utils.IsPrefab(obj)) @@ -1916,11 +2063,18 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize) // broadcasting //////////////////////////////////////////////////////// // helper function to get the right serialization for a connection - static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + // -> unreliableBaselineElapsed: even though we only care about RELIABLE + // components here, GetServerSerializationAtTick still caches all + // the serializations for this frame. and when caching we already + // need to know if the unreliable baseline will be needed or not. + static NetworkWriter SerializeForConnection_ReliableComponents( + NetworkIdentity identity, + NetworkConnectionToClient connection, + bool unreliableBaselineElapsed) { // get serialization for this entity (cached) // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(FrameCountCash); + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed); // is this entity owned by this connection? bool owned = identity.connectionToClient == connection; @@ -1930,21 +2084,63 @@ static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkCon if (owned) { // was it dirty / did we actually serialize anything? - if (serialization.ownerWriter.Position > 0) - return serialization.ownerWriter; + if (serialization.ownerWriterReliable.Position > 0) + return serialization.ownerWriterReliable; } // observers writer if not owned else { // was it dirty / did we actually serialize anything? - if (serialization.observersWriter.Position > 0) - return serialization.observersWriter; + if (serialization.observersWriterReliable.Position > 0) + return serialization.observersWriterReliable; } // nothing was serialized return null; } + // helper function to get the right serialization for a connection + static (NetworkWriter, NetworkWriter) SerializeForConnection_UnreliableComponents( + NetworkIdentity identity, + NetworkConnectionToClient connection, + bool unreliableBaselineElapsed) + { + // get serialization for this entity (cached) + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount, unreliableBaselineElapsed); + + // is this entity owned by this connection? + bool owned = identity.connectionToClient == connection; + + NetworkWriter baselineWriter = null; + NetworkWriter deltaWriter = null; + + // send serialized data + // owner writer if owned + if (owned) + { + // was it dirty / did we actually serialize anything? + if (serialization.ownerWriterUnreliableBaseline.Position > 0) + baselineWriter = serialization.ownerWriterUnreliableBaseline; + + if (serialization.ownerWriterUnreliableDelta.Position > 0) + deltaWriter = serialization.ownerWriterUnreliableDelta; + } + // observers writer if not owned + else + { + // was it dirty / did we actually serialize anything? + if (serialization.observersWriterUnreliableBaseline.Position > 0) + baselineWriter = serialization.observersWriterUnreliableBaseline; + + if (serialization.observersWriterUnreliableDelta.Position > 0) + deltaWriter = serialization.observersWriterUnreliableDelta; + } + + // nothing was serialized + return (baselineWriter, deltaWriter); + } + // helper function to broadcast the world to a connection static void BroadcastToConnection(NetworkConnectionToClient connection, bool unreliableBaselineElapsed) { @@ -2078,7 +2274,8 @@ static bool DisconnectIfInactive(NetworkConnectionToClient connection) public static int FrameCountCash; public static bool ApplicationIsPlayingCash; - static void Broadcast() + // unreliableFullSendIntervalElapsed: indicates that unreliable sync components need a reliable baseline sync this time. + static void Broadcast(bool unreliableBaselineElapsed) { // copy all connections into a helper collection so that // OnTransportDisconnected can be called while iterating. @@ -2103,7 +2300,7 @@ public static void SubConnectionBroadcast(NetworkConnectionToClient connection) //CUSTOM UNITYSTATION CODE// So we can log any errors that go on With Unity funnies with logs on Thread try { - // check for inactivity. disconnects if necessary. + // check for inactivity. disconnects if necessary. if (DisconnectIfInactive(connection)) return; // has this connection joined the world yet? @@ -2187,8 +2384,9 @@ internal static void NetworkLateUpdate() // snapshots _but_ not every single tick. // Unity 2019 doesn't have Time.timeAsDouble yet bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + bool unreliableBaselineElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, unreliableBaselineInterval, ref lastUnreliableBaselineTime); if (!Application.isPlaying || sendIntervalElapsed) - Broadcast(); + Broadcast(unreliableBaselineElapsed); } // process all outgoing messages after updating the world diff --git a/Assets/Mirror/Editor/AndroidManifestHelper.cs b/Assets/Mirror/Editor/AndroidManifestHelper.cs index c76359a8a9..bae0082e71 100644 --- a/Assets/Mirror/Editor/AndroidManifestHelper.cs +++ b/Assets/Mirror/Editor/AndroidManifestHelper.cs @@ -10,104 +10,107 @@ using UnityEditor.Android; #endif - -[InitializeOnLoad] -public class AndroidManifestHelper : IPreprocessBuildWithReport, IPostprocessBuildWithReport -#if UNITY_ANDROID - , IPostGenerateGradleAndroidProject -#endif +namespace Mirror { - public int callbackOrder { get { return 99999; } } - -#if UNITY_ANDROID - public void OnPostGenerateGradleAndroidProject(string path) - { - string manifestFolder = Path.Combine(path, "src/main"); - string sourceFile = manifestFolder + "/AndroidManifest.xml"; - // Load android manifest file - XmlDocument doc = new XmlDocument(); - doc.Load(sourceFile); + [InitializeOnLoad] + public class AndroidManifestHelper : IPreprocessBuildWithReport, IPostprocessBuildWithReport + #if UNITY_ANDROID + , IPostGenerateGradleAndroidProject + #endif + { + public int callbackOrder { get { return 99999; } } - string androidNamepsaceURI; - XmlElement element = (XmlElement)doc.SelectSingleNode("/manifest"); - if (element == null) - { - UnityEngine.Debug.LogError("Could not find manifest tag in android manifest."); - return; - } + #if UNITY_ANDROID + public void OnPostGenerateGradleAndroidProject(string path) + { + string manifestFolder = Path.Combine(path, "src/main"); + string sourceFile = manifestFolder + "/AndroidManifest.xml"; + // Load android manifest file + XmlDocument doc = new XmlDocument(); + doc.Load(sourceFile); - // Get android namespace URI from the manifest - androidNamepsaceURI = element.GetAttribute("xmlns:android"); - if (string.IsNullOrEmpty(androidNamepsaceURI)) - { - UnityEngine.Debug.LogError("Could not find Android Namespace in manifest."); - return; - } - AddOrRemoveTag(doc, - androidNamepsaceURI, - "/manifest", - "uses-permission", - "android.permission.CHANGE_WIFI_MULTICAST_STATE", - true, - false); - AddOrRemoveTag(doc, - androidNamepsaceURI, - "/manifest", - "uses-permission", - "android.permission.INTERNET", - true, - false); - doc.Save(sourceFile); - } -#endif + string androidNamespaceURI; + XmlElement element = (XmlElement)doc.SelectSingleNode("/manifest"); + if (element == null) + { + UnityEngine.Debug.LogError("Could not find manifest tag in android manifest."); + return; + } - static void AddOrRemoveTag(XmlDocument doc, string @namespace, string path, string elementName, string name, bool required, bool modifyIfFound, params string[] attrs) // name, value pairs - { - var nodes = doc.SelectNodes(path + "/" + elementName); - XmlElement element = null; - foreach (XmlElement e in nodes) - { - if (name == null || name == e.GetAttribute("name", @namespace)) + // Get android namespace URI from the manifest + androidNamespaceURI = element.GetAttribute("xmlns:android"); + if (string.IsNullOrEmpty(androidNamespaceURI)) { - element = e; - break; + UnityEngine.Debug.LogError("Could not find Android Namespace in manifest."); + return; } + AddOrRemoveTag(doc, + androidNamespaceURI, + "/manifest", + "uses-permission", + "android.permission.CHANGE_WIFI_MULTICAST_STATE", + true, + false); + AddOrRemoveTag(doc, + androidNamespaceURI, + "/manifest", + "uses-permission", + "android.permission.INTERNET", + true, + false); + doc.Save(sourceFile); } + #endif - if (required) + static void AddOrRemoveTag(XmlDocument doc, string @namespace, string path, string elementName, string name, bool required, bool modifyIfFound, params string[] attrs) // name, value pairs { - if (element == null) + var nodes = doc.SelectNodes(path + "/" + elementName); + XmlElement element = null; + foreach (XmlElement e in nodes) { - var parent = doc.SelectSingleNode(path); - element = doc.CreateElement(elementName); - element.SetAttribute("name", @namespace, name); - parent.AppendChild(element); + if (name == null || name == e.GetAttribute("name", @namespace)) + { + element = e; + break; + } } - for (int i = 0; i < attrs.Length; i += 2) + if (required) { - if (modifyIfFound || string.IsNullOrEmpty(element.GetAttribute(attrs[i], @namespace))) + if (element == null) { - if (attrs[i + 1] != null) - { - element.SetAttribute(attrs[i], @namespace, attrs[i + 1]); - } - else + var parent = doc.SelectSingleNode(path); + element = doc.CreateElement(elementName); + element.SetAttribute("name", @namespace, name); + parent.AppendChild(element); + } + + for (int i = 0; i < attrs.Length; i += 2) + { + if (modifyIfFound || string.IsNullOrEmpty(element.GetAttribute(attrs[i], @namespace))) { - element.RemoveAttribute(attrs[i], @namespace); + if (attrs[i + 1] != null) + { + element.SetAttribute(attrs[i], @namespace, attrs[i + 1]); + } + else + { + element.RemoveAttribute(attrs[i], @namespace); + } } } } - } - else - { - if (element != null && modifyIfFound) + else { - element.ParentNode.RemoveChild(element); + if (element != null && modifyIfFound) + { + element.ParentNode.RemoveChild(element); + } } } - } - public void OnPostprocessBuild(BuildReport report) {} - public void OnPreprocessBuild(BuildReport report) {} + public void OnPostprocessBuild(BuildReport report) {} + public void OnPreprocessBuild(BuildReport report) {} + } } + diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs index 09ac765fce..975108e602 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -97,6 +97,16 @@ protected void DrawDefaultSyncSettings() if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient) EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + // sync method + SerializedProperty syncMethod = serializedObject.FindProperty("syncMethod"); + EditorGUILayout.PropertyField(syncMethod); + + // Unreliable sync method: show a warning! + if (syncMethod.enumValueIndex == (int)SyncMethod.Hybrid) + { + EditorGUILayout.HelpBox("Beware! Hybrid is experimental!\n- Do not use this in production yet!\n- Doesn't support [SyncVars] yet!\n- You need to use OnDe/Serialize manually!", MessageType.Warning); + } + // sync interval EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval")); diff --git a/Assets/Mirror/Examples/Editor.meta b/Assets/Mirror/Examples/Editor.meta new file mode 100644 index 0000000000..a18042571d --- /dev/null +++ b/Assets/Mirror/Examples/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e52b854d04a1b747b68c5274396cd71 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs b/Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs new file mode 100644 index 0000000000..89ffd5f132 --- /dev/null +++ b/Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs @@ -0,0 +1,291 @@ +#if UNITY_EDITOR +using UnityEngine; +using UnityEditor; +using System.IO; +using System.Linq; + +namespace Mirror.Examples.Editor +{ + [InitializeOnLoad] + public class MirrorRenderPipelineConverter + { + private const string CONVERTED_FLAG_FILE = "MirrorExamplesPipelineConverted.txt"; + + static MirrorRenderPipelineConverter() + { + EditorApplication.delayCall += CheckPipelineOnLoad; + } + + private static void CheckPipelineOnLoad() + { + if (HasConvertedFlag()) + return; + + RenderPipelineType currentPipeline = DetectRenderPipeline(); + if (currentPipeline == RenderPipelineType.BuiltIn) + { + SetConvertedFlag(); + return; + } + + string examplesPath = GetExamplesFolderPath(); + if (string.IsNullOrEmpty(examplesPath)) + { + Debug.LogError("Could not locate Examples folder!"); + return; + } + + int choice = EditorUtility.DisplayDialogComplex( + $"Mirror Examples {currentPipeline} Conversion", + $"Mirror examples need to be converted to {currentPipeline}.\n\nThis will only affect {examplesPath} and subfolders.", + "Go Ahead", // 0 - Left (confirm) + "Don't Ask Again", // 1 - Right (cancel) + "Not Now" // 2 - Middle (alternative) + ); + + switch (choice) + { + case 0: // Go Ahead + ConvertMaterials(currentPipeline, examplesPath); + SetConvertedFlag(); + Debug.Log("Mirror Examples materials converted to " + currentPipeline); + break; + case 1: // Don't Ask Again + SetConvertedFlag(); + break; + case 2: // Not Now + break; + } + } + + private enum RenderPipelineType + { + BuiltIn, + URP + } + + private static RenderPipelineType DetectRenderPipeline() + { + var pipeline = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline; + return pipeline != null && pipeline.GetType().Name.Contains("Universal") ? RenderPipelineType.URP : RenderPipelineType.BuiltIn; + } + + private static string GetExamplesFolderPath() + { + string[] guids = AssetDatabase.FindAssets("MirrorRenderPipelineConverter t:Script"); + if (guids.Length == 0) return null; + string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]); + return Path.GetDirectoryName(Path.GetDirectoryName(scriptPath))?.Replace("\\", "/"); + } + + /// + /// Converts materials in the Examples folder to match the current render pipeline. + /// Only processes .mat files and forces conversion for legacy/built-in shaders. + /// + private static void ConvertMaterials(RenderPipelineType targetPipeline, string examplesPath) + { + if (!Directory.Exists(examplesPath)) + { + Debug.LogError($"Examples folder not found at: {examplesPath}"); + return; + } + + try + { + // Find all .mat files in Examples folder and subfolders + string[] materialGuids = AssetDatabase.FindAssets("t:Material", new[] { examplesPath }) + .Where(guid => AssetDatabase.GUIDToAssetPath(guid).EndsWith(".mat")) + .ToArray(); + + foreach (string guid in materialGuids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + Material mat = AssetDatabase.LoadAssetAtPath(path); + + if (mat != null) + { + string shaderName = mat.shader.name; + + // Force conversion for non-URP shaders that won't render correctly + bool needsConversion = shaderName.StartsWith("Legacy Shaders/") || + shaderName == "Standard" || + shaderName == "Standard (Specular setup)"; + + if (!needsConversion && Shader.Find(shaderName) != null) + { + Debug.Log($"Shader '{shaderName}' exists and is assumed URP-compatible, skipping: {path}"); + continue; + } + + if (targetPipeline == RenderPipelineType.URP) + ConvertToURP(mat); + } + } + + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + catch (System.Exception e) + { + Debug.LogError($"Error during material conversion: {e.Message}"); + } + } + + /// + /// Converts a material to URP-compatible shaders, preserving properties like color, texture, + /// tiling, transparency, and forward rendering options (specular highlights and reflections). + /// + private static void ConvertToURP(Material material) + { + string originalShaderName = material.shader.name; + + // Capture all relevant properties before changing the shader + Color initialColor = material.GetColor("_Color"); + Debug.Log($"Initial _Color for '{material.name}': {initialColor} (Hex: {ColorUtility.ToHtmlStringRGBA(initialColor)})", material); + + Texture mainTex = null; + Vector4 mainTexST = Vector4.zero; // x, y = tiling; z, w = offset + if (material.HasProperty("_MainTex")) + { + mainTex = material.GetTexture("_MainTex"); + mainTexST = material.GetVector("_MainTex_ST"); + Debug.Log($"Initial _MainTex for '{material.name}': {(mainTex != null ? mainTex.name : "null")}, Tiling/Offset: {mainTexST}", material); + } + else if (material.mainTexture != null) + { + mainTex = material.mainTexture; + mainTexST = new Vector4(material.mainTextureScale.x, material.mainTextureScale.y, material.mainTextureOffset.x, material.mainTextureOffset.y); + Debug.Log($"Initial mainTexture fallback for '{material.name}': {mainTex.name}, Tiling/Offset: {mainTexST}", material); + } + + Texture bumpMap = null; + if (material.HasProperty("_BumpMap")) + { + bumpMap = material.GetTexture("_BumpMap"); + Debug.Log($"Initial _BumpMap for '{material.name}': {(bumpMap != null ? bumpMap.name : "null")}", material); + } + + float metallic = material.HasProperty("_Metallic") ? material.GetFloat("_Metallic") : 0f; + float smoothness = material.HasProperty("_Glossiness") ? material.GetFloat("_Glossiness") : 0f; + Debug.Log($"Initial Metallic for '{material.name}': {metallic}, Smoothness: {smoothness}", material); + + bool isTransparent = material.renderQueue == (int)UnityEngine.Rendering.RenderQueue.Transparent; + + // Capture Forward Rendering Options (Specular Highlights and Reflections) + float specularHighlights = material.HasProperty("_SpecularHighlights") ? material.GetFloat("_SpecularHighlights") : 0f; // Default off + float environmentReflections = material.HasProperty("_GlossyReflections") ? material.GetFloat("_GlossyReflections") : 0f; // Default off + Debug.Log($"Initial Specular Highlights for '{material.name}': {(specularHighlights > 0 ? "On" : "Off")}, Environment Reflections: {(environmentReflections > 0 ? "On" : "Off")}", material); + + // Handle skybox materials + if (originalShaderName.StartsWith("Skybox/")) + { + if (originalShaderName == "Skybox/6 Sided") + { + Shader urpSixSidedShader = Shader.Find("Skybox/6 Sided"); + if (urpSixSidedShader != null && material.shader != urpSixSidedShader) + { + material.shader = urpSixSidedShader; + Debug.Log($"Converted to URP Skybox/6 Sided: {AssetDatabase.GetAssetPath(material)}", material); + } + return; + } + else + { + Shader urpSkyboxShader = Shader.Find("Skybox/Panoramic"); + if (urpSkyboxShader != null && material.shader != urpSkyboxShader) + { + material.shader = urpSkyboxShader; + if (material.HasProperty("_Tex")) + material.SetTexture("_MainTex", material.GetTexture("_Tex")); + else if (material.HasProperty("_FrontTex")) + material.SetTexture("_MainTex", material.GetTexture("_FrontTex")); + if (material.HasProperty("_Tint")) + material.SetColor("_Tint", material.GetColor("_Tint")); + Debug.Log($"Converted to URP Skybox/Panoramic: {AssetDatabase.GetAssetPath(material)}", material); + } + return; + } + } + + // Convert to URP Lit for all other materials + Shader urpLitShader = Shader.Find("Universal Render Pipeline/Lit"); + if (urpLitShader == null) + { + Debug.LogError("URP Lit shader not found in project!"); + return; + } + + material.shader = urpLitShader; + + // Apply texture and tiling + if (mainTex != null) + { + material.SetTexture("_BaseMap", mainTex); + material.SetVector("_BaseMap_ST", mainTexST); + Debug.Log($"Set _BaseMap for '{material.name}' to: {mainTex.name}, Tiling/Offset: {mainTexST}", material); + } + else + { + Debug.Log($"No albedo texture found for '{material.name}' at {AssetDatabase.GetAssetPath(material)}", material); + } + + // Apply color + Color baseColor = initialColor == Color.clear ? Color.white : initialColor; + material.SetColor("_BaseColor", baseColor); + Debug.Log($"Set _BaseColor for '{material.name}' to: {baseColor} (Hex: {ColorUtility.ToHtmlStringRGBA(baseColor)})", material); + + // Apply normal map + if (bumpMap != null) + { + material.SetTexture("_BumpMap", bumpMap); + Debug.Log($"Set _BumpMap for '{material.name}' to: {bumpMap.name}", material); + } + + // Apply metallic and smoothness + material.SetFloat("_Metallic", metallic); + material.SetFloat("_Smoothness", smoothness); + Debug.Log($"Set Metallic for '{material.name}' to: {metallic}, Smoothness to: {smoothness}", material); + + // Apply forward rendering options + material.SetFloat("_SpecularHighlights", specularHighlights); + material.SetFloat("_EnvironmentReflections", environmentReflections); + Debug.Log($"Set Specular Highlights for '{material.name}' to: {(specularHighlights > 0 ? "On" : "Off")}, Environment Reflections to: {(environmentReflections > 0 ? "On" : "Off")}", material); + + // Set surface type and blending + if (isTransparent) + { + material.SetFloat("_Surface", 1f); // Transparent + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); + material.SetInt("_ZWrite", 0); + material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent; + Debug.Log($"Set '{material.name}' to Transparent Surface Type", material); + } + else + { + material.SetFloat("_Surface", 0f); // Opaque + material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One); + material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero); + material.SetInt("_ZWrite", 1); + material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Geometry; + Debug.Log($"Set '{material.name}' to Opaque Surface Type", material); + } + + Debug.Log($"Converted '{originalShaderName}' to URP Lit: {AssetDatabase.GetAssetPath(material)}", material); + } + + private static bool HasConvertedFlag() + { + string flagPath = Path.Combine(Application.dataPath, "..", CONVERTED_FLAG_FILE); + return File.Exists(flagPath); + } + + private static void SetConvertedFlag() + { + string flagPath = Path.Combine(Application.dataPath, "..", CONVERTED_FLAG_FILE); + File.WriteAllText(flagPath, "Converted: " + System.DateTime.Now.ToString()); + AssetDatabase.Refresh(); + } + } +} +#endif diff --git a/Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs.meta b/Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs.meta new file mode 100644 index 0000000000..85c17389e2 --- /dev/null +++ b/Assets/Mirror/Examples/Editor/MirrorRenderPipelineConverter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b86c03b3e3faa24ca5391f2f60427fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash.meta b/Assets/Mirror/Examples/HexSpatialHash.meta new file mode 100644 index 0000000000..f32561a259 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 508be872df2cab54d8b0390b857b12e8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity b/Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity new file mode 100644 index 0000000000..746076d770 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity @@ -0,0 +1,510 @@ +%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: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 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, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + 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: 0 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + 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_UseShadowmask: 1 +--- !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 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 23800000, guid: 0bc607fa2e315482ebe98797e844e11f, type: 2} +--- !u!1 &88936773 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 88936777} + - component: {fileID: 88936776} + - component: {fileID: 88936774} + - component: {fileID: 88936778} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &88936774 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9021b6cc314944290986ab6feb48db79, type: 3} + m_Name: + m_EditorClassIdentifier: + height: 80 + offsetY: 40 + maxLogCount: 50 + showInEditor: 0 + hotKey: 96 +--- !u!20 &88936776 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 1} + 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: 1 + orthographic size: 50 + 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 &88936777 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 100, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!114 &88936778 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6635375fbc6be456ea640b75add6378e, type: 3} + m_Name: + m_EditorClassIdentifier: + showGUI: 1 + showLog: 0 +--- !u!1 &535739935 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 535739936} + - component: {fileID: 535739937} + m_Layer: 0 + m_Name: SpawnPosition + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &535739936 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 2, y: 1, z: -1} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &535739937 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1282001517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1282001518} + - component: {fileID: 1282001519} + - component: {fileID: 1282001520} + - component: {fileID: 1282001521} + - component: {fileID: 1282001522} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1282001518 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + 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 &1282001519 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} + m_Name: + m_EditorClassIdentifier: + offsetX: 0 + offsetY: 0 +--- !u!114 &1282001520 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 43d8e3a03523cba438aea0b8e793b390, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 0 + runInBackground: 1 + headlessStartMode: 1 + editorAutoStart: 0 + sendRate: 30 + offlineScene: + onlineScene: + offlineSceneLoadDelay: 0 + transport: {fileID: 1282001521} + networkAddress: localhost + maxConnections: 1000 + disconnectInactiveConnections: 0 + disconnectInactiveTimeout: 60 + authenticator: {fileID: 0} + playerPrefab: {fileID: 9038517388597961393, guid: f3f3b7b9663c37141921929c9cfb2d5e, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 1 + spawnPrefabs: [] + exceptionsDisconnect: 1 + snapshotSettings: + bufferTimeMultiplier: 2 + bufferLimit: 32 + catchupNegativeThreshold: -1 + catchupPositiveThreshold: 1 + catchupSpeed: 0.019999999552965164 + slowdownSpeed: 0.03999999910593033 + driftEmaDuration: 1 + dynamicAdjustment: 1 + dynamicAdjustmentTolerance: 1 + deliveryTimeEmaDuration: 2 + evaluationMethod: 0 + evaluationInterval: 3 + timeInterpolationGui: 0 + spawnPrefab: {fileID: 5490426395924729682, guid: f389723f4f9365143bac0d85db592dbd, + type: 3} + spawnPrefabsCount: 1000 + spawnPrefabSpacing: 3 + hexSpatialHash2DInterestManagement: {fileID: 1282001522} +--- !u!114 &1282001521 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6b0fecffa3f624585964b0d0eb21b18e, type: 3} + m_Name: + m_EditorClassIdentifier: + port: 7777 + DualMode: 1 + NoDelay: 1 + Interval: 10 + Timeout: 10000 + RecvBufferSize: 7361536 + SendBufferSize: 7361536 + FastResend: 2 + ReceiveWindowSize: 4096 + SendWindowSize: 4096 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 297433 + UnreliableMaxMessageSize: 1194 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 +--- !u!114 &1282001522 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b8b055f11f85ff428da471a0e625dd4, type: 3} + m_Name: + m_EditorClassIdentifier: + rebuildInterval: 1 + staticRebuildInterval: 10 + visRange: 30 + minMoveDistance: 1 + checkMethod: 1 +--- !u!1 &2054208274 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054208276} + - component: {fileID: 2054208275} + 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 &2054208275 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 0.8 + 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_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &2054208276 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + 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: 50, y: -30, z: 0} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity.meta b/Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity.meta new file mode 100644 index 0000000000..fd74451183 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Hex2DSpatialHash.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d3ffd60e82eb25d4fb848e2becd149bf +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity b/Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity new file mode 100644 index 0000000000..b5667deef3 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity @@ -0,0 +1,509 @@ +%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: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 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, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + 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: 0 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + 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_UseShadowmask: 1 +--- !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 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 23800000, guid: 0bc607fa2e315482ebe98797e844e11f, type: 2} +--- !u!1 &88936773 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 88936777} + - component: {fileID: 88936776} + - component: {fileID: 88936774} + - component: {fileID: 88936778} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &88936774 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9021b6cc314944290986ab6feb48db79, type: 3} + m_Name: + m_EditorClassIdentifier: + height: 80 + offsetY: 40 + maxLogCount: 50 + showInEditor: 0 + hotKey: 96 +--- !u!20 &88936776 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0, g: 0, b: 0, a: 1} + 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: 1 + orthographic size: 50 + 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 &88936777 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 100, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!114 &88936778 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6635375fbc6be456ea640b75add6378e, type: 3} + m_Name: + m_EditorClassIdentifier: + showGUI: 1 + showLog: 0 +--- !u!1 &535739935 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 535739936} + - component: {fileID: 535739937} + m_Layer: 0 + m_Name: SpawnPosition + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &535739936 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 2, y: 0, z: 2} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &535739937 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1282001517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1282001518} + - component: {fileID: 1282001520} + - component: {fileID: 1282001519} + - component: {fileID: 1282001521} + - component: {fileID: 1282001522} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1282001518 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + 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 &1282001519 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} + m_Name: + m_EditorClassIdentifier: + offsetX: 0 + offsetY: 0 +--- !u!114 &1282001520 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3291bd2e9ac86c046bb768d598cd0a3f, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 0 + runInBackground: 1 + headlessStartMode: 1 + editorAutoStart: 0 + sendRate: 30 + offlineScene: + onlineScene: + offlineSceneLoadDelay: 0 + transport: {fileID: 1282001521} + networkAddress: localhost + maxConnections: 1000 + disconnectInactiveConnections: 0 + disconnectInactiveTimeout: 60 + authenticator: {fileID: 0} + playerPrefab: {fileID: 9038517388597961393, guid: 85c3b622590282745ab702bb2ae1767f, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 1 + spawnPrefabs: [] + exceptionsDisconnect: 1 + snapshotSettings: + bufferTimeMultiplier: 2 + bufferLimit: 32 + catchupNegativeThreshold: -1 + catchupPositiveThreshold: 1 + catchupSpeed: 0.019999999552965164 + slowdownSpeed: 0.03999999910593033 + driftEmaDuration: 1 + dynamicAdjustment: 1 + dynamicAdjustmentTolerance: 1 + deliveryTimeEmaDuration: 2 + evaluationMethod: 0 + evaluationInterval: 3 + timeInterpolationGui: 0 + spawnPrefab: {fileID: 5490426395924729682, guid: f389723f4f9365143bac0d85db592dbd, + type: 3} + spawnPrefabsCount: 4096 + spawnPrefabSpacing: 8 +--- !u!114 &1282001521 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6b0fecffa3f624585964b0d0eb21b18e, type: 3} + m_Name: + m_EditorClassIdentifier: + port: 7777 + DualMode: 1 + NoDelay: 1 + Interval: 10 + Timeout: 10000 + RecvBufferSize: 7361536 + SendBufferSize: 7361536 + FastResend: 2 + ReceiveWindowSize: 4096 + SendWindowSize: 4096 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 297433 + UnreliableMaxMessageSize: 1194 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 +--- !u!114 &1282001522 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 58e492e77a2a1a3488412ceed5c2aa2d, type: 3} + m_Name: + m_EditorClassIdentifier: + rebuildInterval: 1 + staticRebuildInterval: 10 + visRange: 30 + cellHeight: 25 + minMoveDistance: 1 +--- !u!1 &2054208274 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054208276} + - component: {fileID: 2054208275} + 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 &2054208275 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 0.8 + 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_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!4 &2054208276 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 0, 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/Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity.meta b/Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity.meta new file mode 100644 index 0000000000..794e7c9f85 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Hex3DSpatialHash.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e43f8c9a8ffac304d8ed2e98d0614ff8 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Mateirals.meta b/Assets/Mirror/Examples/HexSpatialHash/Mateirals.meta new file mode 100644 index 0000000000..b2aea9888e --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Mateirals.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 49e8df070aae8144baca86f4be3c571e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat b/Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat new file mode 100644 index 0000000000..f0b75b5413 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat @@ -0,0 +1,77 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RandomColor + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat.meta b/Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat.meta new file mode 100644 index 0000000000..91ddc54bef --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Mateirals/RandomColor.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 67d89500b29d14f4f909a99b79a5f2a0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Prefabs.meta b/Assets/Mirror/Examples/HexSpatialHash/Prefabs.meta new file mode 100644 index 0000000000..5f0b226e33 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eefa76359c38223449d0fcff0cdd2bbb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab new file mode 100644 index 0000000000..0bd90e456c --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab @@ -0,0 +1,170 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &9038517388597961393 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1054727129312803764} + - component: {fileID: 7123971714698760007} + - component: {fileID: 2530349739548631522} + - component: {fileID: 1425317175859650176} + - component: {fileID: 3718819456640761921} + - component: {fileID: 7085035185352152729} + - component: {fileID: 3256206181403686673} + m_Layer: 0 + m_Name: Hex2DPlayer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1054727129312803764 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + 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 &7123971714698760007 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &2530349739548631522 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + 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_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 67d89500b29d14f4f909a99b79a5f2a0, type: 2} + 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 +--- !u!114 &1425317175859650176 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 131039475 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &3718819456640761921 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8bd7ffa7c966c0c47be237edb68c98c8, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + offset: {x: 0, y: 40, z: -65} + rotation: {x: 35, y: 0, z: 0} + checkMethod: 0 +--- !u!114 &7085035185352152729 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c71d1d9ad0bdf3d498e9caac7d172f56, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + speed: 15 + checkMethod: 0 +--- !u!114 &3256206181403686673 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8f63ea2e505fd484193fb31c5c55ca73, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 1 + syncMode: 0 + syncInterval: 0.033333335 + baselineRate: 1 + unreliableRedundancy: 0 + baselineIsDelta: 1 + debugLog: 0 + useFixedUpdate: 0 + target: {fileID: 1054727129312803764} + bufferSizeLimit: 64 + sendRate: 30 + positionSensitivity: 0.01 + rotationSensitivity: 0.01 + scaleSensitivity: 0.01 + syncPosition: 1 + syncRotation: 1 + syncScale: 0 + debugDraw: 0 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab.meta b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab.meta new file mode 100644 index 0000000000..4af97bbb37 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex2DPlayer.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f3f3b7b9663c37141921929c9cfb2d5e +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab new file mode 100644 index 0000000000..9932aeb8a6 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab @@ -0,0 +1,182 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &9038517388597961393 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1054727129312803764} + - component: {fileID: 7123971714698760007} + - component: {fileID: 2530349739548631522} + - component: {fileID: 1039362684665499303} + - component: {fileID: 1425317175859650176} + - component: {fileID: 1361318049349458668} + - component: {fileID: 7085035185352152729} + - component: {fileID: 3256206181403686673} + m_Layer: 0 + m_Name: Hex3DPlayer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1054727129312803764 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + 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 &7123971714698760007 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &2530349739548631522 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + 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_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 67d89500b29d14f4f909a99b79a5f2a0, type: 2} + 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 +--- !u!136 &1039362684665499303 +CapsuleCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + m_Radius: 0.5 + m_Height: 2 + m_Direction: 1 + m_Center: {x: 0, y: 0, z: 0} +--- !u!114 &1425317175859650176 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 3737203753 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &1361318049349458668 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 71ac1e35462ffad469e77d1c2fe6c9f3, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + offset: {x: 0, y: 45, z: -85} + rotation: {x: 30, y: 0, z: 0} +--- !u!114 &7085035185352152729 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: df2e991210804be44b9ab6b7d6de4d52, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + speed: 15 +--- !u!114 &3256206181403686673 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9038517388597961393} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8f63ea2e505fd484193fb31c5c55ca73, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 1 + syncMode: 0 + syncInterval: 0.033333335 + baselineRate: 1 + unreliableRedundancy: 0 + baselineIsDelta: 1 + debugLog: 0 + target: {fileID: 1054727129312803764} + bufferSizeLimit: 64 + sendRate: 30 + positionSensitivity: 0.01 + rotationSensitivity: 0.01 + scaleSensitivity: 0.01 + syncPosition: 1 + syncRotation: 1 + syncScale: 0 + debugDraw: 0 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab.meta b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab.meta new file mode 100644 index 0000000000..8372d9ad4b --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/Hex3DPlayer.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 85c3b622590282745ab702bb2ae1767f +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab new file mode 100644 index 0000000000..e642ea3957 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab @@ -0,0 +1,118 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &5490426395924729682 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3127827758234776022} + - component: {fileID: 8291014999714722842} + - component: {fileID: 313353148227055195} + - component: {fileID: 4504475309475852547} + - component: {fileID: 7983841345410844371} + m_Layer: 0 + m_Name: SpawnPrefab + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3127827758234776022 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5490426395924729682} + 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 &8291014999714722842 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5490426395924729682} + m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &313353148227055195 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5490426395924729682} + 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_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 67d89500b29d14f4f909a99b79a5f2a0, type: 2} + 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 +--- !u!114 &4504475309475852547 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5490426395924729682} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 0 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &7983841345410844371 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5490426395924729682} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a91a718a70d01b347b75cb768a6f1a92, type: 3} + m_Name: + m_EditorClassIdentifier: + syncDirection: 0 + syncMode: 0 + syncInterval: 0 + color: + serializedVersion: 2 + rgba: 4278190080 diff --git a/Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab.meta b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab.meta new file mode 100644 index 0000000000..433cfb196f --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Prefabs/SpawnPrefab.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f389723f4f9365143bac0d85db592dbd +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts.meta b/Assets/Mirror/Examples/HexSpatialHash/Scripts.meta new file mode 100644 index 0000000000..6e8d603a11 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 481aeeeb2a2fa99469fb03fda331d565 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs new file mode 100644 index 0000000000..d1eee2db86 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs @@ -0,0 +1,86 @@ +using System; +using UnityEngine; + +namespace Mirror.Examples.Hex2D +{ + [AddComponentMenu("")] + [RequireComponent(typeof(HexSpatialHash2DInterestManagement))] + public class Hex2DNetworkManager : NetworkManager + { + // Overrides the base singleton so we don’t have to cast to this type everywhere. + public static new Hex2DNetworkManager singleton => (Hex2DNetworkManager)NetworkManager.singleton; + + [Header("Spawns")] + public GameObject spawnPrefab; + + [Range(1, 3000), Tooltip("Number of prefabs to spawn in a flat 2D grid across the scene.")] + public ushort spawnPrefabsCount = 1000; + + [Range(1, 10), Tooltip("Spacing between grid points in meters.")] + public byte spawnPrefabSpacing = 3; + + [Header("Diagnostics")] + [ReadOnly, SerializeField] HexSpatialHash2DInterestManagement hexSpatialHash2DInterestManagement; + + public override void OnValidate() + { + if (Application.isPlaying) return; + base.OnValidate(); + + if (hexSpatialHash2DInterestManagement == null) + hexSpatialHash2DInterestManagement = GetComponent(); + } + + public override void OnStartClient() + { + NetworkClient.RegisterPrefab(spawnPrefab); + } + + public override void OnStartServer() + { + // Instantiate an empty GameObject to parent spawns + GameObject spawns = new GameObject("Spawns"); + Transform spawnsTransform = spawns.transform; + + int spawned = 0; + + // Spawn prefabs in a 2D grid centered around origin (0,0,0) + int gridSize = (int)Mathf.Sqrt(spawnPrefabsCount); // Square grid size based on count + + // Calculate the starting position to center the grid at (0,0,0) + float halfGrid = (gridSize - 1) * spawnPrefabSpacing * 0.5f; + float startX = -halfGrid; + float startZorY = -halfGrid; // Z for XZ, Y for XY + + //Debug.Log($"Start Positions: X={startX}, Z/Y={startZorY}, gridSize={gridSize}"); + + // Use a 2D loop for a flat grid + for (int x = 0; x < gridSize && spawned < spawnPrefabsCount; ++x) + { + for (int zOrY = 0; zOrY < gridSize && spawned < spawnPrefabsCount; ++zOrY) + { + Vector3 position = Vector3.zero; + + if (hexSpatialHash2DInterestManagement.checkMethod == HexSpatialHash2DInterestManagement.CheckMethod.XZ_FOR_3D) + { + float xPos = startX + x * spawnPrefabSpacing; + float zPos = startZorY + zOrY * spawnPrefabSpacing; + position = new Vector3(xPos, 0.5f, zPos); + } + else // XY_FOR_2D + { + float xPos = startX + x * spawnPrefabSpacing; + float yPos = startZorY + zOrY * spawnPrefabSpacing; + position = new Vector3(xPos, yPos, -0.5f); + } + + GameObject instance = Instantiate(spawnPrefab, position, Quaternion.identity, spawnsTransform); + NetworkServer.Spawn(instance); + ++spawned; + } + } + + //Debug.Log($"Spawned {spawned} objects in a {gridSize}x{gridSize} 2D grid."); + } + } +} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs.meta b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs.meta new file mode 100644 index 0000000000..e71d3cc0ff --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DNetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43d8e3a03523cba438aea0b8e793b390 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs new file mode 100644 index 0000000000..1ef626197b --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs @@ -0,0 +1,51 @@ +using UnityEngine; + +namespace Mirror.Examples.Hex2D +{ + [AddComponentMenu("")] + public class Hex2DPlayer : NetworkBehaviour + { + [Range(1, 20)] + public float speed = 15f; + + [Header("Diagnostics")] + [ReadOnly, SerializeField] HexSpatialHash2DInterestManagement.CheckMethod checkMethod; + + void Awake() + { +#if UNITY_2022_2_OR_NEWER + checkMethod = FindAnyObjectByType().checkMethod; +#else + checkMethod = FindObjectOfType().checkMethod; +#endif + } + + void Update() + { + if (!isLocalPlayer) return; + + float h = Input.GetAxis("Horizontal"); + float v = Input.GetAxis("Vertical"); + Vector3 dir; + + if (checkMethod == HexSpatialHash2DInterestManagement.CheckMethod.XY_FOR_2D) + dir = new Vector3(h, v, 0); + else + dir = new Vector3(h, 0, v); + + transform.position += dir.normalized * (Time.deltaTime * speed); + } + +#if !UNITY_SERVER + void OnGUI() + { + if (isLocalPlayer) + { + GUILayout.BeginArea(new Rect(10, Screen.height - 25, 300, 300)); + GUILayout.Label("Use WASD to move"); + GUILayout.EndArea(); + } + } +#endif + } +} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs.meta b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs.meta new file mode 100644 index 0000000000..51234c6bbb --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c71d1d9ad0bdf3d498e9caac7d172f56 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs new file mode 100644 index 0000000000..9477237ed4 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs @@ -0,0 +1,95 @@ +using UnityEngine; +using UnityEngine.SceneManagement; + +// This sets up the scene camera for the local player + +namespace Mirror.Examples.Hex2D +{ + [AddComponentMenu("")] + [DisallowMultipleComponent] + public class Hex2DPlayerCamera : NetworkBehaviour + { + Camera mainCam; + + public Vector3 offset = new Vector3(0f, 40f, -65f); + public Vector3 rotation = new Vector3(35f, 0f, 0f); + + [Header("Diagnostics")] + [ReadOnly, SerializeField] HexSpatialHash2DInterestManagement.CheckMethod checkMethod; + + void Awake() + { + mainCam = Camera.main; +#if UNITY_2022_2_OR_NEWER + checkMethod = FindAnyObjectByType().checkMethod; +#else + checkMethod = FindObjectOfType().checkMethod; +#endif + } + + public override void OnStartLocalPlayer() + { + if (mainCam != null) + { + // configure and make camera a child of player with 3rd person offset + mainCam.transform.SetParent(transform); + + if (checkMethod == HexSpatialHash2DInterestManagement.CheckMethod.XY_FOR_2D) + { + mainCam.orthographic = true; + mainCam.transform.localPosition = new Vector3(0, 0, -5f); + mainCam.transform.localEulerAngles = Vector3.zero; + } + else + { + mainCam.orthographic = false; + mainCam.transform.localPosition = offset; + mainCam.transform.localEulerAngles = rotation; + } + } + else + Debug.LogWarning("PlayerCamera: Could not find a camera in scene with 'MainCamera' tag."); + } + + void OnApplicationQuit() + { + //Debug.Log("PlayerCamera.OnApplicationQuit"); + ReleaseCamera(); + } + + public override void OnStopLocalPlayer() + { + //Debug.Log("PlayerCamera.OnStopLocalPlayer"); + ReleaseCamera(); + } + + void OnDisable() + { + //Debug.Log("PlayerCamera.OnDisable"); + ReleaseCamera(); + } + + void OnDestroy() + { + //Debug.Log("PlayerCamera.OnDestroy"); + ReleaseCamera(); + } + + void ReleaseCamera() + { + if (mainCam != null && mainCam.transform.parent == transform) + { + //Debug.Log("PlayerCamera.ReleaseCamera"); + + mainCam.transform.SetParent(null); + mainCam.orthographic = true; + mainCam.orthographicSize = 15f; + mainCam.transform.localPosition = new Vector3(0f, 70f, 0f); + mainCam.transform.localEulerAngles = new Vector3(90f, 0f, 0f); + + if (mainCam.gameObject.scene != SceneManager.GetActiveScene()) + SceneManager.MoveGameObjectToScene(mainCam.gameObject, SceneManager.GetActiveScene()); + } + } + } +} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs.meta b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs.meta new file mode 100644 index 0000000000..1fee0c626c --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex2DPlayerCamera.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8bd7ffa7c966c0c47be237edb68c98c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs new file mode 100644 index 0000000000..f9bd2db04d --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs @@ -0,0 +1,70 @@ +using System; +using UnityEngine; + +namespace Mirror.Examples.Hex3D +{ + [AddComponentMenu("")] + public class Hex3DNetworkManager : NetworkManager + { + // Overrides the base singleton so we don't have to cast to this type everywhere. + public static new Hex3DNetworkManager singleton => (Hex3DNetworkManager)NetworkManager.singleton; + + [Header("Spawns")] + public GameObject spawnPrefab; + + [Range(1, 8000)] + public ushort spawnPrefabsCount = 1000; + + [Range(1, 10)] + public byte spawnPrefabSpacing = 3; + + public override void OnValidate() + { + if (Application.isPlaying) return; + base.OnValidate(); + + // Adjust spawnPrefabsCount to have an even cube root + ushort cubeRoot = (ushort)Mathf.Pow(spawnPrefabsCount, 1f / 3f); + spawnPrefabsCount = (ushort)(Mathf.Pow(cubeRoot, 3f)); + } + + public override void OnStartClient() + { + NetworkClient.RegisterPrefab(spawnPrefab); + } + + public override void OnStartServer() + { + // instantiate an empty GameObject + GameObject Spawns = new GameObject("Spawns"); + Transform SpawnsTransform = Spawns.transform; + + int spawned = 0; + + // Spawn prefabs in a cube grid centered around origin (0,0,0) + float cubeRoot = Mathf.Pow(spawnPrefabsCount, 1f / 3f); + int gridSize = Mathf.RoundToInt(cubeRoot); + + // Calculate the starting position to center the grid + float startX = -(gridSize - 1) * spawnPrefabSpacing * 0.5f; + float startY = -(gridSize - 1) * spawnPrefabSpacing * 0.5f; + float startZ = -(gridSize - 1) * spawnPrefabSpacing * 0.5f; + + //Debug.Log($"Start Positions: X={startX}, Y={startY}, Z={startZ}, gridSize={gridSize}"); + + for (int x = 0; x < gridSize; ++x) + for (int y = 0; y < gridSize; ++y) + for (int z = 0; z < gridSize; ++z) + if (spawned < spawnPrefabsCount) + { + float x1 = startX + x * spawnPrefabSpacing; + float y1 = startY + y * spawnPrefabSpacing; + float z1 = startZ + z * spawnPrefabSpacing; + Vector3 position = new Vector3(x1, y1, z1); + + NetworkServer.Spawn(Instantiate(spawnPrefab, position, Quaternion.identity, SpawnsTransform)); + ++spawned; + } + } + } +} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs.meta b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs.meta new file mode 100644 index 0000000000..7789997021 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DNetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3291bd2e9ac86c046bb768d598cd0a3f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs new file mode 100644 index 0000000000..5fe2aac92e --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs @@ -0,0 +1,49 @@ +using UnityEngine; +using Mirror; + +namespace Mirror.Examples.Hex3D +{ + [AddComponentMenu("")] + public class Hex3DPlayer : NetworkBehaviour + { + [Range(1, 20)] + public float speed = 10; + + void Update() + { + if (!isLocalPlayer) return; + + float h = Input.GetAxis("Horizontal"); + float v = Input.GetAxis("Vertical"); + + // if left shift is held, apply v to y instead of z + if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)) + { + Vector3 dir = new Vector3(h, v, 0); + transform.position += dir.normalized * (Time.deltaTime * speed); + } + else + { + Vector3 dir = new Vector3(h, 0, v); + transform.position += dir.normalized * (Time.deltaTime * speed); + } + + if (Input.GetKey(KeyCode.Q)) + transform.Rotate(Vector3.up, -90 * Time.deltaTime); + if (Input.GetKey(KeyCode.E)) + transform.Rotate(Vector3.up, 90 * Time.deltaTime); + } + +#if !UNITY_SERVER + void OnGUI() + { + if (isLocalPlayer) + { + GUILayout.BeginArea(new Rect(10, Screen.height - 50, 300, 300)); + GUILayout.Label("Use WASD+QE to move and rotate\nHold Shift with W/S to move up/down"); + GUILayout.EndArea(); + } + } +#endif + } +} diff --git a/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs.meta b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs.meta new file mode 100644 index 0000000000..4036b30b31 --- /dev/null +++ b/Assets/Mirror/Examples/HexSpatialHash/Scripts/Hex3DPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df2e991210804be44b9ab6b7d6de4d52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs new file mode 100644 index 0000000000..9ab355d849 --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs @@ -0,0 +1,10 @@ +namespace Mirror.Examples.PickupsDropsChilds +{ + public enum EquippedItem : byte + { + nothing, + ball, + bat, + box + } +} diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs.meta b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs.meta new file mode 100644 index 0000000000..1c935e9ecd --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Enumerations.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2bfef79b434bc424eacedbe92f3ca7e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces.meta b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces.meta new file mode 100644 index 0000000000..154f16197d --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 61617d75ae890064c89c20720fd50c0c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs new file mode 100644 index 0000000000..56a1f84c3a --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs @@ -0,0 +1,67 @@ +using UnityEngine; + +namespace Mirror.Examples.PickupsDropsChilds +{ + public class EquippedBall : MonoBehaviour, IEquipped + { + // Note: This example doesn't include animations or sounds for simplicity. + // These are just here for illustration purposes...the implementation + // methods could do something interesting like play a sound or animation. + [Header("Components")] + public Animator animator; + public AudioSource audioSource; + + [Header("Equipped Item")] + [SerializeField] + EquippedItemConfig _equippedItemConfig; + + public EquippedItemConfig equippedItemConfig + { + get => _equippedItemConfig; + set + { + Debug.Log($"{transform.root.name} EquippedItemConfig set from {_equippedItemConfig} to {value}", gameObject); + _equippedItemConfig = value; + } + } + + void Reset() + { + equippedItemConfig = new EquippedItemConfig { usages = 3, maxUsages = 3 }; + } + + // Play appropriate animation or sound + public void Use() + { + // Effectively unlimited uses + if (equippedItemConfig.maxUsages == 0) + { + Debug.Log("Ball used"); + return; + } + + if (equippedItemConfig.usages > 0) + Debug.Log("Ball used"); + else + Debug.Log("Ball is out of uses"); + } + + // Play appropriate animation or sound + public void AddUsages(byte usages) + { + Debug.Log($"Ball added {usages} usages"); + } + + // Play appropriate animation or sound + public void ResetUsages() + { + Debug.Log("Ball reset"); + } + + // Play appropriate animation or sound + public void ResetUsages(byte usages) + { + Debug.Log($"Ball reset usages to {usages}"); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs.meta b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs.meta new file mode 100644 index 0000000000..929da021dc --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBall.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65881ca9607b5ea42b654a7ed2566027 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs new file mode 100644 index 0000000000..1cd30d0291 --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs @@ -0,0 +1,67 @@ +using UnityEngine; + +namespace Mirror.Examples.PickupsDropsChilds +{ + public class EquippedBat : MonoBehaviour, IEquipped + { + // Note: This example doesn't include animations or sounds for simplicity. + // These are just here for illustration purposes...the implementation + // methods could do something interesting like play a sound or animation. + [Header("Components")] + public Animator animator; + public AudioSource audioSource; + + [Header("Equipped Item")] + [SerializeField] + EquippedItemConfig _equippedItemConfig; + + public EquippedItemConfig equippedItemConfig + { + get => _equippedItemConfig; + set + { + Debug.Log($"{transform.root.name} EquippedItemConfig set from {_equippedItemConfig} to {value}", gameObject); + _equippedItemConfig = value; + } + } + + void Reset() + { + equippedItemConfig = new EquippedItemConfig { usages = 5, maxUsages = 5 }; + } + + // Play appropriate animation or sound + public void Use() + { + // Effectively unlimited uses + if (equippedItemConfig.maxUsages == 0) + { + Debug.Log("Bat used"); + return; + } + + if (equippedItemConfig.usages > 0) + Debug.Log("Bat used"); + else + Debug.Log("Bat is out of uses"); + } + + // Play appropriate animation or sound + public void AddUsages(byte usages) + { + Debug.Log($"Bat added {usages} usages"); + } + + // Play appropriate animation or sound + public void ResetUsages() + { + Debug.Log("Bat reset"); + } + + // Play appropriate animation or sound + public void ResetUsages(byte usages) + { + Debug.Log($"Bat reset usages to {usages}"); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs.meta b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs.meta new file mode 100644 index 0000000000..ce2f33228e --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35ca3b53a1e608f46ae9195e9d8cae83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs new file mode 100644 index 0000000000..e738bba211 --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs @@ -0,0 +1,67 @@ +using UnityEngine; + +namespace Mirror.Examples.PickupsDropsChilds +{ + public class EquippedBox : MonoBehaviour, IEquipped + { + // Note: This example doesn't include animations or sounds for simplicity. + // These are just here for illustration purposes...the implementation + // methods could do something interesting like play a sound or animation. + [Header("Components")] + public Animator animator; + public AudioSource audioSource; + + [Header("Equipped Item")] + [SerializeField] + EquippedItemConfig _equippedItemConfig; + + public EquippedItemConfig equippedItemConfig + { + get => _equippedItemConfig; + set + { + Debug.Log($"{transform.root.name} EquippedItemConfig set from {_equippedItemConfig} to {value}", gameObject); + _equippedItemConfig = value; + } + } + + void Reset() + { + equippedItemConfig = new EquippedItemConfig { usages = 0, maxUsages = 0 }; + } + + // Play appropriate animation or sound + public void Use() + { + // Effectively unlimited uses + if (equippedItemConfig.maxUsages == 0) + { + Debug.Log("Box used"); + return; + } + + if (equippedItemConfig.usages > 0) + Debug.Log("Box used"); + else + Debug.Log("Box is out of uses"); + } + + // Play appropriate animation or sound + public void AddUsages(byte usages) + { + Debug.Log($"Box added {usages} usages"); + } + + // Play appropriate animation or sound + public void ResetUsages() + { + Debug.Log("Box reset"); + } + + // Play appropriate animation or sound + public void ResetUsages(byte usages) + { + Debug.Log($"Box reset usages to {usages}"); + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs.meta b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs.meta new file mode 100644 index 0000000000..867f5b4e51 --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/EquippedBox.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5712a4d15653a184cb34b9c1e044563d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs new file mode 100644 index 0000000000..79d3c75765 --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs @@ -0,0 +1,74 @@ +namespace Mirror.Examples.PickupsDropsChilds +{ + interface IEquipped + { + EquippedItemConfig equippedItemConfig { get; set; } + + void Use(); + void AddUsages(byte usages); + void ResetUsages(); + void ResetUsages(byte usages); + } + + [System.Serializable] + public struct EquippedItemConfig : System.IEquatable + { + // Usages remaining...this could be ammo, potion doses, magic item charges, etc. + public byte usages; + + // Maximum usages...set to 0 for effectively unlimited uses + public byte maxUsages; + + public EquippedItemConfig(byte maxUsages) + { + usages = maxUsages; + this.maxUsages = maxUsages; + } + + public EquippedItemConfig(byte usages, byte maxUsages) + { + this.usages = usages; + this.maxUsages = maxUsages; + } + + public void Use() + { + // Reset usages to within allowed range in case higher than maxUsages + ResetUsages(usages); + + // if we have usages left, decrement + if (usages > 0) + usages--; + } + + // Add a specific number of usages + public void AddUsages(byte usages) + { + // Limit usages to maxUsages + this.usages = (byte)Mathd.Clamp(this.usages + usages, 0, maxUsages); + } + + // Fully reload to max usages + public void ResetUsages() + { + this.usages = maxUsages; + } + + // Reload to a specific number of usages + public void ResetUsages(byte usages) + { + // Limit usages to maxUsages + this.usages = (byte)Mathd.Clamp(usages, 0, maxUsages); + } + + public bool Equals(EquippedItemConfig other) + { + return usages == other.usages && maxUsages == other.maxUsages; + } + + public override string ToString() + { + return $"EquippedItemConfig[{usages}/{maxUsages}]"; + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs.meta b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs.meta new file mode 100644 index 0000000000..413606103e --- /dev/null +++ b/Assets/Mirror/Examples/PickupsDropsChilds/Scripts/Interfaces/IEquipped.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b85cf93aed753aa448c8cf58ae3bb79d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid.meta b/Assets/Mirror/Examples/TanksHybrid.meta new file mode 100644 index 0000000000..a1848e6a21 --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c61e0f7353074187a145b29ed897f4d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid/Prefabs.meta b/Assets/Mirror/Examples/TanksHybrid/Prefabs.meta new file mode 100644 index 0000000000..a2569846f0 --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c7190db9409224d11be5397faeb09822 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab new file mode 100644 index 0000000000..444be74606 --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab @@ -0,0 +1,187 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &63476987332307980 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8035186136109819211} + - component: {fileID: 9118274893554935717} + - component: {fileID: 69063397099238371} + m_Layer: 0 + m_Name: 3D Model + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8035186136109819211 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 63476987332307980} + m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.2, y: 0.2, z: 0.2} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 24373266488650541} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!33 &9118274893554935717 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 63476987332307980} + m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0} +--- !u!23 &69063397099238371 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 63476987332307980} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: cba1b63a0bccc4b12ac25f05d0ae2dd1, type: 2} + 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: 0 + 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!1 &5890560936853567077 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 24373266488650541} + - component: {fileID: 1713098107664522388} + - component: {fileID: 7082621516996595528} + - component: {fileID: 2355290524794870353} + - component: {fileID: 4629190479245867726} + m_Layer: 0 + m_Name: Projectile + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &24373266488650541 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5890560936853567077} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 2, y: 2, z: 2} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8035186136109819211} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1713098107664522388 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5890560936853567077} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 999775126 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &7082621516996595528 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5890560936853567077} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2f8639b3a1d2f431586ca8b52bb1337b, type: 3} + m_Name: + m_EditorClassIdentifier: + syncMethod: 0 + syncDirection: 0 + syncMode: 0 + syncInterval: 0.1 + destroyAfter: 2 + rigidBody: {fileID: 4629190479245867726} + force: 2000 +--- !u!136 &2355290524794870353 +CapsuleCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5890560936853567077} + m_Material: {fileID: 0} + m_IsTrigger: 1 + m_Enabled: 1 + m_Radius: 0.1 + m_Height: 0.4 + m_Direction: 2 + m_Center: {x: 0, y: 0, z: 0} +--- !u!54 &4629190479245867726 +Rigidbody: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5890560936853567077} + serializedVersion: 2 + m_Mass: 1 + m_Drag: 0 + m_AngularDrag: 0.05 + m_UseGravity: 0 + m_IsKinematic: 0 + m_Interpolate: 1 + m_Constraints: 0 + m_CollisionDetection: 1 diff --git a/Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab.meta b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab.meta new file mode 100644 index 0000000000..60eec8b1f6 --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Projectile.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 91f895dc7a93a4b9394f46cee0fb90c0 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab new file mode 100644 index 0000000000..06209ad2ae --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab @@ -0,0 +1,377 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1916082411674582 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4492442352427800} + - component: {fileID: 114118589361100106} + - component: {fileID: 114250499875391520} + - component: {fileID: -4567587749865553567} + - component: {fileID: 114654712548978148} + - component: {fileID: 6900008319038825817} + m_Layer: 0 + m_Name: Tank + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4492442352427800 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916082411674582} + 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_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 5803173220413450940} + - {fileID: 2155495746218491392} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &114118589361100106 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916082411674582} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3} + m_Name: + m_EditorClassIdentifier: + sceneId: 0 + _assetId: 1864963335 + serverOnly: 0 + visibility: 0 + hasSpawned: 0 +--- !u!114 &114250499875391520 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916082411674582} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8f63ea2e505fd484193fb31c5c55ca73, type: 3} + m_Name: + m_EditorClassIdentifier: + syncMethod: 1 + syncDirection: 1 + syncMode: 0 + syncInterval: 0 + target: {fileID: 4492442352427800} + syncPosition: 1 + syncRotation: 1 + syncScale: 0 + onlySyncOnChange: 1 + compressRotation: 1 + interpolatePosition: 1 + interpolateRotation: 1 + interpolateScale: 1 + coordinateSpace: 0 + timelineOffset: 1 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + useFixedUpdate: 0 + onlySyncOnChangeCorrectionMultiplier: 2 + rotationSensitivity: 0.01 + positionPrecision: 0.01 + rotationPrecision: 0.001 + scalePrecision: 0.01 + debugDraw: 0 +--- !u!114 &-4567587749865553567 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916082411674582} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8f63ea2e505fd484193fb31c5c55ca73, type: 3} + m_Name: + m_EditorClassIdentifier: + syncMethod: 1 + syncDirection: 1 + syncMode: 0 + syncInterval: 0 + target: {fileID: 5803173220413450936} + syncPosition: 1 + syncRotation: 1 + syncScale: 0 + onlySyncOnChange: 1 + compressRotation: 1 + interpolatePosition: 1 + interpolateRotation: 1 + interpolateScale: 1 + coordinateSpace: 0 + timelineOffset: 1 + showGizmos: 0 + showOverlay: 0 + overlayColor: {r: 0, g: 0, b: 0, a: 0.5} + useFixedUpdate: 0 + onlySyncOnChangeCorrectionMultiplier: 2 + rotationSensitivity: 0.01 + positionPrecision: 0.01 + rotationPrecision: 0.001 + scalePrecision: 0.01 + debugDraw: 0 +--- !u!114 &114654712548978148 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916082411674582} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f45f3637f32e4490e8bf0a852cbcb0e5, type: 3} + m_Name: + m_EditorClassIdentifier: + syncMethod: 1 + syncDirection: 0 + syncMode: 0 + syncInterval: 0.1 + agent: {fileID: 6900008319038825817} + animator: {fileID: 5803173220405953878} + healthBar: {fileID: 955977906578811009} + turret: {fileID: 5803173220413450936} + rotationSpeed: 80 + shootKey: 32 + projectilePrefab: {fileID: 5890560936853567077, guid: 91f895dc7a93a4b9394f46cee0fb90c0, + type: 3} + projectileMount: {fileID: 606281948174800110} + health: 5 +--- !u!195 &6900008319038825817 +NavMeshAgent: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916082411674582} + m_Enabled: 1 + m_AgentTypeID: 0 + m_Radius: 4 + m_Speed: 10 + m_Acceleration: 1 + avoidancePriority: 50 + m_AngularSpeed: 240 + m_StoppingDistance: 0 + m_AutoTraverseOffMeshLink: 1 + m_AutoBraking: 1 + m_AutoRepath: 1 + m_Height: 3.5 + m_BaseOffset: 0.05 + m_WalkableMask: 4294967295 + m_ObstacleAvoidanceType: 0 +--- !u!1 &6882277736259849937 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2155495746218491392} + - component: {fileID: 3883687817794100885} + - component: {fileID: 955977906578811009} + - component: {fileID: 6248426133561649027} + m_Layer: 0 + m_Name: HealthBar + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2155495746218491392 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6882277736259849937} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 5, z: 0} + m_LocalScale: {x: 0.1, y: 0.1, z: 0.1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4492442352427800} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!23 &3883687817794100885 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6882277736259849937} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 10100, guid: 0000000000000000e000000000000000, 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!102 &955977906578811009 +TextMesh: + serializedVersion: 3 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6882277736259849937} + m_Text: ----- + m_OffsetZ: 0 + m_CharacterSize: 2 + m_LineSpacing: 1 + m_Anchor: 4 + m_Alignment: 1 + m_TabSize: 4 + m_FontSize: 100 + m_FontStyle: 0 + m_RichText: 1 + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_Color: + serializedVersion: 2 + rgba: 4285098495 +--- !u!114 &6248426133561649027 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6882277736259849937} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: afa2d590c474413d9fc183551385ed85, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1001 &7130959241934869977 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 4492442352427800} + m_Modifications: + - target: {fileID: 3638700596990255941, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_Name + value: BasePrefab + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: dad07e68d3659e6439279d0d4110cf4c, type: 3} +--- !u!4 &606281948174800110 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 7683056980803567927, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + m_PrefabInstance: {fileID: 7130959241934869977} + m_PrefabAsset: {fileID: 0} +--- !u!95 &5803173220405953878 stripped +Animator: + m_CorrespondingSourceObject: {fileID: 3638700596980764815, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + m_PrefabInstance: {fileID: 7130959241934869977} + m_PrefabAsset: {fileID: 0} +--- !u!4 &5803173220413450936 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 3638700596990361441, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + m_PrefabInstance: {fileID: 7130959241934869977} + m_PrefabAsset: {fileID: 0} +--- !u!4 &5803173220413450940 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 3638700596990361445, guid: dad07e68d3659e6439279d0d4110cf4c, + type: 3} + m_PrefabInstance: {fileID: 7130959241934869977} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab.meta b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab.meta new file mode 100644 index 0000000000..5917073f2c --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Prefabs/Tank.prefab.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c40046c86ca624e6d81e675eb121a8d1 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 100100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid/Readme.txt b/Assets/Mirror/Examples/TanksHybrid/Readme.txt new file mode 100644 index 0000000000..c7fdc0c033 --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Readme.txt @@ -0,0 +1,6 @@ +Tanks demo, running on Mirror's new Hybrid sync. +In other words, Unreliable sync for NetworkTransform etc. + +Note that while Mirror now has hybrid sync baked into the core, +we didn't adapt the Weaver yet. Which means that [SyncVar]s don't work with Hybrid sync yet. +Still need to use OnSerialize/OnDeserialize manually for now. \ No newline at end of file diff --git a/Assets/Mirror/Examples/TanksHybrid/Readme.txt.meta b/Assets/Mirror/Examples/TanksHybrid/Readme.txt.meta new file mode 100644 index 0000000000..afe1d330f5 --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Readme.txt.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ee77369defce149049c146ce8ae88418 +timeCreated: 1684039107 \ No newline at end of file diff --git a/Assets/Mirror/Examples/TanksHybrid/Scenes.meta b/Assets/Mirror/Examples/TanksHybrid/Scenes.meta new file mode 100644 index 0000000000..6d192ad5ef --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c0c9846985feb4de4bd23e5ed5c84024 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.meta b/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.meta new file mode 100644 index 0000000000..1c9ca301de --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0f6ac0e3c083a4fb69d1f59b06a4b6fb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity b/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity new file mode 100644 index 0000000000..d3a5fc4993 --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity @@ -0,0 +1,730 @@ +%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: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 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, g: 0, b: 0, 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: 0 + 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: 0 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 500 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 2 + m_PVRDenoiserTypeDirect: 0 + m_PVRDenoiserTypeIndirect: 0 + m_PVRDenoiserTypeAO: 0 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 0 + 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: 4890085278179872738, guid: 0f3310d22ddad415b91340325ce86f4c, + type: 2} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 2 + agentHeight: 3.5 + agentSlope: 45 + agentClimb: 2 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.6666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 23800000, guid: c772aa575956c59478e2d55eb019e17a, type: 2} +--- !u!1 &88936773 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 88936777} + - component: {fileID: 88936776} + - component: {fileID: 88936778} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &88936776 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0, g: 0, b: 0, 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 &88936777 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_LocalRotation: {x: 0.3420201, y: 0, z: 0, w: 0.9396927} + m_LocalPosition: {x: 0, y: 20, z: -30} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 40, y: 0, z: 0} +--- !u!114 &88936778 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 88936773} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9021b6cc314944290986ab6feb48db79, type: 3} + m_Name: + m_EditorClassIdentifier: + height: 150 + offsetY: 40 + maxLogCount: 50 + showInEditor: 0 + hotKey: 293 +--- !u!1 &251893064 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 251893065} + - component: {fileID: 251893066} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &251893065 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 251893064} + m_LocalRotation: {x: 0, y: -0.92387956, z: 0, w: 0.38268343} + m_LocalPosition: {x: 14, y: 0.4, z: 14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: -135, z: 0} +--- !u!114 &251893066 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 251893064} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &535739935 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 535739936} + - component: {fileID: 535739937} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &535739936 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_LocalRotation: {x: 0, y: -0.38268343, z: 0, w: 0.92387956} + m_LocalPosition: {x: 14, y: 0.4, z: -14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: -45, z: 0} +--- !u!114 &535739937 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 535739935} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1107091652 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1107091656} + - component: {fileID: 1107091655} + - component: {fileID: 1107091653} + - component: {fileID: 1107091654} + m_Layer: 0 + m_Name: Ground + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 4294967295 + m_IsActive: 1 +--- !u!23 &1107091653 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RenderingLayerMask: 4294967295 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 29b49c27a74f145918356859bd7af511, type: 2} + 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: 1 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + 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!64 &1107091654 +MeshCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_Material: {fileID: 0} + m_IsTrigger: 0 + m_Enabled: 1 + serializedVersion: 4 + m_Convex: 0 + m_CookingOptions: 30 + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &1107091655 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1107091656 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1107091652} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 4, y: 1, z: 4} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1282001517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1282001518} + - component: {fileID: 1282001520} + - component: {fileID: 1282001519} + - component: {fileID: 1282001521} + - component: {fileID: 1282001522} + m_Layer: 0 + m_Name: NetworkManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1282001518 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + 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_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1282001519 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6442dc8070ceb41f094e44de0bf87274, type: 3} + m_Name: + m_EditorClassIdentifier: + offsetX: 0 + offsetY: 0 +--- !u!114 &1282001520 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e, type: 3} + m_Name: + m_EditorClassIdentifier: + dontDestroyOnLoad: 0 + runInBackground: 1 + headlessStartMode: 1 + editorAutoStart: 0 + sendRate: 30 + unreliableBaselineRate: 1 + unreliableRedundancy: 0 + offlineScene: + onlineScene: + offlineSceneLoadDelay: 0 + transport: {fileID: 1282001521} + networkAddress: localhost + maxConnections: 100 + disconnectInactiveConnections: 0 + disconnectInactiveTimeout: 60 + authenticator: {fileID: 0} + playerPrefab: {fileID: 1916082411674582, guid: c40046c86ca624e6d81e675eb121a8d1, + type: 3} + autoCreatePlayer: 1 + playerSpawnMethod: 1 + spawnPrefabs: + - {fileID: 5890560936853567077, guid: 91f895dc7a93a4b9394f46cee0fb90c0, type: 3} + exceptionsDisconnect: 1 + snapshotSettings: + bufferTimeMultiplier: 2 + bufferLimit: 32 + catchupNegativeThreshold: -1 + catchupPositiveThreshold: 1 + catchupSpeed: 0.019999999552965164 + slowdownSpeed: 0.03999999910593033 + driftEmaDuration: 1 + dynamicAdjustment: 1 + dynamicAdjustmentTolerance: 1 + deliveryTimeEmaDuration: 2 + evaluationMethod: 0 + evaluationInterval: 3 + timeInterpolationGui: 0 +--- !u!114 &1282001521 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6b0fecffa3f624585964b0d0eb21b18e, type: 3} + m_Name: + m_EditorClassIdentifier: + port: 7777 + DualMode: 1 + NoDelay: 1 + Interval: 10 + Timeout: 10000 + RecvBufferSize: 7361536 + SendBufferSize: 7361536 + FastResend: 2 + ReceiveWindowSize: 4096 + SendWindowSize: 4096 + MaxRetransmit: 40 + MaximizeSocketBuffers: 1 + ReliableMaxMessageSize: 297433 + UnreliableMaxMessageSize: 1194 + debugLog: 0 + statisticsGUI: 0 + statisticsLog: 0 +--- !u!114 &1282001522 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1282001517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bc654f29862fc2643b948f772ebb9e68, type: 3} + m_Name: + m_EditorClassIdentifier: + color: {r: 1, g: 1, b: 1, a: 1} + padding: 2 + width: 180 + height: 25 +--- !u!1 &1458789072 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1458789073} + - component: {fileID: 1458789074} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1458789073 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1458789072} + m_LocalRotation: {x: 0, y: 0.92387956, z: 0, w: 0.38268343} + m_LocalPosition: {x: -14, y: 0.4, z: 14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 135, z: 0} +--- !u!114 &1458789074 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1458789072} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &1501912662 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1501912663} + - component: {fileID: 1501912664} + m_Layer: 0 + m_Name: Spawn + m_TagString: Untagged + m_Icon: {fileID: -964228994112308473, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1501912663 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1501912662} + m_LocalRotation: {x: 0, y: 0.38268343, z: 0, w: 0.92387956} + m_LocalPosition: {x: -14, y: 0.4, z: -14} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 45, z: 0} +--- !u!114 &1501912664 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1501912662} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 41f84591ce72545258ea98cb7518d8b9, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &2054208274 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2054208276} + - component: {fileID: 2054208275} + 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 &2054208275 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_Enabled: 1 + serializedVersion: 10 + m_Type: 1 + m_Shape: 0 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.802082 + 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 &2054208276 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2054208274} + m_LocalRotation: {x: 0.10938167, y: 0.8754261, z: -0.40821788, w: 0.23456976} + m_LocalPosition: {x: 0, y: 10, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 50, y: 150, z: 0} diff --git a/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity.meta b/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity.meta new file mode 100644 index 0000000000..00d54f13fd --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 124987c1fcd8c4fa9b37726b1103c16a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid/NavMesh.asset b/Assets/Mirror/Examples/TanksHybrid/Scenes/MirrorTanksHybrid/NavMesh.asset new file mode 100644 index 0000000000000000000000000000000000000000..9e621ca19e25bcde84c2ad7f2eb9696b8d404123 GIT binary patch literal 6564 zcmbuDO>i7X6~}v}l>{LeDDUv2dX&1fhwygE;(=^9H_!MP!)<3l>^0r1LFUG-EVhh zN29o)OY^3uf8Fo(d#|T^R>s`4-k^^awE!Mr+T;7tAsEcNtDIz8@oxX~h=I zuTVmKc#$)Pg>sS)OZ;wu4#wu=O$|@IN|!TMqw=<9yH4>%+$PZ;pR1hkxMs=X3bK9ls-o z7m+;X@6H^)!|^W+@F9%48SGwM`@!zQwKIlrMfaBqIN!g!ad91KA-{mNKkN8C09)~A z9Opi_6<>AyOF6vb_`Ny&CCB&X@HZX*at?pX@rf-s_iya~;(uuiegS;L@qJrxj(^AT z`*Qf79p_#@7$1)RtK-bkApa-W4jeqVnfu9O2v@j&aeuhcah!8zXIUx7*!}TVaPChB z$>StX@6U(G%SoQzAHPE0$Iw1(?uKsePuyD`$nc_hAM-naKHU2s6z)OVQZOU3zRKWS ze-Gi>X*>%dl}|flU7<_yliqt1R=ID{+I&-mYtXWE~_HEv=^Tga2=_cWT* z+{Skf`WfFN;M>KwLVg9pmBI682JM6K^`V>brT(Mju^Wt=oeGlP1_%p{Lw!1_lR&0(w2f5OXcrtXin$Pa?Rg~9Ip9$G{f!r`l9piBzX+s3i4CH zJKOudcns~^`8P|hgK<~?=^yj|c!smp#OGk2>w7N4M-s*0 z=5%~4*Z4eW3laBKg! z&VDt+)AnmQTtq3Jt3Y*3VuJm zkMD;G#D{TzMTjSoLcfpI{`ZA=A}N%s{T~SNL{cbM`{#ssA}N$V0Q+1YuL|)*Qpo&} zpNGKt{=6o{6G@@tb$&k-;)$eCuJikm5Kkn9a-HAnLOhWaw9owo{{xWu@pojhob;c6 zH0Z2W{Kz-6vr+rOnOYdN+B54>(Cmb*=3&!VJQ~)6;}uk9{b&^x={MUBmi?vU%}(Su zs{y+?Rj1IqV-gRdyRq0abIk9omi8UQ2-eKfnJDNyJ@Krol(e!>EB9$-QY-tla=%s% zXyu?*4r!&-SKZTH>M1Yvw3mA7OVID;Kiv)MwfP{5!sbe+?_;{zUkRGgsWWQ~lGNw? zTG;K_`mrEfS&eLczTR33Z1qSzY%H02P+JKq?be#>)$rFJ={DA;s_j;%QweQsO(Sg1 z1uIC#R6FoZ!*6!|`jMbspAXLlrm7X&&ge^fq!qtf?Y8|Wn63NOpn;jDe5(XY-Ibn$ zH9o=3qjg{0wbN?&YaP?8N;=nCoiM_T%sO_YvnC4KZ9i9Bn^X0UF#?ZY~?58-@a8H!CT6$rJh&$P^5L-L;) zjv0};70n*>-P7;3O)cpsyM4HyqD=f;c;fA;q@N_}8~m6WtXtn~pDp~v=wb|UO@6Xp z(d_=8{8S&4w(|4IUPkgsf0<7{(>MC4b$jpUnf~?r^z&)r(U^T%t~H-V^10g%`Vcvo zPwlR@cxUOaTX{r2vEE%ixAK#|sy|wnkr0~a() != null) + // { + // --health; + // if (health == 0) + // NetworkServer.Destroy(gameObject); + // } + //} + + void RotateTurret() + { + Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); + if (Physics.Raycast(ray, out RaycastHit hit, 100)) + { + Debug.DrawLine(ray.origin, hit.point); + Vector3 lookRotation = new Vector3(hit.point.x, turret.transform.position.y, hit.point.z); + turret.transform.LookAt(lookRotation); + } + } + + // Health is serialized/deserialized manually. + // setting the component to SyncMethod=Unreliable automatically takes care of the rest. + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // Debug.LogWarning($"Tank {name} OnSerialize {(initialState ? "full" : "delta")} health={health}"); + writer.WriteInt(health); + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + health = reader.ReadInt(); + // Debug.LogWarning($"Tank {name} OnDeserialize {(initialState ? "full" : "delta")} health={health}"); + } + } +} diff --git a/Assets/Mirror/Examples/TanksHybrid/Scripts/Tank.cs.meta b/Assets/Mirror/Examples/TanksHybrid/Scripts/Tank.cs.meta new file mode 100644 index 0000000000..3786b981fa --- /dev/null +++ b/Assets/Mirror/Examples/TanksHybrid/Scripts/Tank.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f45f3637f32e4490e8bf0a852cbcb0e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Examples/_Common/Textures/Wall01.jpg b/Assets/Mirror/Examples/_Common/Textures/Wall01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9c7ada0223753b8553125c40234f2b9c4f0223ce GIT binary patch literal 60894 zcmb5V1yq|s*De~|Dc<0g7N>Y|D4O8zP$0NFErk-?f&_{dcefUI*HQ`;cbDSshwuN_ zI{&$AoqN~1d-6`6?AaMfChzQd_T+i?c@;pQAP12HAR!?Ez%L2#ypA*rk(U0brlBea zQIdTr0058%0WX5e1pqiYdAMoFOVR1+8_;2_0+3$Pe~B5?-Q~a0|22QHx|;rP+ewc9 ztHuAf63fEU9r|K&@^XRPUK)St6UhrEvHow&{2y%o-&piN*xSR!hFk(f!Y7;$r4z_FvVZzU0VO zRsg_R5deUv4*(F40|3~D|5f&q{lD~$?xl$8rC-i3$r|7QumsQnAOI%-6u|j{cmP}g zZh*k^0zevoje&uQfsT!diHU=aje`#)z{kVGrzRyM1TxaT0Wr`r(7$DS|L!dd9}7JL z*9UGsArWzL@i&|@5NT2Hdogj5|5Sp6je~=ahfhU7KqbP=z%26rTb?@sgqX+xWB>{h z0|1#2355{pxfekBGIEsv%)|dJD5%J2=tuyJmqG{u02v7d85I=;{pEpz`C^HT0zf4s zqDLd<{(#P)Zie9+K*EE_sX1Yk>iR>vc1i}~)i95L?Uwt7Pg*muwtE7){+8eUr7GY{ z8~=}HUcP5!6x5dz5aECH$S?B0`+-D=%uPi9K^?`6n1RPNKCNN-luQv)%vrDSf|-S4ztEj)3OxWl0$s;-Qdb+fVn6$S6O$yH)C5b>}wtODlJa^ z%G%9u|DTYC9bK|~8$_Y$s3DD&5Y27z(vTySu;YrFdZ2sFb@}1FQKZ+&>kvs}vNyFY zNtqvlZYDHuBzh?2se@kW8ip~Q3szvIngD`fjYH1r(P4FrMDH?H^rpN3o@E@03@wk* zC&S&ZZtSTz|IOfINnC55eEQN;!{vMuWrb?GuPjNd^1-ok5m`;>$?8a=fbg?xr#G6s zvyY=ugdaLX78AD8|82ZFwk?s*0K}< z@VJqKjFRRQVrlYZLk;cGa6z@WcI1s;x1cxKsWq^^sk$x`6ycHA zo&R=~n(l)CLc{owX15rVV~&ty$M@+OK+)^V23|k?7(OoxjQ$dH_seU|xk22ui3aDS zt#tE~S6{7rXz+VypVO0>u&ht5Vm)gZtiv2?#0LvL)d|{b@D@c9=J`Mmrjwi=DjCHq zUE6BNUrsR7eC1T<#+nf_3D#@OwXOybj@H3(os7YWJD2Vn8MY7f-!6||C`)+#r`eF{ zgW~GWtK!ASP!|1QIej?-+ZFextO`0My%X68#;hPc2%`|LZ0+ik*2gRPo(7FF#kB<@ zFr9x(@!(UUnG@sS)`lf3I{NG7SgO<4mb!FjPq#p(6@HG|lrnLWleYGQwIdpGIZQ*( zl?5{;>8?)%35+3g=+Jb(46n|Wt&3c#5)>ON)4XF`Yq9eK^-#g@5wsh`Tir1|zG+|7 zE!V!%WEdHpe0i-UY-n7YDE7$O)+)A!l?WB5SPL<<{9{u(dkMm>Zixq`GU~9RA4d1v z5t+N`iI{nHp}2@lB^Ao*oqPRvz|6pcNo8no`sT7=e%C9`>yY zN_GU5eX6R;dyXKh*zPPeqdlA+6SToM6Z=XJt)n&-C*M0OoKtQ*d(5h-IW69VAI^}L z#Ejh7(z$%-p`yuJ&UJ!g$F(~B7NO?u=F#%J^-24k!NVPUvd{%_rReE$isBaG`who$ zGbnfbN`0e7_Y9czx;2jFWp~f&{&{uES3J4MCLOPhlFm)@l(qFiDi8u?CwNVleutl1 zx1|&taB3(5I$bK~n(eHwA{R}B+cN%cqG|c6UDUX`h)=2&8*CV=zQaMV_~^%)&`Tgz z)k}$z|7wi@>5}3M^KLpu%E0R*H^^eAx?e4h2t+aNv%^-$R_bkiT)|DTQ-E+7w{$mstDYi zdXI*5>s3gj7taCJDG<{;lx^Vd?uGz=n65JKHMH?ClTEkskR&{g5$wAbB^S9~R6b@& zijI8h;n}D3z$l#3Mi^X(y4E^pu(lvQdDxC9+C?*z#;=A__cODt|BL|#8qFKF^QbiT z_$!d4(}qIaDf}YmNCSkW0ACBs4;kXOruhcG4=YnBe$tRC7*C|!75b&phAn?`jDzU1 z7YvNO#WH##kzGrFOFD34*{I%-ijr{2w>JfH3>>HdUOuh|4;tdq3JMNgR>-)xEE5Y& z2wD~<%n+h6&gU_qtOmFV`U*!3C>L*HMtrClLt-q+0l6h@Q+Li<7cv)o4{(`LW-&wF z!B8_=&(EY>J(bg%V@v{$a5G&N^-!73)c1(7#Z=HSTUf|3Qr8^bYGL-RJz-CP@#1Mr zZYZN4p8*Agjju31&^VYPT@RT|9pw}Ul9UJm9MiVrU#oY~Mic5XVOD>Z8tCCb@VF^# z#;O)*o!;YoF(Kws6wLiJNYw>IYoHd`GcXNMC**H7M47%i-=R0+1;>p8C_U5ZGi!GyP1FOCitpJZepYTLU z114eOH>8A&r*9FT>e0pI1wolh2kLCGbl=v+>dzwC&1Dd{rWy7b_Zp{V?)nQ3FyXJK zDsIwpm6ns48TDlv_(IV(YSr4P+$dEOseyv=e~Q$Mh}`e;6iOL%JM)f_ zD?A@=3&)$2n{W5e(7zzRm*Em7w22qzqH5StVd7jH$t?w|Yaz`=li% z!b8cIh;c(Q&mHinuI3^98fBuWN4%NE8_i_%ffL`jaVWe;}60C(PPT5PV=ghMVN4zx{yROqaq5AUE}U24I)oNb1T* zIFW1pSf=BE?|3hqSFXgCZVlf@Z}qhJi^zqFc`W;(Z`kHMVSJ;C3cid zOdt-`)o2+jQgY~+V{;6-RW4^^MM>q{J4YSD*+jK^Eb&4~{921u=G;w8b!?WR$qrVm z`q$WUAIAI!#&Y-wlp;RCCpjxMnNs3?E-rWNr?$iXDQY?(jB_oNdN*0@| zc%_+R>xYVi2-c9nk^1hmK+I(AfACml08xwc87XU*S(H}xD{j3z#MF*ohaXEmJUGrY ziRoD2cEPW-{1oZUtX_n2wV?_>Uah-MhmA)9KB&J!1b**$m^J+)(gbsZQ?|cKLK35> z4j(JAD2@U0gZe~OWqC?2V*?T0s(^!NqKgyr7E#x~t$EF!U#|k8%rd}t*O5mA!SIp9 zT<`lNjX*vx6QURwPPirFTkJ-3#TpOBg^qT;{0s`G?AgLKK=G%B01vU6bZw-N{Cn;Q zeXYU|NN)b0D_tsljFdxtQ8-Ch*8w!4x&K+=&nSuAtbcoNbu7{|2{_p$#9h9B3BCZDV zI`h_Qq{ZX2@@=dxkAt3zB|*Wn+`Xw$?iz_i7NWjhkp8HkTBFT{r3!j~`W9F8UkfdD znm*X^Uj2eq(RRHNNWIm!p4}4(hGu+6OR^Dpy+X7eD4J;?B~HgaNW>%c+i3s!g%XH@k8V<)4P&5 zDo@5X+^W=XSNr(0xw$zO3}%$8w-NnMiCAQkqoDh_A)y093o^L<`018UIz@W3{#Zc1 zz%yWP&qRoM7}7%p$A+uJ*UqGHb>jZksNvDLBsTNhl)S9B^%SA&$p}VSj?h`>28Gb#ZEd!gW|&y151K}< zoO%sSMkzAd$a}7To~G$jO@eWaz->>V+(BprmU)wSU|P-~!`xV*5M|nF!bp7&a;u>_ zTPU4_*I*U15}ty`2C9c(_CAD?(L#i~OS1^MQ7ss6ha4KpScxf^``rpNM?2J;66&-t zW#LZA?j|Cz$xMv`eJB<|huaAn9d;Pd6oSlR+25-sdZ00D6;uARL1YtVgX-%*nAcs4 zxKAVmQ@G!wjju{+UpJBw6${}}&_0@14Io-a8ixixR;YdIwOjL@?Xl^^5dYWs$m$lf zOM}Nli+*zMq%}w{$&VKRAXZCTwo2MC+%s!wr2SNLsJ`kBfv#o(=_MBr_})Z8Arle> z%6#8tB2wbJ4?S-Jh!Aq6*=Ure&Iykqf5;5!DcEgOsmQ!`Ne&nGZvVWA zAR}Q7AO-|vHXUd-)Dn%rO0}=%H-iy-2bzk?ymIS2>H{h%zqrSEfdMkzm+C?F{$t;z ziAVB8(g9)^$Co8eoGg~=paK#+2YiXIKe?fdrSV;obl+w#fMeo`_Ut-IXZE&gNeQS9 z9@hnGvR>;Xj-nH8@{S$;2DOFsxay)^O$(**_~pdy!c*WW6M(;nE>YjDUhG%iHoPab`(-1Ge{ zgZY)7RDoVAd((~elVFq)fnFadbP7p28kBfa8w8XR?d3L^y~;LQlp6=xd?7q~b*Elr zK~@09DYn?ZUkO`NyH_$ZlaU$~P1P1LPi~BaaMCR1o_lPN-**@U#VI2jZV}Ec%SJ15 z0^@y-_wF=xk2tF0yD{Mbc|H4knT0uBjT#F`t}X+ZZXdeH4`T-oWNl;LLX1-eEhgMy zSkB6Awg#?R4l> zZzj25V*0+?vPq=w*Jr@T46CmM$n7kr)Qt`$0L1Ltg0G*y@IlYRjJWZ@pR}ohd4*1H zWhkA+>P8*0j9PQ5r1BR6kp!6xX(MT>_@8Fqp8=t+h)0&sj<3^3iox>%xc6&bV;B|4 z3`5)TV6?%8K!_(uEkpFjzptbSzLso(?1G=1!T$oo{q7k2UN_+X?Z8`U-~Dfp3{6#T+PB>LyUu9zQr73{oqOnubzeX%-|m$s6_J8^4PPFZ zT*SA8e0tsLUX+a{^gQk`05K_HychbDzz}>ow5_A#_@lk^FC(L0MNgg+m=%3H~1L;I74*QaS@UQxWeqI7Gv(?z@ zdsof)dHEFHUx}LWBv6pQ@}A*y}Rh>HdU1dbR6c#AkTtmz;I z>|GsJJEIf3P?Ld&Gi{pT6;Bg1Um6fTKP-s)GYw})X|3yI?_n(x4(ZM}ni&0p$()nx zp2I!gbi%-$14SRZt2hrwHJ$;l(BCdmPFY{><7s>vW`W+sqK+r}xyIpV(4Q1ZROk$9ZVFwhJ*=y1(L7r_B4mV$Nlo4W z%QjAhfKwkIiYidpt8y&EFHT{0v)mrUe`L8&+jgDhd<@F)* zPn6wit8t#R4?WIfRcb_Yf2KSy%CDm?qx5?tb#H8Rbv2VP_=VIjUg4j8>@wmS12P!O zxrp4_OcO*1)hc32O7`a{e0ukI-nA%X9S){9VkTb%2*+Id)5-n8-mDR%L)3k~{pdOG zNqP>f)dblt2(IkOM1sxWgmbQqdB*a%!aBfCFN{$+$=ivmKe%M+mdT7ekNdvNEL3h% z!K>(FsLh;iXvp@;=rXxN0`hI5mPua<0n7XcK&r)oxO^ z0b|NoW}4V41M046{L{%g_*($VfaNw@K3_VOH%|Pw`}+Id|Gqz1?M~iAx>< zu(qM%xSW&$=jlSa_pbIHkKfs20}B5)OMd+6Za1h#7q%l2QpM1s7xP#)3zBZ5( z*Te|9)EBha1+Wk+o3&dAzDRGCZ6M?=xZDN+?lv5q$8 zK>h}OsJ1=>xmxe)!;e^%DSaS1d+%WIR)7-v^;1R%_GT@;2PQI^k!`eVdUV_Sfq&Xm zbyNoqeHltk7#PsK%lQ_x^xDAI#=i?CPOxV{#)t0g-GBs*Fwhlnpvve)L4(BS#sWd+ zr52=v>U=eHjIFc@-3XnPQtOK^b!CtG8*8M?(PZTcfT2wm6YypX+|F2-jSxhn38)8- zSISTML&1u@$a(p)B%KN}YFmShB^QP0oJ~=^^l_!N!5mu-L?XBz2eM5y4P0eLxyg>H zS{4&7sYi}8i7vB(Qtg2NsjSrKk;dD&KDObEHkO(xfLJL7IKi zW^s^Rm9@kLa8R=Cdh+$EkGS#Fn!lxyHAPaNX=8K8TdJFHl|}2&7qbM&eTb(VTL)@eV;>GSR#DsPIyPY_Zm9DWrzL{m|ykgu_ zsVje(?=2f~vGDYGmH8NdKbE|7W{~TT_SJY@N;q`<;TMI7ZDE8FgpQ(hRv2t_S^1{6 zkFh!1h#4n3)cYBr;3Ne3Df2GANb`O1Gk{)CZHK~ilE%X7ELsV&vRwK!MAxF}aYv&G z`{%XyRA+WIn|M2sq!&bxFYs%=^d{s_()O~^w_va?cx4d@{Pt=M5TlI`S=^xX95~R? zp$bKWl7C~?2&STPo;grVjMr=0k8B;aMWOt1=f_gULa{!#0Or9B6FS3H^Y!kZ;cORQ zJEJlc0SKFtG2Tt0&l;d>dtr}Z()rfsw@SZX5cM}LNNLiwW_kvcHB3}WPNX-v{fP-} zhSEhP5|%bjJyyTO0CbqqA!ds^Om4OvwG0W|1K-xiUaDa;~AfTgyud}Whs<%++ZnDFfQw7)%WBdM^ z#tWK`P@VXfrK{9Zp4u3T>lXN68tJ+o@_J%mW}xZDa!v zK16&3qV7}v0eew|qeh4NJdvt3*6YH zLmH}f{G=_RaL!94r;>4XQOv{Iw?!d^5}C^0c#!lD!)}AKkZp1?GFM5$d(ocGpUTAH%AU&E(hg$B?tHU=Ql4_mLGiGiJ^fLH?h{3G za@Z&9(0rvRP~16Ay-TEA4@#xIanE>o-N;wbiDotj_7p)>M zRD15@ErNzqxpIerxC~`F|M67IiYR^;Y*zErHWJiep*zXBwIJVTRPS>QMAxhEKP1Zo9`;Z zB?O3Ww)S69;AIO(wc{&2(j;HF*Q$dK@Nle$_1g%!a zZ@FH53s~5>nLr2AsF$hZdK4mRI+u$~BZln*=t0#I9=HMLxubt-<>KLG5=)HNk@(5_ z>e8SOF}o_;5hUQzVM}>Shk8Sa_KU8C6vtYjsFLwS4fM*kgK0ups+i(Fk4j2BnCrpq zObyWV%xFs+hUr86=%a~f6$a+Bc7AsAPq|A|H+1)+OO$FM_3k0v6(|{@fKb>C!cFh& zZf7XA1xeG^DtZLu(qEXWsqu;`nN|s-@(HjCUw= zda?u5MdZo62o>;?+urOUr%l|yL*a8J2N^%^YkD8fn>#;S!72yi%kGom8#+AyPaBgb zrBtbTik~l$_fX~atvf;YI*e;@CtuC=sPGdRb5M^bX#W{-$^WC{i=MBTKPJNHEZnVe z2s)0kmvEzlvgSD(8+ntE%24bT0&x?0P3`Yz@B5yOswFn>C9+E=*V)5MoFCNguac4j zOwH#so!-GN?ZFnf{B82hBZ;!?tBYp@aFqqf&fUaVHAnNq#GCVv2#f8i+)5 zSsaiO$csD#4>_}DOW&A)=WJMS3r(*gd#pYor$i3Zk*^HrZ&dCS-usMi$wVt+@zp2P ztZda>W$PK?*njnHv<^4jm~jkie+DEgXmqn@)ro?c5~IX*1;>1{5)EYSD7ca}`qRF@ z(f=0n{mEZ}qwgnFs&|+?g*4h**iUum7P~fw<(=>jU|I1x;$rmTOR%xy7 zRd(=`WDw`qtoEOeeUZNaAt`KFDCs*G4U^Yr1v+DxM*%Qd;*w78?7=H0r<6S%bR8OK zk`?nVk5=4I7(#nAna4i3QZm8emC=_-yua7;1~vqKIqFT$yk@#$C&E`^EKba2U))KX zjbxy#uUY7Zy2wFy(2M+lNl$x5X}soCrHWwSAh6$cmq(UpeUBX1&OjqfDxmSVYIhD^ zWomL#83o-0rAWvHt;C+}rhHG}f)tEgZuD2<k$KH9Zq-(=VO9m&jYp6Edo=x&B$k=2 zHb~wmW8j|zgL1+RbK&Uv2OC6+&Wsd&l*VS9+}tB;=_AO#m_$i0reOB7p9HUQs61CI z8@s#0@9mNFbl+wqgE-%)K5(nVx@=Cgo>Vb1qCIDOOoF1LF;U33Iv@7+lvH=)6PYT- z$+;V(oF-g?t6D_|q zhSM4blst1%>d=PXxOa4H$iy(mNVf;gh+hycDpXYW@1SBM5KO5VvxvsuZmtdf23rHK zrZIS&SytqW|1qB$%4sEH7w9$}OcrgZy(116?WEdaa(L3h%zw>G{Vs&br3ZtiZkv!} z75^8x^BUX(dKjCvqa^z7*I#EU3q+XpaKfH4jnzmg-f2Stgw#EBWs2r6Nkc{to3Jcm zYrL=JcnTZdakLC&u|NUPo%8yuo)^Xl+fl$Q=?;(Ve)K8PuWsn!h5)H>*J4#y>O>pt zN6?@n7MD(ms_R#_jUHh_;EU+c&UjWsE?%o;-rBm*?v)VREjsNd9@ACNgGZY)*H<({ z4?+%_q6AqQqmY+8gqZ+K+JCy@QjoV*a9-{`)38*$uv&2c%}%z3PU1USEh1 zN+z37Ko=NVp%zX^3tm+1b^RT-stcEwl{FVnF}TxQ4EL>$H;?9m-CYUh^u~gWp1$Ov zq!Z++2yYx{`pb!X(uTJhNfNrm(7{&mxNCRziN9WWtv~U_NNk*ZNv2Gk^6}#~JpRcS z|8qRk;N{Q?XzERCf)GMxz@fj0BCOChVfPs@jp_Q1a^#0gDmi6oi`OAMqoacV_vr*f z@oI{%1+PjI%F|lR-{RNw=@0^~i3JuRk;!vop)jRjYQ<;^Pg4IA-=1#qCJnl zx2CtZz6j&UZtuK^EqO}tbY}RoKOwyX4PIb}$6@g8=M&af&w%h0btXJd)IDR>=*QN} zOGYoW($=Dz`IDERjo&^V|F1o6f*gWqy~U~X$9GX%Q9Z3bSd-l(>C{1Nw0?CIZYu!~ zQuH%2gm#p9aoj7Ez6X1b;o*TqgwT&%bhj&F;@>jEh|Z-mSY20eRSz_OU#Q^n)zcE{ zti+W#=b-cT%~VxsD&XUs;uBb#kW`f&6ZM#ixh?q2Ol*Wn1@_g7-q6mMb7mg?%}dSG z!sfRb>JkA!9*^~3`Vw|{rq}4)8y3s$QmCEmw)mG~z@*@swD071>_~Z7Sa12Pup&yv zeVppY3_lelPj*5!CG)_Sz>+md?|Ci0ZK4+EFv-i_PS=Rf(pt8C#kz9JDIwDlc@!odmt~2_niOo{>$FBrHmJV>PLqLTm3bgXGH-sFlgM5 z^>*$gllvT#;U;{wI*@;|BOO0zvQ7+Df;f$Tv+ouA`z0-U_kGt#`(4+id{nuA`WO#$ zFP^xa!zmqok09P;g+%-#4dL0j{V89Io8$U4*5AQ1BA?^NVrkn`?BzXBa)8_f6g&d+ zfmZ&wnVHIKK^(!EQ=(t6YhZ{6Jh2uf!eT6P*ib2~r5mYaChPGbk4_+-e+R%R=FKm9{AZ0;;Qg`y zf-&szTd%8A@j%lPG@0{eUPU-DURrH8^ zL3Kn*`aX{x^chh3(~%qh4Prskh&7}6x)^g*gSaG3M^J6R^i|zQ)|65I@SADxGree& zoR}434;#Ub7H77ipN!$5zt*y``?$UyE+rVf408K7orSis1HRC;A7U8T&iwd z&PSXRQfNfVZAkn&zbeXLB71AXzb=$s>M~#iH!WMmeb~L~>HCb<$o#T8S*z~be;h(* zAs{tli*7*vo@AR_&mD-#*5ulBcU6?U=c_m8?5h#P6i__4a1#)=X+!ILDg|#q8u3S3$&9U3LqOuyiN#|nMJu@fm1un-brDdyw<(JmQ({p*`` zO``$F9=u6IE~4CqMJ77y6aMPBpY=!jwZlBF%W*eWM^Bs^F3j^rmXQ7p9oAv0VJ#Qj9&^s=oGZg^p~xSz;3LgUCAm0T?xP=^6`?reLZ8zW^V z=-wdHajUqfFqs4ms_Hkc2;G+z&qweT98*Kk!-+Qzx777zR<)+H_1gq6uBq=~JefTM zjtxi5Tm$iuGXjRl`R6P?VSP0Pt)dMLC^-M16?xfPPkM;5GG=D;JOl10&Rt?Dnxr0B zB!@aVvuPi{US7;PsXYS-Hft#B$enuQTI{@jC+d!03>9pCYS*VlJ5BYiLXU{N;(04^ zzyJDyw(t7NYI-ABrr5Q46!y=3>34x2r&h*odJ}eK92QfN#0q za@G6MDs6*iE0cHH{`0?DczELXUF7Y4hHOsc^sp@MzAbeEyoS#TP5Wj|$K}?ICY~%jDRK3NBZC?|6Dq zq92~0TColn@nrWw`XHYvsMvp_hOXxTDXM5P7p0p71&tyBVh?56+yRk}%#03rsfG@d zWLN7llqs{;YEU?>ozB(U=tLeRGq>R{U;gBQ=VwRpA>yX0s);PXnYiDlAA^PBde=n{ zP()yxzULpo>u;!nV>O$blj!|fZ|!g&)Ys4;fMQ14N6$;4^?RA-ym-NC-%(1AET61| zMwea9U-3@PiIPS({p+a>5%h@O{q$~rvky=>b_K!&sPtf^7x|t`d^FWP}iFKm|t^;FgftSX6Ldc?p(sXpzrtg2Dj{V+3yJCXbS)}^n@m`H!eo;z8 zF@7G$9mi(Sig8$6da{7EThB2uRGMwGW}mlRyhY8rEq+xbDRnL;7c7L`IIv?x1aZY0 zZnj3!>R3B3RDa(Te~{>QTnhfEqoPt(by5DlZ(=^dpO@js%ZXj+Ma8BY<)>a#3{apN zGCszKHq$3oh}f^%-@>+{iF@Opp8?{bRd|V+#$w$>Olw9$>1dV_9#n^oln;-reGE~D zqMh!U`4g?U{{QePbf{%3#_0uLr~nL~YOsEK{WoKHX7RJFNm{+O^@jYU<+`x;?Gzd>5*TAwAD0hfZug|sN`*d8uUZ7n zho6#sxWF*32>$?H3HLg<$i>XjrQnA7;Qz(KdHt6GS6HV$s_;geQtOddqGUqz9oFkRq}^)Q#bnR-0t289+9qnBAm>9o{% zuH;1Hl+R)9pH6@k0jcUl;MH)7F{|DM-P5;TJ25^^j8_v$-sfs?03{Wb2S+4fg+ ziy9D0nv#2!(`m_uHVXKo%9N9q_p5?_t@bu1hgYi8sh?}!&wJQ#HtT?>#rDQYrW0aJ zvTIE@*xg9{eAIJ-+2f^op8>%4Da43arEx1M?I@)_*tv67kpkY&Xdw4UEcYN|yKUVdF4dHdcLHd@xU1nmb z30AHN0+jUnXMk``N(ip_9wp&?@)v7@`XV7SWo))xD|UI44F7i(K&3b^VVKU~!E)oe zq*9dg3R-jYh^#EREJ z?$us{-GTB|a4;0JqU2}Q+TBdKpv#v}bDOx>$xb&b_meeJ)YE)SDTPgU_Oq|g6SB@A!1y}P&Wq;_QaKfiwL32tUmYu} zJ5|J!C87q96jt^|Uw7VW#4q%MwNq25s6kM>A1yEpbdZ{bMvUYNNq&uuDFceerC8^& z44SD%rba3LVL-fCv7;TunkFN_qK4-0oDwWaV?zwbF^zJOCr4&X;iz|Kr%n&3XckoZmuiOc0`muk3MOXFEww6xO>I4hRCAx!poq77bRzCj@A@KT=B#z*AK4o((!AY_Wczedqr2H;N)n3R@YaeRI))1;Kv1r*Sr$0&s1! zOZJ8`k>UMkkSAn{Os(ATTxf-w`Tc7bd4}8^q)_DOxzLvgNtAw{WrPB9FOlS4(37YC zJ{3660stm~R#nwsR8+JKpacs0P2OMS4F7&}C!7cSgRE{ia(-19nYESy1yL2>dX(n6 z!z~jGYHP{Z<+xh>GtAnV7lXR#0-*Gm)4&(f0p*Tjfes$3zvVH!YP-^)aQh7C-S(&n zu{6K?gZ%0iz~nq#4nNzo>VaD)yKnsMzW9ll|L~9I8Z<;a2{`5Ud(EEFD1r=1?`t5?APWqAm1xb#!Si(Lyf}wSfQF!G1Z81U2tF-Q)E3-${h!pB>EdokX>pcyt|Zy zJ@}Fjo=`UG+k}BLSwi}DGKXgn1s#{)bi?k=QtSISDe?Nc4jQM5B}@2RVFe^cAOxPB zTxQXZ)&$2$NG`5I@d}22B4x^tog_mY7j!N(Pa^b)Qv!hhuW4AI%!H+*g2eYJow{V_ z42os&ER89~mNqS(yhQ9x02jowv-25H9QmVz`Ko-sXq2^TEvfF*njApn0_psHPEmXH z>#$lzwBOjU1dzF%w%t#?sU{r|KAcYA9uRPmt4tJV^(~fj6$SQcqRb2tRG5g{%BvPsW2UmecF#;{;%@-2_U8uDoM$Scp)zCl|gZ@_+ad@%jet zM*4M64A0FzkBG)_&%uk`0hdy=ZTVKgt#bJYsOAf z$Zh9N&szC6we9W#-8SMY3o0=7PZs&=siUJwZ{cM2Hy%R`qe7q1wqb6Q1BNtNMy)2QLglEzJVKE2pU1A;0z) zY0M2}(FK&41+T>~(mDS`I}<5bg4l#N@0@jQ!#_}r)&kOgOzoUqFSjNgV@(}J8d-Qh zJ_8bkNYwpF6TXRl=Y&l8V_z6GskDTu_0>G^cPc*v>i8_kMo^Lu^ zUo6^g)SE?P4ggc65b-XjMqc3?@`+}cItTVhNUELpE$ ztbH3eY@(4t>JBzcaG~y{vlltOD3}t5?0jCdIk=YNvU3dsU-XYup=lArFvha?OBI>u zge*}ru4ok!f3>Q|dHs#Ufu^G7Z+W^`K@lO~xC&ci7 ziwjaD8*_)FBs{W=#9_PZ!?QVg27niL9tV=&dD#t&dPx$Ibj|(Ry!g#Eyt#RlQe1Ey zfVPh3S(jxZ9-j`sBeKDAy&KSj4!;)%sG@I`;UPAqnu*UTWZwV?3Av^>Wy%(BvBFA8wr`8|PG~hJF!kGA1 zJ#lCdD0I8Q_ido!@b*UAmm2u{f<(_l<{{LjQVz4<{eD)L59rbvf+-2e7$}*~5&bx@ zgA+n~76RDB>!4`Kb%SOhO6q97r*BvLlX3KN83qu(9BhjJdh|k^H|$oPmH45U{t8_| zuc0N~gyP83(dFA|H;0}_wmSdZnjB9#zES?(9P~iS`pRg-r@B5~qu2%8W1xbihr_s4 z!t8CFWC+&BCq!ee)E-r@JRKH1kx5Wux>BaSbZTsdh+s>Ku1kCGtYF^3#(ILE8(=N| zmo(iiQd0V2O55vgf??n_(ocp`4-KDJ1}V*-&I+Rjo1?fo8Od{5qU920XNrTO?>!0} zK^%;{oYGVS0Wo#_mhW-puq$(U7w^VN|5z}Zbb2F)GQ{vnHTzhr8}}(@P^m?@NYTpn z?>P52c^Jo&gbG+B!mJjsg%i#f&Wno`9RQnh%`uxB9c>g03Zg7nW@L~7RfBlDh(yAC zjxrRfofQ&&Xc{k@O$?=*+ibt#?472hkLyunv(rg?7Jts;3`K~F3CRa z*G#Bz+KjjFARU(B?#OGplt`KRX?E-+>kC4E-%0tg6^{_;V`-@LX72D|7Sh^p#cPF+ z2)q^?xGemPIP;97hEYH$r^*O3(%cz2LngT1_dl`-Wiz1mFnkjltJohf(zMahvW?tP z;dzNAW7Mcg4`IJHM~4Z2dD2S8R)#Roby^uYdEY(k*4l;&PSt{Y9or>K#G2rI>E#41 zL@$BFy80M!t~cA3u?*B)*-!(6MV%gWtB7)$(>i9vURoYCI$87vUS+4F8l`(}N z<2m2(pm7`M-Hgz$@bSx2)A~>n>xhT9Ffn4~^XwNJ1NzmKwZuHMadpj{pu#r5p1=;B z>WURfQ%dPB0cY zk9<*~=P5C&wx>IsbfH6SBgnvN1f7d1+NutdDq&T17VZXdP%&y|0SMRU)ORBtE3azM zu;vEK9+~ayT)9%%NWdzau@qEoLMbwlf;Pa$)hQobnpBgXfDONY+PI2!NcxTxU>s8N z>9dt)9%f}#8b;)wYPsz+VIwY>*KC{-zEAe)QjYRSu#CP;>~c2mTvF#CLStR8=;Q4U6>gElu5eCw$82@$O!^Cl9qY^$yCOs1cvQgt@o~&q?QI^kAtCi zQ;z%7*63C;5*BwWkV2dw_o7K^lH7%j+!Q3?lN~z$09s}%8BM%t9oY)sdwdWx`O@7w zFA}*!i2Sh}Hdi{B^(L2OS==+Xlr}UW+~%E<-YDRBl1W=bon-7Yxb5|!+d&6389~Ty zs=cTHCq!0`BxK1yIusC+`b!L*$El*5Ob*dW50S6iDW8-6f6#R=)bW{n4=b}8ZnF`d9OFuTDB!?xkIvjMR;Xxy- zq#+sKg z5HJbag*>vbmz>5~V>n@qk(03AfgBp*cUKFgav5?r$8T=EeQK%OOL^_>lsf2Q2{{-( z+kI>2pBAn3Tdc(DV;EL!{{YrD_Um6jv@Ik;LV*UH=O-X%(!P7~W@0SjLYfj(R*aL^ zexHEv>006bndi~Y0W!_uyAL2T3v^S1jNwP;MK;h#8JQ6S3~0wwp8m9-2H~y#2gB}S zmOvWTGoWAt5opimk@a$US4g!N3jg$(65D8UP6H73`)H73Q z8%T~b5nu_~zkh`@1Su7|$#CE?Tn#;|HsZ;sG%?!%tfAS40oA z#WMV`nqnl#2SL)3YhtiG@;e1*)ufE)wgn>CBm_#D2w{fS>=y&qAD`t+ERIwVTrvJp z(>(^>w7)RL0?Q*dbCpmtiZ~-0nPgl5bv_1tsE#a69pGutlVTM}Et)8n)vi=DMfBy$ zobQgGohxy3D3{EN1~3_h{{V+?^{&{$#y01<22QvKr*Ew@5dN~c;7Ha?#! z^l+q>(G^h;2t27nMTpGH6na!|sP%8HHkR$72QOB9$2(NMmQ5(I#)>4CM#Cw5V;J_O zzTDR{Do1K#iAlj@q!2sw`O{O$5X`eO=^HPbVf3L!(Ojd13Sk4z#P#;tstasgr826d zNTc}<`hN;EwxnCsb2`j1*Xaz=G-2^|16Kw=T-(C*AtvO^l0F*=5$ zryD346r;_{xK%K}QGypGv;8TxOPQou6o}PSg~xoFs0GO!h6IuS08LhWHP$>q$KzEb zvZ(wR)y33l4V=tQPB1aPO;vn1P`)MO#I}$C>T&$dR&`SL)LjV?j;-)=N2#RLlqX@6 zorh6YMpcr1GAI~rN(k%R3QFQ8M*si=Z|O*t0OMAcAZ@V+I|}WfR+)~Rq9)Z}R}c~OG-An&n1yiiy=$0TBFfy@h~4o6Y@H94(ednVHf(mdE@-ACc+NoMM3 z_1-y*3}P^Ha5f*7D}#u;qr@_(d{H5j)juBZfqW&n2n z^c!HZ!=<$X0a2xQ81Gn4qz-}rUoxfzG}oubCLD$+}h zN7DgKYk2*$m+5?Y49g1Kqaz%g9+(@~rnTeIT`+YO>+%3Pv{ugr?$cjB@C+}cBep#=n!^SvN$XwmWs!Z%yY(4j^HOIle zASE{uc!pkVTPqbT<$aDuI-RI3@TbHtfd`1@a!%#)pbuS~{* zckpW|f_13CCpgU(+Tzh8Zbj5`6j6d?@~Q2(ubYpBJ|WjA9wLaCSO?Xt`H0WHb|Sks z!><>r1&54Uw)+630Up10vHg11w*5Dwjd1wq!M#UfPJF-v^EB0$8-d*B>fMt#RRjPr z=ze*xoI&v0#uF4q;})xx$p=1PVhZQ|Y5xHHGx2N01imJLLXD6{e=Vt;KFz;P?bj6H zOA9dFo$1Z1pH#dkQ#E)SJ`PDg*8U@m?f__Em+OO7{Ws9YwsQ`C*WlzLT13*lUQ zh;cq8!=vN;v0;E+K_WnNj19d%I@FVwOzn@RNyQp9hRkvU$>)(o!}xpmC%;{{uYIJ( zB6O7)%6$4r3cr?5`PL8pf#PnU+<2;Wk%mg>X8iTn8^H*yt2Z3Job>WOKV0Is;SW0c z67hQra7EO}sTTNeGci9dy=peRdxN>uWsy`67gC%I_WD*LABUEbVmP?v`T>9xu-N)n zVEA&=%G|FI-o&U2Ng){uKkgqY&EdrJkKTh)&-c)s#hE2)5mh7YEBl|PS( zMiABG+tgtT(;3-Zf=_ zF_m=cdVeoZ-f1uVH{o)~RxUAgNE@+0K&Ow+n^uc^Z5x5ik1e~f+w`X&iP=a-rcx6F zIqTf*REzMo%}}}HHvkcwh{s>Xr~d$k(Xy!?CcDzd8>jyO+NI~KJof>s_A|hh^@rS;w#A+pGd;s4YR+RfZq;GM+d~0XyG*n zF_6CB%~EhLI=hzY#@Ys+Swpsbia(TXP)qw~iKJVgjgzw+AI_+5J{{de_+JxBe42)1 zZO*{|06yEA>^}|owdPrTN_lJ5h-?G&YQ7({$#EUZ-b__xQxITVS3~ut`SB^z^C=@B z1v%AO&-^~(uqY|wd2~J$vMX($Up8to_y>sEvuW`(jVclt2+mg@{dIg($*OznMM9CX z7gh5Tz-ge|aR|k#B$q)ofEbcS-Trm+__!w!xba1w1RQ2fR}q&Il{ih9;~C%X{&l5K zg}iQAKab*Ks-tpsu^&xUo5peOwdHR|7V5@LOpdD*Kmbp2e_AXzc5k}{g5EDOslYNH zP&%5bli{x$g#rHnX+Z;U0DS(n89WQe9Y{VRG86->XCP?n{(5ts4s#z-iBy= zK`y1p&|=1&y>)(ck{;J`JPmNKB&>7{5s{4i>SFp5$dKfqINGyEz#My0$avhEP7bEX zABL(h@Q)LN8o21xPf*<}S3c9)^7`TwvtSD2JqKE>_-#GAaeon;jXGK5*@kihKjYfD zAAoqZ!v;PhR3rZY;n(=@Ngn`lz7*4oPC7Ksz(~lYS&sXhpL5XEnbwzk#`;V9RhCf; zJ|sdlVD8!5uWCZ>RcH~6iqegNR@i%f{c8^X9OE{`YVjoe!lzQWatGpYY9H*Ui6uq2 zPAeJfgCl7l*0qu**6u0wN2HY`*}4&7@K zz7pf+E@Sb#gp6;KA^kPf2LbUSD2~sH8HYv40Fm?D*0LwJ(C3!aERx*b$1JSBdDs$v zu&HqNwQ&@DxZJ7AvYZAx^(P?xE9X&oQ^nUV;9SVsj#-hi#~z32>M7yjKNc!3t}V+A zzBKLi#b+Y>`fsnFhgd@5CP3XwhtBxvw%IjR5Ameti3&2WQhvYMva8__6BJonjjsFd zr0MNde10zwa2Pv*X52RZp0#MgMv3$Q7>o>h)@{VJou%7La>&GKgls319|1BC^f zTJ4%QHVND&pu}N_=|jZ1cTFQKajVdG`BPL_rG0GjMs^q)cP(9(q)awr$YV^>k-mPIX`5=kDIqGDq1O;4B#fPjW0eEg_a7t+fl5Gv_1VVomx zj=0atyLot)(InBqHN=j(Knu1;_2?SrMo~4BqXo+owynAY`qr_15SHZ`@M9}-hX`YA z0zG~E)3$FcdfrANByBxGEJ^8A@Nl9ymFE!R5+Hp+Q4=kr9Y%X+YO%JnHz^$VR}vgZ zU1JP13>~xj^{a6-mmj)$p=&s!R(aG2wQhR(k50f4J#uU2z8L031BoT0G+P7kl_(@?Zf!pU@IQz6t8X~_d|x4G|KwQh7nQ>;Mg%I>Sv`}fU8 zD~P_*>gAgR1@e9s6&a<~8MU^6oSpIxPTkMbr?m>tl%<5}mgouT@BJy6uA)eb#+l8^%v9bpf*tu|byRYiJ8b%FQ6i40~hO>q+@U zGd3Mcoh3=%{WcU>p_b`Uw5*Xrx|>nczB`{7L0tjzrgo|aB9SMY>L6|j?cSt{8Y2>5 zE>1w$5AHXwOjAZ&h==oOY=!w(nM9DQ!bs(z%D%L<1cRF%&^2Jz-2AafNwCU3-hhTU0IUPSUK#IsEkCzms&Z5NZ z2x0)=JqPxw%X0)4w)TR4WAhZgl|6lTt{5W8k|!$63pS5!&tH`>XC5Gq>If2NRxSyX z0ZuSB@A)2;P>t-NCu@FIwp{Z{xGG1W_x7kqBhL<-U{HaR<=bJC`B$%HlkE>2iKI&) zcS0~h{{TvM;@?jXSr&9z0V>Lp0x|gRar)IBva$(k#%qCzqRd2}FgX}F#&`7{vqX}} zXqsDT66QcZhyf!1%ke9POUu=zNYcLi!ToFKM~Ypw@i!5mUHNEE26_zdy<#}Exr^ct36dzx zO)fwKr~^3q@A=lcUR1LZVFE$67aO90(?nm^Zm$+7IwvE{3NeVNZ`_Rx046=q9A#kg?u3Re1 zmQr;gk<7`~jerzakRw*LT1B0`fxl#fac`t|%NwSp#- zmXQ!5*qu8Ownxs2`emDt6AcayO5-Yb&*4omTKvS6ME3<;428fZhRvfq$n}Ga5OI&C zPVwP`in%I45rRk=AlD<5+_C;rNx);1k6PLaWBXWGrCkvYg|nRc`d4IFZR8Qesz4jC z+>z-_1(G{`MU@0>1D0%LdsTe5dXBgtB>IuIJ%vrvqj5HsJd{-Pb;dWZ8Jq0f;D#`-G{8JyWNKmw$E8hN0eFSe#u6wni;d3pi};4~JYl6& zQdxfC*!BJ5wsKrS%|LmvvVce+{set%597CfPF*mv6$nV*x&D=@+dR5`U&5C9{1L$< zMCK7_gebrVYPXixHuzZ8WFr{JI3G%<@KZ4GF9i(7H8!Iyj>kPJ?@~({G;(}%vSNJ*pRRvgsidAH2AHoO{vQ!g(ZR zk&x?MOQ?LdKfd+vuv};mMAw^JL0b`=UHJ@ zT}Z%XfE_-6ttL2JagvJwNZ))$JNwg-#T~LPgwhVg=W=RXM`f8{c-5T$025)6)82_6 zkVw)tyl>MOp&^I(iz8%$c4LEztht=b@>?orpv}tnh z5DKh3qZ!YCr3z?nWxtOTX^j!JX<$g{{rk}#L#?9=FdG76135nR3yTD3IZ#HcsQHFS z&c~^siGtmxSNm3i4o)QBJ@@(5kzvb1j9fDSH{bK6BDIDR=aRT=U}GeGO-eXYAVAf* z&If+gElVAqD5aKIQTb_{$YaK*KKpd8+oVqjh{UEXJ1E~!^{8C3q%G$#?e~;#@~-gZ ziZQM>!60Rh$LpGJqv2P?NP^1x%U3Q}fu!>80O^mJuax+2{{UxvO1>EK!wV@4ae;*! z{x$aABg5K$5px)9Owl$QZO$v>9vM}*_>sho%OCyNf=T}X$Oq27ABOI^^1IVsuV#|G zsjNBZ=%t80zm+;TtWOffIe8U=hg|M3e;$}WI!)}QvxRQ*^1}H82Ym1SjRbOIn7Ux+ z`G~>y117s>v$#9Y&vKe$(WgQXpppkeoK$0pkT5tckIsbmJlRBwp%?>!*M7tBsoKRf zt*M!JIKb^y1!2xB8>l$R$N&$*hGAHj0^xusAxXyg_4K1n6QFgKTPN#hpZWw*+vtVmgNsOSADXyR9ih;|wn`E#AeO7o<%L>E>L z7?F+19>0O0Y7554DnjnZ{{WVmAW@bFK`T$piY?e``@m!n27B$-YOk}8Lk31F#eWK& z+;dD-WVn7<+jEjWbio-661a`xKYM*G(0Wt17Kf6pZzL>03>RKb4t6ISnyvCAji)+e z*b+xi>q5AmZz|~7x4K#S~O-wZ6+W|3*%vh@R9T@rwW9<*a)pKqn zwLg7E$36c5r6&n!NOqqwoD|Y5!EU=OmJo3lLgXvWaKrNgG zbmhK$;F1>}pXW>4$pYOOqum*bG;Z3LBYlr!=~IFj{=+)mrb$i0t>g^+qv&wM7|M8GF#6YxdgG&*nDGc{WFUBj~py~Q^OiD z=51PZ*U_#TIIlV3j<#JVY+!qRYmYna(}zLN!?_TKRnlGZSRFu6j1t8QuQ3=Q1nWBk zwMtv3I-wFPX&Q@qe)1?HI2b-&Su>2joR40Wn9|dQ#HlASr40Bi2W$~Wjbcf3v~h>~ z;DMD@`Scr~&X9sTLCaZD{Qv_5=e-@J;p%j&^1}H{Xh{cs_wSq!YRZEv%Xu(V^92Mc z7yuG?IUd4{$sAKPs}yyKa$Fy8?^0X1A-0w;DuUQRh&CI3HCrXkzdBhSFheOMw%;rP zigpMJ!K*-E9F^q*~+roKbuGg z51q8t_|xZ;qT(i5WJE>MOL^Vc4O#a3QBnnKg80cQwc)~8AGIkP%B?#(A~80;tlTW(&T=VOigZ<9oo34p|< zlTk20E=Ve|&IaX;8H0uPZz#Tdq)IQF) z537`>6#x`IzJz*IwgX%-d)Xv}Gc;YcF67f>r2fvkm)fxYYTun zD);pC{b-hQnW7S0JX+my0-TLCm2;d76B3J|4oL@W*A3L-BRN%I;4ob6xY~tzByR{= zt~8EeQjv_7C!obKi+S^Et^7EY5_a4VdIfD8OeR^Uc(k2{uKjvc#}$&{*==D~P%s)y zu1;xQC`59rM;@V)x(GNY9Y!mh>0yNo?y&0s1jZW~-#>_|4|f0r%_@>vfMAYDQT~*z z)7;6I)oUJG_+d|(fIaKCGg}C`ODn&dSCHiY0F?%IGTd8|qeHMGJyd#C4cp7aE)Y5_ zYZw}Iuq5P;zpwYH!z!tYIb>47wKER7vVRKq4~-*v9poS!g;Lvm{{T7^F|2l+#6n#& zmIJ8u8TS2YMXYy0$yM_zo!=k>@~Hx{D0tf-p4C(K%SminR8a#)b|itC5WBgxnmbh3 zN;NJ?>C>muvv}$+ah4f9duEoto#TyVLPD&7F$1?lQJV0_XpE?aPV8H9eW`ga{?le) zc+WCSE}nxscl_%)L+=922z1oABh9~BuSAa4SuN5v@}MUtAod$|s~aXF!4g1AD((p1 zZKw^x(rcEvAc9l}3z7~g#+sD332$oI)S8BtBkqF7YWYuuY~JU`Tye>>%PY{ z&7#~#5RIfyvdBm*uB5g`KMkwV`(yM$B9%_2RndY~5cN5tpty;=@p8TMnlxt88C1wx zPzfVW*aIDZI&$JTrquCAA&e$e44wURzIUssg)U~Zk_lEhPCywFV^eN(kEJVY>WZ$I z3hFjF!h4Kwou~%gCUVU9B;*s;pMo26V!)Sa065%YfKjYYbc4xkWR>gl1Enx*`3f1^ zOp-FjBRzEK``)!!d5HU!O|hK&)38SBTU3gCxnj%#AB`|Cua_ZZUB^-VDDg*idK`eG zZN_P5;uh@WqHbGk^~b-x87>uug08UwN&J1OND`welxVeQ+c`d!D=S`f<6K%C6Skec zwJA83mLdgY)VS2@HdC?2YEVl0b+)Bt1m#T!wJZ)}!If8bXWMhwRNWNu+a zgn~%~o`Cl?V=tb>vZhX)?~I=P4I+ydIH)){$T%Y>&}N^8RN5(4GT(F@ZnUI) z^obG4VYZ|9Mn5Xy`92*#83f_)X1XNMoY9um?s|PFh1G4CAP*)+!A`>!B#q=! zp+*7ey($tUoP>}7PBatVo@*G0jZC;jU;%ajnjo`+H+dt8HaHq^-8Cs|mMr{3~sU#K^v3bG}K}NADFS&0jHoRxW{_Ut06gU zr@Wn_NJJ*xPn_+8-}=@E;uAE`s?5ctfkDX|9r2O)*V65$Sjk5RKseasvGhK*^Ph>$ z6w?cfebPiwK|gSVxcw^ew{f0R)9OAAiR19M2?A&3o*4t^WEfIZZV%(utmJ^mA;hm4 z&zaEa)%jH)fp~VF|MXB0|*YN8|+88I2 zX<9KOLJF|csP1wAs|37A?U5sml2&v99dzm^{Dmq|Z>2HDhn7npG6Fgt)Sdi1_-)zZ zW^@_Tj^3ZKPt80N-6S_!d66k*8|nFH`cz|<3*9Zrkx&DMV04q8T67b565bCbv0h=z z0DE=CFB-{pa9UVGta&M`WBApzoN!7+u_jr;#@=0yGwoiw7VPU16nE9;-(Y{eC0#Ef zEUBojBUAT)K3w`^sWroZ;uu2>NwDd*!!_5t5XAh*SiVjaCd22NL!T_HB{(go1mpL$ z6wu*O!D?c{jDSHSaD4>|eLVvZ;*XXI{Q#Wum~C=>Z+L&XdU~E z`p_Yg1d$`P&WS-{0n0|d-u<_w7N2M2#AP=h!;dE*f)DypBbla%3=plp;}o^rjH?te z#cUwcv6Zj`WNo*$YDQxbhA_qDbsAS2e9dDM{*v(*F}M`b=o*-X7;m~VH^+ZU`A@^U zpR#z5jV!nU-%A6w2xdRhzOUjJwj35%ZY9L8{{Rdi^J^QAr>%UG!jZIoBjeD|q%oWm zp8jSv>ij)9^KUK+eVin+A!!sA5%&K8T2eMBs}MO%<#0byKtlrM zYT&wpxF-UNG&a#sGZ|$V1EDw=8TPKE$S%T+;3-g}u@oknNLh6zqk-smK9wk;cq5YP z;TR9RcJ=L0MiON%qE==n1fRN~cdjWw<#dr5IZv1NrY4#PgDAX^k>P;uao(3m(%=^6 zDrq|w?YB?grBn`i1hSE+kPkwCe$*8pzOo6@a87YXwT=aDY>}J;zo9;qIb0bzi656& zLNW*M6+slXl8U4Z7@wR7Z3Lbd^$k#1XgF zfI$q;%q+=)u|2&gvBrdn6^4P3Lms~V^dln}OGuBs@##Ue1PZr`&^ zM$gDqiZIHR@;d#HZg^cB#l(zHWfBV^1)jU+nuW^Z}B6;(_HEMQD7H3Q$ua< zy*CGsd_QwDvfZM@gF4Ftlj*i=o#Dz+cEkwIf;E)LPzgOLC}Kv8Owp+_wygtv7Gd;4;*WywGICOD#gX|ONGgaC6Wcfz)on$ z814RMg$Kkt5T})Q!{a*39dn;uhfdV~K3Cw`_Sr>sEvRTAhfFYx2^~)7{!lxe>1o`} za*@JSBp-O^9-Tf_n3v*D4v;D}<%0lrI@@E{KaE$*@jk-F9BQ?>kduuCj{c;9@6&3& z9KMe(E!tVXExD64gy$rZdK~_=DOPCI$OcfU<)Aoizj&+)d|9@QNdgeeFhEH#quXlo zd|QKB$i@}WJh2F!gCBBtq~XZ&jpEzSI*f;m&d1H)ruE^%T^MM5X>5fXbUo`2AH~aP z*h=yvxBmbwV{o8;6&s(6_S2L=Oi_jm{J2SH9+^G9m1ho6@{R0PO3*wF14$dXcFSWd zeqTywhsSRNGYI5_%Yau-27hM0YQGpGN7Af7?B8c35B+ph_`7VZuF|BMM@F}8{{T8( z9FHj92og>+ER!~*jDNeSZM6=&eDnC#hyL{1qDdi;d3@twJ*(z};{~a85*W$`*2N%Y zVsq=hDVwj1JRV6AV&{|_v1B=KZicgXa{5mw*!YzFn}eHypR`8msUbKZk^%YTcCVgz zX2m#xGim*rZo2k=2z4 zUgI^1o8n<;jB1!x8hn84KDeNd8rqVfr_zIR6&rK-R(I@eTm^K|uLW>N_o+7xZfZzK zRDHvC_xvj<2gC~qfXs|pcLzgt{3z0VNVJ@wl2SxxQtDUh_!_!3vqcncvF_}E1}Ogk zc%P*PRDd{W)F~PAzQY|eS%|!O!y+Zk(id#!_)dELDh=@(%qdiC1|6e4exs!gYk8LG zYo|t#&76_5A8*EoWo+`tE0prN%MVYbWIx-!9PFIu)TlOIS;ZNCBH2bl$0Gy-2Qi$V zod%UgGv#RJX zxdVN%_V<8a^pIOtR=2fTby}Ho>93P;f_9JMT52r3<0po7^fjdbW_S?(#L;V`S|#s z8Ds|9FcWf0pEvo@q4=W|Q3MKPW;td%VyBH?DBj$vVfGL!aIZz!&H~YN@ zTFMU|@FNyxaAX^uCt_$n?bi*F$>*Yjx8K35;Zw>qts~w{Moy4?qqokamWN`#yXL5u z#=IF5_!C9sAI&HP*H_1UB|rjrvyU@0pn>uNuZ{XPFC#}%Nq;#xW9eO#${`5NsjXU_XLhMB52rk(yDSz1|J>S%odQS!LSjJYvTAb?4&Iswiy2CR<_2cm{PS1HT6fjH zlduP4jL_a?fz$x3y$;^J>!#LnD+hr%32m&LZK#7Y9Y39NF9C2_+`CycQ4IN3W+(O9 zuhg~lh=@ioNpN~&VZUKaiINLm8>9&GH{?oH`_jy$$Tl{p>ERu0Ea{hjGe&8wOM_tCyF@Xjh|lU zBRT;-wF9V`+Om8(1b-FyTvBhQCsV&erc8cy^bnBFbfUV2ljNsE(tsb95TW;z}&DIbl3FG9$my`^%bkFZ<#N}Ga@EY)r3RFRF!>rdM%;FWu9)JP0dQ0fIRmxi1 z*}$uhKP^BbZi5x+T=4a_BnEOY?mK=JSvu;u%WEzeyKkJ6*Z1v73Vb_C(oZaLf_%U# zjX;h3=U=NsasWO#Hs9Iq+B*@Ew%QB$vCOD(J`I^9AiIt%G&$6nxP>%AnlFyCE{ zR|v9LOkw3_j5ZYY8||N_Y51ao{^oa?*%?QWXMA+n5z?EolGuf}lTyeV2Ll0s-y36n zscC>(_`L%|(|nDb*QyZt?C=H^)7ATk2Fx?mjl zJvOASV2W2@B91cZWJUlF&apk;Z3q6&_ycWMI%{Q$AsT(&$mi)7>|{t$T-0B zWaOIb^Q`m)mK-)1)GrFML7bAX0ktz^Z1fDmF`PsYX*uoeI#;e@X)>gqSQy8xGaB2) z3Xsjr#&9iFH^gopXI}1r?k~!^dxon4)bOK5B&MCKJ&S_Z# z6Vn~)R<_c}T4`AqNzxZ`MO{Mnyd#AfL59xXIxs`U{NAJ66=N3);r6md9@-NjJxYU< zUPwnXorOgw%Du%JRLd-?bITig=CiF~OP(9UE*-wpV{T*4$WyQ3LthQ}a&X+v3umXG zJfTh)!$xG6OEEdv_3c4t2%%Rv!N|_Wp3sc`4e+e38m|hoj~g*a!6fJ2mtO+-es;=m z_)K{?WrYC$0PXgxn|RVlib$bOe(q?tw-E;r^4@F`LiWc@)$*{&M%Tlh9*cSAsA|kYMDg_~P zmQmcC)ZF$KUIoJKtWr^7Ze|F%9I#h7`BWu)4oY(^V$Y_42tOlE-`oRiLDZV^0LBO% zI(=yvcwot5N-EdZ$rla7pz{9!yvRmRPv)$rg#0zaxTHKt-P$yU8;*w>oCeX}5y>IArCmq@X9>ers>})W?IoGYXw{ z4t*z0UXS3PCEWHp|pFK%p*`t6R7X(Yd%e6QN`e%2P3f-tU%qf zGXTF_(FcS4G?ENK%m;E6-}NT6Q`!BN+m#tI$unsnV|;w6d0=Zmi5w$sotXE>N@`?N zuY`CcudR4+Sn;x#V1HWdUJUU2N(*q@r~d%=ai7kvBMT#G=O9DZ9R+tWBZOiI8|oWv zO{$60EZOF*mIxz)v>sWRXANB6QuN^J=ln@ zgOlhf?J^dB2Kac8jl-Oixjfg#KN^I-2k_da1(RzZ{{S=KkNc|H*`fdpS&aMnBWeFNb2vw6Bx5vK!0Ie+~sDWbAd1`feTVsu~Gv2J;M~(%vHu8B< zM=}=QJ;^7fDAwBMj@a4B3}bTSj-Nbr`uC)(POas=M4kr6*30WIt;Fy4fg*4I-8v?YMxt5YjE-0WR+YM z?ezK5bHbMhVu~C@yMd>3=mlOIaPt=f1;)Y0r3KzpAihR$0)hH(NmCU_(yV;Ap}~u^ znZr=T{Qeywk}wET4vwSL<2^Q~C6$s%8e;3d4&5kMF{@ZzdZ1A;Uvl zd{^ORpYBG)kUIVU0PkN)ggV?#feF;wkPe6Cius3zt~s9+(8dVm#>tb{ruh78>Q<@9 z)ZE4xVsJW)`c{7@y1W47KpVe0D=`-kG)0jze7QS~>Kw@s@{k4clO&h`0q$sbS5}b+ znM+_D8QZV#tt)YK%Wmlz$Y2$4m^i>YpWj-{jcP{eZcin0V^ObGGmRQ1KuhBwoar9Fr4rn%SQCK6pQx_epR~pq!7+vDjA}pYOf+Z}1k${!vl4Q;M!@{3olwZF_IN^! z;#~J0gVK_>k-#Apd9HMvoq(x*pZMVjZd+$0>=^v})TCH_mfC2<$smj%$OtyTADONT zm>tBTBd96V+mrMaT$1ANlN#f20qwb_?iK9`WG$~aErWrc)hts`+(RHoE~>;~ly%4T zuM0b=WFYd*v%aDk2CrfMbe*iGD;Z{&m9J4y=WV-xdb#^aku6|~652p{rfq@7_|L6o z`dRE7d0N{40FiYd=7c=58*`rDI#OF#d-ah>MGJsEef_gd&%#<+-gj=u*AzSz{0rmoM*jZz9dG;69&$$D&+jw zZS7j)+dR5!cspwFzYZ~vD71?udUO?K3pA=!0yKJ#wNt=f9uwgZxFE@Dm~3?z8&#IM zqg)13iZh=1^scp?GGAU@h}d?|YKoOrD1nB3_Uk}Hh``mS0BY_{M_DhRIAR9=#%>eLEenNBY%;#AzL} z#|TixN&?y3`ii2_a?*=?cF&hlE0KaIEG)}?qxi!yE<+Ly=WGw9J@%_hsRQoq9 zd9V%!aY|d*nWJpYDlU++?pKG7sFM=Y$7qKyGt{vq`{ z9QXWc$cHR#;I`27N(#;lfV*IAzVwF?;x({HUiYLhj% zl6lO4?#R1)jm19=%u~sr+D9{o(x7z4-;D_`?pkRg`wPb7VfKC3ew}`F)Fs3Kyzt!9 z901Ca44uI3x7M(p0h$=+;!)g0B#hj#2u?D3b^6!QxYmn549)hK$~DHA&;gPd`u-L3 zzXPWHS{BgQx_qOJn;wU;7_Cl|{{X1dT)ZoUfMEf`gVv;Yfr>E{5$QTc9PWS4m*t*g zzD%q9rx_#Kp3@6k#2P<28<5|TtlVt1zYvBd)MLpXbRS;kq|BERnI$>SaJk5?Ahws9 zB8;;r^6juc01N+*P8l+%k14|5npUS0h=<-80M}Hvn z^)(5k5=^*>fE(m@8LdPeBmNuYAV6?4+t#HV7Qw)F+xfT7Xi>bTMun3h2Xp)0n~MeF zO(DxR;fCL_QmLnCq;yqnE1dav$FQNv1SuLYK_LeRI}m+E4p8y)h=e3+dixL!! zj!TATAlL3Q`*k<@(Xw-Dhjh1LT6KBpx&KU zxP{h9`P1AiWF#@i_crQ%%}h1vpuFdhLM(FZy)#@6U=jq<&!=Sv7~7>n3)_^pl3CEc zPL)&yk-tn*w;@@~(`zr-hQ^cWwLj>}+CWdm2&2`OYk-CAmYWe`UK`DcuI?BO-I=1PZsrfrNbf}S~xwz!&8brqh zmYd?NnqiJJu2ulB?d?;F1`{EaHloC?2+8e}`|UxIr;584=Ih{B9ylbwg>Ru?u=L*-=}0{V=x{{SisHq0=`TWVa5LtraX zrWc7s@W&`Y_x#|U^zT-7)-5Hf%MwJ$2~ZAweQ7`=F^E<$0S9oqin-yd6L8@Y7X=$Q zZM6>gtlF_%eQe08%BZ183_5-Xy)SUlHLzV}hIb4)d)2J4+g+EF?F=)xP`wXMwOu8o zEW&t^-9f{#$F4Dtr6nvU3Ylj_R2g1`U{&4$wqqKWWyof8pErDedRpDBpmA6Gdq%?VX}IPxQ^D&TbO|*^(L)E4ZV-0W0xh6rGxn*GD^5W1_x5u#d>K?#InZ{ zkTZ<*#WnUzX&0Lcym%)|IMNO~^xRU?#TB&kA(_|}3|BknwktBrS|JO^{vkR^6135- zk>)(mvXd$PC|OO{MD+1 zH`rhgGsOhb1C@BdESvNP73l4v`!R7gmByxKCm?!zQ2y0r3J9RLWym2)N8hgJ@ZOcW zy*8F4h?V4Tyr62|+uo8;(7LlBhny=Q+D@M?g0P;ZFW)H z&aSSjuT+=?ho|S)+Ou9bF5DJOOeaH(5ZTf)2lxCdTxYg#xVqp2?Oqb$L^ur{x`C6n zKox5o+{8J+llhTOG5gw)@YS?X_+N+PV3xEUuK3J)wm%1a!K>@(?37j{sH7kr+( zovBq_QVd3V(`BPc3`(Oqa84;gOL086QKXYF9dHjya%tuc{HOsSfvC1JXcAkDw4K*& zKz-Wvt@Aaq5}A;9C-bNZBD=Vhw6m_22NADttp-Lgz*7Lsu4CXh{vFdg-*|&l@0^YonJB>`te=+NiPkK%% zhd69ij>Mc)Cz|Hs;oDY-Ltq>LTOa9J`BdBN$#=_GNm2%a$0Yp!0JR7BcM|fE3DVmG zx93(6y2^}_C=HBiU^W0?W8XBi(FJ#oEFot5!@sGdp*yQMe#0!P_>=-xP%(^peMU1- zw!4mNNS-Z9#ASweQhg8MQ@d#1Az}l76pc>Zch66lq-~6r^R>4#7;F{?%683eO^C6C z!x%Bb3cGhwNk1%7+&=0hB$*oB)309rkq5WKeX$)6$SgXjlczEx8UlJO2K z{6S)9&>Wy7eGWaaDT2?$*OAM>!3yNGqwk?oy>f6zar)QJ{0i~4#~Vjusc!NPI%ih@ z0Qzg_H%<1aS}UpKkT&OFGhaOLpZk}KIQ_DQDDq?tus32e^lH@q065L=G|dx+WL6>b zp1lXsiwxS6NnlC9);Bn%B)lt-knE#Qq0swgxMaB4gUdhkXNox&mP&Q^OZLCWn(M?JlhOl}QqR3=FF0 z0ORFL+TO<=qUFv;;CZu(mlL=Wa_ypGHMg0w)pJV8_ILv~^L^D{T-3uvNRvYycq4Ls zMI9{*7LqBXBo985xT@%7m^c$doUY}13UB;F?C1Ds10TFj^`t^uNhFn4NkLRxvBt_f zeCQW95fHXf54iq$26q_PZ$-J9d18%YjgvUnB%2lhb^NGHXQEjoWz2FMh9KgDtgM4v zYBDy*W)wuXH^>1=BY3dbBxCAo5fzZdAYd0dPJfh9-R_~0&>Tj?1&F~T_CG3td1Oc? zM2*RizRlQGmBjD<995ugM>$cN1v~0?c znN^k4WSn}_7gP@j9~Ss*X(yDS(&|S|FZ9iC?;%J|kV-Qg0-zS_{i_#o3}fPFhs<#d zGP0hXKy9<_n%vx&Uh%nuQ9^K^(?43zKH_lbyO3m!;!yeqmzDnjnD*PfFLV+cK{Es9 zAqeT5orlVwihgcfM;gMLA;;6Da^~G`gmIt=yPYj~_%M2_Z2N_q952RnAa zre%qti4-((#y{!={cEUA zHBS1;3caf0Cb@XTvlWd1!bC?y>V4=IO&y%_r0AJbkicxq^zU50noE_{2B8@8f>7jq zs`iu8NhD2mBr^=L=wrr_>s`2!BDQiAi25dlAdH@%_uSGv>0_8gLJ#F91oz&DbA2Sy zq!7wk5w~U!%P&NM1S(r%-i(*X&YV|&gxx`qH=)m zocF7Aw_X~fLIVEd89leFQXP`!C0t1vP4T$z?_JESSNRuK2-nmfl~?PMNQ}>>FvNfm z>N!8@L9)0hbhgq;{{VN%%xrExL)*P$#jQV!nI;TwLC4*UoPk}3GP83-Yc2V^sY;ARzU(Gj!gaaeEkJ5J8veaL}v}y4xhf27HC>2OodM@S-S0 zH-;971uY&|B%hbiQ}Hr64=QT}Wl%BO=Z@4~PdZ^F(Q?32azB=}Jtg)+R)+f6+z2Cf z2o;2fVcg($6-VN^V}1p9%0)zYNa!+7YeU4KirUsChcgKqW#9hq?hR-Ba#kJ;#yHeQ z@Z)S}I{}~4wa2z<^TO<3!yGw8jG$Vf&NKYAVoXu9BQv(yB=)MG3A-N)@ZUI4Azbab zr(c?94<^BhA#v1ptzOb}deIT%eqlHVY@VOzP9$k`iXwa-icZ`6Q0?Q4z`EDXoSb+4 ziX3c_$U!;@-yywg4GPxHZny}n%y7p(w1|#Ym9%+6_tVmtmhGjLqv)ZD$QyK`p@HCX zWHGvd+<~(ady;Tz zfyv!cA#OvRM3!w{qL#K!T!nQu00fmAVAqkPyO?V_QCo7O8&#rOBFGvutTuLIzTUNZ z0$CL2LJ$eTVgRT`BhF+2%A(a{NjN_`CAZjxxMKiV&+{{52h?V> zp=U!1t;+8lXSc6<<1aQNB!HmR_f@-N0~H(RpX{ynL#84MsXcT1Kb~1GO>#)M-a zq%Ps+)S#cbtI%W9dT7FBR_BAEbqsvJ&Z+}DGr-D(>Qy*pKGpN@4ocJb%Z$G2&VZb4 zj+OK~f^9f-rVt(01}AZ`#(xU=w}(TI9q~R>0siZf4@MqO>sPpZ#=M0fM(!6N;fUU% z#K{^fFesxS3SLR2X_5%z=5^Nc3<2^Kc-I6+q=E-TsHP*12vv;Bo%E|A*c^1A9BAeM za1Qwxqe~z*pu9@5b2F_@2Ap)$)9XvgcY({)7%OM*4UeuV0RSb&k)#cXJt=l+QL!9R zjqn|YcJEMrwrros)+y;Lx^=?%L}Bj!gr+>xCA#T@Y~qYo=znX$fb zN3}0-@eW$*k?|nO&IaDqDB>Uzsgs|?(MOq!!av=BWnC&k?YFPWt69ax%DZf+_!vJd zRh!0;u9;<&$$$zBX$SDCiB-kX2!H@`M{jCGrwf>3Ldt*<{Jrz-T;+t1n!zO0*#o-# z>L*mL9he0T)6$B;Rd;;lw1xHcG&FrOG;y?!326TS0Z>G9sR<=iebLZUiE|uLX>hJ| zu5!MWUorblz$m9l1Yithsw=9f3CLB*&ZF9_SBONA#T~>)?&N}UN%g9vjEtyN1XBz| zk&P-HyS5HZO9=3Lc9nnKS1j9P=9y_M*3_UxMk;y{mG}JV2w8w~R1cWn9@Kc^fX2G; z91SH3=QPFA?aP_Kj%ffN%)kJCv``xhpb#_W80%7$1}Bq%y@oI=u){N>u#Xx#mpB>i zkDXLhUY#EiJTuJ!01l^qhfXVEZsGf5Bt>qRK9F;hkEgyX1$8MNJ@DwC2SczRjEq;( zB)5V&+G6oX9E+%(hWXAj^{oC()!+=*))sKQj-+WhAyXxo_Qg@!@j0(p#cu%;U?|5y zzUOiBtN3he=eb*raj-&g&YFgPdvp~mTU1w?NLnUYS)W>w>^A%>oU%2PJ8LG2StNE6 zxF3hO>0WqC$0mYXE9D4-JNkQ56ReICJ1)J>Mg<1q(&FMsfsjWOQU-1Jd|>0{oK)3P zaU8=Kl0|IBnJO|)dgSEx6j>#ch}#IXELw0i6T4%*IUTjt?DCb313ANQxdR|%_VuDm zz-Cy)ZLH+sonNTy^sVe?6wAZsx{)Q)d2V#7s-5VwaGp^}gN|yD&GH?8rCY6s*~pWc z_|6qe{{S~ls%BVY53CMoBxu!=-qi9uLAFbEVIs&HMe0vbD7SD$C~Jt6OWA|DBjzb+ zmsw|$6(HaYZb#C)45_Ac6RRq?)TTk?9Y0ahqM-s8Qs_BogJJx+KflhElEgzINb@_g zFmN-FH~#=SuxadKJhqxWEOH1VW+$fK)`=8NBRsi`#@!1Z)pZ?vW-ZSWf5bRor+<|z zb067aX%;zUV53Zfezg3v!x9MHv*pVa=%cMyi<@8pMfcY5cg+eLySYGWF%b~WqAkOM>qrD7~ZJ7cM1Oh1DfDEkkPOSsFUt}zs|L+ygp+XxIS{k z2;*%;kb8WwRG$!SqE86VBw(me!A3h3InUu&jrN?Up!^(GUkO-Bs>Fyif*4~U<9)rU z_!P=fnVXtH8O{JX%~5zJ=2$!lU?Yr@?)hRfkK#A)=~tHVNM1|6dW{DPj5cq}71pqH ztd>a=zF8b+C)3iU9HL2BC|r?~kx`7JH`;HdSxe_aXLda?fzumPiFIY6RSXFxjZzSH z9lBOj88ek5NmL^_-!&HDi*k%dIv`s*m<%6mQ3UNY+=%p*kIy8fMgZ(L^&|4B$96An z8J0{&4w81j`Bb_UJSUMN#t0;)Tdt6Pc&Nu?_H}9QO6fvKP~AstntY^cLBmV%ar7s^1wT@%VX9VmQ(4L>txEe{Ncz=U9X#%>;nG)1#<3oDmwi;QQL{#LcF)M)g*^Xl{v`w zqsEq2md4sCt*+*ri6+$GYVDkL18s$JD>*{RDS;+%E#sh$Jzv1~-55p2i505yD$PS)HoB^EYM&E!n^?oUA?cv$uAvGi?Qz_^`-o9Dk zf{)_PFw$X$g*eVL-})L$=<2qxMGe4ajwccgmr{X|kLg{JzRjvNMG7`@F}~lIS_zPa zEgW(qV;~Sg81KD!OALg?GJ&KWNE?^7BELiZ}o8CBE?3DW~Klu$BC>QPzF7?OJp zs?asK!zyW17z}Z}DR*YGa^;P>U@!x&YRb(6maOjE<(x~Ssmh*v)Cw{=uZP{K`^Qx?UaKo)Zh-gf3hc4TBhQgx+e3G#{ zE(dKrYALh+@ZiK5hUAbbS#_*!CXG@vdA?Ytv#nYUGRp1hifZ-ee0M@N|WTsvZ!yBR)5S;ImkAK#k zS>%8jk~QW&!#YVG;B~6XfOxNp&xRZToOISo{-U`*AMqPR3|tln({LEb4*vl64{Fgx z#H5blV^1PQj$tPNe!=bQU4ADu)aZx;a~$qT-yXC-^^%K;_^ z;-N1raHVGlB@CGv`D(31^L?nNJS>a`2_XE5KK0-cN|lX3Q`JWMR*{}OZ^bCY4-ZGo z8@89}`I?LzXT}Uk90D!QSNZc(03Y+$4Mw4J+0#!2URD(=4yJ5)2)&nv+P%IceqfK9ru7hQ1ytmx$Wm zNx~w9?E&Sgk*9v=&AmzQyk9%4uVoWsah4eW0Pd}rU^#tT9ZT*_c>$f)EdZ7oVmjb; zG}OjLul#3ps!t3?jASC?3O#Y=rmXnKjI7F(;c1ONQxv-a{{V3ns-EgKiWl;90Ept1`pv=xT;~|PZ!IQcsP?!U*ZG4bJH~`d}-pi)y%vUvoZ4>M4rO{4uY*M zvl zS$JavA9#r{KPtSonqo;BK3%jc>t(U=QJ` z&zC;MhV{rI@vO117IDH3<(p!Dpqkt~%w!=7od9LMPg+=AN&zhUpMIX5YVYXAL4Vsq z?FvH_I9)}+)6>39CBiQJOL-;KHlJpc+PGN@uTzo-&!ugzXCx|WAwb=U$z$nR{{R6^ z99NuajkNN&+7|{T)JE^rR--h7+W4UvuGws!Mq)&jGb$#9h&JIm3VMSR9mkB>IZ*J#>$pRlpHt?k zHNTCn6-<@^5e7gbZ%%-k+FRYv9mB%GS3+)j?su+FadE7mZ7}VwMk6G851mS59yqU! z%zTu$jErFEU^e$Us7CX~j;CFWZVAcD4f}u3TC;aF$3~EmgOYM{iaI=Hl73-kOyHh? zcJ1w2^qsN7=f+BmSB63X>w*qH_16*nX)}&r360ws8dMw)`>Shlc6lZ|Gqd1eoC8hS zNh-&JS@j1tn1!XY10Ys}aJkJYm7CZS14rQAq`{VxgIHxhHZP z)YY&1N8=_{Z}xg3i?}+nKl`y-hLlM<^Dturx$CudTeLB>e6p&GxFe{kY$U8~{By=G z`Hz9Y8E^p9brOH>$K^^SUNn`Rn}@{~qo|iniTq7&H5gP&pa6rg+-*yn;Ht%#mO1hq zjWw-H80(kuq!F|_P={a|IEh_fJ-{@)5_rXih&Xs`ZH4oH`Bu0Vl0xz>5N;R0)YCDw z*C|#Ir~>NsBC0YeH;y4f&BGA@{&@y5__ZE8j~z(Im17GrU`qzc2ewYFt6K}0-3z&G z8x3TWR&w6OYYGvh%)?+0PjmFCtBe$p_}bmMvfJ6~&N6eWKSP=uw~Nt%k~;-v*apZy zt#2*wKG8I{H%ihZOf;bFzQgBGy@GkjdGQLcV%l&?TxS4pf@!CV=ZB8?sy;0r3;zIf z7cwEz6k&0KNZ9Sr(?;vYMr}W5uoxNWG5XRdQZF8G>~@5Mlby(a?^@=TC6pCHT%3>> zB>v4*QGKt+3=EpY?9k|kQ^fpJ z3JDs**;hH-5B~sc)#!y|kYo*S=FY9n6&5#RF;@tq zU`JD4iUw%ZMl`u4fXCLE4N}R({6y?m{3t7RIGlg`QwhdAMWJFG4mDBJ6R+dep0{x^ zV9Sjs89nKusP@@cWngwR$ZEPCH^yNCM&MxQahXBS(=`cvOU9k@+z4%)VX;4YHR8zbC9;xvRZ$U?C|>^nywdtzYPd)Z;oA}m1$$J_ zx0iY%lDbrl+oSgax#9RFo5J=;h$I3OBw%Nzd(_@GbRsy|0lPB|lvNQH?v8FUG|F-4 z4nJajt!b|jJ%dJO!jdp8cwH!ILF7|5yFxOJFK zaMH%Q5x(N>u<1cKmSYBX4mAvZHRxn5t0w|KPW8xj1$JV5?bjWDd7xpUj7%LjIK@C{ z*8W_tbt8J}$f$&A5(WdzK?Qo}9S6|Y5+x+E0BQk)z*8o0F1~ZBgD$xuQ7?1f)6=p7|LZHuH)hRGA zM&Nb8$v;Y{5`@Z;8)tAb549J|DlyryvEG5^&R!;L1{}!ibqc_PCC@^sDl7x_v?z4Vp3~}5?E!=-OfOu z`BqS%g(PI})}?bg#id3L;~5=xr0y0;Wie`0EC4Ogj{8z2dx)75)LqBqD8P_+0AP*9 zR(v{@h1VVm!GDUF&q0#)ZGMgZ6?TrqjR1lwli}%A;{0tS-Ajy&WE?Y9PnN>sHjX_G zs?2grZRv`ti8+@l&8$dAm2vu2uM!OzX_<(^`Bl%CwJ9Eph|*YNV(7h5cFui%l}D~E zVNT3!0EOpw0y>y=Y!8C^O-+fL<;5gjFoNsds5?~}jhLcfH?c#)!`q714Igr8AJ zQh;Mv!{9HQY}0VbA1O$HgZ$0Z3Jj6WGnm2zMcvvmeCO-F)y0zP(UTz~ibesI{_+o9 z>ePm_43Pp5lH1@8l+wD#7BSTB!@spx8Km;KW-B8Aq>OB8!Z{)=W6}Qr44k$$0ywI+cWg{eHDLtqp7}=B~&mjeX0~GDNWXFlT%;cWEJ%v|V zuo4;M$s}n}Rw*mJxsc@nE%!nF_pI2|eCj}>$DBBY>k&Br0KT-+O33WXfJxZraao=q z;@8F;BIR2;u!IiVkbnEXD$>syv%AJgQr(7dD{O@vtlHG5EazfVCu$@zI+b2f`RDpl zv*9ivjuvJo%t$-a_ytJRs_uH4fSiPTfgU4QcE+Mck=rEtP+74sj^R>719KY#jPyAh zZ9|gll#)h@7~=uR-j87%mT^3iqJRL&VZM6P1THk0ACjDvP!IxlKQllf5ml#dgQ}by zQ&)uy!HT$M^EqSGnoy!VfT#g*K?l82=;wt|^bW&fbEHr#*Om&;gFL=`0ry9xS=d?^ zkY!YYrz`3yXSe%2fG8^_2s!@sK0qaj!CWA4$DuS>m>?|2KQByu zw(1=gRr8Lldm5;cfThnYecXb_zwZG?R%OWp?vw9LN**bWwImP$29=uOR^88@Ea#U4q1{YM}HjwMwu^iG`V?$`u6x2OWC; zv=MI~nT8AXB=1mp8Nt!A`s2M038YrT0!9e=P%$eys-o#&S7&eK?oCkm#4zA)NzPq& z_1>=GkUB8NPH+Y*J>sX3aEar1@Vbzr516GIE&}TZhAatlqDasYp7;Y4)PH>oNI&+hBiv={rWaxQUt2E?)p1xT2N-GdS6X*zSMUsts>zEzF>dYCHAVP>Zr` z?S{`)85s4=P1yty^D(VPTh_SxV}dCH;ul5S4T&IksIEH%jFoJG+@D{SZ~{hMKnQRY zjq9C8HY@o4Rp)g~zCuQS=Q-&>!I3m-&}D7BtTI=n4oM?s46%kd$=|P~EUz2HSy&YV zNI2N^-iTQtiHN(*EGA5qimj}bRA6rEs&zkl152TTXpSM z2OnWl?1`S^YM0W_YU%e3sBn5|?~m2XDxXm;`g)gM0 zLi)!az=V*>r_4ji(eJu;R>T6%7fDxVUNy}wrf#2 z=RlyIYfSi8bJGKGGt+8kh1-?5Ht8dzZ_{rm+jE-mTQ-qQu}JKEii^|K)Ee0wDl~E` zl1_q1)Vc5V{VPG{K_#GAK^VzUPBXt+RiTtChiJsF=E*ypY)x^ZX1R^!X3!J})d>o? z?beAL7Y`%5oRG>1ax;O@e7C5mTeO1IN6V1NP6M&oMmtwpBRCO07?Y$DGBfK(nrBe_ zv{1xsdH`rv(+2Y384up~`BVe@5a&d$>!C)hr)*IagDVXvBMJ_T$Ht0N3P z^KC&}T^z|`$t2)u88iZh+)F%yNQWnP&_?ICN<2vtUN0#D1qmSh@l%CmjwUYTIV2X+ zS8se!)XnE)4WaRkTV|*!i8&<(K__jC7lsiBQOorezg2o4fhJL9SLGLpx(-21$^;>+b^gT7m`^} zIRQePXE@%kXF{@>WDe?q1_5R#abA07a?Kp&8@R?mrQ*7_fyX)W2UQ(S0&XVC__kdL z&DDnEK9zE!&{!%#{{SDH>Nd#02E7%)wY36cf`EY8&UdQ+0PzV(&BSCzLB}k0HO0A> z;yB`pFs$buZNWdi>Sg&|WVnl&JYB;>q10H&Lwl2t(zQht0#iNmYb}0;FCB37lgv>D ze;2KZv{>UV@|9OO7#Ra2*00Jdfkl-WVEGP|z)9qbn9*fM#~rBBOD>W2aLSRcIjW=< z8-u^5=DQq0ovp;E7~rm0jb4<{C{-k$IEe3@djXoXj$1aek|v4G!Ej^mkVwuxnb^`c zsPH_|WU7&dkRIJ|J$D1}rtBbX%~+h4wy~>7(Xl$VVUly6xam+9P~0}5 zl+bl)#seB9!PJ5o6^UXnz-(ylLJacRbq@ZW>T6jkd3D1Po+K!A5(zb1a?7a+X3L;B z>x|R$Nukz9k$%p@80(sP*=)C`={t(jvZ34zoQGH4J2 zpo8V~%>iSLc|n+YwmCku3zygBgyF%`Piix%MZ(CMwC5eoEgR??gO=ZZ^aEHNoVy;r zwIyW@{7m(syGi!c8t^-UHZ;SdFj27sy#bW2fu}9$y$Qr&_6Jex=mkb81WFj?hId+W z;?TXS%K{HKn5a9EPyn_fRg5TITby<^n(=hlvv_r3NKylLI={-U*HK*cC9#84UNT;7 zs>`S`8d!n0p5NA|A~YNpI8OkvE=uW%Ha$VWITdLf?+UQ{@##1xui;f(00!Xp@#)qC zNbcieII)e-miWRJ* z%K9>=eJ$A4LlQh~lx#Rv(!g(%PD^plw2CsMEK9J?2jTLi&ho10+etfbM@+ha5P{z# z7^zkl1DG`Kp}`qmpwwcthe!t-=0*yWkVbn~q;RYVC#VEd1m-IGe4wtK@#~shi3+AYy!i6|bX{S901>Nf z15ia%fJsoDGN&CW&|r;~oRSx&J%7@H%`(X=6P%IwR9@j12<@Dmz-$mIK&mNtMpv#e z)|hEdDhlu`oIZ9LU;d#~9t=7o<2SJ?yBRdDqku9_KVFrg;u#zsb2nET4^xp@j|DwnFl_zOhOCvMN(#<21R|?3RXYj>drOHgmrB^MId*Qvky8d-V71gMY zFk;Uwb!Bt+hQNJxr)HUwTTRBItk}jh{yX~Bu!{*EIJ5>o&76+8p|#Si%P#JWdN*<@ z7m69@D=AlY8yxOFoxjeAin2?EAS^_XF^rm+x(mxZ_Xku(6l4RO>KXQ~2(rJy19O44 z-%32Qtb#XQVG`pdakll_bX$Mo%PITp!AF0m;a5>0v$B+B1D1o$>z|!H1J57^KfAU` z7z5uly4-V)5gSy1jFs2{>Gi8iW{qt`5*HeV4muSZoQlgtmS40xEM%WjHXo1Yij17Z zwrE@j&f$RR^{L(8+W^p^T_AriY|_zP#O}J6S3%RKtyDxOD5D@^z!o5P`TJDkdCj}t zNWkjY4fN5ZarsVP73Olv zDs|FH&*4Rc#VWG85J=qDmT04i)uYH?FmN@OTJ1`?{ zhWMfDo?E>v0>oDuS8(2d({S6#8Fmke7p{5{?^-~lvb3{YO%O8RTp#@RmfjiCTcWO7NAuG`>yP$zA3EwVHcTA6emEIOU~ z55}FgjbxrE+nQZ)Sy2g4*y+=?bm?h`f)J)dyApQ;a543!t=d~TQDV~k!~DeU>IDW5 zjH@TaTR?RJ6S&{2*0x8Edm@ToBl$-7K7+n0hm8RL01&uNue^0D6ZPBktrgTV^Hyl^ zGxveOZOCFRS7CJ^e4aMDlx?&o5{*pfd2r9H*lF9J^r-8Fi12i0lv|* zWP$x?6J4YV@yDq_eHiuIIQiCESYAzNZ6mR^b(262@0=2Sz5f7Bg&-y{%gc&pE7uzG zPt4NHUR;0zmKh)%5&gf`mUVdJ5d_OWnVcNz`TkUqEm)hm-r)I8m0T%i+jE_%f_~L( zQR8hxBoan3>55U6Y*iw4ZD-6&=M~d?B1~OlSBoEbDgE`UgV(Y&&}CUdp*~eUovOMC z6v$Ys6#eFj&uF5NT0CoSKHc-M6gY`gAPpps<>-HSs>z88>(vIWC2%_RsHAd9oVMe9 zovJZ5rRHR)#xQrH#>Epz&Rec1pda9_rzMH(PSpZZ;Obn0268(6DAOsCRmsjUPEIzi zA&rrgwvasqP`aXk&6h3HCZz;sUF0|eY9tLC77?h@3Bm7PmPL!qtL9_ro#=th8pf&= z7RKW}g&CPz*c4s%2il?#vuP)&`BeE@AuP>^!Q1CRxKX&ylb@gAN4J_qU9bS#Jxxg+ zBn3=yXV#=5Dw)m+?r0R6BGN3V`a9vhYJu}LfkGhx#Q%&mYvd7)WCAH)1T z5P8jBq& zn8~cKQo|Jx3jo9iUrn)^Ex!tq@tK|7Q8qX}TDQ_krfttX3#$b=-`r8P6Cl*>qy@=6 zw%k&tSZ(E4H6%YwWKo_(Qn7+E#{dDG)KsPs%E4F-Dnn!piX755l_+vp=Lc*LT532J zJAHDfRP=77`tM4W0o=vr%WO{7HC~yrx>0pHoGwR9(IR629N7yM!O0($aY`7+F?6;) zf2|FYHO@zuAdotms8;QpU)p(&xH!<851md3XLl2;1&xrLjqpV>aVsLIAwkI`dQ@a{ z6pI{dg2!*dfptf=W<2L-X2~pbtM+NAC+Eah8FD%r6uh$HKQUsUl22OfYaEC|qbVc} z#t9S<5{R4?R+mn6=^6Il^rDBA;nb*E^RknGG3!Kz-Z+QwlR}`t0AzLRLXJja>@on^ z6m*~njd_F<5Oz>Y1@mJ&nv@Z|ag92Pxh;dg$X85cA2XR9uv>1RZ|O~;44pIu9R}Nb zP{K|`)-4^}q-l^GGUFpYzlCS~9(ilVVL*(AaG_Z4yPO|W{#Bskm_8r6lwiDIGU_Os`oQg&7Ssw%T105P#3;C}N=#wAHP3CktN-h7q=(>;f^ zEcWisg*@zcDhF)!r|md=ttUO05*XkExvNk_W#(nDpbTop=hPaVoQkF6U`&!_w%=^g z=CHPLub4I)x4lZ}DnS!6gl9SdRqCUzYb`58Z!VN5V~rT}JDi_yN@C6dEUrU&019M$ zRg6cPy3#d5MhG6Y>0v=+=#Qm{(lBw8O)(V^pgM3us2M$ndQw=Yylleku@Qh!<0Jb} zql`%lz*)Qr}a@Uh-)PNKB zv@rdOZdQ^=Rf74pU=DT_4N}a9TP$Gg2vd!{D>}Fw*5i_(IYIz!&Nj^!7-5lAYAvK^ zN$zPxj7;v6IBYI5TRGm17A@ph)uJaj!61HBq#9`BWaY-gAn$|sd(_h6C1pXGNL&C0 zckR6akjH@*2rcF+aC%hL4Z21xiozr(AReDO0ml1D@*dExV)N=2+QR~v` zS@YB%TDXiy2xx;T8a1{F$)sh5M2#oZFi+IeQyxcHn^cZU=O7-W`qv3dyMa1LB4~jp z8x1}FxudK~NqOT!*v1LyC{x=jn4G`RAN4&MI&zVy3C%Tz2fhkR$K%>}e`EV1WK zg)6hSr7dr8(-RpRvCoif1s>N;;t6F&+bXCw+kO4|<0hvntPX-Tc!M4E1M=U}qKP&8 zMGHqHXymCV6?a|6{=+n^e8Fj*!DcMPpIR&3L2oIO%_NF64!aJys`riOa>yA2wgTs- zdsW1=E3SORMyJcGzss-VQX*X|w!rlibcARtmURcH8LoYyLKI~>+qN_L)j;&QML8YF z{c3Sa?Hakt>A=lFB$7EU=glYv3WK@b4M;~Q_9T1$qOEfAs-+lx%;EkVu}QMt;Uuf`K}E{P{5YK&JO$2QaHE-v}zdt05ChBdghqI z0J@8gxf^1LmbHlnLh}zmPkMlv8&q<>aDMS#^gTMsa!J#+-n695;!-+=>p<$VB%~^3 zNZ;CvB&FjKsNXr;VM`>jlm$;fGmpE5J|3oc;+K&aaUt|x zqtmq`{wp21U$TyDiZIL<3+{eZK%NlsO!{Jq`X^YFDutPVRRe5o(*lgX4DoWP{{VWn z10VC`f&Tz*YF+rMxqJAouC3&Z7m)$fPIk!0>r!NZNTtb^LAd~JnyVfK@rwpuwc3Rr z%p3Ib=AF0rea7vOyZDkg0~`{l%xX)Wt}K}$Ne$-3e+&g9$#G>L|m%)5k>CcI_V~_pZgPeOBT0a4CIm1SLO3Q*l zMQPI+5ah zJ~d~1$fF(3ar#n+;jC&zaPeEU>wsp!{{Zc!rTA<})Of7Tzjx2KTzh_1F?O81;`}m8 zDWZbzIpmTyRE<36INRmcvfdd`x4cns6bV!SJ8s)m+&>O17F5R?lUW(eY%}aDvivu& zwv_|%E38YtMgc#SYGkQ>uA#ZJv$sI)+&U=n&QPs`x{^R0H^p)bt2rfN zw+exdA`P4Ywn@!rqWD*5Iy9eexz5-GE=>`>8d^X+oKpQ6fH?(le-GZ(JukJ}@mS%) z!wgzGv3{HUs3!JKCwCf$`y6NqQrLY>^7&?|zwq@5R2+8j7wQSvRHpcTEtKi;1^8pz zrufgTGBdTJkBC`Xf{!D}Kma^Rvv2uSCgRo&C|Ffl-zD9bWEkmLr|=O(R+Eq3Jd8jX z8-BZ2m*L_#Bm8dcf;R+Ue7e)p`(2(R!&*iK8YDyIXom8A2g-=NX3#E};dsdmO3ush$Ad<|rx3kOzVXt3{6!3WJF1+1m#rc9I{A1JzvtKzYnbAkR`Vpx)U%Y;R5aR2AZDNZN{uwP=Jd4@JK(uoi@CG zgLAbznOH6X(r~@I{c9-S4-lX;ae10X1fM5g%==TYd@Z*Yt1EHYA|HE5UZ2{Q#mdmf z#4M+uQd}Y_O(YF~12nDg98_E94MS-GRkLKBYPrBKK4 zzkOO&;CU6Cfk0 z)zk5+cs>*Hc@Rk-ixi^`h)F(O%?e+Jekl>E+VQozhpN17jPLGqS@PrdxmS$#kwA4z z4sp~t&$oJ7DQt*lMvgEBL3Y%0>IGCq@Uz8|GV?f+u6Oe`uj{=m{tfXV1wY0SPD=gK zn}dvJ=e1eNeVJE$@ogEG?FhQg?eFV~T_+|m*+jsu}zp;!x_>JV>N&3r5Q4 ze0A7#9`zLPpB$D@A>(F303#AGz&^x^*OW|XR(^DetQAq;<o7&?lJrP zs*w02#7h}_o;Nf~i@ug8U^CgOqlNHE_?I8L;m3{6i3~zPI|c#%?#F*G-aDU4Z1_yA zgpCX$psGH@_R z#B5GJTK1{u#jhGBR1>FR@7jhYLi=^WqQAp#FB)Pm7#3gp?a$NsjUfL34Lm@_q?5#< zPNF)v&!#u-DLL^k8Zn&_Mz&Q1{Tzb;nTby*;x}J|~So9o@QRxyiba273C|d&S3J z33nPsO6)ssF;2I`9~1xw_{0Q_fD*R<06nQo&xc+n5%T5Yc5aMtQRg7%<%*tCnO{M- zw20lQSg{%m235e%O79Q>9@Q0#MJW5A>D>Om z`8t*}5naJyumiajFW4=u#DUT@CM5&PSFdirojZ3lyoEY|8nQBfT1GOFGjf!>X7YK{hyRqq9x4!#RBgf1IFbtiD-h$kVz+-k8 zj9@6(3M6ytm2D%7VhAp{0~pTLQY9-5LqI|Y-gazb)XEV4J7Vg@h>Iq%-ICkot+3p2S;G6-A` zku=<4Uk9)qyU=c8Iu}%o6l^q}w7XnPBMAa9W?;IV!7IOf4x5UdhSy6vfWLeaa&d!1 zd1R0>spSM8cpU2aQf6~*#4;XYjmiR4p4HgnX%urbu18Fszp+d-Ln)Q?=Pnm=r=g|g zStkz&bAT||AA8gL(>BW-2_lvu!5CxE{VGI9Zdyf^8C;C{vD8&SAhBC`1gwM3a0u=5 zAB}hY*{4;Q`I#_w&XN5$qR$%$;43<@Y_L#sq;&qDt#Vs+hnFdRO@gdY`vd*zC?!*t zu`1w}+b0|T{{TuPRv%=&7A&LPjsQ2XnAM={2h-NO z79LJ?iyE#=6a%O9s2DRNmUhyI2Tu9_04kI)!6He`D#|>>llQ%-BKKU`1I4RgbEc0C z=a^>INqw@%8~(*DEXajpi9iQ9)82=NbxA=w5wWEHIje@BmT0BACMa&B%Aopu`&D!n*2^;q49rUE3lSOL=hqw6ia~5)j62E#96FZRY!6DsSQXXO z5ltgoT8U$oNs!#ni!GF-K-4rG{%60+m>9RX;uFttaUIk!q-r%CM?(D$ zX)EDtXK12F=1GusMnF(@J9j;L?Z3*GHjGPu#dHnVrcMUbx7lU5<`j!cMl~NcFf;G; zq=t^K9I`C#s(@^`JqCJLIR@r=0yK920M1AmC-tt2XyAr7kyV~T4hhcRAyO`d%yAtK zqb0%os{a7Q4oNlH+yYsTF9=Ve9sZS;_-%5rd_lbD4MIx;9(3dn_+xSTR+95su##Bi zCDd+mGpDH^g=BmoAdlh&=OmCMwepoafOHzxCi{J9uh;xYuQAn8G5{dt`~EaL%g2%Q z>Im+jGhh&)XJP7hswr+=FTw;X8vqNh=uH_UD&v=sTgVkQ3tapg)7CE(i@UUv4Yaw~ zAC)b0XK@AU8Cq5n0_qw-20m1+yz|7Y?HX#q@3`$x+ern|Dl(lwV3#K}vj>u4fh3Vc z9XzVp-Os7ddNWSYTf0OP$(_3IP`iy&lc*Gok0{6VH7jfQ;*`fEK_YFRY<#xQ=Ti+r z3)E)PkxV9Wg6wdBcO%z3)a*pi%PYAGvS6xvZIkOvmba4&w5_MgG66q_N+TUj4Z(;0 z7Q!>Waz@`?n5n8#cMWn+M`ZGx<1N&TdSa{P;m}JKr+FSmV$4fyppM^_SeaL&1#m`OLs9u*MqX-bI=NDgdWZqu48$WYHiC7sy!*ENXinKbAmJz zkO#d=V+n<1l`xI}09N$0k(rUiNFoGd1YnU)!q}41-Q;N(nVc>N-_vhFNj!42aSMsv zV{DMypY^L@VF;2ok~DYH1|Ov_a4naZP^fs@1~S^0QSFMdrH*jRXjNTfWOM^bIl(_L zXp?DIW(6Y=j1mv?b;sk1UxmhG<}OnJoUjKV`+f)TrsuSZC<3Y%j4;V3%*=Y9(w`*h z8(BWoPPZCDl`04$6s*?qLoQSfQw$8Y`S+vQTbSl$gHD$l>O1@5nugZeSDj@6rF`f( z-A+4vu}#WmzGHD2{{X~WB}Tz7kVkFWx9@Kx5iIgZgh+4#?nlqltzmA8#pCW9G6Kwm z>Qy*71&-fCUqI0e!TDK6##FG$1fHLOuB<@TCDgMxR!xQp#-M6*y7J^Ixo`d$$Qpls zw5_bn*;ZjHk2XT|^!+`*xuS+9(=2MFInG!TI`{UWuCPvDF{CY!29w;;6GDbM4=|kR zARK$|Lswul6sKy}^tk32^aik8pG)^G|im4=M8IyfO)8Fg*){*T?4Z5QFNNjEi`F^IoHIpB+ z$9W(NsOn8yliMTGgp=xpTUv}9Ha8^y08X`K6`B}C>bk>?T8Y%dBBx3Lk_li4N7>P2 z2r-i6TfY1_@$&k<&Ct58aU(Ha+W zMyYeB2cQ&tsM0n`iiIpe`R&`jhp#Wb-VFdvzx?_0z&+W*7n?)wAX%l(yzDc;sa%*bjOR+f8vX z61gP^&OtdpGJ4iq;xH2MadD`IW>7%sfO>v)tPrxiX&aS^RN)lkKHr68{v}Bv;rMQZ zt7!wFB;@}9=@l2@q=*5-t=2~YbYx>W^~DMap@kJ<}@PjEjX$vQEabsL19=1vO-VJjbO>%pyY&fDoeuw^C`C{?E!~QT$LwH`^b)SEX2O z9J8w_W08@at2($vAar9hDub)0cLx>N-cfk^03eND`DwV83#n0IO>P?@eMhONaTsHY z@WiZ9LXA6(y}uf}y_i|s?B{0kor4AHaZ*BNiA<*lp&qB(_uhjVBO^tEatjh`kz^++ znTbQT=lAJX5K{_=6FRX`u)}-wG{QV-CKUh-e8Ka9U4$!JHld`qT4gMuyoJ?u^AM;3 zSOq71leT&RS+!g-vuTSm zKB++^uyPN!{{W3eD)OYziRO0FJgO<7c>qALDMG_@r}VD0k=;w76Ue{J%kSEya*8e9 zM8abMaBym(O?Mn=%rO~C5K0}C?hgKkr8;Jg?#MwAK_i8Hc3y<+I#n>1);Lp}6S+BU zO(u8s=}lYTKlo;tt>cA*4N@k@WBqee6v>9>TWwItW>@pDOb<6w2miIowk21@23P93IOkt$bb__w!`U*_>|S{s1UI>s`p z$03`sr2ZTgyzwuHXP*IssOycjF#eUR>N7pq)k=`0q^QEH5CWc`-=!?9W0c5|=NV8~ z;Ch<0Ht|g~vBs*bgpjTd^aznxm{cnO*eKlr0=fA*Vo<>pkfD$Vk%r-07zeqcId~IF zs+AZS8yuPzhFv!R5&VT?-yXErBkc%oT4o9xLCbHS-+G!6mhR_>5^kFb0LqMGa(d_1 zo3<8@0BeBqHpqwu>+ky0F*$WY;M)?@4rWlHN zt*+!i08mbW{p;5^?JQ-XDo}2!M(p0>&`>0hLW4{Sk{yP;bUpW~=Hd++0~_ifU}zq- zJapVy+qJZWTTGbcJ-^9mlDed zFros!WDWPdI(4aiQ?d|9^4=+vARj5$oD2_@T+hiik*&;TJ95K)Te1F!(=|nOz)u_G zsne;ojOQP*P`tXD?W0LuotrE{`V7*|rmiJRc_J$9{4@mW1RYJ=ze-Bn%*EsfG3W2L zG64SoDpjuxHps02aK{L`ai5=hd5mpq8b;{R=*ueg!Rx&w!Cwra@pAIyHtH2Y^4D%5 zf=Hd@8dX@iPnaGL5$1735grk<|tr##w>M9e@-#6HB>y3D$ZyQ@uM4JkgCt z+X8t`FmZ(#>x%N*%JS-wG)lQF7h{SVNF#Qb1OS3HI66T2)S$U?!#XC(z-Z1`p8Hf+ z&TmkE;zf~V!X069-Bc@JeCum$(K;J=Vgo=vM%WJLsrIa7vOK>M_;eDlE=Ws~8)2L= z#y+2&Za;-2vc?)b=s45?$GAUQ?>ce`BQU=#x*^7M7|1#H_3i!ZnPhOWf(tfsNhA%c z*7q?iv6-WH0~@Hu-!Li7Fr@DM(2iTmn-T|X)T&My9a-8{W&oa@K&LJ4V}qG;A^{sE z1C3p<4^MIUS5{agjzm+I2L-fs7~Yk6Sk94c5T-n~mB%(RA`HqsMGpPE0IIDdm zQb8QN$zvxQ<&}Z!z6q~G6w%zgQpi>MfTVyy{VLw}Yj?b36iSX529PoB{{T<#r6YAV znP%HuvPKxlPU>_1wPi5mnpOzH`A(dAU{K>|+GK~H96;^o8~pzOIsts!64+rBMT_IF zt_RkV=g829h6G`@U58Aa>4uNQAakloH0%bD1{sYp!R>8}3FWskIT>bU&gayQm0!p* zPGpu&c7wiKxue_1CA*p9XrnpK`6DBxUxbT3Ach&pw+pDm30z}yw!`UE_ZrpAw>ptk zP8m+*`)8*A0F_xr5!D<_^31@JNB|uE{{Sihvun38g3L)LNCe~Fq`zxPrxJNoEF3;| zL6XeFran~MZE!R@*|Lmo0SD<`x`yk8LW?AO$Uqt2SEsn5#$H*+EEdN_ZLn!-M4o8c zXxB?A$k|Bg+zQ8hN&&cVbtDW7f-(HV`jc9wC+z{i(#5mp*!^o8@d@#0Km->>ED7Id ztomEiOT(RJ;C4)`(YGjSQ;>R7CsB+@>`SmG0Cl18(6De@Hjul=Bm)EHGyYX8eqt<( ztgfHC88w!S*`m9oPoWr(H+&CMTzTRXIy$tRql7r^OF+#f;0o*;N0^h2p8l2Vw`Nih zjlk5yx%Q@e5}I8sf+Z1*WVSXZ9Wz3a`GHKnW(1s6-E(N5q5dAgvI%WC z3ykCEKp2}syS|_fPLb36sdy%N6e~rAB%OKNbJCxN&F8jv<+0eY9Y|jND5HZpTUiz$ z;EjU$vC|p!-}E(hLld-(c@i_oT0pwcFc>|3dQ)6EV{3A%NhDzH-_p4yuh;}-wHXyo zK*3y}dW2TWSjwzp1x|Oyl25f~bSX8&Z74Bn=JeIjeCasQMLM#;uZ{86tXAGT5iGNn zW@Q>wH&N^BUYJK5QQbxYW3CTTRtP-4Msk4&&OqsceLl1)W(?4g2*4Utdi&JxFFDAn z0u!8*?fWz~eQqOW!2>-$?FS-R9pgz9DublAruFV30G=y#09z6qk+!Y<1t|A?>E%EJ zs_7U9ugZt6;^I)k-3IalSvTGdgo-Nd(O!0gN*Zhe1<`Rzhw1IXe@-e@fI!4dh~5R*oiB zL>u~$I|``yT#^NR*CYsB6*_YmBoD4CU(&88p3jBw;Sx%%%*jju$6x%%w_o1X zUp@WQw(;{q7eajb0G+C$^5gA1L|I#=g^e*L6b!a{)tlQYC{Vi9V;*8bIU~I@2&{@C z!h6YynrAu79b10!QM5w==QXfdU|5JVHlD@JH{nVdH@6| z?VbI7>y{8Kl02j{d3MJ8QH++>w!{KNnaEY|>sV~!M+&P(@#vA83Rk$_t~&Oph`4-L zIh7uD0E{um%cd$OYgAv&ERz0f>gc+Za84=__t4EU3R{;cR4RjB%5I z2q)C^tw_zR3rWuSnPETtPGYl?+iD-gt@X^2i+Lsq8oL6ii28Kz@~jLf(R@*`f?=VM z7|3pb;m7r@<<_>4MHFzDqt2#NyZZ0xUWX6`(0H*571QM{>+C9jVfz^pO!<)G8R!qC z7TR@rBUH8l6U~Gh9Bc?CtYv$7E&1*=md{g>o|QKm?M-bkkqBd@vyCA2HR)~2EOKgg z!*}h!`5N^00@~pvhG$XK5;Ko_Er$HU zMz{e$ag)C0h}YAoiU34v0mhOt#~n>nHKRkBq?P%Loj!b%lh@jsu;C9YbB{gbZ@A3c zC%F1|rV^ND5lZSn%BD+n#dDa`ph%xS4I63WHx1QnZIGDYq9WPMstT$vgdil(GoJ zdo|qXW-%;C;fOwz!)7__2_Qy{s&*xHCJi#guu0a5k_h=p+Q!j>i zd#*GsvI0+X0L?a$nQjy;Ib3%&o0`3&&3PCyVRMw)x|2s8y0FW(qvg}n=Tn^=#vu_X zZnWLM1H4${k)vG_3<4Mpzve%WtzAMQ+(U9?1S%cydIL{EAm!@vs*pzKb6lDwm6Ykp{{WS?6eR)WXvRS4 zpGv!@NQiu)pG#>Zoc8qn{Ohh`Wie{htfY;yXpd=~N@`pVpXfVw|s{cN!T&-rh{|NMnX3Dv6$$^!;kXFte@0F_jP%(nbLK{QYQ`R@U*} z+}pHq>S;CO9XlV+k7)!hqR_EXApn)w0iLyseK#RXN$w?9l_N3poPE=uEDh2`FkEK_%7Z+%~~QgB5;&L%9X zvanPC0K~?hr6j`Bb6Z>6MHH&8jS$onIVF#!NsA>?(WDNEPy>BdMVfIUFa2fpW zMo(-StHp92%-2?;7U%cgz%)AD%#0D>x_ z2)VZ-avD_{bJL;stX?&CFC>u^JM&{n$kn4sASfdn{ZHXYT-?KM@V1V9&)zFlHy%IX z3<_KQnPng@u6pnF>?#Mvyd6L@wWH3b2TbTU`I@tZFDSQ;Def$tbZF8KstEO^W%!R^@*{>-L|<}G;41jj$|*SH zvrFS{9W1#^ozciW#(%wU5=Cohb^A0V?Y;Jr*VZ>mR=u3`dpeY(xP|t1l z&0ojk?jLT&qkmuwEO0*>>G0>RT-PsXFwy{+$3$;c;U0*Nys=^=je?J1jMDlV({O&uIiZV@_d)x;@t>)w+iD1+ zNZAzXWxxYa>$$9P_>E*n1iZZUF~CRDbJB|F+*D$WLYL@ zB1K4~IXuM4W4BUAO0|73+q$3_C{*pLu+8xogvDwihT+Q}yuvomUvWu4#2ZOKM!Iy2 z3}_Ly4{ZB){Ar7=VoCPJ9J>~kW60N#qhacPeLbkQ{7s;ORgHv;9!3f4S?gbjycQT0 z8eA%_1Zfk2)49g)e_J)r=TSFZFRdd7qb-)uOU#4@hl}N|spURYt1=K7=+YO7bge4)k}Zl-T>39HPW)F+@i4!kC(k6AH;qc zX?7-s%%?_>OhjOslclXhw)eBfUORP2Ek^1_HyfPQkl^k96%0(Vi-yayFxwlFGxhw1 zWEbMqlBfWNZCbTS0VCJ|4&R*;AB#2-$Edn9vCAQW^pCAQr=|SX3jk$wz*i`6bJ!8| z!JtZpGEop^cQ|UxUi?#n^5a=VO|$<12$AeR&X!+@7V<~@TclFQ?ucJ+eX5$)NhEP# zXl8dfI&d<5>&8oowLu-a$I!9HNcZd0ezlZeA6N!5J;{-qZ6*VNKfmKde~8>amd;tE zn4F*BZS=|OS5dF$tS}-+lBztwj;GeMJ~@@&fJr8w;ozxJk%N^TeL7WLzmBY~uDO?z zv@z-gsfdz5>@?>T*BtRL2I4*(yhOi^r+A9T5Cj3}03)qTU0L{OU5^a#)rnBX@ejS+ z?^kxqpbcus5P8VzN%X9j4e@^p;eH*kw>N0ckSJ)^QPOZToDX^g{ywvB_;(IF4SS5JJN`K$%MjQB}l zC%ltL+@4X(A5NPJeLf&?NTcP$Ga+Nx|IPi~KzVu5IqDg5v;v zqwbGlH>$hu8gSXUaIE&^COq8X^#k|Xla1Bci#UW~+{qc!9x;xreJT$inVMM>ks1Sq z_4o7@T6|f-c{u+75R7lKf+<-%QNph$<;it!utqSU&r{bmtY=zF85AIqTMJvo1;W`qHY*kpwCZ4n{`X)djDOxH_u|uFR4-Ib$0DetT1k<6Z)=5-t&l z7{OEeV>I!w-CZeJLdaW7{{YLV8uOHHqe~O8UZmDl{CC4Lphz)|&W2<9RL8^)7pOLH zd!A9ICzNP8Ecz70NXBqFoPVte@ogFas;c7~>?)c5BXIN?h$|kO#=~k-e;e@m6<3%T z7RwVG0zO-N)lH33q^<;tPJ^~c>0Z+&yx;{H^6%5OPvKdW@!t&(sLde8+eS`)c@%N+ z3xx*O%6*Rg;#Jt5per29$s-%ajMneDj1C=_E132xIcw6)i!K>K1+*nAm>}LIrCt4>x`bB zwLWUJdacmM7;K&V-%nkuKE6Hh`x5Ok44j=z3w<$OZ-^co^FC&KsO2&uGSbfpH1y$Sl=YT~DnJ#VJ@r)~xf%O#>@#h9tVvl00 z*p@jptf{l@bsEi~K{YE`k9pa77|MA@DR^O)M;a?iK$4dr}vF z5V$hQi*lOW^*?v=q?hS-F78J+s+tM zSK1-wDBKp<`-6{a@BSrlk^C-NWOX{*PTT(g%Y#=dI~sc@R+QsTz=j0jl>EO6k&sO8 zODm$}9r}8T$6WkG;q%)v7f9u1`|+!18~4R6ZSfYu6fu+xS&1a8Z=X|&o;7@<|Jlv5 B>2m-8 literal 0 HcmV?d00001 diff --git a/Assets/Mirror/Examples/_Common/Textures/Wall01_n.jpg b/Assets/Mirror/Examples/_Common/Textures/Wall01_n.jpg new file mode 100644 index 0000000000000000000000000000000000000000..243814498a44ada3bd3bdb5e8b37e969741509a5 GIT binary patch literal 33321 zcmb5UcT^Ky^fo#PfdmL4^bP?cAWeEN2{lMBqCh};7eR_5ilH~@NR{3~QwY7NbU{Hv z?+B=L5l|5uUf%Dw*8RRe?!9Z>eb$_L=Im#kz2}@Wv*zsC{}%qO1IVj-26_Mp1Ojju z1^BlKS}@SozG-G*s%K!Nd!YaT&=x--?=b;;S=)(X7>0WEbjqlKzj@TurK}ZHgpL9&_@G6+r}ITZjL*fB-NBgz`d#LjoWQFa$zD1)-pXQczG{%nX=; zUz!5Kh@})TcVY^NU}llgN-q_(XzIH&xj`lC9C^fwmXmkMIM%khrehhX>lT$+_Mege zvGIQx0YDc8WogO26+E^G^03;P&MCnMltxqWwG0O>9=OWMMVl z6g%D$ucj%?B;Rk^=5%vG=FvaE_T*j0BdrE(e=WJ|_#Pr;1&ex%P$o4jD7RFiGlJkLaO$EW(-l zO4qMsA;({x9=E9+p=AU81G*Eg;n;1;fi#PSw5JJMs>mMM3Z2jCZM4}LW zmX3O^>>ngGT_OU^k(%!!2K}|0_D&Bh|85~T6ohe4(u>F9AyR3NM4oITfHb2ZbFckx z7Ke0EIaIH!m%lofG!~6hbNpFZt;(ijad3GfZ<78#m?k@~+O;dH(qD3Yk+)v_-1C)o z(X_%$Z_jJh7ouYjna|HyAFllRgi!Xq^I zp(Bzfk@}qBCh3C=Fe_E)m&{+%KfA&uK;)kkY<#y>pb{WY{gTktp5VR&%nVRvkb7K^ zN@+ai{&ZQeiYOkdJ0+q0Q8~2ZywC4#6{3`5U!{q2pQ>T~sWe6KMihE~nLo=QauB}- za=>qx4LcY|4p+`^!%tocyvEsaUsn^wzec1CFgs{@q3rzgIeLDUXG(0-tUtqZR^0!) zo11skh+SfD{B!R0Znma;v@B^1N6^L=_O~)Ij;LE%xIG_Y+(TsD&l?QP)%i-I*NYN! z_&yFb@MHSrJwQ7mFf8m{=RK0#t}kTwj-|IdV&{G;>u|oC6wlTWuYf(?DAqzBFaK!{ zG61)?R3)b*;tdzNn1j@ft+DxFt9ps8;p6x6S0TEe`>1r{&yc5Dvt=|{>ML}@sc=>3D!jarDgr&l=P`Fvp_3>CW zAqJZeuS2%Kf)8)|-(Zwo$exV3dGuUaN@t-Cvp9R0;F?l{lhcvq}P`c0Q{ITE^<|h^^hLK!0yhh~- zZxaB{pZ0et`#m-4vW+Mr7)3S(XM+VqjPdXn5*(dXCzSH4Yu8D(&$`V0sL9)Zt5lP86aeovwjl&WfFn#+D84@i)P&M z%h8T`oez@L%U!7O_KlqmcamY%0o$RY#meODuR^=4+wN6Cn(|_ zHt#(X=b(UNe@WhEpLBN;V)+MLIygrVjBvt@qOln|ceiZ#$w7a#8ohLhm&i8-RIDVU zpPX~&i9$I$#hxOvII|yPTr6<{(JatH2d?M$D0Nd1GN-Ac z)n*bDn@NnKgxJYPU3dR}3JjQmOgOhftgJ#rWL%EArdLf6nc{>fA;*XrF|(@bkXmr% zSamK3^N!U5Dq%*2IZ(>HnQ8>`zznY&qT?9=WXgY5&u)e5jh4s2Y~-c5p!FqW`NZ6B zG1X93gR|Qr+|Sgi%DfqQD%RrscEp;p=@79wNMEWxugYP0#as*L4GS*rp^|T_76u4E zLF@`M&2#WOGe;@1@)T4{vlOFN5~O$Q1H(_p^>8l+Z+a32l@|2ghb3MkWBm8zBi<3m zNb1}JCx*&lq1NZuO7>W-cj$q^iEAY11h4ZyXk?fO zOpY1DNhsBvH1p&1Yvq}=4L*oM$y*Jthc;NHM!J-}#lukX!p=zMuM8cvCXq%kv`8M% zTasW^oW&Lb0vRbJ>rM%|f@_l_0m!-KUT&JF<2Th>RkM;vWkNo|REJVDp}|sq;Z4o< z9Fj6*HQ1vc&uPk!-}4mh+8AiWe~>_NMA_C4rB69{q>6z0{n2ezR}Z!C%1r;j-zn*q zoR0%p@QBqpS_oF_mQRR%?!QiC`q<$5rbTfSNAPWOy>naHH8?K;OZ zgW7By;}BREv&wH7>^q1Fo@tdqCEm5lo^9r}$T*fhD!#;KP2Gib8Zc--j6y1V5^q~P zAhVO*D%x6MH)SI2P&$kJWXEX|6|n?uY#qZ3l3UBd^o)6bmzN)HwQRk5asbdW zkyJkHCMTf;N&&rrCL@cZRide@)2pWJ{&=>B;nUX?5=;*9;VK4ix5+iHjzhoH|@Hi*4m$3wJ!dS%MN3j*A*d4*4L?`oGTxE zY9e{ZVbsIEe=X?ey()2Ufwtr+7JY=`jB+*w~YxS(S95@0zo zE6XZGq4A94sj$?Q$2JI6OnUV8m##f0TX+b~m{E2Avo9xW*Mp zhsn#sjBhaFxgp7$`ziiZ0S{0{Q8B^GbiV>FADg@_@}p-R@HO4A+`F#$y=-h)|2)04 zV&VRxc0o3P9+67f^qRG8Q2ZqsEz~EqQIU^qk-Rie(JykFVZ{bQn}KB${GsLPsXQkT zH_N!oidtkAf=M32S<=?GyWvWz4_WzXN)@(F;=orrYRQ8rU7jU2h1SXn#w|YOti(Hi z$?oHDEQEp=cUMu0*IR((hj>F2f-o)Zh_uLs=2N4x;~6zZeDbj_QTW%q?wUXAlNDvA z{H96vvE1aNvncD=t47dVR#obl6eM|vvg$>pz60U2qy!?t$Z1z*_pYS@4i&}yB9gJ2 z1LB=-^Q&4LCi7m)!W*l&M^fA+m0(!G(EBmSIl#C<3{CDr)AbkPWsZztxlNRGmJ}f^ z468tz{$+-5X2d+64Wo?D=!L=WS{9YX5Vspx;t+JygznOW51n_=!ToZjoxVP&v3fy6#dE*l% z9T6<640A9IR(->2fn#439p^O*^}<|Rfhk0LXUIi{C1*+>OR%viz_kF*2!WCM2n&-p z5Z;<}WYGHnSAb$@xq4il^|ye@F4%MkrfxWExr$^|K?!>b1zlsJN=y^NMHdq><~B3{ zlbP65Vm;2RG(Q^XV=7T9N@}C1re$cNY!DKtOGoFe1`YgK-wkxdF`*8uorkz`zVjLh z`8e?p8dhW{2#$uh*aKjgYQ0-wyg%S8PjcEG_bO+C$Eh#48ZWi@KlwIbKE>O61{?|@ z4ejimW5tv2<3H=PHfz&{3t|Mct1@y6rO$Q8GH{+U8g}((PLnmm-N{+_ts(kZmqChMfU?@Kh(D%ddoDq5rdkvU&=Yuh7* z_j~0jv_$Nl(5RkHk^2%X*K^omOIq3ZoL=!A8qFA8lW4hvm!FYH@33(huRe zg7CdFSI1GpGn98j5FGx=hk_5jNfW8@`mg<+apn3=qk2#TPuX>$!j>EJk*^NAc_`r{ zU!&(x`VHAsV&N_bE;L7xgMEU%hoJ`yi)OpA}of)|)z5+Pa&S z!*HJX4$;BkW9X&|MzJK-YS6_9$p{@X`B;)RK*Sw{*kmjc>6UG3&b{dSr<4cEKdJJ2 z8NGEVp|#QOva<+E*wcG62TRrxa=g6oSQ5EKT?&-4gh-$mxz|4gkP0i(jKspq@=6~3DG&P72+>a+Wj z4zl(D2h+zKZVrjS*I9iuX4%F}kiL`%?8h!VrHXiF9JoYxuj;9~Zo|E5t5Pp*yb+t5 zzceR^a|g6$hE7Oiso#JKkR8uO+P>K^OS>i9(>Ap*`aLlj#;j!4GPNgl+d^W|k~udC zoZ;(Uw&4qzT2CgXqx!6RU;MC1^YbX9Fn^~LDMSh}LwJgFABFsVGD0t?t)E~0Mx|7i zU&?X95naD4{132iP^6mN zgUJowu~+j6u1fUsZH6}-;C+#*Jo9w!hqDOW;6aUTANr1@z2ndJp+*Nz z&n0p*$-(YzD&M(F`OUVrCDTnzwytSZSB6Dr9!5_!9wqVa(?HzYS`2o*^YsIp`ldZj z&pcI_V5w0b@#@(7rdwmSr=Z?uI-prL)?~^z$KOxL`#ls{oU0LIsgS;8t_717?y~mV zPhDtNRG6nS1#SABwGb4J#nXG0qQc>G5T2)-nd8TtKZ(Zq>n78#^=p-{585;D;={*M zC$p$&!hOj2O8{Rgf;L+-nJ4U8cePgU8DSvx~s_gj}-5FU0Ln0?@sjM;_ez z!c4u{@2w!LZS*4N20oZLpBX6DP0iuuNUd_G?Z70*Kfps8rF6{Q=sU$jTiyW+3+*Uk zPH4~}FzKn>+lQ_(c+&84L{Wa5!G4t~>!iA(`T09GtK(+D_V4Csj;K}Uwc4{6 z57TeG+dqhTzc{X6@VNE0^Ez}DN^obVO1YdR{gd$b8f$LqtlSx{u+Nn3IZ2{(LaAHe z!rSBc?M-&xY|Qz)UCpoV(gm>$G(tIoq!cc9*Wy(*hi8KU@8%>&c>Wfk&e=HoZyWfV zcHAqE4x126#!(IaoYZY{&&?m7rvEdTP$|kZw11Eks#r(j?^EhvnfQp=3E|Uy{F-`V zW4)VU#Zb~v?93SQ+P_N=RpF)ah_vI6A7gViv{&+jq&8&L56(K{IU|PmtvmFK>7(3_ z7kIv?l_D6qd(pqpN8E#6YNnEldvmjaZ?a{t#0teuc72Ahv%a9S?lF1xTFHGp5wh2z zr#_JK0kL@lk$RM1`dc$z|4#__DIgi0Y+LJUeU%#ZdO+1Ex_<51(2J-40LhWQ^9koa zPqnE@VrUiww6O2|OOl(@j35&Bt6@}N<2v&LZ2L7a#k$6J`E54WKs7#%b=!k~03y)% zXX@PO4=UyIdsSAabY5b5X?r6E(`se(R>vyctS7fbid|h+*nc^#=!4Y=2J6ai!RqY`KX^fxER)U<#WSmU;myhN#h0f&Xt14k*}S)8Z^v0!=9@ib;qy6njknk zu%y8OYpVqesh~(B=HoEf%HwmbU-@^r!q)2^sUo$=7t@1wdRvV=tHX7b;re_6Bp8^w zD2vpZ_sYr7eLqTLZ<8%tgBH^rJE|tAw$vW))*VNFnEl;Am>4Cn&)?*MRIwM64eq-& z=Jx~-?dnKx3qwDOc|WNg6QKF`(CilxN&!TMXOz}i=RF%-)SL+JZROpMcG#Za{2dNW zkKtt)#S7tz5S%!MXhYGoy{4+wOdk zvY{c)Qc&axMv%DIEhD;Av|r?IH{?Hx3TJ*e)5_!JFqEh?!&=*HDCiO8z5*JY_+%T# zDYo)6_aBh^NpJ++p}2_}K8nqv4+E(Dc(DR|?-N#4ETOm0)j)ywkWA z^WXS!kQ`RdcM&suTQnFGR=`ek4DZ|trV&rF(TW}9lfUR{q^;`qBA1%j1u6Y4#Y|dj z%s?}{pGrrH`GEkfD-1sZ-4yW;bQ))*c~tPHn{md+VT8a~3iMIRw&-gm7CdC_>zu_g z%sP#u2JhFR=7h(+rsY`vnssqxh~iQ&=_Xx`U|kh3#Brf8piBd!LUW`_zua5*tF1=f zsaw%i|93cBecbC3#G_fbDs};G-pyqANoD+%Nq>zxvE~sgo*QPwh8dJq)@p?S_vh!AVXJM^&_pRc8~y1`YYTOAk+_e_6&2!=!K>A$hmV#U zK(LF5Dr~y92)h&n-M98)2D3P2yTE+{>@58gTOGzo>27tl3e*x z)$bQkDYnd6^tdsLfQIcCSni7u5pAssuJipOAA`*sjtNlOA9hr2F;7PY6P+>{Hs)LE zHETFiKR?=q#m?~6ny~*|Ru`>d#3rs?vt+tXNm9uIrV$k5%v2yASHBW21uHK(85+-+Z`Tnc(4Rya^fhGg!weB!Pv2{Dojhg~9$9&7W}$J6Oc6QtCc=eom!1im1;IL3 zZMMBf-F{*vhEw(}<{5}j;$^m1^sg)b(mrt3HfmJ;ZYu55i8tMAnVM8FIAoA<*>UrI zQe)JT(Qk4h+5DW|ERy0Gde&EIAx}ZX--q~Fb_yvSn7d6g6D0woj2%a%ce&e65)F;Z zHrOah5Nqbk-kJ8#z^;H8h?0bkKlCQhr+vOdEXi1`$*&TcK#~CF1RgTr^#U3DIKnYa zJLd7J4IQ0Yh3Uf?>fDN3I->GKouN9KGz`i#W27Tp)|-e6fTY{;%P?7lMilEt#J@kb z`1Yi18_eI;4RUMOv_e9XxcKk%j`*jmTiH^?`(o>Zuzn@;yPPrd6j~}GSe`yDIvob- z4*MGpW8Lp5^fGX}=cRWb^8L1^>7-SztRV?m@t_3KU?@+eio&0~?YAu(PP=GL`0TUW zy7SpOuc!87`PM%j6%%*lijq2nfCw&$BCo4yT0LnP^(WOKdxuP1vG2R;KE`QP&F@93 zO$BlO&4T}_9tU}GDMd{?*B}HvL$(15`*hBGmmmO_TIO10XpY_jV14vEv&nZOy% z!X~|h4U0I0*N?JkG-k)PZyNw0manSK@QH;!&kZoPJ`Z551Hy2L6n~A6X2kzk7dgSX>CTL z2q9|X31jnSjy8TrV5*;n{HcTxl93@#lOK$Yb)3S{U1TEQB!#*>2`kdfO||i!@3o^{>2Px!fTw9OI9YBzjb3}jjG z3O(oh#816}#sMdZhvM<8&R<;b`x2#@itvb!} z)6vd_=vg=M+*wd#$YOk6bB{jyLL5xh`-#3vGB!H z@0H$DkQ2Mque>N2$`{PqL)dyCEprPdbB*=fl9Ws-RG1YEUi2v|)6L>`hRoN}^XCr~ zGDhC8wcSR%@fqim-}i4v(YuMc+s>}ixelDV!7q} zA`jEbsHrfoRs+Wh@w$=6F~6TBgZuZ3T+ThG{{dHDT}vI&KfU+_@!jpLrDo3lYbn*B z6JLC(@mLfq^bjm$!M*W<0iFKpfKaA;HF>lsu{3SnmDyX?oN5+x!p0~BUDXr9p$n@R zx7*>V@$4&t=7GVwU8SBZOj`O@(6*?&YUfXFT_$@?j6ls5e|?7mBpWkCU6BuU*>3Hfu$Kirv3q4g=4*234%eCJoS;>=t$LBwHd^e z3igSSwKtNg%&`{dA0b1`CQrDPN`88|1K-9Yu2aQ!^y|-h0(cRuV3_0sY*R$Gz&XmE zdzxsJXdf6QX(&rQd0+d$-8+nD3KWQCp@!_M#FmEAN~jpgVHSgA8hKD{({?;r6ypWj zmWx8vTTWj4s~#rQObC|T^Q+KY!rrM)%ZSvb!cGPUahO~0Ay zQ4eh(Wwp(Ay;i_hr>HF6x<&3XMq5AQ?HcD=d0>^(wGvs);2OA`@AzI61WztlkC4pX~wCSlYYl#Kbr`P^1L?xP9G&s?nqtL5hMT@w#m1|x#Rr{m{Ao11Kd!G6NhhFM zXNXN(xO?jjb1ijtu4!6Z*iHz$pOFElkd=@{!Vd zS1Rlp2R3uI@`(qlO?SMoI^86|T-!EjS|UM#VM}{FTzQ38Z zIM*ZXXWel0 z&Ow7gP9yPnX?g5)HCT-+hGAk*x=45AAJA{te~(`(Kzil8Ip{@{#{gSK&zai!WE`_g z$tiBE@xkQ6v(FnV49~>_Wm)&#!k!cQG*eQ1xiqLl#XMdgeEn_BAuG`R1pbc0KS!Z( zzIyc!VD{;l8?iC?u+pLLZ!v?iuQqv4bNq_a?t}25k1xtTocd4nY`RIw4epojbv{02 z8xttv-g&3F^JRO*$!J>t8HXQp#N{ z>+k)eC{K3j5MAp%c@AFbvVEO;PCYn%?7R^#-|_46LWix#uxlbm@chjXf1!9K<;*oL zJ=4gRJ4Yv9&xO-}*W7D+_)_=d3ugS4%!$uRw~{3h#c{gW@A|E6MjJ}O$z9KHygkAg z9rcg)II~-lp2BArxiR`i+J9zyZ2NydNE&^3rG0ekv^+s?V-wfG&pS0G_u@H&7bIfu zg|J|+biU^5SGp-*zQ5LX104)im{9EntogClc+PbEgZ)bPB zqx_T}>iMVDuwm0k(%2Z^QRn;FkfSq817T6qW2HR9g`k-9@tg|rpAN?ZHAUDJ^RPRe z_2NdtttmBLDhBnZ_V$OQ-5+**VBtZ5i}x;1|HU8Pqj7q^@AP}k%pp1C-w*O*z2ijq zZf9=0eH9;Y@~g@X5Gt?TJJ@o-)Tbn^Z1e|+-V4OLn{FZ>%s{Cnq6XGD6=%h+7au)* zX=0`em%8M3`cq9#zClIbr1n|=`mg#hO-KTV`0d}XY8@nV zmOsvRsNB4rITonHaAqRds2yk|B-dqJd4D-)y%96IIelIczNhQ}w;RtKmQdY4uL-3$ z{CpzOH}(5L(IZb+9>x!oyc+Ch1G!-_?-%ZI0B6dv7I0+M$9tDX$C#Y)8<(!|FCN+5 zIeifRPGZn2gEm@t&`E0EyFhEX<1R(Oe6_qGVZD4ni9aO!@v^B0P31>b4N4;KpYV6# zsvZW?+pjVvl4ASD#cnK0az!6doPFsFThG|2scPkOd%xO4ix}V=VGcyuYS4Vhfm;o~ zEO^S5YD4F*DNb8?_H0hg`m5>&*R$%FBW=xuf+#126YYB*P@u(SyS&t2)B)SPKHeFo zfpAa*xVd5S3~q3Yr>EAz8dtwj?3S!!%5xU;jUL`l3$KYnwQCn-2SdA~KqZ3eSJ)YW zB)|6$xNiS2Dmp?u#GPt9NO1Kj|6!b_()1r^8VBv>b=#+!f8}E$-K1;ETZ`pE|6Rwus7MSc?_ykKYmW{cD2sig^QU zz{?1`3o7d;O9%M9vs?V7r{DPnd3EBzi=}cglRa#)`P^?;V3AY5STg(W8Pqk0me1@{ z8jglPfO6-=s&Ocd2bcV$skOBmQ@4(z+7MW*C(Aa!DtmW@YU3AeV<1Ph67}{&`kU9R zMr2+`nRN!JXgko-jSuzvETb6&L@YqvI;Q5q10wUy094i7Cq6sh{;ouz(OLAnUWupc ziH%h%hUbF7H%xzmAo4}EGb+X+?r9N;WLS{6)tZXVD|*9a(33o<9Yt7ZRT(FwkV*bU zq5(Q9hMwi9NL~7%zP!Htz^5k^36L9E+SU1m{rrQ)bkC&HH%+A|nqc_Ieh~H1)2$*$ zF^3%l8FMT7@%2%`+?h8_+>dv~{Sj+ZtnG|gzOt8Q)Il-Vy|ZA25}rZO)+%ad6bl16 zO@lZOHOBfkv6&~YR#HOZ9>}SS5{w4#ms@%m{c1?Fr;`vi6Nq%9i8B0XWOcuc8zV%M zzoOp)R{(El3(=6>r2tJMV|b;mr_(#m9^u`9?)x#URP&$3%9msLOb7kB2Nm8@^n(VI z^}n)(zY*ekO<_F4Ey^hky7lsX*>-_%fEuH?e{HvP{V;vs!>4FTjuMkVQE%!2!57(& z1TDj7AIx`>6^#<)6tO6q>7BZ61Ak%CTF&6-Ww&M_XN)>?sQr=mcb(V6KBrdMxgc`X z7#Sf%07wR2@sMBx^vXj;Gnvc0M0TiBVqaMayGm48I=?3cFu$9b7F@bPA-Y!;eAgms-skoZ}<`j#OQk+?mOUEnMNp7lms^;RN*q2{StgtGK+#& zF@2vB56p;air;|{`>02dcY-&;w;+$M4Q8TL;S(#5*$l8Aod0WAFM^LbYAV6pY{sB24L_WQ z|8|o7RR;5daWeW%Q@TxZ_bRwdIS2agu`rT;h-B-Y!n(pTv&n`{J9ZXtyyav}xF(Iq zPEAVT@=vWIY^sBO-~r&wrd9LGl>tj!1%Iud$vyG9kBtjK)>Uh#(%oN6`@3+{{{q6B|J|>49nV47gD8Wdh$2b@@xwXA7kV$I88N%93ynwr0|L1RngnuX$bT~8fnqa)I8(|s)NSEqa~eXTvuWK z3*WVpDHAk^7a2S{L1xovuU}|vdBQmkPVmTLYhaDdOJwlE0xSAA%v^x8TO^v4G7amr zk{cVG+{{CPdm7deyiy?p{h{7ZOWEX#o<6;2Yu6{1yM6wZ|~pb2-OPHV4Eu|cp+ znHPb6Qbpd5hg|%mfjIlj3E*_yA;}tY8*Qtzyht0KD~2ZkM;^ z;FwvYGWNAj>Pa#lfeA_WKA7B8Jh?$`F$Zfwk}U;mD=4&JqE{{rZ#`r-jAndK)S7J4 zFqh8)VA9_VH5t+{Jn;IKKK|a8w)Ij7lw;bzP@2CtbJ;c1L#{qbo-txmw}cul*i&Z^ zAym>pV-4pcVo+@W(^h1K4(Yj%bIEvEtq$NaN6JKb1v(xy^9(Ty4q|RC1K+c_aYFZJ zCFmq0$=EM6UITS<#^G*;CE$V1f52t)={Pe3o-foI+Pl;)DSYW7a9JeQ2P2Ws6|=bB zENPGA)|E%p6>Q&>#nLl}>0I6YyLJQW1cjPuXfQUh#u+@yDOU>>bwr9zeZ`RuK2%S* zO$bZA&F)C>iC2f^v#gRvLA?w;zAd2@EtLcIk29-3sM!11xDHU&Lz08O?Gymo;9ME0UGx&80)xNm0|l>$mbZHTEJn zP2}lHn<`ydyMmg{)));YWw&VH@lwS}tgwy*N@hJmxW~Znfvie z#uhOD^nXA!+!zzr0wXF~9_`IjfZjxWw~SEo6ZqVKM7NtX8ULz``K-3i;2dIBmHQd3 z1fp~sgiafJ$v_Wc+cDFuQeBMm@+2~T5!5SG$(vN zzbzH1osZ?oDdw~vjN+BogZ+I}yLH~;=ds`rAn1B#nm1)kt@rH_B=SlB#(vb z-3X_v`g4eJguSecw5<}0@-r4R!y;v`N%lnkv`GhlSvN{%1;bPE)?~WtnGh-|IlIDU z{5;-=5X7)AA}_G3_U2RDhi_N_PZs4|RsyX8`9(3tEL;Ax8vG=jq69Kx-4jZoRU3ui zBFALbeq>FR*CEfBF_aQzCPAFvI=q2|RVi*opSb(XJmv_BtIV@@PKGhi4z7rW`Uo(z zObo^FF^ie$xZ6yArxt_K()FgMn!)&Y_|II?;nmewtk%++mIO&XAb&6y_+6rE&>`4? zX%_!#cL2)G&39Y(*$KJqUPk7{-0J`rd2b@`*&sEptRhV^(2QbRO;jRAzhC{lxmeBG zvNr=BL61MYYFG2>%H@?L{%2HJ&>#=7%_w@$^?{^Ma(qR_X8s=us*kjyoLkwfv##E5 zOH<1XqBPDv=F($4hJ>nwG@<)3g|>K3XtitF#u$hBje=5)k^-I2qDdI{(Y_K)+I7#x zHiRvKG#Tn&ai7wlv^d(B!NQg4V;cL?#5oI^oEV~&H?zJ&C+trwMFdLqizIb%Z&I?Mn)V|qdpAtE8FO3e~|MlaIjo8!1ea}#d zqNJ%|%|S9Ar2ecueJia_lKauhJ_5h8Mo2o*E+Y5Cayf-I)NLaeeRl*1?|Gwi5Et zb{Dn(S-x$EP%SxYa9$yo3RGrCbjt26Ot3*n5cH}t^&_;brgoljQ-3w9pQ~0`@*u&& zT$KoP5oSRNbjhvstF~JiYUC_(gf!aJtY(YqOJAf*ED9`U$4_gs@o zq~fWfT!OtR{P*U-W?8stw=souvUt;aBsI%#q6)QUR=+&lvL+drP<8o>y~>*FtQL*? z%|b0Nz(S0_H|!$@VV%fmL(<*F1of9c?rc=;<28)EV>487Fe28>18Euy5p;l|Xz%hG z7Wo{VHBnCSM2pP^{sX?9YtR}07Mp$K%&QR6kXS|%6HO9kI5 z2-bkma$J3?Kg>dE^U`&M%Ini@e_$lPpWq}}s|J0|@WB*UOO25t8^G;FS1_EBLhtCA zUq;AIq3d9>`l}4MPM3xwXi1U;XRvB>j1(`yQdU8@vM$ls!U{@O9AatWM^ znNi+yf;|=EyY}hz9f1`5*zx8H6iR1OxFdeA47Y$=)x#Aaqh!{u0V9F(L4}FYr+T&2 zQuj;P87OO!VJv`q-LdxLP z9lN-3)Qp2)6P1?7T6o%T3emqR(FHt}RDV8m);nr>W9TGWBJbk7q+rE@*GK>-k>)MV zv&Zs&g|WPngAk*9qz;&pnu{O%Q9*()E4*e3gWUC9ND_&ADscH~E_y(98>b6B}T2)`rbV|I%B=)E9H$lZ?A zKhI1&uE=U9U0p_hdP-iiw}7ek584P;^w=4?yv~SSzcVxLc8!43`5bo1Iyqux)P;0NmLs>@9qST7~OZ_%gW5fJ7w@rR@P^k?hh0*XUdwZ6IDZJ-zJdp4=t8r-X zb?KK7!J^&ZA%2Y9MvDIGggT9cbflET&+$*(t*B^MyS_SoI2O7{3x5ibcl$L*Z!5RsZVh(%bg{Wfhjuu4ZAvh2gf{VQh=zSlsUvY@ zcb{DfG91fRn-}>`m_nt0xA`Osn%wGfS$6nP4LX$QfJZ`WAFX{k$jjkf&=K<(YY&?F zK1c|f^&HL<4Bk*$Y)kyu%<23o#$~B#@ZC*zib+y77n`~44sHoD5MM=i}*ANjvVLZ>{1oD}!Od1?j+-SxvdAyN5k4Y)VVN5uTZk1}ux8Qe1Cx=cF1+ za2;P>2*)qQge_uX$ue=VaaN~LhyH`_H>MbYq5ci0d02Dz6rR(yLsPCqB& zU1cRT&*dBMC6sI^zfz}q<;H1fapNn#e7Z`Pre($gi@cu8D=?81y$$;Z96#a4pZ>X` zpgCf7t{d0&i8mR2oBI5^12*Yt<3{Im3F`UcTy9x~uzPN{p-Jr*`r7+7?VNRs_f~Vq zof3vNI)_0oV^TxcTLzZ%2K;~GhkV=9tvXuNS9Hr48|B{S2VAD{IWTx0yglZgmp`(h z3hUW@h`EI=N=g_SSKSUM{$pCJN5|&@wR6y1rrb?pA6E4jo_1(*#a&&LoGaV7-P^(hLJ^PXU!A;XYyh{IoGwN?5bem;Cu1z4Smx6}Z z27(NEeKHpt-Z2B;>qYV+81J)ueVDzaz^JUjGtIywmX!EXm|HxJh= zU*jBRq|P{uP4tQI+;T5*8GrD_XPkz+v!?J0+X-%hS~ITn6czQ&u+1*FuJbTcP5|*q zarGXQ#TPhW%RwZUr849Xh7dJJ?HCvgQdM|&>e;^;8MItMZsy*O-3_!qIazfKP-x$z zd^Je@L|n%%uYmWgh9yV>a7DWE#J=tO!s@JQDAi<`T-uoG3)*(z?+^WnpEBLmY3zT( zIaI#3*aBr;GGav9qwaL5RxI8#xo>c{)ln+3&nMwq*^3VeU_QbznGLKl>XeSVFh3bT zH)zsO@m!BrurPLGmwhz?Y#ByZ?d)d%-|NWq~yChY<7OiNXP*8pQWnbbi z>-kT023cF_LoNfnfb)m~E#Fg_ZJKDkvH76e#FE>wVemi`4`Zml3RTQ1}~XGr`$ zH%!}Hh9&hiGq`3h^5K^nqs~)gRZg}1zb-Asce}-Q=O$Eg&gl34daGiRy!0306|YI0 zUTqBI*c{-v=33NHIJRSFQ#agp`3rtL!ehj~t8HJ%RW-o7Gmec_I_uL(<}l)Bn0VSX zb5MarQ%K$*jXvn-7hXwpn111UndfZhf``v}B;IgfO5oyW%^LykvL3xXYBP?w-A#=2_CV6rg&07)U*9-|+rw)2Z71 zUG4E{`@8-t>TZmky{5~n$VV4`GB3!$L_+IU&mXyQ48Tn!7UD@)4n`jaxhYFRvi%KS zJo^{$d+_T-uB+`g%bX_UCk7F5?w%4_axr-9q$}$aGAMV)g){!?RWscer!41r156+%MmZrR;AO50uEU+>kj+L|B zO~*^?T`E7V_rBN1`n*i}Ef&=M`=zv$ltiXR9rWBw6AT&<=U#dXxMfZ1%-1itL?R(- z`?9Xi#Xfojza}D9IXIz4UMW+Vy?!3I*}ZscFGxCq$??s2r_e)IH&bD0%S$aE0Ou`A z^6VcrhV!wkA73hAnp3;fsSC*rh%VYHgq-Pbt1g%Q{mu&;h(xr~Tzus}!VfsWUBg^cyYWY7u({l{;?)jA8s5=Ov2$wKirtlXmJ}7T=jxokIuOcZz zO8eK@IVTH&BT?qfH}`IFP9`x5uig2a6cN!_G>sziqp$w+p>FQ^T|+rop5=4Z+tKko zeZ$RnR4e-0^^&e#z+@AiI0y+XG6)5o08O(jkK7=NuD>ngo_z&H@gix0tR5#X)!z2m z?)Qn&LscK^!+2%T8CEy!sKvCbsPjIH|LDxRi^!&UQ4~bP%9MmQmJ0i>lwjs)G_-gS zaOP6yK3y7c*R_GOhVYH;Z(x_@?RBHijC%LDPj4H%wb_J!xD=@VjOd^^edQL*#8&`!Nq2wo|Z710YJGSmE!vByX6FZtmvtk(rrMAG{fSb6m&% zAb+2YFXhMK$N0)D^gCng=mH5leo@Y;;g-9`ax9w-*E0}I(^s7dx=M>u0dU?pQP%!} z1Q35z@Vf#2#Fi`&(!?WC-eUa2(!G48yyZ_XHmPz# z5jOwHC$vTGUjaEvli<5G*%uC(RLU;9seOP4J^Jy$O2nPE4NIH*uS7!S#L#g|jwpQm zbQQ<6EL0H=YdS8W&W(M=@3o34dcIxlig?EE*p(kLbo}#{I`PSuDf-p*io)X8Q z`ZGsJwF}KOixGX_whL`9p$ZY=e-=)jEOKiwA(JBIl~?R6w+kRKM&isq-LBFNYpA|5 zakSGi7sv4r)YSOjOIExYuM4B2HCX&xH%EyFHMAP&QjeB`(r0CO?D_$MHYL>28gX^1 zM07a+DeioJfWz0GPs3^&T#Acb#_GSC^J$ydte!~$yUAPE^|qD_h>`G%p65!FoC6K2 zJS(NLxlnVgi+&w;!{rHAi@7+Q(~z2DDcVe2+@EKCrTlD|K@5tGf&1Jb1cHVqJdq>L z=&bh9GlQRQetl`i6G8KvW~$fW1)AtqtG8DQsVN7g&w~T$`bS`KC=~UWxFS(P=qL5Xrd` zB7`&-Vw<)P3kUr_drX4j%NPoa~j!Chx9go>@zgZQq7jv}}q2r`Tsvn{F61AzCwMOrpkH8L<-$ z9^=V$cM(w%5|W$7=JvMumTQ^I-YD_^7Z4rCK@S}W`}x1&C1iY9!sA8WwzQu9HbZe3 zL^ulFSF}%^Z@(eeyOZ5^vPN+ZJ%05o;-@${TVR*Z9^B73!*%ITb{!>2*~y^AucZgq zvM#fc0xEwyF7~sGff&~FgroCE^@)-*W$b;tYe<6&o=`A5;|M|+mjj}4u?-EfrE^Sd zkm+wtb4hi>3{tH{>83ai&}=v!?!ue%zKP>WA+QF(A!xCuFo*VL1o4o$tE@HBic)FD z7)E`C6a5d_tifw93?W)8YDsN`KuYW64ZG9+!bQT}N9HW_0fUlF}{`9WU?KnLc`gTWZm) zTS~O#`O*e(nCN9)+1#Z4sQ^#h5mVSKUFQTcj?KmtfBP4}RCY@K7tnPwRbo<_yHz7L z4D@>I*YH?=(Wf1#j9^)s8&?D!55LO-5inH`4G<+p<>GQ!tRXD~P?dC<8EiQH#Tag2 z5@l&B6C8!3owK%M2ey@+r18BRqis{|g4FE_!naeS$-x|c)!%WjJoh=5y>gs04D}WI zrwFj4FElZ<5_V47b!>W3o67A9^Nb`pHumI@`vKT8o_0s?r=G2I$qDp5g|mR{^5}CO zsF<`7tw(@QFpG$k2nIXqcK?n~cPa2~4<+B5H4V$^>6QJYFs{5If1yxbtIi}TUlh&T zf3F@%!=m?={=SOBMMygodZ&*VKz}R1BlifzVxGpD0kJKv$!J40pc)+~9NmlXT8QHn z3zlr%hFsfEbI#xp`#aH8y;PWwn#QUhv<*uSj+Zzni+7n}G#S&Qiy|CQKZ8CW5EgU! zj`njkdAnC%17^o<$KAW+VnAHF=sB4QVU;YbHsBG?Kq386+$fypp6?+K`{wam<*1H`F%52$cGPu|4 zuC8Xb$L$FZdbh_Xp+v~$_$hf;`S9C()tMhLB+3v8EhILG6YF4sHd{>tB%&cnwm@$Qce_dbAf(Uogl0c{uVu zlvL!dYzR2^N)35SmQAv;hZ(k-vL+Po4*Eah+|CrMHyS>#B+V8SL7TN=F8!PH)V=!2 z!Js-Pjv_x=uweR7$C-1sSYAqOm_Gg3#7%EaYUH#y8(TQv$)2-p`hx02&V(M1(?>so z>&Q*Yyz_@}zRza2J<@BI$$)I%q9S`sGCJkvD}{*26l}B9Eb4$q(TR&#h)ww<6OLgx z)UpmSvzq(cPy!Xa=FaREq2#o{N!twQW(|NHR42Gzy5u$$`iQ^1KV;~N#K|QRHFIdF z?8xm?^m51z6;rpkrAJ>mP?_%>G9$f+Lfj&u{ej`^W{lG# zvG|+#1ef7!5CSD~Pu#2kfYNDCJ*MPu*^03QB2Ftb62VobMa*=bB!dO^xw7p7s9(FynOb1_7iT+GA=s^vCF8S1m%E)eRFo8DZCWXsMScU8+!AI9xh ze^*5|qJ*={p-j}=pgJh#KiCtdZy`{dq|^YJ!&^-ktD2@Vm?4QaZcI|rO%%V$XW z^^~T0>3KZ6bOP!p%Fl2d%V5+$4u&D*uY6^~83oStSrnH{B z@SV?BWtMxCaU7lRV{SPURZ?>NXZTjE^hfqKfGHLXz}ze5jOOC1Z_SD>w5;C{>rau| z+)RrG5keXOAVx9#)An_3CQTf|mxGD^>kRccRjS_9v?}oeda<+#`miD<+H`fh2i=|C ztg9`D7RGk9^SQJtiuJ#&jx$kI`64;jfCD8S>*O=-xzghFYoytTP~F>2{s74}X+Nr` z64RbWnpVwWv8u}le>X)1S`*Q5)Qa?7Y%Thrb-Rn{i&)*GYf0`{(PfqpBjKS6_7ExP zNAVk1GGPfRCV+)k;?tYbZ2NC|h)h4^IXUbjiSWNt~c z-rnaHU}OfNTI*i$EyL2kq?m&5Vmi(O0uKEeD!Ps~)9&IfajbVPm5OrIW^rskV|ouo zSnhQ-ZWo-APgDkxW|ju$aQ)NMC=OSyIho-(ka~M#&uwX69KM#hnwoE&W5nN$vix(0ubGsEZYTatsR|sgFf95LWd4Lp@Jq0+hB_tz@9m zHR)0k?v&OT&Z2BXQx~n&Vd~xBpqw<=2gZ**DF&Y(UK(SJL&P!cz`&iqAH4FmQd;(Z zL)r_36+E;87xggqE1BcS`zajqNs5vmA5g4=4O#Job~W`F%qnV$w9t;z*XkP{x~CYC zyJGY=uqCU&u;!&Hxue03>!)n3N$06mOEFm~L@QSN#O*8g*C^;B#mhe0vjjjxc5k1& z&@EPlAC5z456^!#%%dZR=T=vDyOqPy&};#R3M?=r#Z|4muvAx?TC>dJJ+1$6#{SXb z^UqpIXv{dDV0Cp7gT<>kliHUhcG%(|PN84OGz8{fKpch9(@IM|b@W?W{*oSFc@QO+ z>z%D?V5$IO5kbh!H)B+~jwW6-(DlzsFny?G6^QU&|EZ75H+7eeE%+&6F2x=Xtu6tW z-N@%600iVd&YMyuf@%6DXl4Yin-`nxJ06kJjg4yELH&>`f{RiYcR&l}q@XDB}Rzt;`BLoEqU(+mVU3j37vVO4`nxPxT7mBzoff6?LTWaHc@y0fu4E9gh0K3A(+Im!QW*`V zDXc=%LoTJ--_k~wA1PesmyS^6?Q>I7i*IJ^zNEAfU8`Y?F)8E(M)|H&egedui@@prj{6(~*M`vnGNP{h88Zm32#Mb#b6@na4Qw zr!GbFrgcR7>Pvo|$6S%q4@U&VZ=u0RU8q!2MH(c#%`c&`_p3zEtntE8crK$)j!}v^oL#{HU(d zB}Dxk!acl6w-(rvl!>Mtr{tw^Z7LP4xhUPW8{3@-lTvRYSC3s&bbG1Dm08lsxQ1+X z+H}nn^6wkR`Bl28aWbcTNZ!Wm#(cqc3%Ti9iC(UGe|x4E|C@H#ul*P!bVccP6~+2| z6eDFL>wV9*sfgdp*m-pVa1!JV1+zJ1>0<(KOSr4(moMkz7=s_}@q0&tFwSq~g&a67 z?<@?YOvoD9-5Xw>^R*m`WSPyDTU6endjmULHG@WA3VdQ=t-I5wn#f$TveY>Pb1!rk zM=F6t736uN?xsowF$0Uta~%$=)?9=$-{qUh`&<4x3ww5u*~)T$AogYdc>V# z>-&>3&Fr_%XL#gjy%xA_bb)fyzu#3H169o6my1-8FeTQpp*A})ro>QMBmeCL8JvQO z7#K6mngom5RF9A=iim-e#inl}Zpc)xuY10nHk?jgB9|RIk-0;^i07EogxP}v|DoF6 ztLtY@Inm$2nok;j<0phPsWA&L__F9=_u@=lkMj>kfJuY#MJpn!?;M_HAUNYMix#dq zLB`g6k_nk=vUVpnUZK zgio(naZYGdMRunr0 z2at+NvrT6WlkSfwx^<^O|NL5@u+fuyuN=Z1y{=qoj=1CTann}hec}C0y>Wx&qX^l1 ztuBSeV zsm)nxGgQX8x-|2Zbw!X~l)P(Z&ki2q`sOSI5vnxE@-!hoad@(6-BPymIl0{}#&pf2 zaWja|*VH!TTdd&$+vLB1$q3nLKtX>S`nH8wy2UU~qFq>Q@UocU3w5%vh9%d-)+cmj z0DHt%*oXjNP7^N^A=HoAjOXekI&^s$QY-kFU%9Upw6PE+~l%vFEoGGy+AMN6IkMX$>z1#3~h%D9OFD=%F$JHq&$6Z zjfW}jHPH{5857_O3C(`dUO*(L7zqFy>lGanG+}FqYZeuc|^wLXvkm z59DU%&Vbl_0*|g_bV!rrhlT$4ZMMUgOZ*O9|GN*DAthXCfg?j9)Kd9jC1o(b(siD> zLm%<@mqOnTlU37|+Bd2J%IaTT6(cFauz}W zrL&1~A3#!u==@l&s2OZgAH7>GbI!eMnON5yi?O|}UL)7fao{#({9FkV$m(?rF4MuEFbc1@fA`(x!?CM%K(i+1qS($*8;Zpr6OZe<^%3-Ly{Yye*_g}#GT-_Aj z47L#l*OzYF2XG!P7$$nuhlN4f(CFCj^4GonwC;PSmjRj=z!B6A#@w9NVQvwwin4H? zaq5B4v>v{MIfH+{0E|=C{ZC_()_CvKh@JVh6F5Q2Br#unX8g^)GxOtc6mI47Lni}~&C|a0 zlVrMhz^7jN~AE zX=6%HaN;x{1{aFrzvymXpO5Dec13J_@&!}4q|U_@ph#onnumh;Z`KzL0aexWpe!!N z9!mOHsR=Ciyb2AK*~4y&>si{6d>n=gTNHAMbSy$d3>0kldJ-28HYhEcC!1H#oy9r- z&Qbj$z5Sx2IZ%yokPINYLK@_>Jdv1vt5G4Npxoc2%wsiBrcQr!&jexirwjyO;%m6q z=`|mcT2(W~a8B%nKGDN4O-mt2iZwH0CEp>E-$e(PMd|#iB4g}}%FwE{ES33{d-tZW zfnz}rW#R7Uc@{Klsg(j1Y3Wz#i>U+|eN>5Y`PR1=`+96<$z44HGkQ;mnvyS#fSvx*Pp{wJ~fH(VmNPg ziGsv3vEWTUHm=wWz&W~c)!fa6)20ty>XZBhb~UeE$I(IABKL);>8nn7Lad0!!4t6s zp51wWwr-9;i#yLUP0dV_lZ6xw5$lT^nGDi5o;gsQmIHNmRNv40n=h>&^?1=FHjE12 zr@hCsnb7AO1Dt@ooGBGg+j(rcAi@d|-Mf+d>N%ueD`}PG(llCA*q+IPJBOLt&iTZU zo0QM4-)@GSKDkE;ZKOeygsr&Cc#{z>GHz^tJ#SeR5m0&C$sQ7>CKm&(=U-s~x%CaV zRXiDnmQ{9?8QXJSC_}Ny{?N!BvZ}hAVl#VFPGvWcHH%-vrEHnOe1`I54t08$(FUz` zuch?X+9o2P_$nv!a?DW3y-iJn((UWX9lXXSx2!)5(3GASR^TLFe{M^%#Vtr)9jMUJ zP%js;D17Vax8mlTtmYvWR&^~N`;5WoR(lRkYk>SLSeG|lq8`x5ZB*Y|eJ`aw!^{KP z5T{b@#*Dlzn2bPfcTYtn?ceWVs77%@^lQ6cM5J(lZrGt)bwEsng7#np%vXlWQY4JU zl`({_h@>`X-orN_Hn2e}WIQc=hnUlhcF0QEX|b7QW4OZ2294CiBiHqhl$u67|L)}r zgKNo95DRN^s*gsw92O&PXKvDiHhfXTJx5v@Q^5!6W?JWPad|hSGGZ{Xmg9aeI()oYR4UP{NVLDTx`M&3P`pB`x;jm`v}CFax+?EdVoj` zmBN{?Ug}Q^PF+-f58{+OEn@&c+GTw$R9iVt9^Uc`-N_7`J`b9daUlkUW+D?8%1vR+ zxv%8R_<-7vG8~wN^X#Lz`fn$cHbd;?UFRuvSI*_KuY7?2}(?KA^Ml?$=D>T)#8$%_rw6o5ZXmIjf#2Wjz0 zO@DZy>^#CH81Sv9LdN$73sEVC1J9`Q0+Hk@r?hqiVVtl33_+;P1);;36FiJ+&wL{8 zDyaDd*GH5M-}qw z8Nl)WXS>3sdC<4C(yRX)8mVWgc-Cr0m+a7nNZ&<^v5#|7uep^v!Iqa~E=-{V5%^?o z7aZiHE(fffpgj)_vBSLNf8xD6-;@_rnLWvymtmz{)wFXLD{Tld!&NmKfI`DJD~L5$mFQKg+y#n2 z*-v|OMU_zlcfSG+;vx$o#elvU+PPp(tYa~H;&%8ixtdKDF8bOJ6~dgX)!5_Y1M7(c z&Ko>(7sY22S+fMq2l?hGdqua@EakKMPzGi$53TDx$JA1#2vdaWLUIH09oI9ttnKWS zRWW>3J9o$$w+f7As9;4*(!e3h!+y#LqG{_{Yk;tM{S%{BHhLImZm*$+(aDofhZ#=0 z;V3Q2rGDTkR-N?^>r-Zh;quBy)c(`o=2(kT(sIDD{4M;&7ZT(ecGUs0s#W#oG;np` zpEcZYGAm=+?VM;?6{mC0a4z3e?^J*4v@wjmI|URmnS;oNxWNWky2YUv2{xMH@In{E z6_=0?_QsR_Cq^~S56uRW)$}VLSZ6892nUO$)w3pnXQIJ{(g)RU-ng%J1Ajh3UB|f( z=<5Pwx(nM2BT-nBCaR$9BJ^7H3qFEm+MqqUS7Y0x5xpO9WoD@F) zp7ul%lA$nN+A%qANKw)?AYo( zhhwY)OVPF0GsUc3P@LjpVEl#7gL{}S+s#J7%y&VI{)|D}WSH6`aTBS6ogy>=3a+DI zu-ft|4KAFrJl09lUuJ;e?(`7Wb+qQU2Zg=W5|leZOks}|e9CbO5w&D*1AoXjcdWGh z*Gu@(@=0f5$?f+=I!j@T$)i>*M63&{GdN@@eSJFBk8NGK<;)~yrA|I*FfyoiX;Zqv zrEw7Qm$^6+$ttq20B_Eo4%xis;(z?0Z;U-fFQ&{V*oU?g&O621~{fU6$;^-WSf~iv0+9YCQ*cv z=eyo|?-zbnW!zt_t`!sVdZq*q3ymx42|Ow9eWwCJCs>Q2khhX)Z^AN_4Z#^;Ds`;3 zT+g0i!zE8rlhglv55xgZU#AE$4Y>BAyqusEs2>6dBCpx_3oVHO0GHjQGMGR6rWc75 zznf^?&Ij&y2+i|gL)^>V%(aZykVuRAPIYrE#$lSZw%e-l(Ite-MoH9@;Mq1cR(9nP z`rkK!)Q1S7 z_7E;ke6}?z`5?iiiOngP@+AiBwTZbfWJTq*bwTcVjdhLuk}u)~odPfGH|<213RPq4 zNZwr>G+x(i)@rvowW5e#ii*JTxYUmV4fU6s=VQS5Bd1Y`jyc75=JX#FitY)XT_Rf-Aw#4vm3By zAidCk6QO7#o;a)6;pFRJhm9=3F!u0?)1WFlqTbDztYV-IK$&W+YMxg$24a49%NLvv zjU88rKqaG=5gX{jR1P4lgi!jGkOYbD9{zFyy=)*!5qwosC}YD~!wiy(&9jrvw|F0K zx1)z~)+OW3v$?aRpo}k~S>EUqH+=L90jkcG`LNWPu%v;!ZL%K;&#!tev=V-0#Z3Nx zZuFw_w-?rfhUcKt<r)G&dUi9gQ?8y1y_5qTJp=6jnjQpdO}Lelr}HT!cPty@X` zH(z<+>f-&n6#f~4`7)zUT0ZCHP6jaYDk!t|Y6dW69jWFCi>yDrD#V}IoniDkBDF6g zU?5!bQPJZE``fFrPpe+9Nq=>4R5@hk=QnyH4^A16yPyc#G-zL%(lz)O(DW=NDM>`+ ziq4>i{tLd*eb*9~&a*!ij5%@#0XK2cXL&J)6*(;O>TJUMn7oc zi~Ou&2(et-;%^R}(pPl<7Z7~$5HqGu3lm?C z3ANUeu#(E?A37e3>4A$FO-GZ6rZ-(5$In&*f_OaMv!@LwJVLl$(C^ z^e-c)KjgBR&gf@NJz}GF^zWe+G%l3PdHwW1I{xuEVrYW_xUuP7UeOw%sa@*`Yt7`mi)|+zu zuX?0a`l{dIa|?(N3(Qhb0PygwENYi=Q!-A#f)%0S^x%2#4Vubte#DI&1S0O*-=Hvh zYoNQ%*U?Y(Zb<|yMyFqYf2q`ppI|G27*a_fbCTdL{e2K!>R^HP})CN*jc?8 zmyZ&J37UyQuOHQ;?-2t_a{enn*7yI1ZC#k;U^^+DA;y>Q=cBI#rDmwGc|bkd4(dAy z8@UmgsJ=f7IwDZdDjw0H?7))$%n|x`kbmga8&%ELP0GiGL-?L@L6PNz{NjG&#Jv%za#C?SO_k)ay_W+)0Lq5=#4&Fd1_vhg;$9} zL}fXjZQ*)qmDw81Tc);XJxszj%2RkpfBu8+`Q*1H9qa3>6T+8%t_4x2vVgHV+DTdu zR)*cg`b3kSQ?K%If_TFJ1bP2lmQW(=S z0pD;_Bri{{vDwf4=N3@DV*9F3X!SJFaCFxuf6sb!qUk?}35VtxbztpBvhL!buxQS0 zp^E_oxp%lv{axqZq^MvU@>?TUgj`I5AQ8QAu0bZ|m&3d4Z)-m7S6t{OwSGLROg$x( zNj{GV&DQkDOI|7QU(mTZGy)pJMy{|JtcEeREk~ciL8rM+0_h^TdcN9e5eUCpt>x#1 z3?;4mXObVDwmuf^(5mdkpUNyci9a+*k1%gL%enP>zAK`IDPYrw`q!uDKWW6%kInSQ zVr5qSj^-0MgU)yN8*BY#KBa1{J;v~LOHfK_{p+>!UdxB3S9goHPOtJm#wf7FqV4hw zrw?C4n%JLoW}CXnb-v>os6hKit^)Ih)tD2&Z<4NfT{M(@rmx9f^$@#U@eor#G> z2Q3I0)%fbkoa4qmy$sJ@2T{%_D49J7Ra?gBayVt6?(#m}@AVuI#i5-eHbc>5naYM{ zEIimLa3~0JRZdn_03}arUm@k%g0ebgdW0!m&MVm&iF4YB;Is*Vc(_$8cWQs8RAb?m zsqnP8Sbs1!bJ*lD!61gi0l{>;u9V1yr9-tJIB{H2Jz%LCU*M`3WlRfqWiz-FIJdJa zZY5!*s(Xq1b-a;_6mA5eKU3D={HDJ@gcW)bRf@T2>QS)fFO((vE@$SJuXuHOfqLrn zgsFf;`#D`ZgOuw7FZ^o2x#IYOPrj4;4MEiV%SdLBsViTK^bG5s>c{CCU~Oh@rb9_? zO2#(#FS+Cvi5+6bB}6`GAut)M?A4x=#C0)HSq(#p!(IzwH2X$OqGT~iPR(4S?>Io) za+=>Y#W7Jh7I>G>zFT7hFaT}I-f zn5HBuPApzNzUo|vw&1Ax3@^wepdq1#q}o5TD!{Bk6|L`1d5W-!sh@g2>F)i0hYdd1 zwkN#fS|(xm`KXEUR~rH?1wR#=BO~FyW&;Pjua#~;kZ4diYO&{uq3Oe(?pUr5)$6mCCQt^oF#D)Z_v$!iMJc-Mi>ZD+ zx2M|kTDO~E!ilXJCYNSv!jUdyF$-m?uEL2im$mP^VTH7Ca5_g*sVHtFPjy4c<$66~ zKOw|EL_paIN?yKidd~Vol>CrliMGie zNVH}qA-Zs;!SG)|%|d#4x@%*5inj@k!Ok&CY*78hT41m9ZDTH7UjK?#%R&klB@Zpu z$HCvs@y$)um0$Iw9 z4SB#~ki}qvx}srPz5@qbs5y#g0R2GjKUGtK!WCvgF)mR`JE!T%;giXWjL4P5!Fvj1 zU&p;vSp@_^!iSmjM~cTQl^H4geaXU4tr^k3QW;zhaW@f+U#I4_iR-(KJc#@sPyYlr z?PfEu-O0*Qmf!}Tvy`kSso#Re9kiQSQGsg0Jaw;26{L5_c*uo5N}>8*hi%l29&|}? z;9a6s)6Dik16q<7DWa!V7_|cG$=em+)!}^Z|na>3b>SPH`wKL-1P+=og zk}6z+Q8D3$T0&p2yV{v9Aw!Xh!Vfna)`FFr{$y~ z=OV-|nJzr;fY79F>^$RZHK#jTXw^701L%t!AVXMkSa8fN#R_v@t{K3=2`mnN1C|dI+k)$c!>{I>5Z!(>_*CzE=A>YDLXdJz9Uo!Jp z%XD$(mypV8ccNx_l3@q>O$LwK@q<5anTL+kmL|*>-srp_3s5`t$^A5jn6U5GXlB?? zu~|-33|Eg3TZZ|FeM^FHouUF+JmdKe!+jo44@GV?Jox1wn|F?cFm}5j7#jF0lxR7G zMc>e8*5k&lQV43luGDu^hKQIxGBoNb@ZA_~_l)wz5TRy31PAKhLBZ=n^sCbBQDXH@ zCc7*pT3;?kR9-1lk<@x(Jp!)xdoK?Oy;S-oZq<&$DL=r9?#qBYqr@;EkTB59u-35V zV4Ae;>!K5uK>kz$Xodb_@`jfl?+~XS5!Z#zL#3@!Vju0oTT8oliYRyDfV3{0IZT7g zgdH|NCm$fW+$T`GvWy}3EFi+JVKg(#+>R#h4WDT4-In9j{uf}M@r|r1p!e$gUkb?~HFqWPT-c!LCbg$nrlE*CHc)Y>J|d({Y&btm-Rhbj z<|j|}UP;irFzoKOV7k_I&MNX&SqT@W?UziH4MA5-CBQ8G{tUzA!*Pijm^fTPtgaRQ zoz=oC;w(v`eW>)*t>aN}PYf1RKE-maRwh#+i*o&e^AOWZEq>b!nKI$&g(UMSxwLPH z!*dZ3oT`P<(J{|u z;a*65d08>wUw{*SL0g;<%~5+|jDQdM^NL8`kwN_ncx>40*3aqrhZi;0aq%nt3wTSa zMVbs1w=|3bzFIRlu}<)p`s~ZO$bfy^RQIx`>JnTPBx=WrgBRIyrp$F`p6#|00A}w_ z+F5fYX^AcQrfxBqRDPnKY$zHkb+btc&**c4f7a^tE)tLGrd+J+ANiT82Ww8O3VJls z^U5?Qy##>$V($R>;mvM0OeHGO!vMhq;biAPu<;u9!lcp{iB~%qwF#tYeXIE*wy|8ybba3J~TPD zs30_Y>1J2FwsG2E>>C8!KfEWP?~SkQ|JgDKcjOiTDYY0)2RagdLzYBg{q#4WL}%ao zTAgl}k4}s2Ec?^4QJpQ$E`F`61B8ZR8d8)8v?A~D2J^T^RKJw0hyRKh!Y3!oLsM7n zX5g8Xih+t4`&8N9rez7x#GPQX!Q;2skbERN4=d zypw%m!d*8&mD6&E$G7Ye)yQF8CUA)o!o^EbDdtcqZj1Uj-qdr$cOIATA*+!n&4QyF z#Y*Rt;+6qrfzoz^w>g?pg|nyLGbm7d7SoT80zsh&9XW5ohj)2%-_8V@`nsK%q#UQr zvDV~r%4yw2vS#H+lg8`?r?e_yP;UGNDU-Le%xs`#7dw*nSnb^e$v0`_AJNnv(j18j z-)edtQ8eOo2v8gV{djaUWt;f zodwYq2B;#5YS&?Z-_PtN48V4iWMRsE6uBN66dI}(>rnFJ4w<<+zWMB)vX7ulFr!Ig zZXH_NN)g1Uw$#`vVcsJaVev{T>0!QOlgYL10^NM*ygm>zZX@V|qbD@PPIvA~NAg<1 z<#W*~&&L)(bT#L|ehuN?cb2;@8-kkAI8Co7NYClNpc9LsFlK@N|e((DX4481h2R-dc%{k$*@D6d@?inW>xXq)wV1t z>P_AW)1l(4#!DEa?|#=`$F1}B5GSn-hY&5K`X=75zEboR?{-F zFMLH2*#j$~h*bi$VdrVUm~=Vpy24db52W-iRe3Wy|9VdgcW{;}BC`5^+cknIwrs8jn&$?>ZoY(Ls?hpAFWSC|?;2`gZcy6cYP-XAI2y z_1am=G)r$wJ0!0SaIuEnHjtOFetu8pSwDmYaC1VDEly*|g){?T*iz6#JQG8Q>Q`=E zxDOC}s;HJ$Us(xN=^y50i1~vo^VwVMOqtQTUBJOBCi!|^r=xoos@#~5qdYbMKYI9< z6RYN?T?jaKKmGvjrZ6lrdfau&ldJUI;$a8li;Dd)m7`4uo|%{f>W7CUo;Hbt-noUG z0Fv$D#t9Cc)UedRWd#Ir4`_&qHKVkL!xw$E<^`Vl#){dnjivNChYv-TPlYUdgJ#}a zrA$^ZD=3)>7XSqK%T1`Ws=rAiw_SF))iSkHc~lnFFLG*6%}!zRB-%x$aUpL_pA4vM z(7p8l;xx)#|ISw&V2GH~otiAiNpTPd4Gq%f6ebBC+JtG{<7LK>fcU0z_Cj1ghoY*M1NKeuP0fN_90M<7l@ z5%@~OsI4VD3Rjwp2F9c(p}Y~#r~yiq zO$~mrc&LGDhqws^5wz;99CFye$ZcLWb(5#X4pP;lO89=HxlVE87q!sNES^+JFw9`x zJkuQaP*(6f^tE>uynk5gXG-AH)pr*n0>^d#Z1Yyge~#nQwdD;Dk;&Mb|9M{Y>86sf zvp3|(Ieg{*4K~ylsYb8D0|j#d^0F@9-R-I^5pbbaae_aFHy$NIdFAZA zC!c4xXo>m8kXeF2E>+cCyYb|R9oQ_2K}x@!aB*(FlX8`&=i5FJ$2;0souVk7;Ed(t zXL_Rc==x)^(UBXjX^;prl+RW^Bz?E^KR!8!N|OKf$;G{3XmD7o)4LzF3`2JYpQSBC zzT1C|`*Ij5m^~;2T z+_`e~$IM^ej_;=O?j5V9nczp+SZ*sdkZb!{Fcp}gaz#qVoMMas%+x*P9$f=&BlEY4s>6fEcw%$o|vt#YULBawPYl* z95L+eAEg*5|H`LdJu2)2$@xl+?hx!HE>SpD?7`jSz0jTFuYVqB=p8N-wRS(=-8viX+=4O*iVwO=AqTSw;Abcd-q zjKcPH{a+EXPd|@-;UC(V@DFGVOy*VIfgTspr$(U)%rq8V}Z5f4Yq=+K9bJUQwy(3mj8Ih;hlH*<|ax4PFI`QQ^eQsq~ zCed2{;H|Rfecw@-=!U8MuGb>yy3#ky?%_~*kTG6?iqmT83(!qx>{jl)_g8?h=@j;H z&V8B}Sw}LZ?p_k9@8zOrkX;y23^6zPd0v7_;!yGxsS-%8tw6eAKlbg)`jPh3$e&AC z!rGa$pG&tN-~MX$msdWg-{tfT?09<Z}Ls9m9fi-R=1Mm+v2#! zPLky}JZi=Z1NR=wj*d$F73CXK^W2Wo&p4O6^R|xUO3e#|w8=#(t3GqgS&72ip3l}< zDkz`iEmTs@uyT%WADNx|mSjG#ioQe7j=p(%`_`X& z%Yd2~My&RE={~w8lpRvdSv0H+mI<2hQ5y}1`(2q(m`f0$7RqwUSjqZrea{NaV?_+S zXG;?ixBSHTAeG+btUnpNrR;%k)*)~TQQq2*`8U}k(>E>*hgGLN@IP9m-Kbp0*sp|= zoO&`(qX3NMB7-tg{{qH=^zpTLR8LoBH>*;jM%BpdbC#j65 pK@2x&CMbyLYo7FZ?(|z@QzJ0!?&S*Kf>cwZBPGtU-hh9X{}0p` protected EdgegapApiBase( ApiEnvironment apiEnvironment, string apiToken, - EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error + ) { this.SelectedApiEnvironment = apiEnvironment; this._httpClient.BaseAddress = new Uri($"{GetBaseUrl()}/"); this._httpClient.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue("application/json")); + new MediaTypeWithQualityHeaderValue("application/json") + ); string cleanedApiToken = apiToken.Replace("token ", ""); // We already prefixed token below - this._httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("token", cleanedApiToken); + this._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "token", + cleanedApiToken + ); this.LogLevel = logLevel; } - #region HTTP Requests /// /// POST | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. @@ -64,7 +67,10 @@ protected EdgegapApiBase( /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// - protected async Task PostAsync(string relativePath = "", string json = "{}") + protected async Task PostAsync( + string relativePath = "", + string json = "{}" + ) { StringContent stringContent = CreateStringContent(json); Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize POST uri: Can't end with `/`. @@ -92,7 +98,10 @@ protected async Task PostAsync(string relativePath = "", st /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// - protected async Task PatchAsync(string relativePath = "", string json = "{}") + protected async Task PatchAsync( + string relativePath = "", + string json = "{}" + ) { StringContent stringContent = CreateStringContent(json); Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize PATCH uri: Can't end with `/`. @@ -129,11 +138,12 @@ protected async Task PatchAsync(string relativePath = "", s /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// - protected async Task GetAsync(string relativePath = "", string customQuery = "") + protected async Task GetAsync( + string relativePath = "", + string customQuery = "" + ) { - string completeRelativeUri = prepareEdgegapUriWithQuery( - relativePath, - customQuery); + string completeRelativeUri = prepareEdgegapUriWithQuery(relativePath, customQuery); if (IsLogLevelDebug) Debug.Log($"GetAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`"); @@ -160,18 +170,23 @@ protected async Task GetAsync(string relativePath = "", str /// - Success => returns HttpResponseMessage result /// - Error => Catches errs => returns null (no rethrow) /// - protected async Task DeleteAsync(string relativePath = "", string customQuery = "") + protected async Task DeleteAsync( + string relativePath = "", + string customQuery = "" + ) { - string completeRelativeUri = prepareEdgegapUriWithQuery( - relativePath, - customQuery); + string completeRelativeUri = prepareEdgegapUriWithQuery(relativePath, customQuery); if (IsLogLevelDebug) - Debug.Log($"DeleteAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`"); + Debug.Log( + $"DeleteAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`" + ); try { - return await ExecuteRequestAsync(() => _httpClient.DeleteAsync(completeRelativeUri)); + return await ExecuteRequestAsync( + () => _httpClient.DeleteAsync(completeRelativeUri) + ); } catch (Exception e) { @@ -186,7 +201,8 @@ protected async Task DeleteAsync(string relativePath = "", /// private static async Task ExecuteRequestAsync( Func> requestFunc, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { HttpResponseMessage response = null; try @@ -215,16 +231,18 @@ private static async Task ExecuteRequestAsync( // Check for a successful status code if (response == null) { - Debug.Log("!Success (null response) - returning 500"); + Debug.Log("Error: (null response) - returning 500"); return CreateUnknown500Err(); } if (!response.IsSuccessStatusCode) { HttpMethod httpMethod = response.RequestMessage.Method; - Debug.Log($"!Success: {(short)response.StatusCode} {response.ReasonPhrase} - " + - $"{httpMethod} | {response.RequestMessage.RequestUri}` - " + - $"{response.Content?.ReadAsStringAsync().Result}"); + Debug.Log( + $"Error: {(short)response.StatusCode} {response.ReasonPhrase} - " + + $"{httpMethod} | {response.RequestMessage.RequestUri}` - " + + $"{response.Content?.ReadAsStringAsync().Result}" + ); } return response; diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs index 4e7eab14e8..b95153ea73 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs @@ -110,9 +110,49 @@ public async Task> CreateAppVersion(Cr return result; } + + /// + /// GET to v1/apps + /// - Get all applications. + /// - API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/applications-get + /// + /// + /// Http info with GetAppsResult data model + /// - Success: 200 + /// + public async Task> GetApps() + { + HttpResponseMessage response = await GetAsync($"v1/apps"); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + + /// + /// GET to v1/app/{appName}/versions + /// + /// + /// Http info with GetAppVersionsResult data model + /// - Success: 200 + /// + public async Task> GetAppVersions(string appName) + { + HttpResponseMessage response = await GetAsync($"v1/app/{appName}/versions"); + EdgegapHttpResult result = new EdgegapHttpResult(response); + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } #endregion // API Methods - - + + #region Chained API Methods /// /// PATCH and/or POST to v1/app/: Upsert an *existing* application version with new specifications. diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs index dda3ce24af..837eb4f983 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Net; using System.Net.Http; using System.Threading; @@ -11,18 +12,16 @@ namespace Edgegap.Editor.Api { /// /// Wraps the v1/[deploy | status | stop] API endpoints: Deployments Control API. - /// - API Doc | https://docs.edgegap.com/api/#tag/Deployments + /// - API Doc | https://docs.edgegap.com/api/#tag/Deployments /// public class EdgegapDeploymentsApi : EdgegapApiBase { public EdgegapDeploymentsApi( - ApiEnvironment apiEnvironment, - string apiToken, - EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) - : base(apiEnvironment, apiToken, logLevel) - { - } - + ApiEnvironment apiEnvironment, + string apiToken, + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error + ) + : base(apiEnvironment, apiToken, logLevel) { } #region API Methods /// @@ -35,18 +34,16 @@ public EdgegapDeploymentsApi( /// - Success: 200 /// public async Task> CreateDeploymentAsync( - CreateDeploymentRequest request) + CreateDeploymentRequest request + ) { HttpResponseMessage response = await PostAsync("v1/deploy", request.ToString()); - EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapHttpResult result = + new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 - bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 - if (!isSuccess) - return result; - return result; } - + /// /// GET v1/status/{requestId} /// - Retrieve the information for a deployment. @@ -59,18 +56,34 @@ public async Task> CreateDeploymentAsy /// Http info with GetDeploymentStatusResult data model /// - Success: 200 /// - public async Task> GetDeploymentStatusAsync(string requestId) + public async Task> GetDeploymentStatusAsync( + string requestId + ) { HttpResponseMessage response = await GetAsync($"v1/status/{requestId}"); - EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapHttpResult result = + new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 if (!isSuccess) return result; - + return result; } - + + public async Task> GetDeploymentsAsync() + { + HttpResponseMessage response = await GetAsync($"v1/deployments"); + EdgegapHttpResult result = + new EdgegapHttpResult(response); + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + /// /// DELETE v1/stop/{requestId} /// - Delete an instance of deployment. It will stop the running container and all its games. @@ -83,20 +96,23 @@ public async Task> GetDeploymentSta /// Http info with GetDeploymentStatusResult data model /// - Success: 200 /// - public async Task> StopActiveDeploymentAsync(string requestId) + public async Task> StopActiveDeploymentAsync( + string requestId + ) { HttpResponseMessage response = await DeleteAsync($"v1/stop/{requestId}"); - EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapHttpResult result = + new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 if (!isSuccess) return result; - + return result; } #endregion // API Methods - - + + #region Chained API Methods /// /// POST v1/deploy => GET v1/status/{requestId} @@ -109,16 +125,22 @@ public async Task> StopActiveDeplo /// - Success: 200 /// - Error: If createResult.HasErr, returns createResult /// - public async Task> CreateDeploymentAwaitReadyStatusAsync( - CreateDeploymentRequest request, TimeSpan pollInterval) + public async Task< + EdgegapHttpResult + > CreateDeploymentAwaitReadyStatusAsync( + CreateDeploymentRequest request, + TimeSpan pollInterval + ) { - EdgegapHttpResult createResponse = await CreateDeploymentAsync(request); + EdgegapHttpResult createResponse = await CreateDeploymentAsync( + request + ); // Create => bool isCreateSuccess = createResponse.StatusCode == HttpStatusCode.OK; // 200 if (!isCreateSuccess) return createResponse; - + // Await Status READY => string requestId = createResponse.Data.RequestId; _ = await AwaitReadyStatusAsync(requestId, pollInterval); @@ -126,21 +148,25 @@ public async Task> CreateDeploymentAwa // Return no matter what the result; no need to validate return createResponse; } - + /// If you recently deployed but want to await READY status. /// /// public async Task> AwaitReadyStatusAsync( - string requestId, - TimeSpan pollInterval) + string requestId, + TimeSpan pollInterval + ) { Assert.IsTrue(!string.IsNullOrEmpty(requestId)); // Validate - + EdgegapHttpResult statusResponse = null; - CancellationTokenSource cts = new CancellationTokenSource (TimeSpan.FromMinutes( // MIRROR CHANGE: 'new()' not supported in Unity 2020 - EdgegapWindowMetadata.DEPLOYMENT_AWAIT_READY_STATUS_TIMEOUT_MINS)); + CancellationTokenSource cts = new CancellationTokenSource( + TimeSpan.FromMinutes( // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapWindowMetadata.DEPLOYMENT_AWAIT_READY_STATUS_TIMEOUT_MINS + ) + ); bool isReady = false; - + while (!isReady && !cts.Token.IsCancellationRequested) { await Task.Delay(pollInterval, cts.Token); @@ -150,19 +176,22 @@ public async Task> AwaitReadyStatus return statusResponse; } - + /// If you recently stopped a deployment, but want to await TERMINATED (410) status. /// /// - public async Task> AwaitTerminatedDeleteStatusAsync( - string requestId, - TimeSpan pollInterval) + public async Task< + EdgegapHttpResult + > AwaitTerminatedDeleteStatusAsync(string requestId, TimeSpan pollInterval) { EdgegapHttpResult deleteResponse = null; - CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes( // MIRROR CHANGE: 'new()' not supported in Unity 2020 - EdgegapWindowMetadata.DEPLOYMENT_AWAIT_READY_STATUS_TIMEOUT_MINS)); + CancellationTokenSource cts = new CancellationTokenSource( + TimeSpan.FromMinutes( // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapWindowMetadata.DEPLOYMENT_AWAIT_READY_STATUS_TIMEOUT_MINS + ) + ); bool isStopped = false; - + while (!isStopped && !cts.Token.IsCancellationRequested) { await Task.Delay(pollInterval, cts.Token); diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs index 86e4b26a31..5f05e11087 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs @@ -7,18 +7,16 @@ namespace Edgegap.Editor.Api { /// /// Wraps the v1/ip API endpoint: "IP Lookup" API. - /// - API Doc | https://docs.edgegap.com/api/#tag/IP-Lookup + /// - API Doc | https://docs.edgegap.com/api/#tag/IP-Lookup /// public class EdgegapIpApi : EdgegapApiBase { public EdgegapIpApi( - ApiEnvironment apiEnvironment, - string apiToken, - EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) - : base(apiEnvironment, apiToken, logLevel) - { - } - + ApiEnvironment apiEnvironment, + string apiToken, + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error + ) + : base(apiEnvironment, apiToken, logLevel) { } #region API Methods /// @@ -34,12 +32,9 @@ public EdgegapIpApi( public async Task> GetYourPublicIp() { HttpResponseMessage response = await GetAsync("v1/ip"); - EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapHttpResult result = + new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 - bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 - if (!isSuccess) - return result; - return result; } #endregion // API Methods diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs index 968821cc93..eae55f7b39 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs @@ -12,14 +12,14 @@ public class CreateDeploymentRequest /// *Required: The name of the App you want to deploy. [JsonProperty("app_name")] public string AppName { get; set; } - + /// /// *Required: The name of the App Version you want to deploy; /// if not present, the last version created is picked. /// [JsonProperty("version_name")] public string VersionName { get; set; } - + /// /// *Required: The List of IP of your user. /// @@ -30,14 +30,17 @@ public class CreateDeploymentRequest /// *Required: The list of IP of your user with their location (latitude, longitude). /// [JsonProperty("geo_ip_list")] - public string[] GeoIpList { get; set; } = {}; + public string[] GeoIpList { get; set; } = { }; #endregion // Required - - + + /// + /// The list of tags assigned to the deployment + /// + [JsonProperty("tags")] + public string[] Tags { get; set; } = { EdgegapWindowMetadata.DEFAULT_DEPLOYMENT_TAG }; + /// Used by Newtonsoft - public CreateDeploymentRequest() - { - } + public CreateDeploymentRequest() { } /// Init with required info; used for a single external IP address. /// The name of the application. @@ -46,18 +49,14 @@ public CreateDeploymentRequest() /// the last version created is picked. /// /// Obtain from IpApi. - public CreateDeploymentRequest( - string appName, - string versionName, - string externalIp) + public CreateDeploymentRequest(string appName, string versionName, string externalIp) { this.AppName = appName; this.VersionName = versionName; this.IpList = new[] { externalIp }; } - + /// Parse to json str - public override string ToString() => - JsonConvert.SerializeObject(this); + public override string ToString() => JsonConvert.SerializeObject(this); } } diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs index 55b03f2c2f..6cdc37a31a 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs @@ -3,7 +3,7 @@ namespace Edgegap.Editor.Api.Models.Results { /// Edgegap error, generally just containing `message` - public class EdgegapErrorResult + public class EdgegapErrorResult { /// Friendly, UI-facing error message from Edgegap; can be lengthy. [JsonProperty("message")] diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs index 55b4892b85..54713a2a30 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs @@ -15,13 +15,13 @@ public class EdgegapHttpResult { /// HTTP Status code for the request. public HttpStatusCode StatusCode { get; } - + /// This could be err, success, or null. public string Json { get; } - + /// eg: "POST" public HttpMethod HttpMethod; - + /// /// Typically is sent by servers together with the status code. /// Useful for fallback err descriptions, often based on the status code. @@ -30,29 +30,32 @@ public class EdgegapHttpResult /// Contains `message` with friendly info. public bool HasErr => Error != null; - public EdgegapErrorResult Error { get; set; } - + public EdgegapErrorResult Error + { + get { return JsonConvert.DeserializeObject(Json); } + } + #region Common Shortcuts /// OK public bool IsResultCode200 => StatusCode == HttpStatusCode.OK; - + /// NoContent public bool IsResultCode204 => StatusCode == HttpStatusCode.NoContent; - + /// Forbidden public bool IsResultCode403 => StatusCode == HttpStatusCode.Forbidden; - + /// Conflict public bool IsResultCode409 => StatusCode == HttpStatusCode.Conflict; /// BadRequest public bool IsResultCode400 => StatusCode == HttpStatusCode.BadRequest; - + /// Gone public bool IsResultCode410 => StatusCode == HttpStatusCode.Gone; #endregion // Common Shortcuts - - + + /// /// Constructor that initializes the class based on an HttpResponseMessage. /// @@ -65,16 +68,13 @@ public EdgegapHttpResult(HttpResponseMessage httpResponse) { // TODO: This can be read async with `await`, but can't do this in a Constructor. // Instead, make a factory builder Task => - this.Json = httpResponse.Content.ReadAsStringAsync().Result; - - this.Error = JsonConvert.DeserializeObject(Json); - if (Error != null && string.IsNullOrEmpty(Error.ErrorMessage)) - Error = null; + Json = httpResponse.Content.ReadAsStringAsync().Result; } catch (Exception e) { - Debug.LogError("Error (reading httpResponse.Content): Client expected json, " + - $"but server returned !json: {e} - "); + Debug.LogError( + $"Couldn't parse error response. HTTP {httpResponse.StatusCode}.\n{e}" + ); } } } @@ -87,16 +87,16 @@ public class EdgegapHttpResult : EdgegapHttpResult { /// The actual result model from Json. Could be null! public TResult Data { get; set; } - - - public EdgegapHttpResult(HttpResponseMessage httpResponse, bool isLogLevelDebug = false) + + public EdgegapHttpResult(HttpResponseMessage httpResponse, bool isLogLevelDebug = false) : base(httpResponse) { this.HttpMethod = httpResponse.RequestMessage.Method; - + // Assuming JSON content and using Newtonsoft.Json for deserialization - bool isDeserializable = httpResponse.Content != null && - httpResponse.Content.Headers.ContentType.MediaType == "application/json"; + bool isDeserializable = + httpResponse.Content != null + && httpResponse.Content.Headers.ContentType.MediaType == "application/json"; if (isDeserializable) { @@ -106,7 +106,9 @@ public EdgegapHttpResult(HttpResponseMessage httpResponse, bool isLogLevelDebug } catch (Exception e) { - Debug.LogError($"Error (deserializing EdgegapHttpResult.Data): {e} - json: {Json}"); + Debug.LogError( + $"Error (deserializing EdgegapHttpResult.Data): {e} - json: {Json}" + ); throw; } } diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs new file mode 100644 index 0000000000..f82c6de250 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `[GET] v1/app/{app_name}/versions`. + /// GET API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/app-versions-get + /// + public class GetAppVersionsResult + { + [JsonProperty("versions")] + public List Versions { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs.meta new file mode 100644 index 0000000000..b29dd7a2b5 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppVersionsResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39dff8eb2a96db14581701965c2663c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs new file mode 100644 index 0000000000..462bce9774 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `[GET] v1/apps`. + /// GET API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/applications-get + /// + public class GetAppsResult + { + [JsonProperty("applications")] + public List Applications { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs.meta new file mode 100644 index 0000000000..bbf3927e6a --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetAppsResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88f306b7c80ebad4eada4c62e875d2a6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs new file mode 100644 index 0000000000..2a64bf96d2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model of a deployment for `GET v1/deployments`. + /// API Doc | https://docs.edgegap.com/api/#tag/Deployments/operation/deployments-get + /// + public class GetDeploymentResult + { + [JsonProperty("request_id")] + public string RequestId { get; set; } + + [JsonProperty("ready")] + public bool Ready { get; set; } + + [JsonProperty("tags")] + public string[] Tags { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs.meta new file mode 100644 index 0000000000..db84279a62 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87df84c48a738aa48b66d0c42aacb3db +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs new file mode 100644 index 0000000000..1028eff5ca --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `GET v1/deployments`. + /// API Doc | https://docs.edgegap.com/api/#tag/Deployments/operation/deployments-get + /// + public class GetDeploymentsResult + { + [JsonProperty("data")] + public GetDeploymentResult[] Data { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs.meta new file mode 100644 index 0000000000..12a913d0d7 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentsResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d39faf2ee20bb0647b63b9b72910d2c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs index 5d4d6a0b44..65ee1a14ff 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs @@ -6,84 +6,79 @@ public class StopActiveDeploymentResult { [JsonProperty("message")] public string Message { get; set; } - + [JsonProperty("deployment_summary")] public DeploymentSummaryData DeploymentSummary { get; set; } - public class DeploymentSummaryData { [JsonProperty("request_id")] public string RequestId { get; set; } - + [JsonProperty("fqdn")] public string Fqdn { get; set; } - + [JsonProperty("app_name")] public string AppName { get; set; } - + [JsonProperty("app_version")] public string AppVersion { get; set; } - + [JsonProperty("current_status")] public string CurrentStatus { get; set; } - + [JsonProperty("running")] public bool Running { get; set; } - + [JsonProperty("whitelisting_active")] public bool WhitelistingActive { get; set; } - + [JsonProperty("start_time")] public string StartTime { get; set; } - + [JsonProperty("removal_time")] public string RemovalTime { get; set; } - + [JsonProperty("elapsed_time")] - public int ElapsedTime { get; set; } - + public int? ElapsedTime { get; set; } + [JsonProperty("last_status")] public string LastStatus { get; set; } - + [JsonProperty("error")] public bool Error { get; set; } - + [JsonProperty("error_detail")] public string ErrorDetail { get; set; } - + [JsonProperty("ports")] public PortsData Ports { get; set; } - + [JsonProperty("public_ip")] public string PublicIp { get; set; } - + [JsonProperty("sessions")] public SessionData[] Sessions { get; set; } - + [JsonProperty("location")] public LocationData Location { get; set; } - + [JsonProperty("tags")] public string[] Tags { get; set; } - + [JsonProperty("sockets")] public string Sockets { get; set; } - + [JsonProperty("sockets_usage")] public string SocketsUsage { get; set; } - + [JsonProperty("command")] public string Command { get; set; } - + [JsonProperty("arguments")] public string Arguments { get; set; } - } - - public class PortsData - { - } + public class PortsData { } } } diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs new file mode 100644 index 0000000000..4a7a49bd02 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models +{ + public class VersionData + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("is_active")] + public bool IsActive { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs.meta new file mode 100644 index 0000000000..6cd9419121 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/VersionData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7dfe72e265bef484ea78bd7e105e0e77 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs b/Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs new file mode 100644 index 0000000000..cc89963514 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs @@ -0,0 +1,70 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Edgegap.Editor +{ + public class CustomPopupContent : PopupWindowContent + { + private Vector2 scrollPos; + private List _btnNames; + private Action _onBtnClick; + private string _defaultValue = ""; + + private float _minHeight = 25; + private float _maxHeight = 100; + private float _width; + + public CustomPopupContent( + List btnNames, + Action btnCallback, + string defaultValue, + float width = 400 + ) + { + _btnNames = btnNames; + _onBtnClick = btnCallback; + _width = width; + _defaultValue = defaultValue; + } + + public override Vector2 GetWindowSize() + { + float height = _minHeight; + + if (_btnNames.Count > 0) + { + height *= _btnNames.Count; + } + + return new Vector2(_width, height <= _maxHeight ? height : _maxHeight); + } + + public override void OnGUI(Rect rect) + { + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + + foreach (string name in _btnNames) + { + if (GUILayout.Button(name, GUILayout.Width(_width - 25))) + { + if (name == "Create New Application") + { + _onBtnClick(_defaultValue); + } + else + { + _onBtnClick(name); + } + + editorWindow.Close(); + } + } + + EditorGUILayout.EndScrollView(); + } + } +} +#endif diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs.meta new file mode 100644 index 0000000000..9d520111bf --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/CustomPopupContent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a68367cec4322b4d8c9ac1545bf430a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs index e5e9e9aa52..0ae77b8c11 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs @@ -1,4 +1,5 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR +using Edgegap.Editor; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -10,18 +11,20 @@ using System.Threading.Tasks; using UnityEditor; using UnityEditor.Build.Reporting; - +using UnityEngine; using Debug = UnityEngine.Debug; namespace Edgegap { internal static class EdgegapBuildUtils { + public static bool IsLogLevelDebug => + EdgegapWindowMetadata.LOG_LEVEL == EdgegapWindowMetadata.LogLevel.Debug; public static bool IsArmCPU() => RuntimeInformation.ProcessArchitecture == Architecture.Arm || RuntimeInformation.ProcessArchitecture == Architecture.Arm64; - public static BuildReport BuildServer() + public static BuildReport BuildServer(string folderName) { IEnumerable scenes = EditorBuildSettings.scenes .Where(s => s.enabled) @@ -37,13 +40,13 @@ public static BuildReport BuildServer() options = BuildOptions.EnableHeadlessMode, // obsolete and missing UNITY_SERVER define #endif // END MIRROR CHANGE - locationPathName = "Builds/EdgegapServer/ServerBuild" + locationPathName = $"Builds/{folderName}/ServerBuild" }; return BuildPipeline.BuildPlayer(options); } - public static async Task DockerSetupAndInstallationCheck(string path) + public static async Task DockerSetupAndInstallationCheck(string path) { if (!File.Exists(path)) { @@ -52,14 +55,58 @@ public static async Task DockerSetupAndInstallationCheck(string path) string output = null; string error = null; - await RunCommand_DockerVersion(msg => output = msg, msg => error = msg); // MIRROR CHANGE + await RunCommand_DockerVersion(msg => output = msg, + (msg) => + { + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) + { + error = msg; + } + }); + if (!string.IsNullOrEmpty(error)) { Debug.LogError(error); - return false; + return error; } + Debug.Log($"[Edgegap] Docker version detected: {output}"); // MIRROR CHANGE - return true; + + await RunCommand_DockerPS(null, + (msg) => + { + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) + { + error = msg; + } + }); + + if (!string.IsNullOrEmpty(error)) + { + Debug.LogError(error); + return error; + } + + return null; + } + + public static async Task InstallLinuxModules(string unityVersion, Action outputReciever = null, Action errorReciever = null) + { + await RunCommand_InstallLinuxRequirements("linux-mono", unityVersion, outputReciever); + await RunCommand_InstallLinuxRequirements("linux-il2cpp", unityVersion, outputReciever); + } + + static async Task RunCommand_DockerPS(Action outputReciever = null, Action errorReciever = null) + { +#if UNITY_EDITOR_WIN + await RunCommand("cmd.exe", "/c docker ps -q", outputReciever, errorReciever); +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", "-c \"docker ps -q\"", outputReciever, errorReciever); +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", "-c \"docker ps -q\"", outputReciever, errorReciever); +#else + Debug.LogError("The platform is not supported yet."); +#endif } // MIRROR CHANGE @@ -76,8 +123,116 @@ static async Task RunCommand_DockerVersion(Action outputReciever = null, #endif } - // MIRROR CHANGE - public static async Task RunCommand_DockerBuild(string dockerfilePath, string registry, string imageRepo, string tag, string projectPath, Action onStatusUpdate) + public static async Task RunCommand_DockerImage(Action outputReciever, Action errorReciever) + { +#if UNITY_EDITOR_WIN + await RunCommand("cmd.exe", "/c docker image ls --format \"{{.Repository}}:{{.Tag}}\"", outputReciever, + +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", "-c \"docker image ls --format \"{{.Repository}}:{{.Tag}}\"\"", outputReciever, +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", "-c \"docker image ls --format \"{{.Repository}}:{{.Tag}}\"\"", outputReciever, +#endif + (msg) => + { + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) + { + errorReciever(msg); + } + }); + } + + public static async Task RunCommand_DockerRun(string image, string extraParams) + { + // ARM -> x86 support: + string runCommand = IsArmCPU() ? "run --platform linux/amd64" : "run"; + +#if UNITY_EDITOR_WIN + await RunCommand("docker.exe", $"{runCommand} --name edgegap-server-test -d {extraParams} {image}", +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker {runCommand} --name edgegap-server-test -d {extraParams} {image}\"", +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker {runCommand} --name edgegap-server-test -d {extraParams} {image}\"", +#endif + null, + (msg) => + { + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) + { + throw new Exception(msg); + } + }); + } + + public static async Task RunCommand_DockerStop() + { + //Stopping running container +#if UNITY_EDITOR_WIN + await RunCommand("docker.exe", $"stop edgegap-server-test", +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker stop edgegap-server-test\"", +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker stop edgegap-server-test\"", +#endif + null, + (msg) => + { + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) + { + throw new Exception(msg); + } + }); + + //Deleting the stopped container +#if UNITY_EDITOR_WIN + await RunCommand("docker.exe", $"rm edgegap-server-test", +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker rm edgegap-server-test\"", +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker rm edgegap-server-test\"", +#endif + null, + (msg) => + { + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) + { + throw new Exception(msg); + } + }); + } + + static async Task RunCommand_InstallLinuxRequirements(string module, string unityVersion, Action outputReciever = null, Action errorReciever = null) + { + string error = null; +#if UNITY_EDITOR_WIN + await RunCommand("cmd.exe", + $"\"C:\\Program Files\\Unity Hub\\Unity Hub.exe\" -- --headless install-modules --version {unityVersion} -m {module}", + outputReciever, +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", + $"/Applications/Unity/Hub.app/Contents/MacOS/Unity/Hub -- --headless install-modules --version {unityVersion} -m linux-mono linux-il2cpp", + outputReciever, +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", + $"~/Applications/Unity/Hub.AppImage --headless install-modules --version {unityVersion} -m linux-mono linux-il2cpp", + outputReciever, +#endif + (msg) => + { + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) + { + error = msg; + } + outputReciever(msg); + }); + + if (error != null) + { + errorReciever(error); + } + } + + public static async Task RunCommand_DockerBuild(string dockerfilePath, string registry, string imageRepo, string tag, string projectPath, Action onStatusUpdate, string extraParams = null) { string realErrorMessage = null; @@ -88,6 +243,13 @@ public static async Task RunCommand_DockerBuild(string dockerfilePath, string re // would show an error in a linux .go file with 'not found'. string buildCommand = IsArmCPU() ? "buildx build --platform linux/amd64" : "build"; + if (!string.IsNullOrEmpty(extraParams)) + { + buildCommand += $" {extraParams}"; + } + + bool done = false; + #if UNITY_EDITOR_WIN await RunCommand("docker.exe", $"{buildCommand} -f \"{dockerfilePath}\" -t \"{registry}/{imageRepo}:{tag}\" \"{projectPath}\"", onStatusUpdate, #elif UNITY_EDITOR_OSX @@ -97,37 +259,42 @@ await RunCommand("/bin/bash", $"-c \"docker {buildCommand} -f {dockerfilePath} - #endif (msg) => { - if (msg.Contains("ERROR")) + if (msg.ToLowerInvariant().Contains("error") || msg.ToLowerInvariant().Contains("invalid")) { realErrorMessage = msg; } + if (msg.ToLowerInvariant().Contains("done")) + { + done = true; + } + Debug.Log(msg); onStatusUpdate(msg); }); - if(realErrorMessage != null) + if (realErrorMessage != null) { throw new Exception(realErrorMessage); } + else if (!done) + { + throw new Exception("Couldn't complete containerization, see console log for details."); + } } - public static async Task<(bool, string)> RunCommand_DockerPush(string registry, string imageRepo, string tag, Action onStatusUpdate) + public static async Task RunCommand_DockerPush(string registry, string imageRepo, string tag, Action onStatusUpdate) { - string error = string.Empty; + string error = null; #if UNITY_EDITOR_WIN - await RunCommand("docker.exe", $"push {registry}/{imageRepo}:{tag}", onStatusUpdate, (msg) => error += msg + "\n"); + await RunCommand("docker.exe", $"push {registry}/{imageRepo}:{tag}", onStatusUpdate, #elif UNITY_EDITOR_OSX - await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n"); + await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, #elif UNITY_EDITOR_LINUX - await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n"); + await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, #endif - if (!string.IsNullOrEmpty(error)) - { - Debug.LogError(error); - return (false, error); - } - return (true, null); + (msg) => error += msg + "\n"); + + return error ?? ""; } - // END MIRROR CHANGE static async Task RunCommand(string command, string arguments, Action outputReciever = null, Action errorReciever = null) { @@ -141,7 +308,6 @@ static async Task RunCommand(string command, string arguments, Action ou CreateNoWindow = true, }; - // MIRROR CHANGE #if !UNITY_EDITOR_WIN // on mac, commands like 'docker' aren't found because it's not in the application's PATH // even if it runs on mac's terminal. @@ -154,7 +320,6 @@ static async Task RunCommand(string command, string arguments, Action ou startInfo.EnvironmentVariables["PATH"] = customPath; // Debug.Log("PATH: " + customPath); #endif - // END MIRROR CHANGE Process proc = new Process() { StartInfo = startInfo, }; proc.EnableRaisingEvents = true; @@ -217,7 +382,7 @@ public static string IncrementTag(string tag) public static void UpdateEdgegapAppTag(string tag) { - // throw new NotImplementedException(); + // throw new NotImplementedException(); } /// Run a Docker cmd with streaming log response. TODO: Plugin to other Docker cmds @@ -240,13 +405,13 @@ static async Task RunCommand_DockerLogin( try { #if UNITY_EDITOR_WIN - await RunCommand("cmd.exe", $"/c docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"", outputReciever, errorReciever); + await RunCommand("cmd.exe", $"/c docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"", outputReciever, errorReciever); #elif UNITY_EDITOR_OSX - await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever); + await RunCommand("/bin/bash", $"-c \"docker login -u '{repoUsername}' --password '{repoPasswordToken}' '{registryUrl}'\"", outputReciever, errorReciever); #elif UNITY_EDITOR_LINUX - await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever); + await RunCommand("/bin/bash", $"-c \"docker login -u '{repoUsername}' --password '{repoPasswordToken}' '{registryUrl}'\"", outputReciever, errorReciever); #else - Debug.LogError("The platform is not supported yet."); + Debug.LogError("The platform is not supported yet."); #endif } catch (Exception e) @@ -275,10 +440,9 @@ public static async Task LoginContainerRegistry( { string error = null; await RunCommand_DockerLogin(registryUrl, repoUsername, repoPasswordToken, onStatusUpdate, msg => error = msg); // MIRROR CHANGE - if (error.Contains("ERROR")) + if (error.ToLowerInvariant().Contains("error") || error.ToLowerInvariant().Contains("invalid")) { - Debug.LogError(error); - return false; + throw new Exception(error); } return true; } diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uss b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uss index 6228fed92d..697fb45b08 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uss +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uss @@ -38,6 +38,14 @@ color: rgb(144, 190, 109); } +.text--link { + color: rgb(26, 142, 173); +} + +.text--link:hover { + color: rgb(23, 190, 235); +} + .container { padding: 4px 4px; } @@ -90,15 +98,26 @@ border-bottom-color: rgba(0, 0, 0, 0.35); height: 27px; -unity-font-style: bold; - background-color: rgb(36, 76, 87); min-width: 170px; max-width: 200px; } -.button-edgegap:hover { +.button-blue { + background-color: rgb(36, 76, 87); +} + +.button-blue:hover { background-color: rgb(56, 96, 107); } +.button-red { + background-color: rgb(135, 36, 23); +} + +.button-red:hover { + background-color: rgb(156, 58, 45); +} + .button-purple-hover:hover { background-color: rgb(44, 30, 210); } @@ -119,6 +138,7 @@ .text-edgegap { font-size: 11px; color: rgb(222, 222, 222); + overflow: hidden; /* MIRROR CHANGE: disable hardcoded font path -unity-font-definition: url('./Fonts/Spartan-Regular%20SDF.asset?fileID=11400000&guid=8b0fb2c68be09174f8ea5057b27a545c&type=2#Spartan-Regular SDF'); */ @@ -149,26 +169,32 @@ Toggle > #unity-checkmark { padding-bottom: 10px; } -.unity-text-field { - min-width: auto; - width: 400px; +.container-row { + background-color: rgb(37, 37, 37); + padding-top: 0; + padding-bottom: 0; + padding-left: 10px; padding-right: 5px; + margin-bottom: 3px; + justify-content: flex-start; + overflow: hidden; +} + +.unity-text-field { + min-width: 400px; + padding: 5px; white-space: normal; -unity-text-align: middle-left; opacity: 1; - padding-bottom: 5px; - padding-top: 5px; align-items: center; + align-content: stretch; + flex-grow: 10; + flex-shrink: 1; + overflow: hidden; } -.unity-text-field > Label { -} - -.container-row { - background-color: rgb(37, 37, 37); - padding-top: 0; - padding-bottom: 0; - margin-bottom: 3px; +#ApiTokenMaskedTxt { + flex-shrink: 0; } .checkmark-edgegap { @@ -179,6 +205,10 @@ Toggle > #unity-checkmark { margin-right: 15px; } +.unity-text-field__input { + text-overflow: clip; +} + .unity-text-field__input > TextElement { color: rgb(255, 255, 255); /* MIRROR CHANGE: disable hardcoded font path @@ -211,3 +241,58 @@ Toggle > #unity-checkmark { .button-purple-hover { } + +.label-right-padding Label { + min-width: 220px; +} + +.btn-text-link-blue { + background-color: rgba(0, 0, 0, 0); + border-top-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-left-width: 0px; + color: rgb(26, 142, 173); + height: 27px; + -unity-font-style: bold; + min-width: 170px; + max-width: 500px; +} + +.btn-text-link-blue:hover { + color: rgb(23, 190, 235); +} + +.btn-text-link { + background-color: rgba(0, 0, 0, 0); + border-top-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-left-width: 0px; + color: rgb(200, 200, 200); + height: 27px; + -unity-font-style: bold; + min-width: 170px; + max-width: 500px; + left: -5px; +} + +.btn-text-link:hover { + color: rgb(250, 250, 250); +} + +.btn-show-dropdown { + margin-left: 15px; + margin-right: 15px; + padding: 0; + background-color: rgba(0, 0, 0, 0); + color: rgb(200, 200, 200); + border-top-width: 0px; + border-right-width: 0px; + border-bottom-width: 0px; + border-left-width: 0px; +} + +.btn-show-dropdown:hover { + color: rgb(250, 250, 250); +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uxml b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uxml index f927c5481d..4df0318cf4 100755 --- a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uxml +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.uxml @@ -1,102 +1,163 @@ -