From 50b6b14293de9f343c398870c5f2f29a1b2db1da Mon Sep 17 00:00:00 2001 From: Antony Vitillo Date: Thu, 12 Feb 2026 17:02:32 +0800 Subject: [PATCH] Update to Unity 6 Update to Multiplayer Services Update to recent Mirror version Created new scene for the UTP integration, so it is not overridden by Mirror update Made players movable both from client and server --- .../Authenticators/BasicAuthenticator.cs | 11 +- .../Authenticators/BasicAuthenticator.cs.meta | 7 + .../Authenticators/DeviceAuthenticator.cs | 8 +- .../DeviceAuthenticator.cs.meta | 7 + .../Mirror.Authenticators.asmdef | 8 +- .../Mirror.Authenticators.asmdef.meta | 7 + .../TimeoutAuthenticator.cs.meta | 7 + .../Mirror.CompilerSymbols.asmdef.meta | 7 + .../CompilerSymbols/PreprocessorDefine.cs | 47 +- .../PreprocessorDefine.cs.meta | 9 +- Assets/Mirror/Components/AssemblyInfo.cs.meta | 7 + .../Components/Discovery/NetworkDiscovery.cs | 23 +- .../Discovery/NetworkDiscovery.cs.meta | 7 + .../Discovery/NetworkDiscoveryBase.cs | 94 +- .../Discovery/NetworkDiscoveryBase.cs.meta | 9 +- .../Discovery/NetworkDiscoveryHUD.cs | 18 +- .../Discovery/NetworkDiscoveryHUD.cs.meta | 7 + .../Discovery/ServerRequest.cs.meta | 9 +- .../Discovery/ServerResponse.cs.meta | 9 +- .../Experimental/NetworkLerpRigidbody.cs | 93 - .../Experimental/NetworkRigidbody.cs | 361 --- .../Experimental/NetworkRigidbody2D.cs | 360 -- Assets/Mirror/Components/GUIConsole.cs | 32 +- Assets/Mirror/Components/GUIConsole.cs.meta | 7 + .../InterestManagement/Distance.meta | 4 +- .../Distance/DistanceInterestManagement.cs | 25 +- .../DistanceInterestManagement.cs.meta | 7 + .../DistanceInterestManagementCustomRange.cs | 2 +- ...tanceInterestManagementCustomRange.cs.meta | 7 + .../Components/InterestManagement/Match.meta | 4 +- .../Match/MatchInterestManagement.cs | 176 +- .../Match/MatchInterestManagement.cs.meta | 7 + .../InterestManagement/Match/NetworkMatch.cs | 29 +- .../Match/NetworkMatch.cs.meta | 7 + .../Components/InterestManagement/Scene.meta | 4 +- .../Scene/SceneInterestManagement.cs | 23 +- .../Scene/SceneInterestManagement.cs.meta | 7 + .../SceneDistance.meta} | 2 +- .../SceneDistanceInterestManagement.cs | 178 + .../SceneDistanceInterestManagement.cs.meta | 18 + .../InterestManagement/SpatialHashing.meta | 4 +- .../SpatialHashing/Grid2D.cs | 42 +- .../SpatialHashing/Grid2D.cs.meta | 7 + .../SpatialHashing/Grid3D.cs | 106 + .../SpatialHashing/Grid3D.cs.meta | 10 + .../SpatialHashing/HexGrid2D.cs | 170 + .../SpatialHashing/HexGrid2D.cs.meta | 18 + .../SpatialHashing/HexGrid3D.cs | 243 ++ .../SpatialHashing/HexGrid3D.cs.meta | 18 + .../HexSpatialHash2DInterestManagement.cs | 345 ++ ...HexSpatialHash2DInterestManagement.cs.meta | 18 + .../HexSpatialHash3DInterestManagement.cs | 336 ++ ...HexSpatialHash3DInterestManagement.cs.meta | 18 + .../SpatialHashing3DInterestManagement.cs | 146 + ...SpatialHashing3DInterestManagement.cs.meta | 18 + .../SpatialHashingInterestManagement.cs | 22 +- .../SpatialHashingInterestManagement.cs.meta | 7 + .../InterestManagement/Team/NetworkTeam.cs | 26 +- .../Team/NetworkTeam.cs.meta | 7 + .../Team/TeamInterestManagement.cs | 185 +- .../Team/TeamInterestManagement.cs.meta | 7 + ...kTransform2k.meta => LagCompensation.meta} | 2 +- .../LagCompensation/HistoryCollider.cs | 109 + .../LagCompensation/HistoryCollider.cs.meta | 18 + .../LagCompensation/LagCompensator.cs | 197 ++ .../LagCompensation/LagCompensator.cs.meta | 18 + .../Components/Mirror.Components.asmdef | 10 +- .../Components/Mirror.Components.asmdef.meta | 7 + Assets/Mirror/Components/NetworkAnimator.cs | 160 +- .../Mirror/Components/NetworkAnimator.cs.meta | 7 + .../Components/NetworkDiagnosticsDebugger.cs | 31 + .../NetworkDiagnosticsDebugger.cs.meta | 18 + .../Components/NetworkLobbyManager.cs.meta | 7 + .../Components/NetworkLobbyPlayer.cs.meta | 7 + .../Mirror/Components/NetworkPingDisplay.cs | 14 +- .../Components/NetworkPingDisplay.cs.meta | 7 + .../Mirror/Components/NetworkRigidbody.meta | 3 + .../NetworkRigidbodyReliable.cs | 115 + .../NetworkRigidbodyReliable.cs.meta | 18 + .../NetworkRigidbodyReliable2D.cs | 135 + .../NetworkRigidbodyReliable2D.cs.meta | 18 + .../NetworkRigidbodyUnreliable.cs | 115 + .../NetworkRigidbodyUnreliable.cs.meta | 18 + .../NetworkRigidbodyUnreliable2D.cs | 136 + .../NetworkRigidbodyUnreliable2D.cs.meta | 18 + .../Mirror/Components/NetworkRoomManager.cs | 231 +- .../Components/NetworkRoomManager.cs.meta | 7 + Assets/Mirror/Components/NetworkRoomPlayer.cs | 8 +- .../Components/NetworkRoomPlayer.cs.meta | 7 + Assets/Mirror/Components/NetworkStatistics.cs | 52 +- .../Components/NetworkStatistics.cs.meta | 7 + .../Mirror/Components/NetworkTransform.meta | 3 + .../NetworkTransform/NetworkTransformBase.cs | 547 ++++ .../NetworkTransformBase.cs.meta | 9 +- .../NetworkTransformHybrid.cs | 717 ++++ .../NetworkTransformHybrid.cs.meta | 18 + .../NetworkTransformReliable.cs | 448 +++ .../NetworkTransformReliable.cs.meta | 18 + .../NetworkTransformUnreliable.cs | 462 +++ .../NetworkTransformUnreliable.cs.meta | 18 + .../TransformSnapshot.cs} | 20 +- .../TransformSnapshot.cs.meta} | 7 + .../NetworkTransform/TransformSyncData.cs | 156 + .../TransformSyncData.cs.meta | 18 + .../NetworkTransform2k/NetworkTransform.cs | 17 - .../NetworkTransformBase.cs | 776 ----- .../NetworkTransformChild.cs | 14 - .../NetworkTransformChild.cs.meta | 11 - .../Mirror/Components/PredictedRigidbody.meta | 3 + .../PredictedRigidbody/LocalGhostMaterial.mat | 85 + .../LocalGhostMaterial.mat.meta | 15 + .../PredictedRigidbody/PredictedRigidbody.cs | 1021 ++++++ .../PredictedRigidbody.cs.meta | 22 + .../PredictedRigidbodyPhysicsGhost.cs | 15 + .../PredictedRigidbodyPhysicsGhost.cs.meta | 18 + .../PredictedRigidbodyRemoteGhost.cs | 1 + .../PredictedRigidbodyRemoteGhost.cs.meta | 18 + .../PredictedRigidbody/PredictedSyncData.cs | 54 + .../PredictedSyncData.cs.meta | 10 + .../PredictedRigidbody/PredictionUtils.cs | 430 +++ .../PredictionUtils.cs.meta | 18 + .../RemoteGhostMaterial.mat | 85 + .../RemoteGhostMaterial.mat.meta | 15 + .../PredictedRigidbody/RigidbodyState.cs | 60 + .../PredictedRigidbody/RigidbodyState.cs.meta | 18 + Assets/Mirror/Components/Profiling.meta | 3 + .../Components/Profiling/BaseUIGraph.cs | 217 ++ .../Components/Profiling/BaseUIGraph.cs.meta | 21 + .../Components/Profiling/FpsMinMaxAvgGraph.cs | 40 + .../Profiling/FpsMinMaxAvgGraph.cs.meta | 18 + .../Mirror/Components/Profiling/LineGraph.mat | 89 + .../Components/Profiling/LineGraph.mat.meta | 15 + .../Profiling/NetworkBandwidthGraph.cs | 85 + .../Profiling/NetworkBandwidthGraph.cs.meta | 18 + .../Profiling/NetworkGraphLines.shader | 178 + .../Profiling/NetworkGraphLines.shader.meta | 10 + .../Profiling/NetworkGraphStacked.shader | 138 + .../Profiling/NetworkGraphStacked.shader.meta | 10 + .../Components/Profiling/NetworkPingGraph.cs | 34 + .../Profiling/NetworkPingGraph.cs.meta | 18 + .../Profiling/NetworkRuntimeProfiler.cs | 315 ++ .../Profiling/NetworkRuntimeProfiler.cs.meta | 18 + .../Profiling/Prefabs.meta} | 2 +- .../Profiling/Prefabs/BandwidthGraph.prefab | 1776 ++++++++++ .../Prefabs/BandwidthGraph.prefab.meta | 14 + .../Profiling/Prefabs/FPSMinMaxAvg.prefab | 1976 +++++++++++ .../Prefabs/FPSMinMaxAvg.prefab.meta | 14 + .../Profiling/Prefabs/GraphCanvas.prefab | 765 +++++ .../Profiling/Prefabs/GraphCanvas.prefab.meta | 14 + .../Profiling/Prefabs/NetworkGraph.prefab | 2888 +++++++++++++++++ .../Prefabs/NetworkGraph.prefab.meta | 14 + .../Profiling/Prefabs/PingGraph.prefab | 1776 ++++++++++ .../Profiling/Prefabs/PingGraph.prefab.meta | 14 + .../Components/Profiling/StackedGraph.mat | 88 + .../Profiling/StackedGraph.mat.meta | 15 + .../Components/Profiling/ToggleHotkey.cs | 15 + .../Components/Profiling/ToggleHotkey.cs.meta | 18 + Assets/Mirror/Components/RemoteStatistics.cs | 441 +++ .../Components/RemoteStatistics.cs.meta | 18 + Assets/Mirror/{Runtime.meta => Core.meta} | 0 .../Mirror/{Runtime => Core}/AssemblyInfo.cs | 1 + .../{Runtime => Core}/AssemblyInfo.cs.meta | 7 + Assets/Mirror/{Runtime => Core}/Attributes.cs | 32 +- .../{Runtime => Core}/Attributes.cs.meta | 7 + Assets/Mirror/{Runtime => Core}/Batching.meta | 0 .../{Runtime => Core}/Batching/Batcher.cs | 89 +- .../Batching/Batcher.cs.meta | 7 + .../{Runtime => Core}/Batching/Unbatcher.cs | 59 +- .../Batching/Unbatcher.cs.meta | 7 + Assets/Mirror/Core/ConnectionQuality.cs | 74 + Assets/Mirror/Core/ConnectionQuality.cs.meta | 18 + Assets/Mirror/Core/HostMode.cs | 44 + .../HostMode.cs.meta} | 9 +- Assets/Mirror/Core/InterestManagement.cs | 146 + .../InterestManagement.cs.meta | 7 + Assets/Mirror/Core/InterestManagementBase.cs | 103 + .../Core/InterestManagementBase.cs.meta | 18 + Assets/Mirror/Core/LagCompensation.meta | 3 + Assets/Mirror/Core/LagCompensation/Capture.cs | 13 + .../Core/LagCompensation/Capture.cs.meta | 18 + .../Core/LagCompensation/HistoryBounds.cs | 139 + .../LagCompensation/HistoryBounds.cs.meta | 18 + .../Core/LagCompensation/LagCompensation.cs | 144 + .../LagCompensation/LagCompensation.cs.meta | 18 + .../LagCompensationSettings.cs | 19 + .../LagCompensationSettings.cs.meta | 18 + .../Core/LagCompensation/MinMaxBounds.cs | 73 + .../Core/LagCompensation/MinMaxBounds.cs.meta | 18 + Assets/Mirror/Core/LocalConnectionToClient.cs | 80 + .../LocalConnectionToClient.cs.meta | 9 +- .../LocalConnectionToServer.cs | 25 +- .../LocalConnectionToServer.cs.meta | 7 + Assets/Mirror/Core/Messages.cs | 186 ++ .../Mirror/{Runtime => Core}/Messages.cs.meta | 7 + Assets/Mirror/{Runtime => Core}/Mirror.asmdef | 10 +- Assets/Mirror/Core/Mirror.asmdef.meta | 14 + .../{Runtime => Core}/NetworkAuthenticator.cs | 4 +- .../NetworkAuthenticator.cs.meta | 7 + .../{Runtime => Core}/NetworkBehaviour.cs | 550 +++- .../NetworkBehaviour.cs.meta | 7 + Assets/Mirror/Core/NetworkBehaviourHybrid.cs | 483 +++ .../Core/NetworkBehaviourHybrid.cs.meta | 18 + Assets/Mirror/Core/NetworkBehaviourSyncVar.cs | 33 + .../Core/NetworkBehaviourSyncVar.cs.meta | 18 + .../Mirror/{Runtime => Core}/NetworkClient.cs | 1013 ++++-- .../{Runtime => Core}/NetworkClient.cs.meta | 7 + .../Core/NetworkClient_TimeInterpolation.cs | 151 + .../NetworkClient_TimeInterpolation.cs.meta | 18 + .../{Runtime => Core}/NetworkConnection.cs | 110 +- .../NetworkConnection.cs.meta | 7 + .../Mirror/Core/NetworkConnectionToClient.cs | 230 ++ .../NetworkConnectionToClient.cs.meta | 7 + .../NetworkConnectionToServer.cs | 6 +- .../NetworkConnectionToServer.cs.meta | 7 + .../{Runtime => Core}/NetworkDiagnostics.cs | 0 .../NetworkDiagnostics.cs.meta | 7 + .../{Runtime => Core}/NetworkIdentity.cs | 1016 +++--- .../{Runtime => Core}/NetworkIdentity.cs.meta | 7 + .../Mirror/{Runtime => Core}/NetworkLoop.cs | 41 +- .../{Runtime => Core}/NetworkLoop.cs.meta | 7 + .../{Runtime => Core}/NetworkManager.cs | 542 ++-- .../{Runtime => Core}/NetworkManager.cs.meta | 7 + .../{Runtime => Core}/NetworkManagerHUD.cs | 103 +- .../NetworkManagerHUD.cs.meta | 7 + .../{Runtime => Core}/NetworkMessage.cs | 0 .../{Runtime => Core}/NetworkMessage.cs.meta | 7 + Assets/Mirror/Core/NetworkMessages.cs | 210 ++ .../NetworkMessages.cs.meta} | 7 + .../Mirror/{Runtime => Core}/NetworkReader.cs | 127 +- .../{Runtime => Core}/NetworkReader.cs.meta | 7 + .../NetworkReaderExtensions.cs | 316 +- .../NetworkReaderExtensions.cs.meta | 7 + .../{Runtime => Core}/NetworkReaderPool.cs | 15 +- .../NetworkReaderPool.cs.meta | 7 + Assets/Mirror/Core/NetworkReaderPooled.cs | 12 + .../NetworkReaderPooled.cs.meta | 7 + .../Mirror/{Runtime => Core}/NetworkServer.cs | 1801 ++++++---- .../{Runtime => Core}/NetworkServer.cs.meta | 7 + .../{Runtime => Core}/NetworkStartPosition.cs | 0 .../NetworkStartPosition.cs.meta | 7 + Assets/Mirror/Core/NetworkTime.cs | 243 ++ .../{Runtime => Core}/NetworkTime.cs.meta | 7 + .../Mirror/{Runtime => Core}/NetworkWriter.cs | 90 +- .../{Runtime => Core}/NetworkWriter.cs.meta | 7 + Assets/Mirror/Core/NetworkWriterExtensions.cs | 471 +++ .../NetworkWriterExtensions.cs.meta | 7 + .../{Runtime => Core}/NetworkWriterPool.cs | 11 +- .../NetworkWriterPool.cs.meta | 7 + Assets/Mirror/Core/NetworkWriterPooled.cs | 10 + .../NetworkWriterPooled.cs.meta | 7 + Assets/Mirror/Core/PortTransport.cs | 13 + .../PortTransport.cs.meta} | 9 +- Assets/Mirror/Core/Prediction.meta | 3 + Assets/Mirror/Core/Prediction/Prediction.cs | 195 ++ .../Mirror/Core/Prediction/Prediction.cs.meta | 18 + .../Mirror/{Runtime => Core}/RemoteCalls.cs | 39 +- .../{Runtime => Core}/RemoteCalls.cs.meta | 9 +- .../SnapshotInterpolation.meta | 0 .../Core/SnapshotInterpolation/Snapshot.cs | 17 + .../SnapshotInterpolation/Snapshot.cs.meta | 7 + .../SnapshotInterpolation.cs | 390 +++ .../SnapshotInterpolation.cs.meta | 7 + .../SnapshotInterpolationSettings.cs | 70 + .../SnapshotInterpolationSettings.cs.meta | 10 + .../SnapshotInterpolation/TimeSnapshot.cs | 15 + .../TimeSnapshot.cs.meta | 18 + .../{Runtime => Core}/SyncDictionary.cs | 248 +- .../{Runtime => Core}/SyncDictionary.cs.meta | 7 + Assets/Mirror/{Runtime => Core}/SyncList.cs | 151 +- .../Mirror/{Runtime => Core}/SyncList.cs.meta | 7 + Assets/Mirror/{Runtime => Core}/SyncObject.cs | 14 +- .../{Runtime => Core}/SyncObject.cs.meta | 7 + Assets/Mirror/{Runtime => Core}/SyncSet.cs | 179 +- .../Mirror/{Runtime => Core}/SyncSet.cs.meta | 7 + .../Logging.meta => Core/Threading.meta} | 2 +- .../Threading/ConcurrentNetworkWriterPool.cs | 45 + .../ConcurrentNetworkWriterPool.cs.meta | 18 + .../ConcurrentNetworkWriterPooled.cs | 10 + .../ConcurrentNetworkWriterPooled.cs.meta | 10 + .../Mirror/Core/Threading/ConcurrentPool.cs | 44 + .../Core/Threading/ConcurrentPool.cs.meta | 18 + Assets/Mirror/Core/Threading/ThreadLog.cs | 112 + .../Mirror/Core/Threading/ThreadLog.cs.meta | 18 + Assets/Mirror/Core/Threading/WorkerThread.cs | 169 + .../Core/Threading/WorkerThread.cs.meta | 18 + Assets/Mirror/Core/Tools.meta | 3 + Assets/Mirror/Core/Tools/AccurateInterval.cs | 86 + .../Core/Tools/AccurateInterval.cs.meta | 18 + .../{Runtime => Core/Tools}/Compression.cs | 315 +- .../Tools}/Compression.cs.meta | 7 + Assets/Mirror/Core/Tools/DeltaCompression.cs | 58 + .../Core/Tools/DeltaCompression.cs.meta | 18 + .../Core/Tools/ExponentialMovingAverage.cs | 53 + .../Tools}/ExponentialMovingAverage.cs.meta | 7 + Assets/Mirror/Core/Tools/Extensions.cs | 145 + .../Tools}/Extensions.cs.meta | 9 +- Assets/Mirror/Core/Tools/Half.cs | 773 +++++ Assets/Mirror/Core/Tools/Half.cs.meta | 10 + .../Mirror/{Runtime => Core/Tools}/Mathd.cs | 24 +- .../{Runtime => Core/Tools}/Mathd.cs.meta | 7 + Assets/Mirror/{Runtime => Core/Tools}/Pool.cs | 17 +- .../{Runtime => Core/Tools}/Pool.cs.meta | 7 + Assets/Mirror/Core/Tools/Readme.txt | 1 + Assets/Mirror/Core/Tools/Readme.txt.meta | 10 + Assets/Mirror/Core/Tools/TimeSample.cs | 61 + Assets/Mirror/Core/Tools/TimeSample.cs.meta | 18 + Assets/Mirror/Core/Tools/Utils.cs | 222 ++ .../{Runtime => Core/Tools}/Utils.cs.meta | 7 + Assets/Mirror/Core/Tools/Vector3Long.cs | 125 + Assets/Mirror/Core/Tools/Vector3Long.cs.meta | 18 + Assets/Mirror/Core/Tools/Vector4Long.cs | 126 + Assets/Mirror/Core/Tools/Vector4Long.cs.meta | 10 + Assets/Mirror/{Runtime => Core}/Transport.cs | 28 +- .../{Runtime => Core}/Transport.cs.meta | 7 + Assets/Mirror/Core/TransportError.cs | 17 + .../TransportError.cs.meta} | 9 +- Assets/Mirror/Core/WeaverFuse.cs | 22 + .../WeaverFuse.cs.meta} | 9 +- Assets/Mirror/Editor/AndroidManifestHelper.cs | 159 +- .../Editor/AndroidManifestHelper.cs.meta | 7 + Assets/Mirror/Editor/EditorHelper.cs | 10 + Assets/Mirror/Editor/EditorHelper.cs.meta | 7 + .../Empty/EnterPlayModeSettingsCheck.cs | 1 - .../Empty/EnterPlayModeSettingsCheck.cs.meta | 11 - Assets/Mirror/Editor/Empty/LogLevelWindow.cs | 1 - .../Editor/Empty/LogLevelWindow.cs.meta | 11 - .../Editor/Empty/Logging/LogLevelWindow.cs | 1 - .../Empty/Logging/LogLevelWindow.cs.meta | 11 - .../Editor/Empty/Logging/LogLevelsGUI.cs | 1 - .../Editor/Empty/Logging/LogLevelsGUI.cs.meta | 11 - .../Editor/Empty/Logging/LogSettingsEditor.cs | 1 - .../Empty/Logging/LogSettingsEditor.cs.meta | 11 - .../Empty/Logging/NetworkLogSettingsEditor.cs | 1 - .../Logging/NetworkLogSettingsEditor.cs.meta | 11 - .../Editor/Empty/ScriptableObjectUtility.cs | 1 - .../Empty/ScriptableObjectUtility.cs.meta | 11 - Assets/Mirror/Editor/Icon/MirrorIcon.png | 3 + Assets/Mirror/Editor/Icon/MirrorIcon.png.meta | 117 + Assets/Mirror/Editor/InspectorHelper.cs.meta | 7 + .../Mirror/Editor/LagCompensatorInspector.cs | 14 + .../Editor/LagCompensatorInspector.cs.meta | 18 + Assets/Mirror/Editor/Mirror.Editor.asmdef | 5 +- .../Mirror/Editor/Mirror.Editor.asmdef.meta | 7 + .../Editor/NetworkBehaviourInspector.cs | 13 +- .../Editor/NetworkBehaviourInspector.cs.meta | 7 + .../Editor/NetworkInformationPreview.cs | 8 +- .../Editor/NetworkInformationPreview.cs.meta | 7 + Assets/Mirror/Editor/NetworkManagerEditor.cs | 95 + .../Editor/NetworkManagerEditor.cs.meta | 7 + .../Mirror/Editor/NetworkScenePostProcess.cs | 41 +- .../Editor/NetworkScenePostProcess.cs.meta | 7 + Assets/Mirror/Editor/ReadOnlyDrawer.cs | 19 + Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta | 18 + Assets/Mirror/Editor/SceneDrawer.cs.meta | 7 + .../Editor/SyncObjectCollectionsDrawer.cs | 8 +- .../SyncObjectCollectionsDrawer.cs.meta | 11 +- .../Editor/SyncVarAttributeDrawer.cs.meta | 7 + Assets/Mirror/Editor/SyncVarDrawer.cs | 35 - Assets/Mirror/Editor/SyncVarDrawer.cs.meta | 3 - .../Mirror/Editor/Weaver/AssemblyInfo.cs.meta | 7 + Assets/Mirror/Editor/Weaver/Empty.meta | 8 - .../Weaver/Empty/GenericArgumentResolver.cs | 1 - .../Empty/GenericArgumentResolver.cs.meta | 11 - .../Weaver/Empty/MessageClassProcessor.cs | 1 - .../Empty/MessageClassProcessor.cs.meta | 11 - Assets/Mirror/Editor/Weaver/Empty/Program.cs | 1 - .../Editor/Weaver/Empty/Program.cs.meta | 11 - .../Weaver/Empty/SyncDictionaryProcessor.cs | 1 - .../Empty/SyncDictionaryProcessor.cs.meta | 11 - .../Editor/Weaver/Empty/SyncEventProcessor.cs | 1 - .../Weaver/Empty/SyncEventProcessor.cs.meta | 11 - .../Editor/Weaver/Empty/SyncListProcessor.cs | 1 - .../Weaver/Empty/SyncListProcessor.cs.meta | 11 - Assets/Mirror/Editor/Weaver/EntryPoint.meta | 4 +- .../EntryPoint/CompilationFinishedHook.cs | 13 +- .../CompilationFinishedHook.cs.meta | 7 + .../CompilationFinishedLogger.cs.meta | 11 +- .../EntryPoint/EnterPlayModeHook.cs.meta | 11 +- .../Weaver/EntryPointILPostProcessor.meta | 4 +- .../CompiledAssemblyFromFile.cs.meta | 11 +- .../ILPostProcessorAssemblyResolver.cs | 120 +- .../ILPostProcessorAssemblyResolver.cs.meta | 11 +- .../ILPostProcessorFromFile.cs.meta | 11 +- .../ILPostProcessorHook.cs.meta | 11 +- .../ILPostProcessorLogger.cs | 1 + .../ILPostProcessorLogger.cs.meta | 11 +- .../ILPostProcessorReflectionImporter.cs.meta | 11 +- ...rocessorReflectionImporterProvider.cs.meta | 11 +- Assets/Mirror/Editor/Weaver/Extensions.cs | 30 +- .../Mirror/Editor/Weaver/Extensions.cs.meta | 7 + Assets/Mirror/Editor/Weaver/Helpers.cs.meta | 7 + Assets/Mirror/Editor/Weaver/Logger.cs.meta | 7 + .../Weaver/Processors/CommandProcessor.cs | 16 +- .../Processors/CommandProcessor.cs.meta | 7 + .../Weaver/Processors/MethodProcessor.cs.meta | 7 + .../Processors/MonoBehaviourProcessor.cs.meta | 7 + .../Processors/NetworkBehaviourProcessor.cs | 125 +- .../NetworkBehaviourProcessor.cs.meta | 7 + .../Processors/ReaderWriterProcessor.cs | 46 +- .../Processors/ReaderWriterProcessor.cs.meta | 7 + .../Editor/Weaver/Processors/RpcProcessor.cs | 7 +- .../Weaver/Processors/RpcProcessor.cs.meta | 7 + .../ServerClientAttributeProcessor.cs | 13 +- .../ServerClientAttributeProcessor.cs.meta | 7 + .../Processors/SyncObjectInitializer.cs.meta | 7 + .../Weaver/Processors/SyncObjectProcessor.cs | 2 +- .../Processors/SyncObjectProcessor.cs.meta | 7 + .../SyncVarAttributeAccessReplacer.cs | 68 +- .../SyncVarAttributeAccessReplacer.cs.meta | 7 + .../Processors/SyncVarAttributeProcessor.cs | 88 +- .../SyncVarAttributeProcessor.cs.meta | 7 + .../Weaver/Processors/TargetRpcProcessor.cs | 46 +- .../Processors/TargetRpcProcessor.cs.meta | 7 + Assets/Mirror/Editor/Weaver/Readers.cs | 33 +- Assets/Mirror/Editor/Weaver/Readers.cs.meta | 7 + Assets/Mirror/Editor/Weaver/Resolvers.cs | 32 + Assets/Mirror/Editor/Weaver/Resolvers.cs.meta | 7 + .../Editor/Weaver/SyncVarAccessLists.cs.meta | 11 +- .../Weaver/TypeReferenceComparer.cs.meta | 7 + .../Editor/Weaver/Unity.Mirror.CodeGen.asmdef | 2 +- .../Weaver/Unity.Mirror.CodeGen.asmdef.meta | 7 + Assets/Mirror/Editor/Weaver/Weaver.cs | 34 +- Assets/Mirror/Editor/Weaver/Weaver.cs.meta | 7 + .../Editor/Weaver/WeaverExceptions.cs.meta | 7 + Assets/Mirror/Editor/Weaver/WeaverTypes.cs | 51 +- .../Mirror/Editor/Weaver/WeaverTypes.cs.meta | 11 +- Assets/Mirror/Editor/Weaver/Writers.cs | 33 +- Assets/Mirror/Editor/Weaver/Writers.cs.meta | 7 + Assets/Mirror/Editor/Welcome.cs | 23 + Assets/Mirror/Editor/Welcome.cs.meta | 18 + .../KCP/kcp2k/where-allocation => }/LICENSE | 3 +- Assets/Mirror/LICENSE.meta | 9 + .../Plugins/Mono.Cecil/License.txt.meta | 7 + .../Mono.Cecil/Mono.CecilX.Mdb.dll.meta | 7 + .../Mono.Cecil/Mono.CecilX.Pdb.dll.meta | 7 + .../Mono.Cecil/Mono.CecilX.Rocks.dll.meta | 7 + .../Plugins/Mono.Cecil/Mono.CecilX.dll.meta | 7 + Assets/Mirror/Presets.meta | 8 + .../Presets/Network Transform (Reliable).meta | 8 + .../ClientAuth-Balanced.preset | 123 + .../ClientAuth-Balanced.preset.meta | 15 + .../ClientAuth-Casual.preset | 123 + .../ClientAuth-Casual.preset.meta | 15 + .../ClientAuth-Responsive.preset | 123 + .../ClientAuth-Responsive.preset.meta | 15 + .../ServerAuth-Balanced.preset | 123 + .../ServerAuth-Balanced.preset.meta | 15 + .../Network Transform (Unreliable).meta | 8 + .../ClientAuth-Balanced.preset | 123 + .../ClientAuth-Balanced.preset.meta | 15 + .../ClientAuth-Casual.preset | 123 + .../ClientAuth-Casual.preset.meta | 15 + .../ClientAuth-Responsive.preset | 123 + .../ClientAuth-Responsive.preset.meta | 15 + .../ServerAuth-Balanced.preset | 123 + .../ServerAuth-Balanced.preset.meta | 15 + Assets/Mirror/Readme.txt.meta | 7 + Assets/Mirror/Runtime/Empty.meta | 3 - Assets/Mirror/Runtime/Empty/ClientScene.cs | 1 - .../Mirror/Runtime/Empty/ClientScene.cs.meta | 11 - Assets/Mirror/Runtime/Empty/Cloud.meta | 8 - .../Runtime/Empty/Cloud/ApiConnector.cs | 1 - .../Runtime/Empty/Cloud/ApiConnector.cs.meta | 11 - .../Mirror/Runtime/Empty/Cloud/ApiUpdater.cs | 1 - .../Runtime/Empty/Cloud/ApiUpdater.cs.meta | 11 - Assets/Mirror/Runtime/Empty/Cloud/Ball.cs | 1 - .../Mirror/Runtime/Empty/Cloud/Ball.cs.meta | 11 - .../Mirror/Runtime/Empty/Cloud/BallManager.cs | 1 - .../Runtime/Empty/Cloud/BallManager.cs.meta | 11 - Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs | 1 - .../Runtime/Empty/Cloud/BaseApi.cs.meta | 11 - Assets/Mirror/Runtime/Empty/Cloud/Events.cs | 1 - .../Mirror/Runtime/Empty/Cloud/Events.cs.meta | 11 - .../Mirror/Runtime/Empty/Cloud/Extensions.cs | 1 - .../Runtime/Empty/Cloud/Extensions.cs.meta | 11 - .../Runtime/Empty/Cloud/ICoroutineRunner.cs | 1 - .../Empty/Cloud/ICoroutineRunner.cs.meta | 11 - .../Runtime/Empty/Cloud/IRequestCreator.cs | 1 - .../Empty/Cloud/IRequestCreator.cs.meta | 11 - .../Runtime/Empty/Cloud/IUnityEqualCheck.cs | 1 - .../Empty/Cloud/IUnityEqualCheck.cs.meta | 11 - .../Empty/Cloud/InstantiateNetworkManager.cs | 1 - .../Cloud/InstantiateNetworkManager.cs.meta | 11 - .../Mirror/Runtime/Empty/Cloud/JsonStructs.cs | 1 - .../Runtime/Empty/Cloud/JsonStructs.cs.meta | 11 - .../Mirror/Runtime/Empty/Cloud/ListServer.cs | 1 - .../Runtime/Empty/Cloud/ListServer.cs.meta | 11 - .../Runtime/Empty/Cloud/ListServerBaseApi.cs | 1 - .../Empty/Cloud/ListServerBaseApi.cs.meta | 11 - .../Empty/Cloud/ListServerClientApi.cs | 1 - .../Empty/Cloud/ListServerClientApi.cs.meta | 11 - .../Runtime/Empty/Cloud/ListServerJson.cs | 1 - .../Empty/Cloud/ListServerJson.cs.meta | 11 - .../Empty/Cloud/ListServerServerApi.cs | 1 - .../Empty/Cloud/ListServerServerApi.cs.meta | 11 - Assets/Mirror/Runtime/Empty/Cloud/Logger.cs | 1 - .../Mirror/Runtime/Empty/Cloud/Logger.cs.meta | 11 - .../Empty/Cloud/NetworkManagerListServer.cs | 1 - .../Cloud/NetworkManagerListServer.cs.meta | 11 - .../Cloud/NetworkManagerListServerPong.cs | 1 - .../NetworkManagerListServerPong.cs.meta | 11 - Assets/Mirror/Runtime/Empty/Cloud/Player.cs | 1 - .../Mirror/Runtime/Empty/Cloud/Player.cs.meta | 11 - .../Empty/Cloud/QuickListServerDebug.cs | 1 - .../Empty/Cloud/QuickListServerDebug.cs.meta | 11 - .../Runtime/Empty/Cloud/QuitButtonHUD.cs | 1 - .../Runtime/Empty/Cloud/QuitButtonHUD.cs.meta | 11 - .../Runtime/Empty/Cloud/RequestCreator.cs | 1 - .../Empty/Cloud/RequestCreator.cs.meta | 11 - .../Runtime/Empty/Cloud/ServerListManager.cs | 1 - .../Empty/Cloud/ServerListManager.cs.meta | 11 - .../Runtime/Empty/Cloud/ServerListUI.cs | 1 - .../Runtime/Empty/Cloud/ServerListUI.cs.meta | 11 - .../Runtime/Empty/Cloud/ServerListUIItem.cs | 1 - .../Empty/Cloud/ServerListUIItem.cs.meta | 11 - .../Runtime/Empty/DotNetCompatibility.cs | 1 - .../Runtime/Empty/DotNetCompatibility.cs.meta | 11 - .../Mirror/Runtime/Empty/FallbackTransport.cs | 1 - .../Runtime/Empty/FallbackTransport.cs.meta | 11 - Assets/Mirror/Runtime/Empty/LogFactory.cs | 1 - .../Mirror/Runtime/Empty/LogFactory.cs.meta | 11 - Assets/Mirror/Runtime/Empty/LogFilter.cs | 1 - Assets/Mirror/Runtime/Empty/LogFilter.cs.meta | 11 - Assets/Mirror/Runtime/Empty/Logging.meta | 8 - .../Empty/Logging/ConsoleColorLogHandler.cs | 1 - .../Logging/ConsoleColorLogHandler.cs.meta | 11 - .../Empty/Logging/EditorLogSettingsLoader.cs | 1 - .../Logging/EditorLogSettingsLoader.cs.meta | 11 - .../Runtime/Empty/Logging/LogFactory.cs | 1 - .../Runtime/Empty/Logging/LogFactory.cs.meta | 11 - .../Runtime/Empty/Logging/LogSettings.cs | 2 - .../Runtime/Empty/Logging/LogSettings.cs.meta | 11 - .../Empty/Logging/NetworkHeadlessLogger.cs | 1 - .../Logging/NetworkHeadlessLogger.cs.meta | 11 - .../Empty/Logging/NetworkLogSettings.cs | 1 - .../Empty/Logging/NetworkLogSettings.cs.meta | 11 - .../Runtime/Empty/NetworkMatchChecker.cs | 1 - .../Runtime/Empty/NetworkMatchChecker.cs.meta | 11 - .../Runtime/Empty/NetworkOwnerChecker.cs | 1 - .../Runtime/Empty/NetworkOwnerChecker.cs.meta | 11 - .../Runtime/Empty/NetworkProximityChecker.cs | 1 - .../Empty/NetworkProximityChecker.cs.meta | 11 - .../Runtime/Empty/NetworkSceneChecker.cs | 1 - .../Runtime/Empty/NetworkSceneChecker.cs.meta | 11 - .../Mirror/Runtime/Empty/NetworkVisibility.cs | 1 - .../Runtime/Empty/NetworkVisibility.cs.meta | 11 - Assets/Mirror/Runtime/Empty/StringHash.cs | 1 - .../Mirror/Runtime/Empty/StringHash.cs.meta | 11 - .../Runtime/ExponentialMovingAverage.cs | 37 - Assets/Mirror/Runtime/Extensions.cs | 57 - Assets/Mirror/Runtime/InterestManagement.cs | 99 - .../Mirror/Runtime/LocalConnectionToClient.cs | 47 - Assets/Mirror/Runtime/MessagePacking.cs | 148 - Assets/Mirror/Runtime/Messages.cs | 116 - Assets/Mirror/Runtime/Mirror.asmdef.meta | 7 - .../Runtime/NetworkConnectionToClient.cs | 103 - Assets/Mirror/Runtime/NetworkReaderPooled.cs | 22 - Assets/Mirror/Runtime/NetworkTime.cs | 159 - .../Mirror/Runtime/NetworkWriterExtensions.cs | 372 --- Assets/Mirror/Runtime/NetworkWriterPooled.cs | 17 - .../Runtime/SnapshotInterpolation/Snapshot.cs | 20 - .../SnapshotInterpolation.cs | 325 -- Assets/Mirror/Runtime/SyncVar.cs | 148 - Assets/Mirror/Runtime/SyncVar.cs.meta | 11 - Assets/Mirror/Runtime/SyncVarGameObject.cs | 145 - .../Mirror/Runtime/SyncVarGameObject.cs.meta | 11 - .../Mirror/Runtime/SyncVarNetworkBehaviour.cs | 168 - .../Runtime/SyncVarNetworkBehaviour.cs.meta | 11 - .../Mirror/Runtime/SyncVarNetworkIdentity.cs | 118 - .../Runtime/SyncVarNetworkIdentity.cs.meta | 11 - .../Transports/KCP/MirrorTransport.meta | 8 - .../Transports/KCP/kcp2k/KCP.asmdef.meta | 7 - .../Runtime/Transports/KCP/kcp2k/VERSION.meta | 7 - .../KCP/kcp2k/highlevel/ErrorCode.cs.meta | 3 - .../KCP/kcp2k/highlevel/Extensions.cs | 33 - .../KCP/kcp2k/highlevel/Extensions.cs.meta | 3 - .../KCP/kcp2k/highlevel/KcpChannel.cs.meta | 3 - .../KCP/kcp2k/highlevel/KcpClient.cs | 148 - .../KCP/kcp2k/highlevel/KcpClient.cs.meta | 3 - .../kcp2k/highlevel/KcpClientConnection.cs | 156 - .../highlevel/KcpClientConnection.cs.meta | 3 - .../KCP/kcp2k/highlevel/KcpConnection.cs | 668 ---- .../KCP/kcp2k/highlevel/KcpConnection.cs.meta | 3 - .../KCP/kcp2k/highlevel/KcpHeader.cs | 19 - .../KCP/kcp2k/highlevel/KcpHeader.cs.meta | 3 - .../KCP/kcp2k/highlevel/KcpServer.cs | 375 --- .../KCP/kcp2k/highlevel/KcpServer.cs.meta | 3 - .../kcp2k/highlevel/KcpServerConnection.cs | 22 - .../highlevel/KcpServerConnection.cs.meta | 3 - .../KCP/kcp2k/highlevel/NonAlloc.meta | 3 - .../NonAlloc/KcpClientConnectionNonAlloc.cs | 24 - .../KcpClientConnectionNonAlloc.cs.meta | 3 - .../highlevel/NonAlloc/KcpClientNonAlloc.cs | 20 - .../NonAlloc/KcpClientNonAlloc.cs.meta | 3 - .../NonAlloc/KcpServerConnectionNonAlloc.cs | 25 - .../KcpServerConnectionNonAlloc.cs.meta | 3 - .../highlevel/NonAlloc/KcpServerNonAlloc.cs | 77 - .../NonAlloc/KcpServerNonAlloc.cs.meta | 3 - .../KCP/kcp2k/kcp/AssemblyInfo.cs.meta | 3 - .../KCP/kcp2k/where-allocation.meta | 8 - .../KCP/kcp2k/where-allocation/LICENSE.meta | 7 - .../KCP/kcp2k/where-allocation/Scripts.meta | 8 - .../where-allocation/Scripts/AssemblyInfo.cs | 3 - .../Scripts/AssemblyInfo.cs.meta | 3 - .../where-allocation/Scripts/Extensions.cs | 58 - .../Scripts/Extensions.cs.meta | 11 - .../Scripts/IPEndPointNonAlloc.cs | 208 -- .../Scripts/IPEndPointNonAlloc.cs.meta | 11 - .../Scripts/where-allocations.asmdef.meta | 7 - .../KCP/kcp2k/where-allocation/VERSION | 2 - .../KCP/kcp2k/where-allocation/VERSION.meta | 7 - .../Runtime/Transports/MultiplexTransport.cs | 306 -- .../SimpleWebTransport/CHANGELOG.md.meta | 7 - .../SimpleWebTransport/Common/Log.cs | 116 - .../SimpleWebTransport/LICENSE.meta | 7 - .../SimpleWebTransport/README.txt.meta | 7 - .../SimpleWebTransport.asmdef | 14 - .../SimpleWebTransport.asmdef.meta | 7 - .../Transports/Telepathy/Telepathy/Empty.meta | 8 - .../Telepathy/Telepathy/Empty/Logger.cs | 1 - .../Telepathy/Telepathy/Empty/Logger.cs.meta | 11 - .../Telepathy/Telepathy/Empty/Message.cs | 1 - .../Telepathy/Telepathy/Empty/Message.cs.meta | 11 - .../Telepathy/Telepathy/Empty/SafeQueue.cs | 1 - .../Telepathy/Empty/SafeQueue.cs.meta | 11 - .../Telepathy/Empty/ThreadExtensions.cs | 1 - .../Telepathy/Empty/ThreadExtensions.cs.meta | 11 - .../Telepathy/Telepathy/LICENSE.meta | 7 - .../Telepathy/Telepathy/Telepathy.asmdef.meta | 7 - .../Telepathy/Telepathy/VERSION.meta | 7 - Assets/Mirror/Runtime/Utils.cs | 121 - Assets/Mirror/ScriptTemplates.meta | 8 + ...ctions-NewNetworkManagerWithActions.cs.txt | 365 +++ ...s-NewNetworkManagerWithActions.cs.txt.meta | 14 + ...thenticator-NewNetworkAuthenticator.cs.txt | 106 + ...icator-NewNetworkAuthenticator.cs.txt.meta | 14 + ...ions-NewNetworkBehaviourWithActions.cs.txt | 136 + ...NewNetworkBehaviourWithActions.cs.txt.meta | 14 + ...Management-CustomInterestManagement.cs.txt | 94 + ...ement-CustomInterestManagement.cs.txt.meta | 14 + Assets/Mirror/ScriptTemplates/Editor.meta | 8 + .../Editor/MoveToAssetsFolder.cs | 42 + .../Editor/MoveToAssetsFolder.cs.meta | 18 + Assets/Mirror/{Runtime => }/Transports.meta | 2 +- Assets/Mirror/Transports/Edgegap.meta | 8 + .../Transports/Edgegap/EdgegapLobby.meta | 8 + .../EdgegapLobby/EdgegapLobbyKcpTransport.cs | 345 ++ .../EdgegapLobbyKcpTransport.cs.meta | 18 + .../Edgegap/EdgegapLobby/LobbyApi.cs | 295 ++ .../Edgegap/EdgegapLobby/LobbyApi.cs.meta | 18 + .../LobbyServiceCreateDialogue.cs | 138 + .../LobbyServiceCreateDialogue.cs.meta | 18 + .../EdgegapLobby/LobbyTransportInspector.cs | 64 + .../LobbyTransportInspector.cs.meta | 18 + .../Edgegap/EdgegapLobby/Models.meta | 3 + .../Models/ListLobbiesResponse.cs | 12 + .../Models/ListLobbiesResponse.cs.meta | 18 + .../Edgegap/EdgegapLobby/Models/Lobby.cs | 45 + .../Edgegap/EdgegapLobby/Models/Lobby.cs.meta | 18 + .../Edgegap/EdgegapLobby/Models/LobbyBrief.cs | 17 + .../EdgegapLobby/Models/LobbyBrief.cs.meta | 18 + .../EdgegapLobby/Models/LobbyCreateRequest.cs | 27 + .../Models/LobbyCreateRequest.cs.meta | 18 + .../EdgegapLobby/Models/LobbyIdRequest.cs | 14 + .../Models/LobbyIdRequest.cs.meta | 18 + .../Models/LobbyJoinOrLeaveRequest.cs | 17 + .../Models/LobbyJoinOrLeaveRequest.cs.meta | 18 + .../EdgegapLobby/Models/LobbyUpdateRequest.cs | 12 + .../Models/LobbyUpdateRequest.cs.meta | 18 + .../Transports/Edgegap/EdgegapRelay.meta | 8 + .../Edgegap/EdgegapRelay/EdgegapKcpClient.cs | 141 + .../EdgegapRelay/EdgegapKcpClient.cs.meta | 18 + .../Edgegap/EdgegapRelay/EdgegapKcpServer.cs | 203 ++ .../EdgegapRelay/EdgegapKcpServer.cs.meta | 18 + .../EdgegapRelay/EdgegapKcpTransport.cs | 162 + .../EdgegapRelay/EdgegapKcpTransport.cs.meta | 18 + .../Edgegap/EdgegapRelay/Protocol.cs | 29 + .../Edgegap/EdgegapRelay/Protocol.cs.meta | 18 + .../Transports/Edgegap/EdgegapRelay/README.md | 20 + .../Edgegap/EdgegapRelay/README.md.meta | 14 + .../EdgegapRelay/RelayCredentialsFromArgs.cs | 25 + .../RelayCredentialsFromArgs.cs.meta | 18 + Assets/Mirror/Transports/Edgegap/edgegap.png | 3 + .../Transports/Edgegap/edgegap.png.meta | 130 + Assets/Mirror/Transports/Encryption.meta | 3 + .../Mirror/Transports/Encryption/Editor.meta | 3 + .../Editor/EncryptionTransportEditor.asmdef | 18 + .../EncryptionTransportEditor.asmdef.meta | 14 + .../Editor/EncryptionTransportInspector.cs | 90 + .../EncryptionTransportInspector.cs.meta | 10 + .../Encryption/EncryptedConnection.cs | 554 ++++ .../Encryption/EncryptedConnection.cs.meta | 10 + .../Encryption/EncryptionCredentials.cs | 119 + .../Encryption/EncryptionCredentials.cs.meta | 10 + .../Encryption/EncryptionTransport.cs | 289 ++ .../Encryption/EncryptionTransport.cs.meta | 18 + .../Mirror/Transports/Encryption/Plugins.meta | 8 + .../Encryption/Plugins/BouncyCastle.meta | 8 + .../Plugins/BouncyCastle/LICENSE.md | 15 + .../Plugins/BouncyCastle/LICENSE.md.meta | 14 + .../Mirror.BouncyCastle.Cryptography.dll | Bin 0 -> 6856192 bytes .../Mirror.BouncyCastle.Cryptography.dll.meta | 40 + .../Transports/Encryption/PubKeyInfo.cs | 12 + .../Transports/Encryption/PubKeyInfo.cs.meta | 10 + .../ThreadedEncryptionKcpTransport.cs | 281 ++ .../ThreadedEncryptionKcpTransport.cs.meta | 18 + .../Mirror/{Runtime => }/Transports/KCP.meta | 2 +- .../KCP}/KcpTransport.cs | 208 +- .../KCP}/KcpTransport.cs.meta | 7 + .../Transports/KCP/ThreadedKcpTransport.cs | 327 ++ .../KCP/ThreadedKcpTransport.cs.meta | 18 + .../{Runtime => }/Transports/KCP/kcp2k.meta | 0 .../Transports/KCP/kcp2k/KCP.asmdef | 3 +- .../Transports/KCP/kcp2k/KCP.asmdef.meta | 14 + .../KCP/kcp2k/LICENSE.txt} | 0 .../Transports/KCP/kcp2k/LICENSE.txt.meta | 14 + .../KCP/kcp2k/VERSION.txt} | 125 + .../Transports/KCP/kcp2k/VERSION.txt.meta | 14 + Assets/Mirror/Transports/KCP/kcp2k/empty.meta | 3 + .../KCP/kcp2k/empty/KcpServerNonAlloc.cs | 1 + .../KCP/kcp2k/empty/KcpServerNonAlloc.cs.meta | 10 + .../Transports/KCP/kcp2k/highlevel.meta | 0 .../Transports/KCP/kcp2k/highlevel/Common.cs | 75 + .../KCP/kcp2k/highlevel/Common.cs.meta | 10 + .../KCP/kcp2k/highlevel/ErrorCode.cs | 0 .../KCP/kcp2k/highlevel/ErrorCode.cs.meta | 10 + .../KCP/kcp2k/highlevel/Extensions.cs | 166 + .../KCP/kcp2k/highlevel/Extensions.cs.meta | 10 + .../KCP/kcp2k/highlevel/KcpChannel.cs | 4 +- .../KCP/kcp2k/highlevel/KcpChannel.cs.meta | 10 + .../KCP/kcp2k/highlevel/KcpClient.cs | 292 ++ .../KCP/kcp2k/highlevel/KcpClient.cs.meta | 10 + .../KCP/kcp2k/highlevel/KcpConfig.cs | 97 + .../KCP/kcp2k/highlevel/KcpConfig.cs.meta | 10 + .../KCP/kcp2k/highlevel/KcpHeader.cs | 57 + .../KCP/kcp2k/highlevel/KcpHeader.cs.meta | 10 + .../Transports/KCP/kcp2k/highlevel/KcpPeer.cs | 791 +++++ .../KCP/kcp2k/highlevel/KcpPeer.cs.meta | 10 + .../KCP/kcp2k/highlevel/KcpServer.cs | 412 +++ .../KCP/kcp2k/highlevel/KcpServer.cs.meta | 10 + .../kcp2k/highlevel/KcpServerConnection.cs | 126 + .../highlevel/KcpServerConnection.cs.meta | 10 + .../KCP/kcp2k/highlevel/KcpState.cs | 4 + .../KCP/kcp2k/highlevel/KcpState.cs.meta | 18 + .../Transports/KCP/kcp2k/highlevel/Log.cs | 4 +- .../KCP/kcp2k/highlevel/Log.cs.meta | 7 + .../Transports/KCP/kcp2k/kcp.meta | 0 .../Transports/KCP/kcp2k/kcp/AckItem.cs | 8 + .../Transports/KCP/kcp2k/kcp/AckItem.cs.meta | 18 + .../Transports/KCP/kcp2k/kcp/AssemblyInfo.cs | 0 .../KCP/kcp2k/kcp/AssemblyInfo.cs.meta | 10 + .../Transports/KCP/kcp2k/kcp/Kcp.cs | 282 +- .../Transports/KCP/kcp2k/kcp/Kcp.cs.meta | 7 + .../Transports/KCP/kcp2k/kcp/Pool.cs | 0 .../Transports/KCP/kcp2k/kcp/Pool.cs.meta | 7 + .../Transports/KCP/kcp2k/kcp/Segment.cs | 33 +- .../Transports/KCP/kcp2k/kcp/Segment.cs.meta | 7 + .../Transports/KCP/kcp2k/kcp/Utils.cs | 32 +- .../Transports/KCP/kcp2k/kcp/Utils.cs.meta | 7 + Assets/Mirror/Transports/Latency.meta | 8 + .../Latency}/LatencySimulation.cs | 243 +- .../Latency}/LatencySimulation.cs.meta | 7 + Assets/Mirror/Transports/Middleware.meta | 8 + .../Middleware}/MiddlewareTransport.cs | 5 + .../Middleware}/MiddlewareTransport.cs.meta | 7 + .../Transports/Mirror.Transports.asmdef | 19 + .../Transports/Mirror.Transports.asmdef.meta | 14 + Assets/Mirror/Transports/Multiplex.meta | 8 + .../Multiplex/MultiplexTransport.cs | 458 +++ .../Multiplex}/MultiplexTransport.cs.meta | 7 + .../SimpleWeb.meta} | 0 .../SimpleWeb}/.cert.example.Json | 0 .../SimpleWeb/.cert.example.Json.meta | 9 + .../Mirror/Transports/SimpleWeb/Editor.meta | 8 + .../Editor/ClientWebsocketSettingsDrawer.cs | 71 + .../ClientWebsocketSettingsDrawer.cs.meta | 10 + .../Transports/SimpleWeb/SimpleWeb.meta | 8 + .../SimpleWeb/SimpleWeb}/AssemblyInfo.cs | 2 +- .../SimpleWeb/SimpleWeb}/AssemblyInfo.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/CHANGELOG.md | 0 .../SimpleWeb/SimpleWeb/CHANGELOG.md.meta | 14 + .../SimpleWeb/SimpleWeb}/Client.meta | 0 .../Client/ClientWebsocketSettings.cs | 17 + .../Client/ClientWebsocketSettings.cs.meta | 10 + .../SimpleWeb}/Client/SimpleWebClient.cs | 40 +- .../SimpleWeb}/Client/SimpleWebClient.cs.meta | 7 + .../SimpleWeb}/Client/StandAlone.meta | 0 .../Client/StandAlone/ClientHandshake.cs | 23 +- .../Client/StandAlone/ClientHandshake.cs.meta | 7 + .../Client/StandAlone/ClientSslHelper.cs | 4 +- .../Client/StandAlone/ClientSslHelper.cs.meta | 7 + .../StandAlone/WebSocketClientStandAlone.cs | 15 +- .../WebSocketClientStandAlone.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Client/Webgl.meta | 0 .../SimpleWeb}/Client/Webgl/SimpleWebJSLib.cs | 0 .../Client/Webgl/SimpleWebJSLib.cs.meta | 7 + .../Client/Webgl/WebSocketClientWebGl.cs | 54 +- .../Client/Webgl/WebSocketClientWebGl.cs.meta | 7 + .../SimpleWeb}/Client/Webgl/plugin.meta | 0 .../Client/Webgl/plugin/SimpleWeb.jslib | 73 +- .../Client/Webgl/plugin/SimpleWeb.jslib.meta | 7 + .../SimpleWeb/SimpleWeb}/Common.meta | 0 .../SimpleWeb/SimpleWeb}/Common/BufferPool.cs | 54 +- .../SimpleWeb}/Common/BufferPool.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Common/Connection.cs | 66 +- .../SimpleWeb}/Common/Connection.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Common/Constants.cs | 1 - .../SimpleWeb}/Common/Constants.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Common/EventType.cs | 0 .../SimpleWeb}/Common/EventType.cs.meta | 7 + .../SimpleWeb/SimpleWeb/Common/Log.cs | 270 ++ .../SimpleWeb/SimpleWeb}/Common/Log.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Common/Message.cs | 0 .../SimpleWeb}/Common/Message.cs.meta | 7 + .../SimpleWeb}/Common/MessageProcessor.cs | 28 +- .../Common/MessageProcessor.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Common/ReadHelper.cs | 19 +- .../SimpleWeb}/Common/ReadHelper.cs.meta | 7 + .../SimpleWeb}/Common/ReceiveLoop.cs | 50 +- .../SimpleWeb}/Common/ReceiveLoop.cs.meta | 7 + .../SimpleWeb/SimpleWeb/Common/Request.cs | 26 + .../SimpleWeb/Common/Request.cs.meta | 18 + .../SimpleWeb/SimpleWeb}/Common/SendLoop.cs | 20 +- .../SimpleWeb}/Common/SendLoop.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Common/TcpConfig.cs | 0 .../SimpleWeb}/Common/TcpConfig.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/Common/Utils.cs | 0 .../SimpleWeb/SimpleWeb}/Common/Utils.cs.meta | 7 + .../SimpleWeb/SimpleWeb}/LICENSE | 0 .../SimpleWeb/SimpleWeb/LICENSE.meta | 14 + .../SimpleWeb/SimpleWeb}/README.txt | 0 .../SimpleWeb/SimpleWeb/README.txt.meta | 14 + .../SimpleWeb/SimpleWeb}/Server.meta | 0 .../SimpleWeb}/Server/ServerHandshake.cs | 35 +- .../SimpleWeb}/Server/ServerHandshake.cs.meta | 7 + .../SimpleWeb}/Server/ServerSslHelper.cs | 12 +- .../SimpleWeb}/Server/ServerSslHelper.cs.meta | 7 + .../SimpleWeb}/Server/SimpleWebServer.cs | 38 +- .../SimpleWeb}/Server/SimpleWebServer.cs.meta | 7 + .../SimpleWeb}/Server/WebSocketServer.cs | 53 +- .../SimpleWeb}/Server/WebSocketServer.cs.meta | 7 + .../SimpleWeb/SimpleWebTransport.asmdef} | 3 +- .../SimpleWeb/SimpleWebTransport.asmdef.meta | 14 + .../SimpleWeb/SimpleWeb}/SslConfigLoader.cs | 7 +- .../SimpleWeb}/SslConfigLoader.cs.meta | 7 + .../SimpleWeb}/SimpleWebTransport.cs | 284 +- .../SimpleWeb}/SimpleWebTransport.cs.meta | 7 + .../{Runtime => }/Transports/Telepathy.meta | 0 .../Transports/Telepathy/Telepathy.meta | 0 .../Transports/Telepathy/Telepathy/Client.cs | 9 +- .../Telepathy/Telepathy/Client.cs.meta | 7 + .../Transports/Telepathy/Telepathy/Common.cs | 0 .../Telepathy/Telepathy/Common.cs.meta | 7 + .../Telepathy/Telepathy/ConnectionState.cs | 0 .../Telepathy/ConnectionState.cs.meta | 7 + .../Telepathy/Telepathy/EventType.cs | 0 .../Telepathy/Telepathy/EventType.cs.meta | 7 + .../Transports/Telepathy/Telepathy/LICENSE | 0 .../Telepathy/Telepathy/LICENSE.meta | 14 + .../Transports/Telepathy/Telepathy/Log.cs | 0 .../Telepathy/Telepathy/Log.cs.meta | 7 + .../Telepathy/MagnificentReceivePipe.cs | 0 .../Telepathy/MagnificentReceivePipe.cs.meta | 7 + .../Telepathy/MagnificentSendPipe.cs | 0 .../Telepathy/MagnificentSendPipe.cs.meta | 7 + .../Telepathy/NetworkStreamExtensions.cs | 0 .../Telepathy/NetworkStreamExtensions.cs.meta | 7 + .../Transports/Telepathy/Telepathy/Pool.cs | 0 .../Telepathy/Telepathy/Pool.cs.meta | 7 + .../Transports/Telepathy/Telepathy/Server.cs | 41 +- .../Telepathy/Telepathy/Server.cs.meta | 7 + .../Telepathy/Telepathy/Telepathy.asmdef | 0 .../Telepathy/Telepathy/Telepathy.asmdef.meta | 14 + .../Telepathy/Telepathy/ThreadFunctions.cs | 0 .../Telepathy/ThreadFunctions.cs.meta | 7 + .../Transports/Telepathy/Telepathy/Utils.cs | 0 .../Telepathy/Telepathy/Utils.cs.meta | 7 + .../Transports/Telepathy/Telepathy/VERSION | 3 + .../Telepathy/Telepathy/VERSION.meta | 14 + .../Telepathy/TelepathyTransport.cs | 77 +- .../Telepathy/TelepathyTransport.cs.meta | 7 + Assets/Mirror/Transports/Threaded.meta | 8 + .../Transports/Threaded/ThreadedTransport.cs | 756 +++++ .../Threaded/ThreadedTransport.cs.meta | 18 + Assets/Mirror/version.txt | 1 + Assets/Mirror/version.txt.meta | 9 + Assets/Prefabs/Player.prefab | 58 +- Assets/Resources.meta | 8 + Assets/Scenes/Main Mirror+Relay.unity | 457 +++ .../Main Mirror+Relay.unity.meta} | 2 +- Assets/Scenes/Main.unity | 296 +- Assets/Scenes/Main.unity.meta | 7 + ...__Network Manager-NewNetworkManager.cs.txt | 42 +- ...work Manager-NewNetworkManager.cs.txt.meta | 7 + ...twork Behaviour-NewNetworkBehaviour.cs.txt | 23 +- ... Behaviour-NewNetworkBehaviour.cs.txt.meta | 7 + ... Room Manager-NewNetworkRoomManager.cs.txt | 18 +- ... Manager-NewNetworkRoomManager.cs.txt.meta | 7 + ...om Player-NewNetworkRoomPlayer.cs.txt.meta | 7 + ...twork Discovery-NewNetworkDiscovery.cs.txt | 28 +- ... Discovery-NewNetworkDiscovery.cs.txt.meta | 7 + ...twork Transform-NewNetworkTransform.cs.txt | 114 +- ... Transform-NewNetworkTransform.cs.txt.meta | 7 + Assets/Scripts/MenuUI.cs | 2 +- Assets/Scripts/MyNetworkManager.cs | 8 +- Assets/Scripts/Player.cs | 1 + Assets/UTPTransport/Relay/RelayUtils.cs | 6 +- .../Relay/WrappedRelayServiceSDK.cs | 8 +- Assets/UTPTransport/Tests/UTPTests.asmdef | 25 +- Assets/UTPTransport/Utp/UtpClient.cs | 7 +- Assets/UTPTransport/Utp/UtpServer.cs | 7 +- Assets/UTPTransport/UtpTransport.asmdef | 4 +- Assets/UTPTransport/UtpTransport.cs | 50 +- Packages/manifest.json | 23 +- Packages/packages-lock.json | 219 +- ProjectSettings/EditorBuildSettings.asset | 5 +- ProjectSettings/MemorySettings.asset | 35 + ProjectSettings/MultiplayerManager.asset | 7 + .../com.unity.services.core/Settings.json | 4 + ProjectSettings/ProjectSettings.asset | 261 +- ProjectSettings/ProjectVersion.txt | 4 +- README.md | 9 +- 922 files changed, 43893 insertions(+), 11966 deletions(-) delete mode 100644 Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs delete mode 100644 Assets/Mirror/Components/Experimental/NetworkRigidbody.cs delete mode 100644 Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs rename Assets/Mirror/Components/{Experimental.meta => InterestManagement/SceneDistance.meta} (77%) create mode 100644 Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs create mode 100644 Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs.meta create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs.meta 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 create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs create mode 100644 Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta rename Assets/Mirror/Components/{NetworkTransform2k.meta => LagCompensation.meta} (77%) create mode 100644 Assets/Mirror/Components/LagCompensation/HistoryCollider.cs create mode 100644 Assets/Mirror/Components/LagCompensation/HistoryCollider.cs.meta create mode 100644 Assets/Mirror/Components/LagCompensation/LagCompensator.cs create mode 100644 Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta create mode 100644 Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs create mode 100644 Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta create mode 100644 Assets/Mirror/Components/NetworkRigidbody.meta create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs.meta create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs.meta create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs.meta create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs create mode 100644 Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs.meta create mode 100644 Assets/Mirror/Components/NetworkTransform.meta create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs rename Assets/Mirror/Components/{NetworkTransform2k => NetworkTransform}/NetworkTransformBase.cs.meta (51%) create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs.meta create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs.meta create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs create mode 100644 Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs.meta rename Assets/Mirror/Components/{NetworkTransform2k/NetworkTransformSnapshot.cs => NetworkTransform/TransformSnapshot.cs} (78%) rename Assets/Mirror/Components/{NetworkTransform2k/NetworkTransformSnapshot.cs.meta => NetworkTransform/TransformSnapshot.cs.meta} (59%) create mode 100644 Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs create mode 100644 Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta delete mode 100644 Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs delete mode 100644 Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs delete mode 100644 Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs delete mode 100644 Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat create mode 100644 Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs create mode 100644 Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat create mode 100644 Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta create mode 100644 Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs create mode 100644 Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs.meta create mode 100644 Assets/Mirror/Components/Profiling.meta create mode 100644 Assets/Mirror/Components/Profiling/BaseUIGraph.cs create mode 100644 Assets/Mirror/Components/Profiling/BaseUIGraph.cs.meta create mode 100644 Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs create mode 100644 Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs.meta create mode 100644 Assets/Mirror/Components/Profiling/LineGraph.mat create mode 100644 Assets/Mirror/Components/Profiling/LineGraph.mat.meta create mode 100644 Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs create mode 100644 Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs.meta create mode 100644 Assets/Mirror/Components/Profiling/NetworkGraphLines.shader create mode 100644 Assets/Mirror/Components/Profiling/NetworkGraphLines.shader.meta create mode 100644 Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader create mode 100644 Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader.meta create mode 100644 Assets/Mirror/Components/Profiling/NetworkPingGraph.cs create mode 100644 Assets/Mirror/Components/Profiling/NetworkPingGraph.cs.meta create mode 100644 Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs create mode 100644 Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs.meta rename Assets/Mirror/{Editor/Empty.meta => Components/Profiling/Prefabs.meta} (77%) create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab.meta create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab.meta create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab.meta create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab.meta create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab create mode 100644 Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab.meta create mode 100644 Assets/Mirror/Components/Profiling/StackedGraph.mat create mode 100644 Assets/Mirror/Components/Profiling/StackedGraph.mat.meta create mode 100644 Assets/Mirror/Components/Profiling/ToggleHotkey.cs create mode 100644 Assets/Mirror/Components/Profiling/ToggleHotkey.cs.meta create mode 100644 Assets/Mirror/Components/RemoteStatistics.cs create mode 100644 Assets/Mirror/Components/RemoteStatistics.cs.meta rename Assets/Mirror/{Runtime.meta => Core.meta} (100%) rename Assets/Mirror/{Runtime => Core}/AssemblyInfo.cs (92%) rename Assets/Mirror/{Runtime => Core}/AssemblyInfo.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/Attributes.cs (60%) rename Assets/Mirror/{Runtime => Core}/Attributes.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/Batching.meta (100%) rename Assets/Mirror/{Runtime => Core}/Batching/Batcher.cs (54%) rename Assets/Mirror/{Runtime => Core}/Batching/Batcher.cs.meta (62%) rename Assets/Mirror/{Runtime => Core}/Batching/Unbatcher.cs (72%) rename Assets/Mirror/{Runtime => Core}/Batching/Unbatcher.cs.meta (62%) create mode 100644 Assets/Mirror/Core/ConnectionQuality.cs create mode 100644 Assets/Mirror/Core/ConnectionQuality.cs.meta create mode 100644 Assets/Mirror/Core/HostMode.cs rename Assets/Mirror/{Components/Experimental/NetworkRigidbody.cs.meta => Core/HostMode.cs.meta} (55%) create mode 100644 Assets/Mirror/Core/InterestManagement.cs rename Assets/Mirror/{Runtime => Core}/InterestManagement.cs.meta (62%) create mode 100644 Assets/Mirror/Core/InterestManagementBase.cs create mode 100644 Assets/Mirror/Core/InterestManagementBase.cs.meta create mode 100644 Assets/Mirror/Core/LagCompensation.meta create mode 100644 Assets/Mirror/Core/LagCompensation/Capture.cs create mode 100644 Assets/Mirror/Core/LagCompensation/Capture.cs.meta create mode 100644 Assets/Mirror/Core/LagCompensation/HistoryBounds.cs create mode 100644 Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta create mode 100644 Assets/Mirror/Core/LagCompensation/LagCompensation.cs create mode 100644 Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta create mode 100644 Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs create mode 100644 Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs.meta create mode 100644 Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs create mode 100644 Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta create mode 100644 Assets/Mirror/Core/LocalConnectionToClient.cs rename Assets/Mirror/{Runtime => Core}/LocalConnectionToClient.cs.meta (53%) rename Assets/Mirror/{Runtime => Core}/LocalConnectionToServer.cs (82%) rename Assets/Mirror/{Runtime => Core}/LocalConnectionToServer.cs.meta (61%) create mode 100644 Assets/Mirror/Core/Messages.cs rename Assets/Mirror/{Runtime => Core}/Messages.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/Mirror.asmdef (59%) create mode 100644 Assets/Mirror/Core/Mirror.asmdef.meta rename Assets/Mirror/{Runtime => Core}/NetworkAuthenticator.cs (93%) rename Assets/Mirror/{Runtime => Core}/NetworkAuthenticator.cs.meta (62%) rename Assets/Mirror/{Runtime => Core}/NetworkBehaviour.cs (63%) rename Assets/Mirror/{Runtime => Core}/NetworkBehaviour.cs.meta (62%) create mode 100644 Assets/Mirror/Core/NetworkBehaviourHybrid.cs create mode 100644 Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta create mode 100644 Assets/Mirror/Core/NetworkBehaviourSyncVar.cs create mode 100644 Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta rename Assets/Mirror/{Runtime => Core}/NetworkClient.cs (62%) rename Assets/Mirror/{Runtime => Core}/NetworkClient.cs.meta (63%) create mode 100644 Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs create mode 100644 Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta rename Assets/Mirror/{Runtime => Core}/NetworkConnection.cs (68%) rename Assets/Mirror/{Runtime => Core}/NetworkConnection.cs.meta (62%) create mode 100644 Assets/Mirror/Core/NetworkConnectionToClient.cs rename Assets/Mirror/{Runtime => Core}/NetworkConnectionToClient.cs.meta (61%) rename Assets/Mirror/{Runtime => Core}/NetworkConnectionToServer.cs (81%) rename Assets/Mirror/{Runtime => Core}/NetworkConnectionToServer.cs.meta (61%) rename Assets/Mirror/{Runtime => Core}/NetworkDiagnostics.cs (100%) rename Assets/Mirror/{Runtime => Core}/NetworkDiagnostics.cs.meta (62%) rename Assets/Mirror/{Runtime => Core}/NetworkIdentity.cs (65%) rename Assets/Mirror/{Runtime => Core}/NetworkIdentity.cs.meta (62%) rename Assets/Mirror/{Runtime => Core}/NetworkLoop.cs (87%) rename Assets/Mirror/{Runtime => Core}/NetworkLoop.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/NetworkManager.cs (79%) rename Assets/Mirror/{Runtime => Core}/NetworkManager.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/NetworkManagerHUD.cs (60%) rename Assets/Mirror/{Runtime => Core}/NetworkManagerHUD.cs.meta (62%) rename Assets/Mirror/{Runtime => Core}/NetworkMessage.cs (100%) rename Assets/Mirror/{Runtime => Core}/NetworkMessage.cs.meta (63%) create mode 100644 Assets/Mirror/Core/NetworkMessages.cs rename Assets/Mirror/{Runtime/MessagePacking.cs.meta => Core/NetworkMessages.cs.meta} (62%) rename Assets/Mirror/{Runtime => Core}/NetworkReader.cs (59%) rename Assets/Mirror/{Runtime => Core}/NetworkReader.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/NetworkReaderExtensions.cs (51%) rename Assets/Mirror/{Runtime => Core}/NetworkReaderExtensions.cs.meta (61%) rename Assets/Mirror/{Runtime => Core}/NetworkReaderPool.cs (71%) rename Assets/Mirror/{Runtime => Core}/NetworkReaderPool.cs.meta (62%) create mode 100644 Assets/Mirror/Core/NetworkReaderPooled.cs rename Assets/Mirror/{Runtime => Core}/NetworkReaderPooled.cs.meta (62%) rename Assets/Mirror/{Runtime => Core}/NetworkServer.cs (58%) rename Assets/Mirror/{Runtime => Core}/NetworkServer.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/NetworkStartPosition.cs (100%) rename Assets/Mirror/{Runtime => Core}/NetworkStartPosition.cs.meta (62%) create mode 100644 Assets/Mirror/Core/NetworkTime.cs rename Assets/Mirror/{Runtime => Core}/NetworkTime.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/NetworkWriter.cs (64%) rename Assets/Mirror/{Runtime => Core}/NetworkWriter.cs.meta (63%) create mode 100644 Assets/Mirror/Core/NetworkWriterExtensions.cs rename Assets/Mirror/{Runtime => Core}/NetworkWriterExtensions.cs.meta (61%) rename Assets/Mirror/{Runtime => Core}/NetworkWriterPool.cs (77%) rename Assets/Mirror/{Runtime => Core}/NetworkWriterPool.cs.meta (62%) create mode 100644 Assets/Mirror/Core/NetworkWriterPooled.cs rename Assets/Mirror/{Runtime => Core}/NetworkWriterPooled.cs.meta (62%) create mode 100644 Assets/Mirror/Core/PortTransport.cs rename Assets/Mirror/{Components/Experimental/NetworkLerpRigidbody.cs.meta => Core/PortTransport.cs.meta} (54%) create mode 100644 Assets/Mirror/Core/Prediction.meta create mode 100644 Assets/Mirror/Core/Prediction/Prediction.cs create mode 100644 Assets/Mirror/Core/Prediction/Prediction.cs.meta rename Assets/Mirror/{Runtime => Core}/RemoteCalls.cs (75%) rename Assets/Mirror/{Runtime => Core}/RemoteCalls.cs.meta (55%) rename Assets/Mirror/{Runtime => Core}/SnapshotInterpolation.meta (100%) create mode 100644 Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs rename Assets/Mirror/{Runtime => Core}/SnapshotInterpolation/Snapshot.cs.meta (60%) create mode 100644 Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs rename Assets/Mirror/{Runtime => Core}/SnapshotInterpolation/SnapshotInterpolation.cs.meta (59%) create mode 100644 Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs create mode 100644 Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs.meta create mode 100644 Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs create mode 100644 Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta rename Assets/Mirror/{Runtime => Core}/SyncDictionary.cs (61%) rename Assets/Mirror/{Runtime => Core}/SyncDictionary.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/SyncList.cs (66%) rename Assets/Mirror/{Runtime => Core}/SyncList.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/SyncObject.cs (83%) rename Assets/Mirror/{Runtime => Core}/SyncObject.cs.meta (63%) rename Assets/Mirror/{Runtime => Core}/SyncSet.cs (64%) rename Assets/Mirror/{Runtime => Core}/SyncSet.cs.meta (64%) rename Assets/Mirror/{Editor/Empty/Logging.meta => Core/Threading.meta} (77%) create mode 100644 Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs create mode 100644 Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs.meta create mode 100644 Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs create mode 100644 Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs.meta create mode 100644 Assets/Mirror/Core/Threading/ConcurrentPool.cs create mode 100644 Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta create mode 100644 Assets/Mirror/Core/Threading/ThreadLog.cs create mode 100644 Assets/Mirror/Core/Threading/ThreadLog.cs.meta create mode 100644 Assets/Mirror/Core/Threading/WorkerThread.cs create mode 100644 Assets/Mirror/Core/Threading/WorkerThread.cs.meta create mode 100644 Assets/Mirror/Core/Tools.meta create mode 100644 Assets/Mirror/Core/Tools/AccurateInterval.cs create mode 100644 Assets/Mirror/Core/Tools/AccurateInterval.cs.meta rename Assets/Mirror/{Runtime => Core/Tools}/Compression.cs (59%) rename Assets/Mirror/{Runtime => Core/Tools}/Compression.cs.meta (62%) create mode 100644 Assets/Mirror/Core/Tools/DeltaCompression.cs create mode 100644 Assets/Mirror/Core/Tools/DeltaCompression.cs.meta create mode 100644 Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs rename Assets/Mirror/{Runtime => Core/Tools}/ExponentialMovingAverage.cs.meta (60%) create mode 100644 Assets/Mirror/Core/Tools/Extensions.cs rename Assets/Mirror/{Runtime => Core/Tools}/Extensions.cs.meta (58%) create mode 100644 Assets/Mirror/Core/Tools/Half.cs create mode 100644 Assets/Mirror/Core/Tools/Half.cs.meta rename Assets/Mirror/{Runtime => Core/Tools}/Mathd.cs (67%) rename Assets/Mirror/{Runtime => Core/Tools}/Mathd.cs.meta (63%) rename Assets/Mirror/{Runtime => Core/Tools}/Pool.cs (75%) rename Assets/Mirror/{Runtime => Core/Tools}/Pool.cs.meta (63%) create mode 100644 Assets/Mirror/Core/Tools/Readme.txt create mode 100644 Assets/Mirror/Core/Tools/Readme.txt.meta create mode 100644 Assets/Mirror/Core/Tools/TimeSample.cs create mode 100644 Assets/Mirror/Core/Tools/TimeSample.cs.meta create mode 100644 Assets/Mirror/Core/Tools/Utils.cs rename Assets/Mirror/{Runtime => Core/Tools}/Utils.cs.meta (63%) create mode 100644 Assets/Mirror/Core/Tools/Vector3Long.cs create mode 100644 Assets/Mirror/Core/Tools/Vector3Long.cs.meta create mode 100644 Assets/Mirror/Core/Tools/Vector4Long.cs create mode 100644 Assets/Mirror/Core/Tools/Vector4Long.cs.meta rename Assets/Mirror/{Runtime => Core}/Transport.cs (88%) rename Assets/Mirror/{Runtime => Core}/Transport.cs.meta (63%) create mode 100644 Assets/Mirror/Core/TransportError.cs rename Assets/Mirror/{Components/NetworkTransform2k/NetworkTransform.cs.meta => Core/TransportError.cs.meta} (54%) create mode 100644 Assets/Mirror/Core/WeaverFuse.cs rename Assets/Mirror/{Components/Experimental/NetworkRigidbody2D.cs.meta => Core/WeaverFuse.cs.meta} (55%) delete mode 100644 Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs delete mode 100644 Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta delete mode 100644 Assets/Mirror/Editor/Empty/LogLevelWindow.cs delete mode 100644 Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta delete mode 100644 Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs delete mode 100644 Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta delete mode 100644 Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs delete mode 100644 Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta delete mode 100644 Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs delete mode 100644 Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta delete mode 100644 Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs delete mode 100644 Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta delete mode 100644 Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs delete mode 100644 Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta create mode 100644 Assets/Mirror/Editor/Icon/MirrorIcon.png create mode 100644 Assets/Mirror/Editor/Icon/MirrorIcon.png.meta create mode 100644 Assets/Mirror/Editor/LagCompensatorInspector.cs create mode 100644 Assets/Mirror/Editor/LagCompensatorInspector.cs.meta create mode 100644 Assets/Mirror/Editor/ReadOnlyDrawer.cs create mode 100644 Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta delete mode 100644 Assets/Mirror/Editor/SyncVarDrawer.cs delete mode 100644 Assets/Mirror/Editor/SyncVarDrawer.cs.meta delete mode 100644 Assets/Mirror/Editor/Weaver/Empty.meta delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/Program.cs delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs delete mode 100644 Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta create mode 100644 Assets/Mirror/Editor/Welcome.cs create mode 100644 Assets/Mirror/Editor/Welcome.cs.meta rename Assets/Mirror/{Runtime/Transports/KCP/kcp2k/where-allocation => }/LICENSE (90%) create mode 100644 Assets/Mirror/LICENSE.meta create mode 100644 Assets/Mirror/Presets.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable).meta create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable).meta create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset.meta create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset create mode 100644 Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset.meta delete mode 100644 Assets/Mirror/Runtime/Empty.meta delete mode 100644 Assets/Mirror/Runtime/Empty/ClientScene.cs delete mode 100644 Assets/Mirror/Runtime/Empty/ClientScene.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Ball.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Events.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Logger.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Player.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs delete mode 100644 Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/FallbackTransport.cs delete mode 100644 Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/LogFactory.cs delete mode 100644 Assets/Mirror/Runtime/Empty/LogFactory.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/LogFilter.cs delete mode 100644 Assets/Mirror/Runtime/Empty/LogFilter.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Logging.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs delete mode 100644 Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkVisibility.cs delete mode 100644 Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta delete mode 100644 Assets/Mirror/Runtime/Empty/StringHash.cs delete mode 100644 Assets/Mirror/Runtime/Empty/StringHash.cs.meta delete mode 100644 Assets/Mirror/Runtime/ExponentialMovingAverage.cs delete mode 100644 Assets/Mirror/Runtime/Extensions.cs delete mode 100644 Assets/Mirror/Runtime/InterestManagement.cs delete mode 100644 Assets/Mirror/Runtime/LocalConnectionToClient.cs delete mode 100644 Assets/Mirror/Runtime/MessagePacking.cs delete mode 100644 Assets/Mirror/Runtime/Messages.cs delete mode 100644 Assets/Mirror/Runtime/Mirror.asmdef.meta delete mode 100644 Assets/Mirror/Runtime/NetworkConnectionToClient.cs delete mode 100644 Assets/Mirror/Runtime/NetworkReaderPooled.cs delete mode 100644 Assets/Mirror/Runtime/NetworkTime.cs delete mode 100644 Assets/Mirror/Runtime/NetworkWriterExtensions.cs delete mode 100644 Assets/Mirror/Runtime/NetworkWriterPooled.cs delete mode 100644 Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs delete mode 100644 Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs delete mode 100644 Assets/Mirror/Runtime/SyncVar.cs delete mode 100644 Assets/Mirror/Runtime/SyncVar.cs.meta delete mode 100644 Assets/Mirror/Runtime/SyncVarGameObject.cs delete mode 100644 Assets/Mirror/Runtime/SyncVarGameObject.cs.meta delete mode 100644 Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs delete mode 100644 Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta delete mode 100644 Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs delete mode 100644 Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION delete mode 100644 Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta delete mode 100644 Assets/Mirror/Runtime/Transports/MultiplexTransport.cs delete mode 100644 Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta delete mode 100644 Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs delete mode 100644 Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta delete mode 100644 Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta delete mode 100644 Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef delete mode 100644 Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta delete mode 100644 Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta delete mode 100644 Assets/Mirror/Runtime/Utils.cs create mode 100644 Assets/Mirror/ScriptTemplates.meta create mode 100644 Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt create mode 100644 Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt.meta create mode 100644 Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt create mode 100644 Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta create mode 100644 Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt create mode 100644 Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt.meta create mode 100644 Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt create mode 100644 Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta create mode 100644 Assets/Mirror/ScriptTemplates/Editor.meta create mode 100644 Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs create mode 100644 Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs.meta rename Assets/Mirror/{Runtime => }/Transports.meta (77%) create mode 100644 Assets/Mirror/Transports/Edgegap.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs create mode 100644 Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs.meta create mode 100644 Assets/Mirror/Transports/Edgegap/edgegap.png create mode 100644 Assets/Mirror/Transports/Edgegap/edgegap.png.meta create mode 100644 Assets/Mirror/Transports/Encryption.meta create mode 100644 Assets/Mirror/Transports/Encryption/Editor.meta create mode 100644 Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef create mode 100644 Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta create mode 100644 Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs create mode 100644 Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta create mode 100644 Assets/Mirror/Transports/Encryption/EncryptedConnection.cs create mode 100644 Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta create mode 100644 Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs create mode 100644 Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta create mode 100644 Assets/Mirror/Transports/Encryption/EncryptionTransport.cs create mode 100644 Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta create mode 100644 Assets/Mirror/Transports/Encryption/Plugins.meta create mode 100644 Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle.meta create mode 100644 Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md create mode 100644 Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md.meta create mode 100644 Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll create mode 100644 Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll.meta create mode 100644 Assets/Mirror/Transports/Encryption/PubKeyInfo.cs create mode 100644 Assets/Mirror/Transports/Encryption/PubKeyInfo.cs.meta create mode 100644 Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs create mode 100644 Assets/Mirror/Transports/Encryption/ThreadedEncryptionKcpTransport.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP.meta (77%) rename Assets/Mirror/{Runtime/Transports/KCP/MirrorTransport => Transports/KCP}/KcpTransport.cs (65%) rename Assets/Mirror/{Runtime/Transports/KCP/MirrorTransport => Transports/KCP}/KcpTransport.cs.meta (62%) create mode 100644 Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs create mode 100644 Assets/Mirror/Transports/KCP/ThreadedKcpTransport.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k.meta (100%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/KCP.asmdef (81%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/KCP.asmdef.meta rename Assets/Mirror/{Runtime/Transports/KCP/kcp2k/LICENSE => Transports/KCP/kcp2k/LICENSE.txt} (100%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/LICENSE.txt.meta rename Assets/Mirror/{Runtime/Transports/KCP/kcp2k/VERSION => Transports/KCP/kcp2k/VERSION.txt} (52%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/VERSION.txt.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/empty.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/empty/KcpServerNonAlloc.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/highlevel.meta (100%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/highlevel/ErrorCode.cs (100%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/highlevel/KcpChannel.cs (78%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/highlevel/Log.cs (69%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/highlevel/Log.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp.meta (100%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/kcp/AckItem.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs (100%) create mode 100644 Assets/Mirror/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Kcp.cs (79%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Kcp.cs.meta (57%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Pool.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Pool.cs.meta (57%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Segment.cs (63%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Segment.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Utils.cs (66%) rename Assets/Mirror/{Runtime => }/Transports/KCP/kcp2k/kcp/Utils.cs.meta (57%) create mode 100644 Assets/Mirror/Transports/Latency.meta rename Assets/Mirror/{Runtime/Transports => Transports/Latency}/LatencySimulation.cs (50%) rename Assets/Mirror/{Runtime/Transports => Transports/Latency}/LatencySimulation.cs.meta (60%) create mode 100644 Assets/Mirror/Transports/Middleware.meta rename Assets/Mirror/{Runtime/Transports => Transports/Middleware}/MiddlewareTransport.cs (88%) rename Assets/Mirror/{Runtime/Transports => Transports/Middleware}/MiddlewareTransport.cs.meta (60%) create mode 100644 Assets/Mirror/Transports/Mirror.Transports.asmdef create mode 100644 Assets/Mirror/Transports/Mirror.Transports.asmdef.meta create mode 100644 Assets/Mirror/Transports/Multiplex.meta create mode 100644 Assets/Mirror/Transports/Multiplex/MultiplexTransport.cs rename Assets/Mirror/{Runtime/Transports => Transports/Multiplex}/MultiplexTransport.cs.meta (60%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport.meta => Transports/SimpleWeb.meta} (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb}/.cert.example.Json (100%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/.cert.example.Json.meta create mode 100644 Assets/Mirror/Transports/SimpleWeb/Editor.meta create mode 100644 Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs create mode 100644 Assets/Mirror/Transports/SimpleWeb/Editor/ClientWebsocketSettingsDrawer.cs.meta create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb.meta rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/AssemblyInfo.cs (84%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/AssemblyInfo.cs.meta (55%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/CHANGELOG.md (100%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/CHANGELOG.md.meta rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client.meta (100%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Client/ClientWebsocketSettings.cs.meta rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/SimpleWebClient.cs (95%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/SimpleWebClient.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/StandAlone.meta (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/StandAlone/ClientHandshake.cs (70%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/StandAlone/ClientHandshake.cs.meta (52%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/StandAlone/ClientSslHelper.cs (87%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/StandAlone/ClientSslHelper.cs.meta (52%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/StandAlone/WebSocketClientStandAlone.cs (88%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/StandAlone/WebSocketClientStandAlone.cs.meta (51%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl.meta (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl/SimpleWebJSLib.cs (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl/SimpleWebJSLib.cs.meta (53%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl/WebSocketClientWebGl.cs (83%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl/WebSocketClientWebGl.cs.meta (52%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl/plugin.meta (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl/plugin/SimpleWeb.jslib (75%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Client/Webgl/plugin/SimpleWeb.jslib.meta (76%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common.meta (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/BufferPool.cs (81%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/BufferPool.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Connection.cs (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Connection.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Constants.cs (99%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Constants.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/EventType.cs (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/EventType.cs.meta (54%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Log.cs rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Log.cs.meta (55%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Message.cs (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Message.cs.meta (55%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/MessageProcessor.cs (92%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/MessageProcessor.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/ReadHelper.cs (88%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/ReadHelper.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/ReceiveLoop.cs (86%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/ReceiveLoop.cs.meta (54%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/Common/Request.cs.meta rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/SendLoop.cs (93%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/SendLoop.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/TcpConfig.cs (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/TcpConfig.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Utils.cs (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Common/Utils.cs.meta (55%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/LICENSE (100%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/LICENSE.meta rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/README.txt (100%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/README.txt.meta rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server.meta (100%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/ServerHandshake.cs (72%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/ServerHandshake.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/ServerSslHelper.cs (83%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/ServerSslHelper.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/SimpleWebServer.cs (85%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/SimpleWebServer.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/WebSocketServer.cs (77%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/Server/WebSocketServer.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef => Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef} (83%) create mode 100644 Assets/Mirror/Transports/SimpleWeb/SimpleWeb/SimpleWebTransport.asmdef.meta rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/SslConfigLoader.cs (93%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb/SimpleWeb}/SslConfigLoader.cs.meta (54%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb}/SimpleWebTransport.cs (51%) rename Assets/Mirror/{Runtime/Transports/SimpleWebTransport => Transports/SimpleWeb}/SimpleWebTransport.cs.meta (60%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy.meta (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy.meta (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Client.cs (98%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Client.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Common.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Common.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/ConnectionState.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/ConnectionState.cs.meta (54%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/EventType.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/EventType.cs.meta (55%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/LICENSE (100%) create mode 100644 Assets/Mirror/Transports/Telepathy/Telepathy/LICENSE.meta rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Log.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Log.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/MagnificentReceivePipe.cs.meta (54%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/MagnificentSendPipe.cs.meta (54%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/NetworkStreamExtensions.cs.meta (54%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Pool.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Pool.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Server.cs (93%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Server.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Telepathy.asmdef (100%) create mode 100644 Assets/Mirror/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/ThreadFunctions.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/ThreadFunctions.cs.meta (54%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Utils.cs (100%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/Utils.cs.meta (56%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/Telepathy/VERSION (97%) create mode 100644 Assets/Mirror/Transports/Telepathy/Telepathy/VERSION.meta rename Assets/Mirror/{Runtime => }/Transports/Telepathy/TelepathyTransport.cs (81%) rename Assets/Mirror/{Runtime => }/Transports/Telepathy/TelepathyTransport.cs.meta (60%) create mode 100644 Assets/Mirror/Transports/Threaded.meta create mode 100644 Assets/Mirror/Transports/Threaded/ThreadedTransport.cs create mode 100644 Assets/Mirror/Transports/Threaded/ThreadedTransport.cs.meta create mode 100644 Assets/Mirror/version.txt create mode 100644 Assets/Mirror/version.txt.meta create mode 100644 Assets/Resources.meta create mode 100644 Assets/Scenes/Main Mirror+Relay.unity rename Assets/{Mirror/Runtime/Transports/KCP/kcp2k/LICENSE.meta => Scenes/Main Mirror+Relay.unity.meta} (74%) create mode 100644 ProjectSettings/MemorySettings.asset create mode 100644 ProjectSettings/MultiplayerManager.asset create mode 100644 ProjectSettings/Packages/com.unity.services.core/Settings.json diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs b/Assets/Mirror/Authenticators/BasicAuthenticator.cs index 0fdc7e2..92ff68c 100644 --- a/Assets/Mirror/Authenticators/BasicAuthenticator.cs +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -17,7 +16,7 @@ public class BasicAuthenticator : NetworkAuthenticator public string username; public string password; - readonly HashSet connectionsPendingDisconnect = new HashSet(); + readonly HashSet connectionsPendingDisconnect = new HashSet(); #region Messages @@ -51,7 +50,7 @@ public override void OnStartServer() /// /// Called on server from StopServer to reset the Authenticator - /// Server message handlers should be registered in this method. + /// Server message handlers should be unregistered in this method. /// public override void OnStopServer() { @@ -60,7 +59,7 @@ public override void OnStopServer() } /// - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate /// /// Connection to client. public override void OnServerAuthenticate(NetworkConnectionToClient conn) @@ -153,7 +152,7 @@ public override void OnStopClient() } /// - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate /// public override void OnClientAuthenticate() { @@ -163,7 +162,7 @@ public override void OnClientAuthenticate() authPassword = password }; - NetworkClient.connection.Send(authRequestMessage); + NetworkClient.Send(authRequestMessage); } /// diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta index 4765013..d4581cf 100644 --- a/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/BasicAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs index 6723cc9..5e0ea56 100644 --- a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs +++ b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs @@ -4,7 +4,7 @@ namespace Mirror.Authenticators { /// - /// An authenicator that identifies the user by their device. + /// An authenticator that identifies the user by their device. /// A GUID is used as a fallback when the platform doesn't support SystemInfo.deviceUniqueIdentifier. /// Note: deviceUniqueIdentifier can be spoofed, so security is not guaranteed. /// See https://docs.unity3d.com/ScriptReference/SystemInfo-deviceUniqueIdentifier.html for details. @@ -47,7 +47,7 @@ public override void OnStopServer() } /// - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate /// /// Connection to client. public override void OnServerAuthenticate(NetworkConnectionToClient conn) @@ -94,7 +94,7 @@ public override void OnStopClient() } /// - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate /// public override void OnClientAuthenticate() { @@ -111,7 +111,7 @@ public override void OnClientAuthenticate() } // send the deviceUniqueIdentifier to the server - NetworkClient.connection.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } ); + NetworkClient.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } ); } /// diff --git a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta index 9ca9f64..3c115ca 100644 --- a/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta +++ b/Assets/Mirror/Authenticators/DeviceAuthenticator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/DeviceAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef index 16cdfbc..70eacf3 100644 --- a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef @@ -1,14 +1,16 @@ { "name": "Mirror.Authenticators", + "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], - "optionalUnityReferences": [], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [] + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta index 2731701..af5d5bd 100644 --- a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta index b19ddec..a2d3a37 100644 --- a/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta +++ b/Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Authenticators/TimeoutAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta index 8b23823..a58bca4 100644 --- a/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta +++ b/Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs index 89867c1..26aff41 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs @@ -11,47 +11,34 @@ static class PreprocessorDefine [InitializeOnLoadMethod] public static void AddDefineSymbols() { +#if UNITY_2021_2_OR_NEWER + string currentDefines = PlayerSettings.GetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup)); +#else + // Deprecated in Unity 2023.1 string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup); +#endif + // Remove oldest when adding next month's symbol. + // Keep a rolling 12 months of symbols. HashSet defines = new HashSet(currentDefines.Split(';')) { "MIRROR", - "MIRROR_17_0_OR_NEWER", - "MIRROR_18_0_OR_NEWER", - "MIRROR_24_0_OR_NEWER", - "MIRROR_26_0_OR_NEWER", - "MIRROR_27_0_OR_NEWER", - "MIRROR_28_0_OR_NEWER", - "MIRROR_29_0_OR_NEWER", - "MIRROR_30_0_OR_NEWER", - "MIRROR_30_5_2_OR_NEWER", - "MIRROR_32_1_2_OR_NEWER", - "MIRROR_32_1_4_OR_NEWER", - "MIRROR_35_0_OR_NEWER", - "MIRROR_35_1_OR_NEWER", - "MIRROR_37_0_OR_NEWER", - "MIRROR_38_0_OR_NEWER", - "MIRROR_39_0_OR_NEWER", - "MIRROR_40_0_OR_NEWER", - "MIRROR_41_0_OR_NEWER", - "MIRROR_42_0_OR_NEWER", - "MIRROR_43_0_OR_NEWER", - "MIRROR_44_0_OR_NEWER", - "MIRROR_46_0_OR_NEWER", - "MIRROR_47_0_OR_NEWER", - "MIRROR_53_0_OR_NEWER", - "MIRROR_55_0_OR_NEWER", - "MIRROR_57_0_OR_NEWER", - "MIRROR_58_0_OR_NEWER", - "MIRROR_65_0_OR_NEWER", - "MIRROR_66_0_OR_NEWER" + "MIRROR_89_OR_NEWER", + "MIRROR_90_OR_NEWER", + "MIRROR_93_OR_NEWER", + "MIRROR_96_OR_NEWER" }; - // only touch PlayerSettings if we actually modified it. + // only touch PlayerSettings if we actually modified it, // otherwise it shows up as changed in git each time. string newDefines = string.Join(";", defines); if (newDefines != currentDefines) { +#if UNITY_2021_2_OR_NEWER + PlayerSettings.SetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup), newDefines); +#else + // Deprecated in Unity 2023.1 PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines); +#endif } } } diff --git a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta index 30806d0..27860f5 100644 --- a/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta +++ b/Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/AssemblyInfo.cs.meta b/Assets/Mirror/Components/AssemblyInfo.cs.meta index f9af1fa..565e5cb 100644 --- a/Assets/Mirror/Components/AssemblyInfo.cs.meta +++ b/Assets/Mirror/Components/AssemblyInfo.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs index ddb38ea..5fa9397 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs @@ -6,7 +6,7 @@ namespace Mirror.Discovery { [Serializable] - public class ServerFoundUnityEvent : UnityEvent {}; + public class ServerFoundUnityEvent : UnityEvent {}; [DisallowMultipleComponent] [AddComponentMenu("Network/Network Discovery")] @@ -14,27 +14,6 @@ public class NetworkDiscovery : NetworkDiscoveryBase /// Process the request from a client /// diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta index c691a61..c6c23e0 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta +++ b/Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/NetworkDiscovery.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs index ac57b75..677bad3 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs @@ -23,29 +23,46 @@ public abstract class NetworkDiscoveryBase : MonoBehaviour { public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } } - // each game should have a random unique handshake, this way you can tell if this is the same game or not - [HideInInspector] - public long secretHandshake; + [SerializeField] + [Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")] + public bool enableActiveDiscovery = true; + + // broadcast address needs to be configurable on iOS: + // https://github.com/vis2k/Mirror/pull/3255 + [Tooltip("iOS may require LAN IP address here (e.g. 192.168.x.x), otherwise leave blank.")] + public string BroadcastAddress = ""; [SerializeField] [Tooltip("The UDP port the server will listen for multi-cast messages")] protected int serverBroadcastListenPort = 47777; - [SerializeField] - [Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")] - public bool enableActiveDiscovery = true; - [SerializeField] [Tooltip("Time in seconds between multi-cast messages")] [Range(1, 60)] float ActiveDiscoveryInterval = 3; + [Tooltip("Transport to be advertised during discovery")] + public Transport transport; + + [Tooltip("Invoked when a server is found")] + public ServerFoundUnityEvent OnServerFound; + + // Each game should have a random unique handshake, + // this way you can tell if this is the same game or not + [HideInInspector] + public long secretHandshake; + + public long ServerId { get; private set; } + protected UdpClient serverUdpClient; protected UdpClient clientUdpClient; #if UNITY_EDITOR - void OnValidate() + public virtual void OnValidate() { + if (transport == null) + transport = GetComponent(); + if (secretHandshake == 0) { secretHandshake = RandomLong(); @@ -54,22 +71,31 @@ void OnValidate() } #endif - public static long RandomLong() - { - int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); - int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); - return value1 + ((long)value2 << 32); - } - /// /// virtual so that inheriting classes' Start() can call base.Start() too /// public virtual void Start() { + ServerId = RandomLong(); + + // active transport gets initialized in Awake + // so make sure we set it here in Start() after Awake + // Or just let the user assign it in the inspector + if (transport == null) + transport = Transport.active; + // Server mode? then start advertising -#if UNITY_SERVER - AdvertiseServer(); -#endif + if (Utils.IsHeadless()) + { + AdvertiseServer(); + } + } + + public static long RandomLong() + { + int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); + int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); + return value1 + ((long)value2 << 32); } // Ensure the ports are cleared no matter when Game/Unity UI exits @@ -144,6 +170,8 @@ public void AdvertiseServer() MulticastLoopback = false }; + //Debug.Log($"Discovery: Advertising Server {Dns.GetHostName()}"); + // listen for client pings _ = ServerListenAsync(); } @@ -162,9 +190,7 @@ public async Task ServerListenAsync() // socket has been closed break; } - catch (Exception) - { - } + catch (Exception) {} } } @@ -243,11 +269,12 @@ protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint AndroidJavaObject multicastLock; bool hasMulticastLock; #endif + void BeginMulticastLock() { #if UNITY_ANDROID if (hasMulticastLock) return; - + if (Application.platform == RuntimePlatform.Android) { using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic("currentActivity")) @@ -267,7 +294,7 @@ void EndpMulticastLock() { #if UNITY_ANDROID if (!hasMulticastLock) return; - + multicastLock?.Call("release"); hasMulticastLock = false; #endif @@ -324,16 +351,16 @@ public void StopDiscovery() /// ClientListenAsync Task public async Task ClientListenAsync() { - // while clientUpdClient to fix: + // while clientUpdClient to fix: // https://github.com/vis2k/Mirror/pull/2908 // // If, you cancel discovery the clientUdpClient is set to null. // However, nothing cancels ClientListenAsync. If we change the if(true) - // to check if the client is null. You can properly cancel the discovery, + // to check if the client is null. You can properly cancel the discovery, // and kill the listen thread. // - // Prior to this fix, if you cancel the discovery search. It crashes the - // thread, and is super noisy in the output. As well as causes issues on + // Prior to this fix, if you cancel the discovery search. It crashes the + // thread, and is super noisy in the output. As well as causes issues on // the quest. while (clientUdpClient != null) { @@ -369,6 +396,18 @@ public void BroadcastDiscoveryRequest() IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort); + if (!string.IsNullOrWhiteSpace(BroadcastAddress)) + { + try + { + endPoint = new IPEndPoint(IPAddress.Parse(BroadcastAddress), serverBroadcastListenPort); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { writer.WriteLong(secretHandshake); @@ -381,6 +420,7 @@ public void BroadcastDiscoveryRequest() ArraySegment data = writer.ToArraySegment(); + //Debug.Log($"Discovery: Sending BroadcastDiscoveryRequest {request}"); clientUdpClient.SendAsync(data.Array, data.Count, endPoint); } catch (Exception) diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta index 7dfbaf6..cbd7616 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs index e43c3d7..a34bbe0 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Mirror.Discovery @@ -17,11 +18,20 @@ public class NetworkDiscoveryHUD : MonoBehaviour #if UNITY_EDITOR void OnValidate() { - if (networkDiscovery == null) + if (Application.isPlaying) return; + Reset(); + } + + void Reset() + { + networkDiscovery = GetComponent(); + + // Add default event handler if not already present + if (!Enumerable.Range(0, networkDiscovery.OnServerFound.GetPersistentEventCount()) + .Any(i => networkDiscovery.OnServerFound.GetPersistentMethodName(i) == nameof(OnDiscoveredServer))) { - networkDiscovery = GetComponent(); UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer); - UnityEditor.Undo.RecordObjects(new Object[] { this, networkDiscovery }, "Set NetworkDiscovery"); + UnityEditor.Undo.RecordObjects(new UnityEngine.Object[] { this, networkDiscovery }, "Set NetworkDiscovery"); } } #endif @@ -125,6 +135,8 @@ void Connect(ServerResponse info) public void OnDiscoveredServer(ServerResponse info) { + Debug.Log($"Discovered Server: {info.serverId} | {info.EndPoint} | {info.uri}"); + // Note that you can check the versioning to decide if you can connect to the server or not using this method discoveredServers[info.serverId] = info; } diff --git a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta index f93b275..c56f65f 100644 --- a/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta +++ b/Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta b/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta index 84f3232..84d4d68 100644 --- a/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta +++ b/Assets/Mirror/Components/Discovery/ServerRequest.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/ServerRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta b/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta index 44f23ba..3ca8f78 100644 --- a/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta +++ b/Assets/Mirror/Components/Discovery/ServerResponse.cs.meta @@ -5,7 +5,14 @@ MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 - icon: {instanceID: 0} + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Discovery/ServerResponse.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs deleted file mode 100644 index 06a87a9..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs +++ /dev/null @@ -1,93 +0,0 @@ -using UnityEngine; - -namespace Mirror.Experimental -{ - [AddComponentMenu("Network/ Experimental/Network Lerp Rigidbody")] - [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")] - public class NetworkLerpRigidbody : NetworkBehaviour - { - [Header("Settings")] - [SerializeField] internal Rigidbody target = null; - [Tooltip("How quickly current velocity approaches target velocity")] - [SerializeField] float lerpVelocityAmount = 0.5f; - [Tooltip("How quickly current position approaches target position")] - [SerializeField] float lerpPositionAmount = 0.5f; - - [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - [SerializeField] bool clientAuthority = false; - - float nextSyncTime; - - - [SyncVar()] - Vector3 targetVelocity; - - [SyncVar()] - Vector3 targetPosition; - - /// - /// Ignore value if is host or client with Authority - /// - /// - bool IgnoreSync => isServer || ClientWithAuthority; - - bool ClientWithAuthority => clientAuthority && hasAuthority; - - void OnValidate() - { - if (target == null) - { - target = GetComponent(); - } - } - - void Update() - { - if (isServer) - { - SyncToClients(); - } - else if (ClientWithAuthority) - { - SendToServer(); - } - } - - void SyncToClients() - { - targetVelocity = target.velocity; - targetPosition = target.position; - } - - void SendToServer() - { - float now = Time.time; - if (now > nextSyncTime) - { - nextSyncTime = now + syncInterval; - CmdSendState(target.velocity, target.position); - } - } - - [Command] - void CmdSendState(Vector3 velocity, Vector3 position) - { - target.velocity = velocity; - target.position = position; - targetVelocity = velocity; - targetPosition = position; - } - - void FixedUpdate() - { - if (IgnoreSync) { return; } - - target.velocity = Vector3.Lerp(target.velocity, targetVelocity, lerpVelocityAmount); - target.position = Vector3.Lerp(target.position, targetPosition, lerpPositionAmount); - // add velocity to position as position would have moved on server at that velocity - target.position += target.velocity * Time.fixedDeltaTime; - - // TODO does this also need to sync acceleration so and update velocity? - } - } -} diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs deleted file mode 100644 index 4989d29..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs +++ /dev/null @@ -1,361 +0,0 @@ -using UnityEngine; - -namespace Mirror.Experimental -{ - [AddComponentMenu("Network/ Experimental/Network Rigidbody")] - [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")] - public class NetworkRigidbody : NetworkBehaviour - { - [Header("Settings")] - [SerializeField] internal Rigidbody target = null; - - [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - public bool clientAuthority = false; - - [Header("Velocity")] - - [Tooltip("Syncs Velocity every SyncInterval")] - [SerializeField] bool syncVelocity = true; - - [Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")] - [SerializeField] bool clearVelocity = false; - - [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] - [SerializeField] float velocitySensitivity = 0.1f; - - - [Header("Angular Velocity")] - - [Tooltip("Syncs AngularVelocity every SyncInterval")] - [SerializeField] bool syncAngularVelocity = true; - - [Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")] - [SerializeField] bool clearAngularVelocity = false; - - [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] - [SerializeField] float angularVelocitySensitivity = 0.1f; - - /// - /// Values sent on client with authority after they are sent to the server - /// - readonly ClientSyncState previousValue = new ClientSyncState(); - - void OnValidate() - { - if (target == null) - { - target = GetComponent(); - } - } - - - #region Sync vars - [SyncVar(hook = nameof(OnVelocityChanged))] - Vector3 velocity; - - [SyncVar(hook = nameof(OnAngularVelocityChanged))] - Vector3 angularVelocity; - - [SyncVar(hook = nameof(OnIsKinematicChanged))] - bool isKinematic; - - [SyncVar(hook = nameof(OnUseGravityChanged))] - bool useGravity; - - [SyncVar(hook = nameof(OnuDragChanged))] - float drag; - - [SyncVar(hook = nameof(OnAngularDragChanged))] - float angularDrag; - - /// - /// Ignore value if is host or client with Authority - /// - /// - bool IgnoreSync => isServer || ClientWithAuthority; - - bool ClientWithAuthority => clientAuthority && hasAuthority; - - void OnVelocityChanged(Vector3 _, Vector3 newValue) - { - if (IgnoreSync) - return; - - target.velocity = newValue; - } - - - void OnAngularVelocityChanged(Vector3 _, Vector3 newValue) - { - if (IgnoreSync) - return; - - target.angularVelocity = newValue; - } - - void OnIsKinematicChanged(bool _, bool newValue) - { - if (IgnoreSync) - return; - - target.isKinematic = newValue; - } - - void OnUseGravityChanged(bool _, bool newValue) - { - if (IgnoreSync) - return; - - target.useGravity = newValue; - } - - void OnuDragChanged(float _, float newValue) - { - if (IgnoreSync) - return; - - target.drag = newValue; - } - - void OnAngularDragChanged(float _, float newValue) - { - if (IgnoreSync) - return; - - target.angularDrag = newValue; - } - #endregion - - - internal void Update() - { - if (isServer) - { - SyncToClients(); - } - else if (ClientWithAuthority) - { - SendToServer(); - } - } - - internal void FixedUpdate() - { - if (clearAngularVelocity && !syncAngularVelocity) - { - target.angularVelocity = Vector3.zero; - } - - if (clearVelocity && !syncVelocity) - { - target.velocity = Vector3.zero; - } - } - - /// - /// Updates sync var values on server so that they sync to the client - /// - [Server] - void SyncToClients() - { - // only update if they have changed more than Sensitivity - - Vector3 currentVelocity = syncVelocity ? target.velocity : default; - Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; - - bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); - bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity); - - if (velocityChanged) - { - velocity = currentVelocity; - previousValue.velocity = currentVelocity; - } - - if (angularVelocityChanged) - { - angularVelocity = currentAngularVelocity; - previousValue.angularVelocity = currentAngularVelocity; - } - - // other rigidbody settings - isKinematic = target.isKinematic; - useGravity = target.useGravity; - drag = target.drag; - angularDrag = target.angularDrag; - } - - /// - /// Uses Command to send values to server - /// - [Client] - void SendToServer() - { - if (!hasAuthority) - { - Debug.LogWarning("SendToServer called without authority"); - return; - } - - SendVelocity(); - SendRigidBodySettings(); - } - - [Client] - void SendVelocity() - { - float now = Time.time; - if (now < previousValue.nextSyncTime) - return; - - Vector3 currentVelocity = syncVelocity ? target.velocity : default; - Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; - - bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); - bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity); - - // if angularVelocity has changed it is likely that velocity has also changed so just sync both values - // however if only velocity has changed just send velocity - if (angularVelocityChanged) - { - CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity); - previousValue.velocity = currentVelocity; - previousValue.angularVelocity = currentAngularVelocity; - } - else if (velocityChanged) - { - CmdSendVelocity(currentVelocity); - previousValue.velocity = currentVelocity; - } - - - // only update syncTime if either has changed - if (angularVelocityChanged || velocityChanged) - { - previousValue.nextSyncTime = now + syncInterval; - } - } - - [Client] - void SendRigidBodySettings() - { - // These shouldn't change often so it is ok to send in their own Command - if (previousValue.isKinematic != target.isKinematic) - { - CmdSendIsKinematic(target.isKinematic); - previousValue.isKinematic = target.isKinematic; - } - if (previousValue.useGravity != target.useGravity) - { - CmdSendUseGravity(target.useGravity); - previousValue.useGravity = target.useGravity; - } - if (previousValue.drag != target.drag) - { - CmdSendDrag(target.drag); - previousValue.drag = target.drag; - } - if (previousValue.angularDrag != target.angularDrag) - { - CmdSendAngularDrag(target.angularDrag); - previousValue.angularDrag = target.angularDrag; - } - } - - /// - /// Called when only Velocity has changed on the client - /// - [Command] - void CmdSendVelocity(Vector3 velocity) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.velocity = velocity; - target.velocity = velocity; - } - - /// - /// Called when angularVelocity has changed on the client - /// - [Command] - void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - if (syncVelocity) - { - this.velocity = velocity; - - target.velocity = velocity; - - } - this.angularVelocity = angularVelocity; - target.angularVelocity = angularVelocity; - } - - [Command] - void CmdSendIsKinematic(bool isKinematic) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.isKinematic = isKinematic; - target.isKinematic = isKinematic; - } - - [Command] - void CmdSendUseGravity(bool useGravity) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.useGravity = useGravity; - target.useGravity = useGravity; - } - - [Command] - void CmdSendDrag(float drag) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.drag = drag; - target.drag = drag; - } - - [Command] - void CmdSendAngularDrag(float angularDrag) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.angularDrag = angularDrag; - target.angularDrag = angularDrag; - } - - /// - /// holds previously synced values - /// - public class ClientSyncState - { - /// - /// Next sync time that velocity will be synced, based on syncInterval. - /// - public float nextSyncTime; - public Vector3 velocity; - public Vector3 angularVelocity; - public bool isKinematic; - public bool useGravity; - public float drag; - public float angularDrag; - } - } -} diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs b/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs deleted file mode 100644 index c14b260..0000000 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs +++ /dev/null @@ -1,360 +0,0 @@ -using UnityEngine; - -namespace Mirror.Experimental -{ - [AddComponentMenu("Network/ Experimental/Network Rigidbody 2D")] - public class NetworkRigidbody2D : NetworkBehaviour - { - [Header("Settings")] - [SerializeField] internal Rigidbody2D target = null; - - [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - public bool clientAuthority = false; - - [Header("Velocity")] - - [Tooltip("Syncs Velocity every SyncInterval")] - [SerializeField] bool syncVelocity = true; - - [Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")] - [SerializeField] bool clearVelocity = false; - - [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] - [SerializeField] float velocitySensitivity = 0.1f; - - - [Header("Angular Velocity")] - - [Tooltip("Syncs AngularVelocity every SyncInterval")] - [SerializeField] bool syncAngularVelocity = true; - - [Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")] - [SerializeField] bool clearAngularVelocity = false; - - [Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")] - [SerializeField] float angularVelocitySensitivity = 0.1f; - - /// - /// Values sent on client with authority after they are sent to the server - /// - readonly ClientSyncState previousValue = new ClientSyncState(); - - void OnValidate() - { - if (target == null) - { - target = GetComponent(); - } - } - - - #region Sync vars - [SyncVar(hook = nameof(OnVelocityChanged))] - Vector2 velocity; - - [SyncVar(hook = nameof(OnAngularVelocityChanged))] - float angularVelocity; - - [SyncVar(hook = nameof(OnIsKinematicChanged))] - bool isKinematic; - - [SyncVar(hook = nameof(OnGravityScaleChanged))] - float gravityScale; - - [SyncVar(hook = nameof(OnuDragChanged))] - float drag; - - [SyncVar(hook = nameof(OnAngularDragChanged))] - float angularDrag; - - /// - /// Ignore value if is host or client with Authority - /// - /// - bool IgnoreSync => isServer || ClientWithAuthority; - - bool ClientWithAuthority => clientAuthority && hasAuthority; - - void OnVelocityChanged(Vector2 _, Vector2 newValue) - { - if (IgnoreSync) - return; - - target.velocity = newValue; - } - - - void OnAngularVelocityChanged(float _, float newValue) - { - if (IgnoreSync) - return; - - target.angularVelocity = newValue; - } - - void OnIsKinematicChanged(bool _, bool newValue) - { - if (IgnoreSync) - return; - - target.isKinematic = newValue; - } - - void OnGravityScaleChanged(float _, float newValue) - { - if (IgnoreSync) - return; - - target.gravityScale = newValue; - } - - void OnuDragChanged(float _, float newValue) - { - if (IgnoreSync) - return; - - target.drag = newValue; - } - - void OnAngularDragChanged(float _, float newValue) - { - if (IgnoreSync) - return; - - target.angularDrag = newValue; - } - #endregion - - - internal void Update() - { - if (isServer) - { - SyncToClients(); - } - else if (ClientWithAuthority) - { - SendToServer(); - } - } - - internal void FixedUpdate() - { - if (clearAngularVelocity && !syncAngularVelocity) - { - target.angularVelocity = 0f; - } - - if (clearVelocity && !syncVelocity) - { - target.velocity = Vector2.zero; - } - } - - /// - /// Updates sync var values on server so that they sync to the client - /// - [Server] - void SyncToClients() - { - // only update if they have changed more than Sensitivity - - Vector2 currentVelocity = syncVelocity ? target.velocity : default; - float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; - - bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); - bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity) > angularVelocitySensitivity); - - if (velocityChanged) - { - velocity = currentVelocity; - previousValue.velocity = currentVelocity; - } - - if (angularVelocityChanged) - { - angularVelocity = currentAngularVelocity; - previousValue.angularVelocity = currentAngularVelocity; - } - - // other rigidbody settings - isKinematic = target.isKinematic; - gravityScale = target.gravityScale; - drag = target.drag; - angularDrag = target.angularDrag; - } - - /// - /// Uses Command to send values to server - /// - [Client] - void SendToServer() - { - if (!hasAuthority) - { - Debug.LogWarning("SendToServer called without authority"); - return; - } - - SendVelocity(); - SendRigidBodySettings(); - } - - [Client] - void SendVelocity() - { - float now = Time.time; - if (now < previousValue.nextSyncTime) - return; - - Vector2 currentVelocity = syncVelocity ? target.velocity : default; - float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default; - - bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity); - bool angularVelocityChanged = syncAngularVelocity && previousValue.angularVelocity != currentAngularVelocity;//((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity); - - // if angularVelocity has changed it is likely that velocity has also changed so just sync both values - // however if only velocity has changed just send velocity - if (angularVelocityChanged) - { - CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity); - previousValue.velocity = currentVelocity; - previousValue.angularVelocity = currentAngularVelocity; - } - else if (velocityChanged) - { - CmdSendVelocity(currentVelocity); - previousValue.velocity = currentVelocity; - } - - - // only update syncTime if either has changed - if (angularVelocityChanged || velocityChanged) - { - previousValue.nextSyncTime = now + syncInterval; - } - } - - [Client] - void SendRigidBodySettings() - { - // These shouldn't change often so it is ok to send in their own Command - if (previousValue.isKinematic != target.isKinematic) - { - CmdSendIsKinematic(target.isKinematic); - previousValue.isKinematic = target.isKinematic; - } - if (previousValue.gravityScale != target.gravityScale) - { - CmdChangeGravityScale(target.gravityScale); - previousValue.gravityScale = target.gravityScale; - } - if (previousValue.drag != target.drag) - { - CmdSendDrag(target.drag); - previousValue.drag = target.drag; - } - if (previousValue.angularDrag != target.angularDrag) - { - CmdSendAngularDrag(target.angularDrag); - previousValue.angularDrag = target.angularDrag; - } - } - - /// - /// Called when only Velocity has changed on the client - /// - [Command] - void CmdSendVelocity(Vector2 velocity) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.velocity = velocity; - target.velocity = velocity; - } - - /// - /// Called when angularVelocity has changed on the client - /// - [Command] - void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - if (syncVelocity) - { - this.velocity = velocity; - - target.velocity = velocity; - - } - this.angularVelocity = angularVelocity; - target.angularVelocity = angularVelocity; - } - - [Command] - void CmdSendIsKinematic(bool isKinematic) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.isKinematic = isKinematic; - target.isKinematic = isKinematic; - } - - [Command] - void CmdChangeGravityScale(float gravityScale) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.gravityScale = gravityScale; - target.gravityScale = gravityScale; - } - - [Command] - void CmdSendDrag(float drag) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.drag = drag; - target.drag = drag; - } - - [Command] - void CmdSendAngularDrag(float angularDrag) - { - // Ignore messages from client if not in client authority mode - if (!clientAuthority) - return; - - this.angularDrag = angularDrag; - target.angularDrag = angularDrag; - } - - /// - /// holds previously synced values - /// - public class ClientSyncState - { - /// - /// Next sync time that velocity will be synced, based on syncInterval. - /// - public float nextSyncTime; - public Vector2 velocity; - public float angularVelocity; - public bool isKinematic; - public float gravityScale; - public float drag; - public float angularDrag; - } - } -} diff --git a/Assets/Mirror/Components/GUIConsole.cs b/Assets/Mirror/Components/GUIConsole.cs index c6acbb8..7055fe1 100644 --- a/Assets/Mirror/Components/GUIConsole.cs +++ b/Assets/Mirror/Components/GUIConsole.cs @@ -31,27 +31,38 @@ public LogEntry(string message, LogType type) public class GUIConsole : MonoBehaviour { - public int height = 150; + public int height = 80; + public int offsetY = 40; // only keep the recent 'n' entries. otherwise memory would grow forever // and drawing would get slower and slower. public int maxLogCount = 50; + // Unity Editor has the Console window, we don't need to show it there. + // unless for testing, so keep it as option. + public bool showInEditor = false; + // log as queue so we can remove the first entry easily - Queue log = new Queue(); + readonly Queue log = new Queue(); // hotkey to show/hide at runtime for easier debugging // (sometimes we need to temporarily hide/show it) - // => F12 makes sense. nobody can find ^ in other games. - public KeyCode hotKey = KeyCode.F12; + // Default is BackQuote, because F keys are already assigned in browsers + [Tooltip("Hotkey to show/hide the console at runtime\nBack Quote is usually on the left above Tab\nChange with caution - F keys are generally already taken in Browsers")] + public KeyCode hotKey = KeyCode.BackQuote; // GUI bool visible; Vector2 scroll = Vector2.zero; + // only show at runtime, or if showInEditor is enabled + bool show => !Application.isEditor || showInEditor; + void Awake() { - Application.logMessageReceived += OnLog; + // only show at runtime, or if showInEditor is enabled + if (show) + Application.logMessageReceived += OnLog; } // OnLog logs everything, even Debug.Log messages in release builds @@ -90,7 +101,7 @@ void OnLog(string message, string stackTrace, LogType type) void Update() { - if (Input.GetKeyDown(hotKey)) + if (show && Input.GetKeyDown(hotKey)) visible = !visible; } @@ -98,7 +109,12 @@ void OnGUI() { if (!visible) return; - scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width), GUILayout.Height(height)); + // If this offset is changed, also change width in NetworkManagerHUD::OnGUI + int offsetX = 300 + 20; + + GUILayout.BeginArea(new Rect(offsetX, offsetY, Screen.width - offsetX - 10, height)); + + scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width - offsetX - 10), GUILayout.Height(height)); foreach (LogEntry entry in log) { if (entry.type == LogType.Error || entry.type == LogType.Exception) @@ -110,6 +126,8 @@ void OnGUI() GUI.color = Color.white; } GUILayout.EndScrollView(); + + GUILayout.EndArea(); } } } diff --git a/Assets/Mirror/Components/GUIConsole.cs.meta b/Assets/Mirror/Components/GUIConsole.cs.meta index 5664216..283ae43 100644 --- a/Assets/Mirror/Components/GUIConsole.cs.meta +++ b/Assets/Mirror/Components/GUIConsole.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/GUIConsole.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Distance.meta b/Assets/Mirror/Components/InterestManagement/Distance.meta index 9847902..832357e 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance.meta +++ b/Assets/Mirror/Components/InterestManagement/Distance.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: fa4cbc6b9c584db4971985cb9f369077 -timeCreated: 1613110605 \ No newline at end of file +timeCreated: 1613110605 diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs index 116051b..e4f56ca 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs @@ -8,22 +8,38 @@ namespace Mirror public class DistanceInterestManagement : InterestManagement { [Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")] - public int visRange = 10; + public int visRange = 500; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] public float rebuildInterval = 1; double lastRebuildTime; + // cache custom ranges to avoid runtime TryGetComponent lookups + readonly Dictionary CustomRanges = new Dictionary(); + // helper function to get vis range for a given object, or default. + [ServerCallback] int GetVisRange(NetworkIdentity identity) { - return identity.TryGetComponent(out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; + return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; } [ServerCallback] - public override void Reset() + public override void ResetState() { lastRebuildTime = 0D; + CustomRanges.Clear(); + } + + public override void OnSpawned(NetworkIdentity identity) + { + if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom)) + CustomRanges[identity] = custom; + } + + public override void OnDestroyed(NetworkIdentity identity) + { + CustomRanges.Remove(identity); } public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) @@ -59,9 +75,8 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet= lastRebuildTime + rebuildInterval) diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta index 1a575af..b0e89d9 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs index 25f5347..12556e5 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs @@ -10,6 +10,6 @@ namespace Mirror public class DistanceInterestManagementCustomRange : NetworkBehaviour { [Tooltip("The maximum range that objects will be visible at.")] - public int visRange = 20; + public int visRange = 100; } } diff --git a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta index 406ca78..369f702 100644 --- a/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Distance/DistanceInterestManagementCustomRange.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Match.meta b/Assets/Mirror/Components/InterestManagement/Match.meta index e429883..2cfaabc 100644 --- a/Assets/Mirror/Components/InterestManagement/Match.meta +++ b/Assets/Mirror/Components/InterestManagement/Match.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 5eca5245ae6bb460e9a92f7e14d5493a -timeCreated: 1622649517 \ No newline at end of file +timeCreated: 1622649517 diff --git a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs index ff2eadc..d741d0c 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs @@ -7,119 +7,123 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")] public class MatchInterestManagement : InterestManagement { - readonly Dictionary> matchObjects = - new Dictionary>(); + [Header("Diagnostics")] + [ReadOnly, SerializeField] + internal ushort matchCount; - readonly Dictionary lastObjectMatch = - new Dictionary(); + readonly Dictionary> matchObjects = + new Dictionary>(); readonly HashSet dirtyMatches = new HashSet(); - public override void OnSpawned(NetworkIdentity identity) + // LateUpdate so that all spawns/despawns/changes are done + [ServerCallback] + void LateUpdate() { - if (!identity.TryGetComponent(out NetworkMatch networkMatch)) - return; - - Guid currentMatch = networkMatch.matchId; - lastObjectMatch[identity] = currentMatch; - - // Guid.Empty is never a valid matchId...do not add to matchObjects collection - if (currentMatch == Guid.Empty) - return; - - // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}"); - if (!matchObjects.TryGetValue(currentMatch, out HashSet objects)) + // Rebuild all dirty matches + // dirtyMatches will be empty if no matches changed members + // by spawning or destroying or changing matchId in this frame. + foreach (Guid dirtyMatch in dirtyMatches) { - objects = new HashSet(); - matchObjects.Add(currentMatch, objects); + // rebuild always, even if matchObjects[dirtyMatch] is empty. + // Players might have left the match, but they may still be spawned. + RebuildMatchObservers(dirtyMatch); + + // clean up empty entries in the dict + if (matchObjects[dirtyMatch].Count == 0) + matchObjects.Remove(dirtyMatch); } - objects.Add(identity); + dirtyMatches.Clear(); + + matchCount = (ushort)matchObjects.Count; } - public override void OnDestroyed(NetworkIdentity identity) + [ServerCallback] + void RebuildMatchObservers(Guid matchId) { - lastObjectMatch.TryGetValue(identity, out Guid currentMatch); - lastObjectMatch.Remove(identity); - if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet objects) && objects.Remove(identity)) - RebuildMatchObservers(currentMatch); + foreach (NetworkMatch networkMatch in matchObjects[matchId]) + if (networkMatch.netIdentity != null) + NetworkServer.RebuildObservers(networkMatch.netIdentity, false); } - // internal so we can update from tests + // called by NetworkMatch.matchId setter [ServerCallback] - internal void Update() + internal void OnMatchChanged(NetworkMatch networkMatch, Guid oldMatch) { - // for each spawned: - // if match changed: - // add previous to dirty - // add new to dirty - foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values) - { - // Ignore objects that don't have a NetworkMatch component - if (!netIdentity.TryGetComponent(out NetworkMatch networkMatch)) - continue; + // This object is in a new match so observers in the prior match + // and the new match need to rebuild their respective observers lists. - Guid newMatch = networkMatch.matchId; - lastObjectMatch.TryGetValue(netIdentity, out Guid currentMatch); - - // Guid.Empty is never a valid matchId - // Nothing to do if matchId hasn't changed - if (newMatch == Guid.Empty || newMatch == currentMatch) - continue; + // Remove this object from the hashset of the match it just left + // Guid.Empty is never a valid matchId + if (oldMatch != Guid.Empty) + { + dirtyMatches.Add(oldMatch); + matchObjects[oldMatch].Remove(networkMatch); + } - // Mark new/old matches as dirty so they get rebuilt - UpdateDirtyMatches(newMatch, currentMatch); + // Guid.Empty is never a valid matchId + if (networkMatch.matchId == Guid.Empty) + return; - // This object is in a new match so observers in the prior match - // and the new match need to rebuild their respective observers lists. - UpdateMatchObjects(netIdentity, newMatch, currentMatch); - } + dirtyMatches.Add(networkMatch.matchId); - // rebuild all dirty matchs - foreach (Guid dirtyMatch in dirtyMatches) - RebuildMatchObservers(dirtyMatch); + // Make sure this new match is in the dictionary + if (!matchObjects.ContainsKey(networkMatch.matchId)) + matchObjects[networkMatch.matchId] = new HashSet(); - dirtyMatches.Clear(); + // Add this object to the hashset of the new match + matchObjects[networkMatch.matchId].Add(networkMatch); } - void UpdateDirtyMatches(Guid newMatch, Guid currentMatch) + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) { - // Guid.Empty is never a valid matchId - if (currentMatch != Guid.Empty) - dirtyMatches.Add(currentMatch); + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + return; - dirtyMatches.Add(newMatch); - } + Guid networkMatchId = networkMatch.matchId; - void UpdateMatchObjects(NetworkIdentity netIdentity, Guid newMatch, Guid currentMatch) - { - // Remove this object from the hashset of the match it just left - // Guid.Empty is never a valid matchId - if (currentMatch != Guid.Empty) - matchObjects[currentMatch].Remove(netIdentity); + // Guid.Empty is never a valid matchId...do not add to matchObjects collection + if (networkMatchId == Guid.Empty) + return; - // Set this to the new match this object just entered - lastObjectMatch[netIdentity] = newMatch; + // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}"); + if (!matchObjects.TryGetValue(networkMatchId, out HashSet objects)) + { + objects = new HashSet(); + matchObjects.Add(networkMatchId, objects); + } - // Make sure this new match is in the dictionary - if (!matchObjects.ContainsKey(newMatch)) - matchObjects.Add(newMatch, new HashSet()); + objects.Add(networkMatch); - // Add this object to the hashset of the new match - matchObjects[newMatch].Add(netIdentity); + // Match ID could have been set in NetworkBehaviour::OnStartServer on this object. + // Since that's after OnCheckObserver is called it would be missed, so force Rebuild here. + // Add the current match to dirtyMatches for LateUpdate to rebuild it. + dirtyMatches.Add(networkMatchId); } - void RebuildMatchObservers(Guid matchId) + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) { - foreach (NetworkIdentity netIdentity in matchObjects[matchId]) - if (netIdentity != null) - NetworkServer.RebuildObservers(netIdentity, false); + // 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 LateUpdate do it once. + // We must add the current match to dirtyMatches for LateUpdate to rebuild it. + if (identity.TryGetComponent(out NetworkMatch currentMatch)) + { + if (currentMatch.matchId != Guid.Empty && + matchObjects.TryGetValue(currentMatch.matchId, out HashSet objects) && + objects.Remove(currentMatch)) + dirtyMatches.Add(currentMatch.matchId); + } } + [ServerCallback] public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { // Never observed if no NetworkMatch component - if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch)) + if (!identity.TryGetComponent(out NetworkMatch identityNetworkMatch)) return false; // Guid.Empty is never a valid matchId @@ -127,7 +131,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection return false; // Never observed if no NetworkMatch component - if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch)) + if (!newObserver.identity.TryGetComponent(out NetworkMatch newObserverNetworkMatch)) return false; // Guid.Empty is never a valid matchId @@ -137,24 +141,24 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId; } + [ServerCallback] public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) { - if (!identity.TryGetComponent(out NetworkMatch networkMatch)) + if (!identity.TryGetComponent(out NetworkMatch networkMatch)) return; - Guid matchId = networkMatch.matchId; - // Guid.Empty is never a valid matchId - if (matchId == Guid.Empty) + if (networkMatch.matchId == Guid.Empty) return; - if (!matchObjects.TryGetValue(matchId, out HashSet objects)) + // Abort if this match hasn't been created yet by OnSpawned or OnMatchChanged + if (!matchObjects.TryGetValue(networkMatch.matchId, out HashSet objects)) return; // Add everything in the hashset for this object's current match - foreach (NetworkIdentity networkIdentity in objects) - if (networkIdentity != null && networkIdentity.connectionToClient != null) - newObservers.Add(networkIdentity.connectionToClient); + foreach (NetworkMatch netMatch in objects) + if (netMatch.netIdentity != null && netMatch.netIdentity.connectionToClient != null) + newObservers.Add(netMatch.netIdentity.connectionToClient); } } } diff --git a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta index 605d215..bc4b6da 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Match/MatchInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs index f6689f2..0d55fd0 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs +++ b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs @@ -9,7 +9,34 @@ namespace Mirror [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] public class NetworkMatch : NetworkBehaviour { + Guid _matchId; + +#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes + [SerializeField, ReadOnly] + [Tooltip("Match ID is shown here on server for debugging purposes.")] + string MatchID = string.Empty; +#pragma warning restore IDE0052 + ///Set this to the same value on all networked objects that belong to a given match - public Guid matchId; + public Guid matchId + { + get => _matchId; + set + { + if (!NetworkServer.active) + throw new InvalidOperationException("matchId can only be set at runtime on active server"); + + if (_matchId == value) + return; + + Guid oldMatch = _matchId; + _matchId = value; + MatchID = value.ToString(); + + // Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement + if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement) + matchInterestManagement.OnMatchChanged(this, oldMatch); + } + } } } diff --git a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta index 47fc70a..a7158ac 100644 --- a/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Match/NetworkMatch.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Scene.meta b/Assets/Mirror/Components/InterestManagement/Scene.meta index 28b469f..f1fa700 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene.meta +++ b/Assets/Mirror/Components/InterestManagement/Scene.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 7655d309a46a4bd4860edf964228b3f6 -timeCreated: 1622649517 \ No newline at end of file +timeCreated: 1622649517 diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs index 8cbfa3b..c688dfd 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs @@ -17,6 +17,7 @@ public class SceneInterestManagement : InterestManagement HashSet dirtyScenes = new HashSet(); + [ServerCallback] public override void OnSpawned(NetworkIdentity identity) { Scene currentScene = identity.gameObject.scene; @@ -31,17 +32,23 @@ public override void OnSpawned(NetworkIdentity identity) objects.Add(identity); } + [ServerCallback] public override void OnDestroyed(NetworkIdentity identity) { - Scene currentScene = lastObjectScene[identity]; - lastObjectScene.Remove(identity); - if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) - RebuildSceneObservers(currentScene); + // 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 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); + if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) + dirtyScenes.Add(currentScene); + } } - // internal so we can update from tests [ServerCallback] - internal void Update() + void LateUpdate() { // for each spawned: // if scene changed: @@ -49,7 +56,9 @@ internal void Update() // add new to dirty foreach (NetworkIdentity identity in NetworkServer.spawned.Values) { - Scene currentScene = lastObjectScene[identity]; + if (!lastObjectScene.TryGetValue(identity, out Scene currentScene)) + continue; + Scene newScene = identity.gameObject.scene; if (newScene == currentScene) continue; diff --git a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta index 9cc1ff5..99925b0 100644 --- a/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Scene/SceneInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Experimental.meta b/Assets/Mirror/Components/InterestManagement/SceneDistance.meta similarity index 77% rename from Assets/Mirror/Components/Experimental.meta rename to Assets/Mirror/Components/InterestManagement/SceneDistance.meta index 57cce38..f010228 100644 --- a/Assets/Mirror/Components/Experimental.meta +++ b/Assets/Mirror/Components/InterestManagement/SceneDistance.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: bfbf2a1f2b300c5489dcab219ef2846e +guid: f4d8c634a8103664db5f90fe8bab9544 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs new file mode 100644 index 0000000..cdd5f16 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Scene/Scene Distance Interest Management")] + public class SceneDistanceInterestManagement : InterestManagement + { + [Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")] + public int visRange = 500; + + [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] + public float rebuildInterval = 1; + double lastRebuildTime; + + // cache custom ranges to avoid runtime TryGetComponent lookups + readonly Dictionary CustomRanges = new Dictionary(); + + // helper function to get vis range for a given object, or default. + [ServerCallback] + int GetVisRange(NetworkIdentity identity) + { + return CustomRanges.TryGetValue(identity, out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange; + } + + [ServerCallback] + public override void ResetState() + { + lastRebuildTime = 0D; + CustomRanges.Clear(); + } + + // Use Scene instead of string scene.name because when additively + // loading multiples of a subscene the name won't be unique + readonly Dictionary> sceneObjects = + new Dictionary>(); + + readonly Dictionary lastObjectScene = + new Dictionary(); + + HashSet dirtyScenes = new HashSet(); + + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) + { + if (identity.TryGetComponent(out DistanceInterestManagementCustomRange custom)) + CustomRanges[identity] = custom; + + Scene currentScene = identity.gameObject.scene; + lastObjectScene[identity] = currentScene; + // Debug.Log($"SceneInterestManagement.OnSpawned({identity.name}) currentScene: {currentScene}"); + if (!sceneObjects.TryGetValue(currentScene, out HashSet objects)) + { + objects = new HashSet(); + sceneObjects.Add(currentScene, objects); + } + + objects.Add(identity); + } + + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) + { + CustomRanges.Remove(identity); + + // 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 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); + if (sceneObjects.TryGetValue(currentScene, out HashSet objects) && objects.Remove(identity)) + dirtyScenes.Add(currentScene); + } + } + + [ServerCallback] + void LateUpdate() + { + // for each spawned: + // if scene changed: + // add previous to dirty + // add new to dirty + // else + // if rebuild interval reached: + // rebuild all + foreach (NetworkIdentity identity in NetworkServer.spawned.Values) + { + if (!lastObjectScene.TryGetValue(identity, out Scene currentScene)) + continue; + + Scene newScene = identity.gameObject.scene; + if (newScene == currentScene) + { + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + + // no scene change, so we're done here + continue; + } + + // Mark new/old scenes as dirty so they get rebuilt + dirtyScenes.Add(currentScene); + dirtyScenes.Add(newScene); + + // This object is in a new scene so observers in the prior scene + // and the new scene need to rebuild their respective observers lists. + + // Remove this object from the hashset of the scene it just left + sceneObjects[currentScene].Remove(identity); + + // Set this to the new scene this object just entered + lastObjectScene[identity] = newScene; + + // Make sure this new scene is in the dictionary + if (!sceneObjects.ContainsKey(newScene)) + sceneObjects.Add(newScene, new HashSet()); + + // Add this object to the hashset of the new scene + sceneObjects[newScene].Add(identity); + } + + // rebuild all dirty scenes + foreach (Scene dirtyScene in dirtyScenes) + RebuildSceneObservers(dirtyScene); + + dirtyScenes.Clear(); + } + + void RebuildSceneObservers(Scene scene) + { + foreach (NetworkIdentity netIdentity in sceneObjects[scene]) + if (netIdentity != null) + NetworkServer.RebuildObservers(netIdentity, false); + } + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Check for scene match first, then distance + if (identity.gameObject.scene != newObserver.identity.gameObject.scene) return false; + + int range = GetVisRange(identity); + return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range; + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // abort if no entry in sceneObjects yet (created in OnSpawned) + if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet objects)) + return; + + int range = GetVisRange(identity); + Vector3 position = identity.transform.position; + + // Add everything in the hashset for this object's current scene if within range + foreach (NetworkIdentity networkIdentity in objects) + if (networkIdentity != null && networkIdentity.connectionToClient != null) + { + // brute force distance check + // -> only player connections can be observers, so it's enough if we + // go through all connections instead of all spawned identities. + // -> compared to UNET's sphere cast checking, this one is orders of + // magnitude faster. if we have 10k monsters and run a sphere + // cast 10k times, we will see a noticeable lag even with physics + // layers. but checking to every connection is fast. + NetworkConnectionToClient conn = networkIdentity.connectionToClient; + if (conn != null && conn.isAuthenticated && conn.identity != null) + if (Vector3.Distance(conn.identity.transform.position, position) < range) + newObservers.Add(conn); + } + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs.meta new file mode 100644 index 0000000..3f74f73 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6471bc9a8f893944783fd54e9bfb6ed2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SceneDistance/SceneDistanceInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta index 00f5cd6..6a35e1e 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: cfa12b73503344d49b398b01bcb07967 -timeCreated: 1613110634 \ No newline at end of file +timeCreated: 1613110634 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs index 88f7197..d557713 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs @@ -1,11 +1,13 @@ // Grid2D from uMMORPG: get/set values of type T at any point // -> not named 'Grid' because Unity already has a Grid type. causes warnings. +// -> struct to avoid memory indirection. it's accessed a lot. using System.Collections.Generic; using UnityEngine; namespace Mirror { - public class Grid2D + // struct to avoid memory indirection. it's accessed a lot. + public struct Grid2D { // the grid // note that we never remove old keys. @@ -16,21 +18,27 @@ public class Grid2D // => makes the code a lot easier too // => this is FINE because in the worst case, every grid position in the // game world is filled with a player anyway! - Dictionary> grid = new Dictionary>(); + readonly Dictionary> grid; // cache a 9 neighbor grid of vector2 offsets so we can use them more easily - Vector2Int[] neighbourOffsets = + readonly Vector2Int[] neighbourOffsets; + + public Grid2D(int initialCapacity) { - Vector2Int.up, - Vector2Int.up + Vector2Int.left, - Vector2Int.up + Vector2Int.right, - Vector2Int.left, - Vector2Int.zero, - Vector2Int.right, - Vector2Int.down, - Vector2Int.down + Vector2Int.left, - Vector2Int.down + Vector2Int.right - }; + grid = new Dictionary>(initialCapacity); + + neighbourOffsets = new[] { + Vector2Int.up, + Vector2Int.up + Vector2Int.left, + Vector2Int.up + Vector2Int.right, + Vector2Int.left, + Vector2Int.zero, + Vector2Int.right, + Vector2Int.down, + Vector2Int.down + Vector2Int.left, + Vector2Int.down + Vector2Int.right + }; + } // helper function so we can add an entry without worrying public void Add(Vector2Int position, T value) @@ -38,7 +46,15 @@ public void Add(Vector2Int position, T value) // initialize set in grid if it's not in there yet if (!grid.TryGetValue(position, out HashSet hashSet)) { + // each grid entry may hold hundreds of entities. + // let's create the HashSet with a large initial capacity + // in order to avoid resizing & allocations. +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have "new HashSet(capacity)" yet hashSet = new HashSet(); +#else + hashSet = new HashSet(128); +#endif grid[position] = hashSet; } diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta index f1d3cf0..b8dd422 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs new file mode 100644 index 0000000..64ca497 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs @@ -0,0 +1,106 @@ +// Grid3D based on Grid2D +// -> not named 'Grid' because Unity already has a Grid type. causes warnings. +// -> struct to avoid memory indirection. it's accessed a lot. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // struct to avoid memory indirection. it's accessed a lot. + public struct Grid3D + { + // the grid + // note that we never remove old keys. + // => over time, HashSets will be allocated for every possible + // grid position in the world + // => Clear() doesn't clear them so we don't constantly reallocate the + // entries when populating the grid in every Update() call + // => makes the code a lot easier too + // => this is FINE because in the worst case, every grid position in the + // game world is filled with a player anyway! + readonly Dictionary> grid; + + // cache a 9 x 3 neighbor grid of vector3 offsets so we can use them more easily + readonly Vector3Int[] neighbourOffsets; + + public Grid3D(int initialCapacity) + { + grid = new Dictionary>(initialCapacity); + + neighbourOffsets = new Vector3Int[9 * 3]; + int i = 0; + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + for (int z = -1; z <= 1; z++) + { + neighbourOffsets[i] = new Vector3Int(x, y, z); + i += 1; + } + } + } + } + + // helper function so we can add an entry without worrying + public void Add(Vector3Int position, T value) + { + // initialize set in grid if it's not in there yet + if (!grid.TryGetValue(position, out HashSet hashSet)) + { + // each grid entry may hold hundreds of entities. + // let's create the HashSet with a large initial capacity + // in order to avoid resizing & allocations. +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have "new HashSet(capacity)" yet + hashSet = new HashSet(); +#else + hashSet = new HashSet(128); +#endif + grid[position] = hashSet; + } + + // add to it + hashSet.Add(value); + } + + // helper function to get set at position without worrying + // -> result is passed as parameter to avoid allocations + // -> result is not cleared before. this allows us to pass the HashSet from + // GetWithNeighbours and avoid .UnionWith which is very expensive. + void GetAt(Vector3Int position, HashSet result) + { + // return the set at position + if (grid.TryGetValue(position, out HashSet hashSet)) + { + foreach (T entry in hashSet) + result.Add(entry); + } + } + + // helper function to get at position and it's 8 neighbors without worrying + // -> result is passed as parameter to avoid allocations + public void GetWithNeighbours(Vector3Int position, HashSet result) + { + // clear result first + result.Clear(); + + // add neighbours + foreach (Vector3Int offset in neighbourOffsets) + GetAt(position + offset, result); + } + + // clear: clears the whole grid + // IMPORTANT: we already allocated HashSets and don't want to do + // reallocate every single update when we rebuild the grid. + // => so simply remove each position's entries, but keep + // every position in there + // => see 'grid' comments above! + // => named ClearNonAlloc to make it more obvious! + public void ClearNonAlloc() + { + foreach (HashSet hashSet in grid.Values) + hashSet.Clear(); + } + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs.meta new file mode 100644 index 0000000..a3268e7 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: b157c08313c64752b0856469b1b70771 +timeCreated: 1713533175 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/Grid3D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs new file mode 100644 index 0000000..29bd6ce --- /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 0000000..83542fb --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e9b8dc0273250624c91b6681065741ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs new file mode 100644 index 0000000..b7a0fd3 --- /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 0000000..79a4425 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 9c4fe05752c9a85458b8e44611fe832b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexGrid3D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs new file mode 100644 index 0000000..a394ce0 --- /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 0000000..3bcfc72 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 9b8b055f11f85ff428da471a0e625dd4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash2DInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs new file mode 100644 index 0000000..64415da --- /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/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta new file mode 100644 index 0000000..9931906 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 58e492e77a2a1a3488412ceed5c2aa2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/HexSpatialHash3DInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs new file mode 100644 index 0000000..1953de7 --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs @@ -0,0 +1,146 @@ +// extremely fast spatial hashing interest management based on uMMORPG GridChecker. +// => 30x faster in initial tests +// => scales way higher +// checks on three dimensions (XYZ) which includes the vertical axes. +// this is slower than XY checking for regular spatial hashing. +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")] + public class SpatialHashing3DInterestManagement : InterestManagement + { + [Tooltip("The maximum range that objects will be visible at.")] + public int visRange = 30; + + // we use a 9 neighbour grid. + // so we always see in a distance of 2 grids. + // for example, our own grid and then one on top / below / left / right. + // + // this means that grid resolution needs to be distance / 2. + // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // + // on first sight, it seems we need distance / 3 (we see left/us/right). + // but that's not the case. + // resolution would be 10, and we only see 1 cell far, so 10+10=20. + public int resolution => visRange / 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance + + [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] + public float rebuildInterval = 1; + double lastRebuildTime; + + [Header("Debug Settings")] + public bool showSlider; + + // the grid + // begin with a large capacity to avoid resizing & allocations. + Grid3D grid = new Grid3D(1024); + + // project 3d world position to grid position + Vector3Int ProjectToGrid(Vector3 position) => + Vector3Int.RoundToInt(position / resolution); + + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // calculate projected positions + Vector3Int projected = ProjectToGrid(identity.transform.position); + Vector3Int observerProjected = ProjectToGrid(newObserver.identity.transform.position); + + // distance needs to be at max one of the 8 neighbors, which is + // 1 for the direct neighbors + // 1.41 for the diagonal neighbors (= sqrt(2)) + // => use sqrMagnitude and '2' to avoid computations. same result. + return (projected - observerProjected).sqrMagnitude <= 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance + } + + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // add everyone in 9 neighbour grid + // -> pass observers to GetWithNeighbours directly to avoid allocations + // and expensive .UnionWith computations. + Vector3Int current = ProjectToGrid(identity.transform.position); + grid.GetWithNeighbours(current, newObservers); + } + + [ServerCallback] + public override void ResetState() + { + lastRebuildTime = 0D; + } + + // update everyone's position in the grid + // (internal so we can update from tests) + [ServerCallback] + internal void Update() + { + // NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL + // entities every INTERVAL. consider the other approach later. + + // IMPORTANT: refresh grid every update! + // => newly spawned entities get observers assigned via + // OnCheckObservers. this can happen any time and we don't want + // them broadcast to old (moved or destroyed) connections. + // => players do move all the time. we want them to always be in the + // correct grid position. + // => note that the actual 'rebuildall' doesn't need to happen all + // the time. + // NOTE: consider refreshing grid only every 'interval' too. but not + // for now. stability & correctness matter. + + // clear old grid results before we update everyone's position. + // (this way we get rid of destroyed connections automatically) + // + // NOTE: keeps allocated HashSets internally. + // clearing & populating every frame works without allocations + grid.ClearNonAlloc(); + + // put every connection into the grid at it's main player's position + // NOTE: player sees in a radius around him. NOT around his pet too. + foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values) + { + // authenticated and joined world with a player? + if (connection.isAuthenticated && connection.identity != null) + { + // calculate current grid position + Vector3Int position = ProjectToGrid(connection.identity.transform.position); + + // put into grid + grid.Add(position, connection); + } + } + + // rebuild all spawned entities' observers every 'interval' + // this will call OnRebuildObservers which then returns the + // observers at grid[position] for each entity. + if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval) + { + RebuildAll(); + lastRebuildTime = NetworkTime.localTime; + } + } + +// OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // slider from dotsnet. it's nice to play around with in the benchmark + // demo. + void OnGUI() + { + if (!showSlider) return; + + // only show while server is running. not on client, etc. + if (!NetworkServer.active) return; + + int height = 30; + int width = 250; + GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height)); + GUILayout.BeginHorizontal("Box"); + GUILayout.Label("Radius:"); + visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150))); + GUILayout.Label(visRange.ToString()); + GUILayout.EndHorizontal(); + GUILayout.EndArea(); + } +#endif + } +} diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta new file mode 100644 index 0000000..dfd947a --- /dev/null +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 120b4d6121d94e0280cd2ec536b0ea8f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashing3DInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs index eb4c2c5..0cb5e23 100644 --- a/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/SpatialHashing/SpatialHashingInterestManagement.cs @@ -1,6 +1,8 @@ // extremely fast spatial hashing interest management based on uMMORPG GridChecker. // => 30x faster in initial tests // => scales way higher +// checks on two dimensions only(!), for example: XZ for 3D games or XY for 2D games. +// this is faster than XYZ checking but doesn't check vertical distance. using System.Collections.Generic; using UnityEngine; @@ -12,8 +14,17 @@ public class SpatialHashingInterestManagement : InterestManagement [Tooltip("The maximum range that objects will be visible at.")] public int visRange = 30; - // if we see 8 neighbors then 1 entry is visRange/3 - public int resolution => visRange / 3; + // we use a 9 neighbour grid. + // so we always see in a distance of 2 grids. + // for example, our own grid and then one on top / below / left / right. + // + // this means that grid resolution needs to be distance / 2. + // so for example, for distance = 30 we see 2 cells = 15 * 2 distance. + // + // on first sight, it seems we need distance / 3 (we see left/us/right). + // but that's not the case. + // resolution would be 10, and we only see 1 cell far, so 10+10=20. + public int resolution => visRange / 2; [Tooltip("Rebuild all every 'rebuildInterval' seconds.")] public float rebuildInterval = 1; @@ -27,11 +38,12 @@ public enum CheckMethod [Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")] public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D; - // debugging + [Header("Debug Settings")] public bool showSlider; // the grid - Grid2D grid = new Grid2D(); + // begin with a large capacity to avoid resizing & allocations. + Grid2D grid = new Grid2D(1024); // project 3d world position to grid position Vector2Int ProjectToGrid(Vector3 position) => @@ -62,7 +74,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet _teamId; + set + { + if (Application.IsPlaying(gameObject) && !NetworkServer.active) + throw new InvalidOperationException("teamId can only be set at runtime on active server"); + + if (_teamId == value) + return; + + string oldTeam = _teamId; + _teamId = value; + + //Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement + if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement) + teamInterestManagement.OnTeamChanged(this, oldTeam); + } + } [Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")] public bool forceShown; diff --git a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta index ca75a7a..71ad16a 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Team/NetworkTeam.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs index 22a8eb0..cb5bdb7 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using UnityEngine; namespace Mirror @@ -6,148 +6,143 @@ namespace Mirror [AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")] public class TeamInterestManagement : InterestManagement { - readonly Dictionary> teamObjects = - new Dictionary>(); - - readonly Dictionary lastObjectTeam = - new Dictionary(); + readonly Dictionary> teamObjects = + new Dictionary>(); readonly HashSet dirtyTeams = new HashSet(); - public override void OnSpawned(NetworkIdentity identity) + // LateUpdate so that all spawns/despawns/changes are done + [ServerCallback] + void LateUpdate() { - if (!identity.TryGetComponent(out NetworkTeam networkTeam)) - return; - - string currentTeam = networkTeam.teamId; - lastObjectTeam[identity] = currentTeam; - - // string.Empty is never a valid teamId...do not add to teamObjects collection - if (currentTeam == string.Empty) - return; - - // Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentTeam}"); - if (!teamObjects.TryGetValue(currentTeam, out HashSet objects)) + // Rebuild all dirty teams + // dirtyTeams will be empty if no teams changed members + // by spawning or destroying or changing teamId in this frame. + foreach (string dirtyTeam in dirtyTeams) { - objects = new HashSet(); - teamObjects.Add(currentTeam, objects); + // rebuild always, even if teamObjects[dirtyTeam] is empty. + // Players might have left the team, but they may still be spawned. + RebuildTeamObservers(dirtyTeam); + + // clean up empty entries in the dict + if (teamObjects[dirtyTeam].Count == 0) + teamObjects.Remove(dirtyTeam); } - objects.Add(identity); + dirtyTeams.Clear(); } - public override void OnDestroyed(NetworkIdentity identity) + [ServerCallback] + void RebuildTeamObservers(string teamId) { - lastObjectTeam.TryGetValue(identity, out string currentTeam); - lastObjectTeam.Remove(identity); - if (currentTeam != string.Empty && teamObjects.TryGetValue(currentTeam, out HashSet objects) && objects.Remove(identity)) - RebuildTeamObservers(currentTeam); + foreach (NetworkTeam networkTeam in teamObjects[teamId]) + if (networkTeam.netIdentity != null) + NetworkServer.RebuildObservers(networkTeam.netIdentity, false); } - // internal so we can update from tests + // called by NetworkTeam.teamId setter [ServerCallback] - internal void Update() + internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam) { - // for each spawned: - // if team changed: - // add previous to dirty - // add new to dirty - foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values) - { - // Ignore objects that don't have a NetworkTeam component - if (!netIdentity.TryGetComponent(out NetworkTeam networkTeam)) - continue; - - string newTeam = networkTeam.teamId; - if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam)) - continue; + // This object is in a new team so observers in the prior team + // and the new team need to rebuild their respective observers lists. - // string.Empty is never a valid teamId - // Nothing to do if teamId hasn't changed - if (string.IsNullOrWhiteSpace(newTeam) || newTeam == currentTeam) - continue; + // Remove this object from the hashset of the team it just left + // Null / Empty string is never a valid teamId + if (!string.IsNullOrWhiteSpace(oldTeam)) + { + dirtyTeams.Add(oldTeam); + teamObjects[oldTeam].Remove(networkTeam); + } - // Mark new/old Teams as dirty so they get rebuilt - UpdateDirtyTeams(newTeam, currentTeam); + // Null / Empty string is never a valid teamId + if (string.IsNullOrWhiteSpace(networkTeam.teamId)) + return; - // This object is in a new team so observers in the prior team - // and the new team need to rebuild their respective observers lists. - UpdateTeamObjects(netIdentity, newTeam, currentTeam); - } + dirtyTeams.Add(networkTeam.teamId); - // rebuild all dirty teams - foreach (string dirtyTeam in dirtyTeams) - RebuildTeamObservers(dirtyTeam); + // Make sure this new team is in the dictionary + if (!teamObjects.ContainsKey(networkTeam.teamId)) + teamObjects[networkTeam.teamId] = new HashSet(); - dirtyTeams.Clear(); + // Add this object to the hashset of the new team + teamObjects[networkTeam.teamId].Add(networkTeam); } - void UpdateDirtyTeams(string newTeam, string currentTeam) + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) { - // string.Empty is never a valid teamId - if (currentTeam != string.Empty) - dirtyTeams.Add(currentTeam); + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) + return; - dirtyTeams.Add(newTeam); - } + string networkTeamId = networkTeam.teamId; - void UpdateTeamObjects(NetworkIdentity netIdentity, string newTeam, string currentTeam) - { - // Remove this object from the hashset of the team it just left - // string.Empty is never a valid teamId - if (!string.IsNullOrWhiteSpace(currentTeam)) - teamObjects[currentTeam].Remove(netIdentity); + // Null / Empty string is never a valid teamId...do not add to teamObjects collection + if (string.IsNullOrWhiteSpace(networkTeamId)) + return; - // Set this to the new team this object just entered - lastObjectTeam[netIdentity] = newTeam; + // Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}"); + if (!teamObjects.TryGetValue(networkTeamId, out HashSet objects)) + { + objects = new HashSet(); + teamObjects.Add(networkTeamId, objects); + } - // Make sure this new team is in the dictionary - if (!teamObjects.ContainsKey(newTeam)) - teamObjects.Add(newTeam, new HashSet()); + objects.Add(networkTeam); - // Add this object to the hashset of the new team - teamObjects[newTeam].Add(netIdentity); + // Team ID could have been set in NetworkBehaviour::OnStartServer on this object. + // Since that's after OnCheckObserver is called it would be missed, so force Rebuild here. + // Add the current team to dirtyTeames for LateUpdate to rebuild it. + dirtyTeams.Add(networkTeamId); } - void RebuildTeamObservers(string teamId) + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) { - foreach (NetworkIdentity netIdentity in teamObjects[teamId]) - if (netIdentity != null) - NetworkServer.RebuildObservers(netIdentity, false); + // 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 LateUpdate do it once. + // We must add the current team to dirtyTeames for LateUpdate to rebuild it. + if (identity.TryGetComponent(out NetworkTeam currentTeam)) + { + if (!string.IsNullOrWhiteSpace(currentTeam.teamId) && + teamObjects.TryGetValue(currentTeam.teamId, out HashSet objects) && + objects.Remove(currentTeam)) + dirtyTeams.Add(currentTeam.teamId); + } } public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) { // Always observed if no NetworkTeam component - if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) + if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam)) return true; if (identityNetworkTeam.forceShown) return true; - // string.Empty is never a valid teamId + // Null / Empty string is never a valid teamId if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId)) return false; // Always observed if no NetworkTeam component - if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam)) + if (!newObserver.identity.TryGetComponent(out NetworkTeam newObserverNetworkTeam)) return true; - if (newObserverNetworkTeam.forceShown) - return true; - - // string.Empty is never a valid teamId + // Null / Empty string is never a valid teamId if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId)) return false; - // Observed only if teamId's match + //Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}"); + + // Observed only if teamId's team return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId; } public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) { // If this object doesn't have a NetworkTeam then it's visible to all clients - if (!identity.TryGetComponent(out NetworkTeam networkTeam)) + if (!identity.TryGetComponent(out NetworkTeam networkTeam)) { AddAllConnections(newObservers); return; @@ -160,18 +155,18 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet objects)) + // Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged + if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet objects)) return; // Add everything in the hashset for this object's current team - foreach (NetworkIdentity networkIdentity in objects) - if (networkIdentity != null && networkIdentity.connectionToClient != null) - newObservers.Add(networkIdentity.connectionToClient); + foreach (NetworkTeam netTeam in objects) + if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null) + newObservers.Add(netTeam.netIdentity.connectionToClient); } void AddAllConnections(HashSet newObservers) diff --git a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta index 6e8748a..2bf3f09 100644 --- a/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta +++ b/Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/InterestManagement/Team/TeamInterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform2k.meta b/Assets/Mirror/Components/LagCompensation.meta similarity index 77% rename from Assets/Mirror/Components/NetworkTransform2k.meta rename to Assets/Mirror/Components/LagCompensation.meta index fe99bf0..669a5b8 100644 --- a/Assets/Mirror/Components/NetworkTransform2k.meta +++ b/Assets/Mirror/Components/LagCompensation.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 44e823b93c7d2477c8796766dc364c59 +guid: 00ac1d0527f234939aba22b4d7cbf280 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs new file mode 100644 index 0000000..876eb18 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs @@ -0,0 +1,109 @@ +// Applies HistoryBounds to the physics world by projecting to a trigger Collider. +// This way we can use Physics.Raycast on it. +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/ Lag Compensation/ History Collider")] + public class HistoryCollider : MonoBehaviour + { + [Header("Components")] + [Tooltip("The object's actual collider. We need to know where it is, and how large it is.")] + public Collider actualCollider; + + [Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")] + public BoxCollider boundsCollider; + + [Header("History")] + [Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")] + public int boundsLimit = 8; + + [Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")] + public int boundsPerBucket = 2; + + [Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")] + public float captureInterval = 0.100f; // 100 ms + double lastCaptureTime = 0; + + [Header("Debug")] + public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f); + public Color currentColor = Color.red; + + protected HistoryBounds history = null; + + protected virtual void Awake() + { + history = new HistoryBounds(boundsLimit, boundsPerBucket); + + // ensure colliders were set. + // bounds collider should always be a trigger. + if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set."); + if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set."); + if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject."); + if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger."); + } + + // capturing and projecting onto colliders should use physics update + protected virtual void FixedUpdate() + { + // capture current bounds every interval + if (NetworkTime.localTime >= lastCaptureTime + captureInterval) + { + lastCaptureTime = NetworkTime.localTime; + CaptureBounds(); + } + + // project bounds onto helper collider + ProjectBounds(); + } + + protected virtual void CaptureBounds() + { + // grab current collider bounds + // this is in world space coordinates, and axis aligned + // TODO double check + Bounds bounds = actualCollider.bounds; + + // insert into history + history.Insert(bounds); + } + + protected virtual void ProjectBounds() + { + // grab total collider encapsulating all of history + Bounds total = history.total; + + // don't assign empty bounds, this will throw a Unity warning + if (history.boundsCount == 0) return; + + // scale projection doesn't work yet. + // for now, don't allow scale changes. + if (transform.lossyScale != Vector3.one) + { + Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1)."); + return; + } + + // counter rotate the child collider against the gameobject's rotation. + // we need this to always be axis aligned. + boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation); + + // project world space bounds to collider's local space + boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center); + boundsCollider.size = total.size; // TODO projection? + } + + // TODO runtime drawing for debugging? + protected virtual void OnDrawGizmos() + { + // draw total bounds + Gizmos.color = historyColor; + Gizmos.DrawWireCube(history.total.center, history.total.size); + + // draw current bounds + Gizmos.color = currentColor; + Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size); + } + } +} diff --git a/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs.meta b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs.meta new file mode 100644 index 0000000..e267f0a --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/HistoryCollider.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: f5f2158d9776d4b569858f793be4da60 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/LagCompensation/HistoryCollider.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/LagCompensation/LagCompensator.cs b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs new file mode 100644 index 0000000..63669c0 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs @@ -0,0 +1,197 @@ +// Add this component to a Player object with collider. +// Automatically keeps a history for lag compensation. +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public struct Capture3D : Capture + { + public double timestamp { get; set; } + public Vector3 position; + public Vector3 size; + + public Capture3D(double timestamp, Vector3 position, Vector3 size) + { + this.timestamp = timestamp; + this.position = position; + this.size = size; + } + + public void DrawGizmo() + { + Gizmos.DrawWireCube(position, size); + } + + public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) => + new Capture3D( + 0, // interpolated snapshot is applied directly. don't need timestamps. + Vector3.LerpUnclamped(from.position, to.position, (float)t), + Vector3.LerpUnclamped(from.size, to.size, (float)t) + ); + + public override string ToString() => $"(time={timestamp} pos={position} size={size})"; + } + + [DisallowMultipleComponent] + [AddComponentMenu("Network/ Lag Compensation/ Lag Compensator")] + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation")] + public class LagCompensator : NetworkBehaviour + { + [Header("Components")] + [Tooltip("The collider to keep a history of.")] + public Collider trackedCollider; // assign this in inspector + + [Header("Settings")] + public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings(); + double lastCaptureTime; + + // lag compensation history of + readonly Queue> history = new Queue>(); + + [Header("Debugging")] + public Color historyColor = Color.white; + + [ServerCallback] + protected virtual void Update() + { + // capture lag compensation snapshots every interval. + // NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet. + if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval) + { + lastCaptureTime = NetworkTime.localTime; + Capture(); + } + } + + [ServerCallback] + protected virtual void Capture() + { + // capture current state + Capture3D capture = new Capture3D( + NetworkTime.localTime, + trackedCollider.bounds.center, + trackedCollider.bounds.size + ); + + // insert into history + LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture); + } + + protected virtual void OnDrawGizmos() + { + // draw history + Gizmos.color = historyColor; + LagCompensation.DrawGizmos(history); + } + + // sampling //////////////////////////////////////////////////////////// + // sample the sub-tick (=interpolated) history of this object for a hit test. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + [ServerCallback] + public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample) + { + // never trust the client: estimate client time instead. + // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking + // the estimation is very good. the error is as low as ~6ms for the demo. + // note that passing 'rtt' is fine: EstimateTime halves it to latency. + double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime); + + // sample the history to get the nearest snapshots around 'timestamp' + if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t)) + { + // interpolate to get a decent estimation at exactly 'timestamp' + sample = Capture3D.Interpolate(resultBefore, resultAfter, t); + return true; + } + else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}"); + + sample = default; + return false; + } + + // convenience tests /////////////////////////////////////////////////// + // there are multiple different ways to check a hit against the sample: + // - raycasting + // - bounds.contains + // - increasing bounds by tolerance and checking contains + // - threshold to bounds.closestpoint + // let's offer a few solutions directly and see which users prefer. + + // bounds check: checks distance to closest point on bounds in history @ -rtt. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + // this is super simple and fast, but not 100% physically accurate since we don't raycast. + [ServerCallback] + public virtual bool BoundsCheck( + NetworkConnectionToClient viewer, + Vector3 hitPoint, + float toleranceDistance, + out float distance, + out Vector3 nearest) + { + // first, sample the history at -rtt of the viewer. + if (Sample(viewer, out Capture3D capture)) + { + // now that we know where the other player was at that time, + // we can see if the hit point was within tolerance of it. + // TODO consider rotations??? + // TODO consider original collider shape?? + Bounds bounds = new Bounds(capture.position, capture.size); + nearest = bounds.ClosestPoint(hitPoint); + distance = Vector3.Distance(nearest, hitPoint); + return distance <= toleranceDistance; + } + nearest = hitPoint; + distance = 0; + return false; + } + + // raycast check: creates a collider the sampled position and raycasts to hitPoint. + // 'viewer' needs to be the player who fired! + // for example, if A fires at B, then call B.Sample(viewer, point, tolerance). + // this is physically accurate (checks against walls etc.), with the cost + // of a runtime instantiation. + // + // originPoint: where the player fired the weapon. + // hitPoint: where the player's local raycast hit. + // tolerance: scale up the sampled collider by % in order to have a bit of a tolerance. + // 0 means no extra tolerance, 0.05 means 5% extra tolerance. + // layerMask: the layer mask to use for the raycast. + [ServerCallback] + public virtual bool RaycastCheck( + NetworkConnectionToClient viewer, + Vector3 originPoint, + Vector3 hitPoint, + float tolerancePercent, + int layerMask, + out RaycastHit hit) + { + // first, sample the history at -rtt of the viewer. + if (Sample(viewer, out Capture3D capture)) + { + // instantiate a real physics collider on demand. + // TODO rotation?? + // TODO different collier types?? + GameObject temp = new GameObject("LagCompensatorTest"); + temp.transform.position = capture.position; + BoxCollider tempCollider = temp.AddComponent(); + tempCollider.size = capture.size * (1 + tolerancePercent); + + // raycast + Vector3 direction = hitPoint - originPoint; + float maxDistance = direction.magnitude * 2; + bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask); + + // cleanup + Destroy(temp); + return result; + } + + hit = default; + return false; + } + } +} diff --git a/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta new file mode 100644 index 0000000..e13b9c0 --- /dev/null +++ b/Assets/Mirror/Components/LagCompensation/LagCompensator.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a898831dd60c4cdfbfd9a6ea5702ed01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/LagCompensation/LagCompensator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Mirror.Components.asmdef b/Assets/Mirror/Components/Mirror.Components.asmdef index a61c7db..90c360e 100644 --- a/Assets/Mirror/Components/Mirror.Components.asmdef +++ b/Assets/Mirror/Components/Mirror.Components.asmdef @@ -1,14 +1,16 @@ { "name": "Mirror.Components", + "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], - "optionalUnityReferences": [], "includePlatforms": [], "excludePlatforms": [], - "allowUnsafeCode": false, + "allowUnsafeCode": true, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [] + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file diff --git a/Assets/Mirror/Components/Mirror.Components.asmdef.meta b/Assets/Mirror/Components/Mirror.Components.asmdef.meta index 263b6f0..79c4587 100644 --- a/Assets/Mirror/Components/Mirror.Components.asmdef.meta +++ b/Assets/Mirror/Components/Mirror.Components.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Mirror.Components.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkAnimator.cs b/Assets/Mirror/Components/NetworkAnimator.cs index 2360fe3..b573ad7 100644 --- a/Assets/Mirror/Components/NetworkAnimator.cs +++ b/Assets/Mirror/Components/NetworkAnimator.cs @@ -13,8 +13,8 @@ namespace Mirror /// If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit. /// The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically synchronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients. /// + // [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours [AddComponentMenu("Network/Network Animator")] - [RequireComponent(typeof(NetworkIdentity))] [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")] public class NetworkAnimator : NetworkBehaviour { @@ -31,11 +31,12 @@ public class NetworkAnimator : NetworkBehaviour public Animator animator; /// - /// Syncs animator.speed + /// Syncs animator.speed. + /// Default to 1 because Animator.speed defaults to 1. /// [SyncVar(hook = nameof(OnAnimatorSpeedChanged))] - float animatorSpeed; - float previousSpeed; + float animatorSpeed = 1f; + float previousSpeed = 1f; // Note: not an object[] array because otherwise initialization is real annoying int[] lastIntParameters; @@ -67,11 +68,11 @@ bool SendMessagesAllowed return true; } - return (hasAuthority && clientAuthority); + return (isOwned && clientAuthority); } } - void Awake() + void Initialize() { // store the animator parameters in a variable - the "Animator.parameters" getter allocates // a new parameter array every time it is accessed so we should avoid doing it in a loop @@ -87,6 +88,17 @@ void Awake() layerWeight = new float[animator.layerCount]; } + // fix https://github.com/MirrorNetworking/Mirror/issues/2810 + // both Awake and Enable need to initialize arrays. + // in case users call SetActive(false) -> SetActive(true). + void Awake() => Initialize(); + void OnEnable() => Initialize(); + + public virtual void Reset() + { + syncDirection = SyncDirection.ClientToServer; + } + void FixedUpdate() { if (!SendMessagesAllowed) @@ -137,7 +149,7 @@ void OnAnimatorSpeedChanged(float _, float value) { // skip if host or client with authority // they will have already set the speed so don't set again - if (isServer || (hasAuthority && clientAuthority)) + if (isServer || (isOwned && clientAuthority)) return; animator.speed = value; @@ -227,7 +239,7 @@ void SendAnimationParametersMessage(byte[] parameters) void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weight, NetworkReader reader) { - if (hasAuthority && clientAuthority) + if (isOwned && clientAuthority) return; // usually transitions will be triggered by parameters, if not, play anims directly. @@ -245,7 +257,7 @@ void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weigh void HandleAnimParamsMsg(NetworkReader reader) { - if (hasAuthority && clientAuthority) + if (isOwned && clientAuthority) return; ReadParameters(reader); @@ -302,9 +314,18 @@ ulong NextDirtyBits() bool WriteParameters(NetworkWriter writer, bool forceAll = false) { + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize parameterCount to be 100% sure we deserialize correct amount of bytes. + // (255 parameters should be enough for everyone, write it as byte) + byte parameterCount = (byte)parameters.Length; + writer.WriteByte(parameterCount); + ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits(); writer.WriteULong(dirtyBits); - for (int i = 0; i < parameters.Length; i++) + + // iterate on byte count. if it's >256, it won't break + // serialization - just not serialize excess layers. + for (int i = 0; i < parameterCount; i++) { if ((dirtyBits & (1ul << i)) == 0) continue; @@ -331,11 +352,20 @@ bool WriteParameters(NetworkWriter writer, bool forceAll = false) void ReadParameters(NetworkReader reader) { + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize parameterCount to be 100% sure we deserialize correct amount of bytes. + // mismatch shows error to make this super easy to debug. + byte parameterCount = reader.ReadByte(); + if (parameterCount != parameters.Length) + { + Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?", gameObject); + return; + } + bool animatorEnabled = animator.enabled; // need to read values from NetworkReader even if animator is disabled - ulong dirtyBits = reader.ReadULong(); - for (int i = 0; i < parameters.Length; i++) + for (int i = 0; i < parameterCount; i++) { if ((dirtyBits & (1ul << i)) == 0) continue; @@ -362,54 +392,54 @@ void ReadParameters(NetworkReader reader) } } - /// - /// Custom Serialization - /// - /// - /// - /// - public override bool OnSerialize(NetworkWriter writer, bool initialState) + public override void OnSerialize(NetworkWriter writer, bool initialState) { - bool changed = base.OnSerialize(writer, initialState); + base.OnSerialize(writer, initialState); if (initialState) { - for (int i = 0; i < animator.layerCount; i++) + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize layerCount to be 100% sure we deserialize correct amount of bytes. + // (255 layers should be enough for everyone, write it as byte) + byte layerCount = (byte)animator.layerCount; + writer.WriteByte(layerCount); + + // iterate on byte count. if it's >256, it won't break + // serialization - just not serialize excess layers. + for (int i = 0; i < layerCount; i++) { - if (animator.IsInTransition(i)) - { - AnimatorStateInfo st = animator.GetNextAnimatorStateInfo(i); - writer.WriteInt(st.fullPathHash); - writer.WriteFloat(st.normalizedTime); - } - else - { - AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i); - writer.WriteInt(st.fullPathHash); - writer.WriteFloat(st.normalizedTime); - } + AnimatorStateInfo st = animator.IsInTransition(i) + ? animator.GetNextAnimatorStateInfo(i) + : animator.GetCurrentAnimatorStateInfo(i); + writer.WriteInt(st.fullPathHash); + writer.WriteFloat(st.normalizedTime); writer.WriteFloat(animator.GetLayerWeight(i)); } - WriteParameters(writer, initialState); - return true; + WriteParameters(writer, true); } - return changed; } - /// - /// Custom Deserialization - /// - /// - /// public override void OnDeserialize(NetworkReader reader, bool initialState) { base.OnDeserialize(reader, initialState); if (initialState) { - for (int i = 0; i < animator.layerCount; i++) + // fix: https://github.com/MirrorNetworking/Mirror/issues/2852 + // serialize layerCount to be 100% sure we deserialize correct amount of bytes. + // mismatch shows error to make this super easy to debug. + byte layerCount = reader.ReadByte(); + if (layerCount != animator.layerCount) + { + Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?", gameObject); + return; + } + + for (int i = 0; i < layerCount; i++) { int stateHash = reader.ReadInt(); float normalizedTime = reader.ReadFloat(); - animator.SetLayerWeight(i, reader.ReadFloat()); + float weight = reader.ReadFloat(); + + animator.SetLayerWeight(i, weight); animator.Play(stateHash, i, normalizedTime); } @@ -437,13 +467,13 @@ public void SetTrigger(int hash) { if (!isClient) { - Debug.LogWarning("Tried to set animation in the server for a client-controlled animator"); + Debug.LogWarning("Tried to set animation in the server for a client-controlled animator", gameObject); return; } - if (!hasAuthority) + if (!isOwned) { - Debug.LogWarning("Only the client with authority can set animations"); + Debug.LogWarning("Only the client with authority can set animations", gameObject); return; } @@ -457,7 +487,7 @@ public void SetTrigger(int hash) { if (!isServer) { - Debug.LogWarning("Tried to set animation in the client for a server-controlled animator"); + Debug.LogWarning("Tried to set animation in the client for a server-controlled animator", gameObject); return; } @@ -476,9 +506,7 @@ public void ResetTrigger(string triggerName) ResetTrigger(Animator.StringToHash(triggerName)); } - /// - /// Causes an animation trigger to be reset for a networked object. - /// + /// Causes an animation trigger to be reset for a networked object. /// Hash id of trigger (from the Animator). public void ResetTrigger(int hash) { @@ -486,13 +514,13 @@ public void ResetTrigger(int hash) { if (!isClient) { - Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator"); + Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator", gameObject); return; } - if (!hasAuthority) + if (!isOwned) { - Debug.LogWarning("Only the client with authority can reset animations"); + Debug.LogWarning("Only the client with authority can reset animations", gameObject); return; } @@ -506,7 +534,7 @@ public void ResetTrigger(int hash) { if (!isServer) { - Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator"); + Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator", gameObject); return; } @@ -558,7 +586,7 @@ void CmdOnAnimationTriggerServerMessage(int hash) // handle and broadcast // host should have already the trigger - bool isHostOwner = isClient && hasAuthority; + bool isHostOwner = isClient && isOwned; if (!isHostOwner) { HandleAnimTriggerMsg(hash); @@ -576,7 +604,7 @@ void CmdOnAnimationResetTriggerServerMessage(int hash) // handle and broadcast // host should have already the trigger - bool isHostOwner = isClient && hasAuthority; + bool isHostOwner = isClient && isOwned; if (!isHostOwner) { HandleAnimResetTriggerMsg(hash); @@ -611,22 +639,22 @@ void RpcOnAnimationParametersClientMessage(byte[] parameters) HandleAnimParamsMsg(networkReader); } - [ClientRpc] + [ClientRpc(includeOwner = false)] void RpcOnAnimationTriggerClientMessage(int hash) { - // host/owner handles this before it is sent - if (isServer || (clientAuthority && hasAuthority)) return; - - HandleAnimTriggerMsg(hash); + // already handled on server in SetTrigger + // or CmdOnAnimationTriggerServerMessage + if (!isServer) + HandleAnimTriggerMsg(hash); } - [ClientRpc] + [ClientRpc(includeOwner = false)] void RpcOnAnimationResetTriggerClientMessage(int hash) { - // host/owner handles this before it is sent - if (isServer || (clientAuthority && hasAuthority)) return; - - HandleAnimResetTriggerMsg(hash); + // already handled on server in ResetTrigger + // or CmdOnAnimationResetTriggerServerMessage + if (!isServer) + HandleAnimResetTriggerMsg(hash); } #endregion diff --git a/Assets/Mirror/Components/NetworkAnimator.cs.meta b/Assets/Mirror/Components/NetworkAnimator.cs.meta index 211ce78..2e48ed4 100644 --- a/Assets/Mirror/Components/NetworkAnimator.cs.meta +++ b/Assets/Mirror/Components/NetworkAnimator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkAnimator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs new file mode 100644 index 0000000..d3c3632 --- /dev/null +++ b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs @@ -0,0 +1,31 @@ +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Diagnostics Debugger")] + public class NetworkDiagnosticsDebugger : MonoBehaviour + { + public bool logInMessages = true; + public bool logOutMessages = true; + void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo) + { + if (logInMessages) + Debug.Log(msgInfo); + } + void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo) + { + if (logOutMessages) + Debug.Log(msgInfo); + } + void OnEnable() + { + NetworkDiagnostics.InMessageEvent += OnInMessage; + NetworkDiagnostics.OutMessageEvent += OnOutMessage; + } + void OnDisable() + { + NetworkDiagnostics.InMessageEvent -= OnInMessage; + NetworkDiagnostics.OutMessageEvent -= OnOutMessage; + } + } +} diff --git a/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta new file mode 100644 index 0000000..04e4335 --- /dev/null +++ b/Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: bc9f0a0fe4124424b8f9d4927795ee01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkLobbyManager.cs.meta b/Assets/Mirror/Components/NetworkLobbyManager.cs.meta index a32c8c7..5e4ca94 100644 --- a/Assets/Mirror/Components/NetworkLobbyManager.cs.meta +++ b/Assets/Mirror/Components/NetworkLobbyManager.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkLobbyManager.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta b/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta index 7a21eec..23ce3a4 100644 --- a/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta +++ b/Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkLobbyPlayer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs b/Assets/Mirror/Components/NetworkPingDisplay.cs index 61e9241..255e62d 100644 --- a/Assets/Mirror/Components/NetworkPingDisplay.cs +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs @@ -13,20 +13,26 @@ public class NetworkPingDisplay : MonoBehaviour { public Color color = Color.white; public int padding = 2; - int width = 150; - int height = 25; + public int width = 150; + public int height = 25; void OnGUI() { // only while client is active if (!NetworkClient.active) return; - // show rtt in bottom right corner, right aligned + // show stats in bottom right corner, right aligned GUI.color = color; Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height); + GUILayout.BeginArea(rect); GUIStyle style = GUI.skin.GetStyle("Label"); style.alignment = TextAnchor.MiddleRight; - GUI.Label(rect, $"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms", style); + GUILayout.BeginHorizontal(style); + GUILayout.Label($"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms"); + GUI.color = NetworkClient.connectionQuality.ColorCode(); + GUILayout.Label($"Q: {new string('-', (int)NetworkClient.connectionQuality)}"); + GUILayout.EndHorizontal(); + GUILayout.EndArea(); GUI.color = Color.white; } } diff --git a/Assets/Mirror/Components/NetworkPingDisplay.cs.meta b/Assets/Mirror/Components/NetworkPingDisplay.cs.meta index 221a745..c141614 100644 --- a/Assets/Mirror/Components/NetworkPingDisplay.cs.meta +++ b/Assets/Mirror/Components/NetworkPingDisplay.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkPingDisplay.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody.meta b/Assets/Mirror/Components/NetworkRigidbody.meta new file mode 100644 index 0000000..ce84fee --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 80106690aef541a5b8e2f8fb3d5949ad +timeCreated: 1686733778 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs new file mode 100644 index 0000000..a88341a --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody (Reliable)")] + public class NetworkRigidbodyReliable : NetworkTransformReliable + { + 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/NetworkRigidbodyReliable.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs.meta new file mode 100644 index 0000000..0ebea63 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: cb803efbe62c34d7baece46c9ffebad9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs new file mode 100644 index 0000000..9872ccc --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs @@ -0,0 +1,135 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody 2D (Reliable)")] + public class NetworkRigidbodyReliable2D : NetworkTransformReliable + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody2D rb; + bool wasKinematic; + + protected override void OnValidate() + { + 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 NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", 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 NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); + return; + } +#if UNITY_6000_0_OR_NEWER + wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic); +#else + wasKinematic = rb.isKinematic; +#endif + 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. +#if UNITY_6000_0_OR_NEWER + public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; + public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; +#else + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; +#endif + + // 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 UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // 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 UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // 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 UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + } + + 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.eulerAngles.z; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs.meta new file mode 100644 index 0000000..c02ae5d --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyReliable2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs new file mode 100644 index 0000000..e2a34c0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody (Unreliable)")] + public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable + { + 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/NetworkRigidbodyUnreliable.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs.meta new file mode 100644 index 0000000..646d27c --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3b20dc110904e47f8a154cdcf6433eae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs new file mode 100644 index 0000000..6c021f0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs @@ -0,0 +1,136 @@ +using UnityEngine; + +namespace Mirror +{ + // [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target + [AddComponentMenu("Network/Network Rigidbody 2D (Unreliable)")] + public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable + { + bool clientAuthority => syncDirection == SyncDirection.ClientToServer; + + Rigidbody2D 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 NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", 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 NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this); + return; + } + +#if UNITY_6000_0_OR_NEWER + wasKinematic = rb.bodyType.HasFlag(RigidbodyType2D.Kinematic); +#else + wasKinematic = rb.isKinematic; +#endif + 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. +#if UNITY_6000_0_OR_NEWER + public override void OnStopServer() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; + public override void OnStopClient() => rb.bodyType = wasKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic; +#else + public override void OnStopServer() => rb.isKinematic = wasKinematic; + public override void OnStopClient() => rb.isKinematic = wasKinematic; +#endif + // 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 UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // 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 UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + // 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 UNITY_6000_0_OR_NEWER + if (!owned) rb.bodyType = RigidbodyType2D.Kinematic; +#else + if (!owned) rb.isKinematic = true; +#endif + } + } + + 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.eulerAngles.z; + } + } +} diff --git a/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs.meta b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs.meta new file mode 100644 index 0000000..99239af --- /dev/null +++ b/Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 1c7e12ad9b9ae443c9fdf37e9f5ecd36 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRigidbody/NetworkRigidbodyUnreliable2D.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs b/Assets/Mirror/Components/NetworkRoomManager.cs index d432fbb..698b190 100644 --- a/Assets/Mirror/Components/NetworkRoomManager.cs +++ b/Assets/Mirror/Components/NetworkRoomManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -26,7 +25,6 @@ public struct PendingPlayer } [Header("Room Settings")] - [FormerlySerializedAs("m_ShowRoomGUI")] [SerializeField] [Tooltip("This flag controls whether the default UI is shown for the room")] @@ -58,23 +56,22 @@ public struct PendingPlayer /// List of players that are in the Room /// [FormerlySerializedAs("m_PendingPlayers")] - public List pendingPlayers = new List(); + public HashSet pendingPlayers = new HashSet(); [Header("Diagnostics")] - /// /// True when all players have submitted a Ready message /// [Tooltip("Diagnostic flag indicating all players are ready to play")] [FormerlySerializedAs("allPlayersReady")] - [SerializeField] bool _allPlayersReady; + [ReadOnly, SerializeField] bool _allPlayersReady; /// /// These slots track players that enter the room. /// The slotId on players is global to the game - across all players. /// - [Tooltip("List of Room Player objects")] - public List roomSlots = new List(); + [ReadOnly, Tooltip("List of Room Player objects")] + public HashSet roomSlots = new HashSet(); public bool allPlayersReady { @@ -102,8 +99,7 @@ public bool allPlayersReady public override void OnValidate() { - // always >= 0 - maxConnections = Mathf.Max(maxConnections, 0); + base.OnValidate(); // always <= maxConnections minPlayers = Mathf.Min(minPlayers, maxConnections); @@ -120,56 +116,13 @@ public override void OnValidate() Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component."); } } - - base.OnValidate(); - } - - public void ReadyStatusChanged() - { - int CurrentPlayers = 0; - int ReadyPlayers = 0; - - foreach (NetworkRoomPlayer item in roomSlots) - { - if (item != null) - { - CurrentPlayers++; - if (item.readyToBegin) - ReadyPlayers++; - } - } - - if (CurrentPlayers == ReadyPlayers) - CheckReadyToBegin(); - else - allPlayersReady = false; - } - - /// - /// Called on the server when a client is ready. - /// The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process. - /// - /// Connection from client. - public override void OnServerReady(NetworkConnectionToClient conn) - { - Debug.Log($"NetworkRoomManager OnServerReady {conn}"); - base.OnServerReady(conn); - - if (conn != null && conn.identity != null) - { - GameObject roomPlayer = conn.identity.gameObject; - - // if null or not a room player, don't replace it - if (roomPlayer != null && roomPlayer.GetComponent() != null) - SceneLoadedForPlayer(conn, roomPlayer); - } } void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer) { - Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}"); + //Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}"); - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { // cant be ready in room, add to ready list PendingPlayer pending; @@ -193,29 +146,7 @@ void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer) return; // replace room player with game player - NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true); - } - - /// - /// CheckReadyToBegin checks all of the players in the room to see if their readyToBegin flag is set. - /// If all of the players are ready, then the server switches from the RoomScene to the PlayScene, essentially starting the game. This is called automatically in response to NetworkRoomPlayer.CmdChangeReadyState. - /// - public void CheckReadyToBegin() - { - if (!IsSceneActive(RoomScene)) - return; - - int numberOfReadyPlayers = NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.identity.gameObject.GetComponent().readyToBegin); - bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers; - if (enoughReadyPlayers) - { - pendingPlayers.Clear(); - allPlayersReady = true; - } - else - { - allPlayersReady = false; - } + NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, ReplacePlayerOptions.KeepAuthority); } internal void CallOnClientEnterRoom() @@ -238,6 +169,31 @@ internal void CallOnClientExitRoom() } } + /// + /// CheckReadyToBegin checks all of the players in the room to see if their readyToBegin flag is set. + /// If all of the players are ready, then the server switches from the RoomScene to the PlayScene, essentially starting the game. This is called automatically in response to NetworkRoomPlayer.CmdChangeReadyState. + /// + public void CheckReadyToBegin() + { + if (!Utils.IsSceneActive(RoomScene)) + return; + + int numberOfReadyPlayers = NetworkServer.connections.Count(conn => + conn.Value != null && + conn.Value.identity != null && + conn.Value.identity.TryGetComponent(out NetworkRoomPlayer nrp) && + nrp.readyToBegin); + + bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers; + if (enoughReadyPlayers) + { + pendingPlayers.Clear(); + allPlayersReady = true; + } + else + allPlayersReady = false; + } + #region server handlers /// @@ -247,15 +203,10 @@ internal void CallOnClientExitRoom() /// Connection from client. public override void OnServerConnect(NetworkConnectionToClient conn) { - if (numPlayers >= maxConnections) - { - conn.Disconnect(); - return; - } - // cannot join game in progress - if (!IsSceneActive(RoomScene)) + if (!Utils.IsSceneActive(RoomScene)) { + Debug.Log($"Not in Room scene...disconnecting {conn}"); conn.Disconnect(); return; } @@ -278,7 +229,7 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn) if (roomPlayer != null) roomSlots.Remove(roomPlayer); - foreach (NetworkIdentity clientOwnedObject in conn.clientOwnedObjects) + foreach (NetworkIdentity clientOwnedObject in conn.owned) { roomPlayer = clientOwnedObject.GetComponent(); if (roomPlayer != null) @@ -294,21 +245,41 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn) player.GetComponent().readyToBegin = false; } - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) RecalculateRoomPlayerIndices(); OnRoomServerDisconnect(conn); base.OnServerDisconnect(conn); -#if UNITY_SERVER - if (numPlayers < 1) + // Restart the server if we're headless and no players are connected. + // This will send server to offline scene, where auto-start will run. + if (Utils.IsHeadless() && numPlayers < 1) StopServer(); -#endif } // Sequential index used in round-robin deployment of players into instances and score positioning public int clientIndex; + /// + /// Called on the server when a client is ready. + /// The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process. + /// + /// Connection from client. + public override void OnServerReady(NetworkConnectionToClient conn) + { + //Debug.Log($"NetworkRoomManager OnServerReady {conn}"); + base.OnServerReady(conn); + + if (conn != null && conn.identity != null) + { + GameObject roomPlayer = conn.identity.gameObject; + + // if null or not a room player, don't replace it + if (roomPlayer != null && roomPlayer.GetComponent() != null) + SceneLoadedForPlayer(conn, roomPlayer); + } + } + /// /// Called on the server when a client adds a new player with NetworkClient.AddPlayer. /// The default implementation for this function creates a new player object from the playerPrefab. @@ -319,11 +290,8 @@ public override void OnServerAddPlayer(NetworkConnectionToClient conn) // increment the index before adding the player, so first player starts at 1 clientIndex++; - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { - if (roomSlots.Count == maxConnections) - return; - allPlayersReady = false; //Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}"); @@ -335,7 +303,11 @@ public override void OnServerAddPlayer(NetworkConnectionToClient conn) NetworkServer.AddPlayerForConnection(conn, newRoomGameObject); } else - OnRoomServerAddPlayer(conn); + { + // Late joiners not supported...should've been kicked by OnServerDisconnect + Debug.Log($"Not in Room scene...disconnecting {conn}"); + conn.Disconnect(); + } } [Server] @@ -343,10 +315,9 @@ public void RecalculateRoomPlayerIndices() { if (roomSlots.Count > 0) { - for (int i = 0; i < roomSlots.Count; i++) - { - roomSlots[i].index = i; - } + int i = 0; + foreach (NetworkRoomPlayer player in roomSlots) + player.index = i++; } } @@ -371,7 +342,7 @@ public override void ServerChangeScene(string newSceneName) { // re-add the room object roomPlayer.GetComponent().readyToBegin = false; - NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject); + NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject, ReplacePlayerOptions.KeepAuthority); } } @@ -472,10 +443,7 @@ public override void OnStartClient() /// public override void OnClientConnect() { -#pragma warning disable 618 - // obsolete method calls new method - OnRoomClientConnect(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientConnect(); base.OnClientConnect(); } @@ -485,9 +453,7 @@ public override void OnClientConnect() /// public override void OnClientDisconnect() { -#pragma warning disable 618 - OnRoomClientDisconnect(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientDisconnect(); base.OnClientDisconnect(); } @@ -507,7 +473,7 @@ public override void OnStopClient() /// public override void OnClientSceneChanged() { - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) { if (NetworkClient.isConnected) CallOnClientEnterRoom(); @@ -516,10 +482,7 @@ public override void OnClientSceneChanged() CallOnClientExitRoom(); base.OnClientSceneChanged(); -#pragma warning disable 618 - // obsolete method calls new method - OnRoomClientSceneChanged(NetworkClient.connection); -#pragma warning restore 618 + OnRoomClientSceneChanged(); } #endregion @@ -612,6 +575,30 @@ public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient c return true; } + /// + /// This is called on server from NetworkRoomPlayer.CmdChangeReadyState when client indicates change in Ready status. + /// + public virtual void ReadyStatusChanged() + { + int CurrentPlayers = 0; + int ReadyPlayers = 0; + + foreach (NetworkRoomPlayer item in roomSlots) + { + if (item != null) + { + CurrentPlayers++; + if (item.readyToBegin) + ReadyPlayers++; + } + } + + if (CurrentPlayers == ReadyPlayers) + CheckReadyToBegin(); + else + allPlayersReady = false; + } + /// /// This is called on the server when all the players in the room are ready. /// The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the room are ready, such as adding a countdown or a confirmation for a group leader. @@ -647,19 +634,11 @@ public virtual void OnRoomClientExit() {} /// public virtual void OnRoomClientConnect() {} - // Deprecated 2021-10-30 - [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] - public virtual void OnRoomClientConnect(NetworkConnection conn) => OnRoomClientConnect(); - /// /// This is called on the client when disconnected from a server. /// public virtual void OnRoomClientDisconnect() {} - // Deprecated 2021-10-30 - [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] - public virtual void OnRoomClientDisconnect(NetworkConnection conn) => OnRoomClientDisconnect(); - /// /// This is called on the client when a client is started. /// @@ -675,16 +654,6 @@ public virtual void OnRoomStopClient() {} /// public virtual void OnRoomClientSceneChanged() {} - // Deprecated 2021-10-30 - [Obsolete("Remove NetworkConnection from your override and use NetworkClient.connection instead.")] - public virtual void OnRoomClientSceneChanged(NetworkConnection conn) => OnRoomClientSceneChanged(); - - /// - /// Called on the client when adding a player to the room fails. - /// This could be because the room is full, or the connection is not allowed to have more players. - /// - public virtual void OnRoomClientAddPlayerFailed() {} - #endregion #region optional UI @@ -697,7 +666,7 @@ public virtual void OnGUI() if (!showRoomGUI) return; - if (NetworkServer.active && IsSceneActive(GameplayScene)) + if (NetworkServer.active && Utils.IsSceneActive(GameplayScene)) { GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f)); if (GUILayout.Button("Return to Room")) @@ -705,7 +674,7 @@ public virtual void OnGUI() GUILayout.EndArea(); } - if (IsSceneActive(RoomScene)) + if (Utils.IsSceneActive(RoomScene)) GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS"); } diff --git a/Assets/Mirror/Components/NetworkRoomManager.cs.meta b/Assets/Mirror/Components/NetworkRoomManager.cs.meta index 76e7d42..744bcdc 100644 --- a/Assets/Mirror/Components/NetworkRoomManager.cs.meta +++ b/Assets/Mirror/Components/NetworkRoomManager.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRoomManager.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs b/Assets/Mirror/Components/NetworkRoomPlayer.cs index d2763d5..6b7cda4 100644 --- a/Assets/Mirror/Components/NetworkRoomPlayer.cs +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs @@ -25,14 +25,14 @@ public class NetworkRoomPlayer : NetworkBehaviour /// Invoke CmdChangeReadyState method on the client to set this flag. /// When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server. /// - [Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")] + [ReadOnly, Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")] [SyncVar(hook = nameof(ReadyStateChanged))] public bool readyToBegin; /// /// Diagnostic index of the player, e.g. Player1, Player2, etc. /// - [Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")] + [ReadOnly, Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")] [SyncVar(hook = nameof(IndexChanged))] public int index; @@ -41,7 +41,7 @@ public class NetworkRoomPlayer : NetworkBehaviour /// /// Do not use Start - Override OnStartHost / OnStartClient instead! /// - public void Start() + public virtual void Start() { if (NetworkManager.singleton is NetworkRoomManager room) { @@ -139,7 +139,7 @@ public virtual void OnGUI() if (!room.showRoomGUI) return; - if (!NetworkManager.IsSceneActive(room.RoomScene)) + if (!Utils.IsSceneActive(room.RoomScene)) return; DrawPlayerReadyState(); diff --git a/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta b/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta index 0299bea..f9d2978 100644 --- a/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta +++ b/Assets/Mirror/Components/NetworkRoomPlayer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkRoomPlayer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkStatistics.cs b/Assets/Mirror/Components/NetworkStatistics.cs index a95d4a9..c00bb19 100644 --- a/Assets/Mirror/Components/NetworkStatistics.cs +++ b/Assets/Mirror/Components/NetworkStatistics.cs @@ -4,9 +4,11 @@ namespace Mirror { /// - /// Shows Network messages and bytes sent & received per second. - /// Add this component to the same object as Network Manager. + /// Shows Network messages and bytes sent and received per second. /// + /// + /// Add this component to the same object as Network Manager. + /// [AddComponentMenu("Network/Network Statistics")] [DisallowMultipleComponent] [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-statistics")] @@ -17,43 +19,43 @@ public class NetworkStatistics : MonoBehaviour // --------------------------------------------------------------------- - // CLIENT + // CLIENT (public fields for other components to grab statistics) // long bytes to support >2GB - int clientIntervalReceivedPackets; - long clientIntervalReceivedBytes; - int clientIntervalSentPackets; - long clientIntervalSentBytes; + [HideInInspector] public int clientIntervalReceivedPackets; + [HideInInspector] public long clientIntervalReceivedBytes; + [HideInInspector] public int clientIntervalSentPackets; + [HideInInspector] public long clientIntervalSentBytes; // results from last interval // long bytes to support >2GB - int clientReceivedPacketsPerSecond; - long clientReceivedBytesPerSecond; - int clientSentPacketsPerSecond; - long clientSentBytesPerSecond; + [HideInInspector] public int clientReceivedPacketsPerSecond; + [HideInInspector] public long clientReceivedBytesPerSecond; + [HideInInspector] public int clientSentPacketsPerSecond; + [HideInInspector] public long clientSentBytesPerSecond; // --------------------------------------------------------------------- - // SERVER + // SERVER (public fields for other components to grab statistics) // capture interval // long bytes to support >2GB - int serverIntervalReceivedPackets; - long serverIntervalReceivedBytes; - int serverIntervalSentPackets; - long serverIntervalSentBytes; + [HideInInspector] public int serverIntervalReceivedPackets; + [HideInInspector] public long serverIntervalReceivedBytes; + [HideInInspector] public int serverIntervalSentPackets; + [HideInInspector] public long serverIntervalSentBytes; // results from last interval // long bytes to support >2GB - int serverReceivedPacketsPerSecond; - long serverReceivedBytesPerSecond; - int serverSentPacketsPerSecond; - long serverSentBytesPerSecond; + [HideInInspector] public int serverReceivedPacketsPerSecond; + [HideInInspector] public long serverReceivedBytesPerSecond; + [HideInInspector] public int serverSentPacketsPerSecond; + [HideInInspector] public long serverSentBytesPerSecond; - // NetworkManager sets Transport.activeTransport in Awake(). + // NetworkManager sets Transport.active in Awake(). // so let's hook into it in Start(). void Start() { // find available transport - Transport transport = Transport.activeTransport; + Transport transport = Transport.active; if (transport != null) { transport.OnClientDataReceived += OnClientReceive; @@ -67,7 +69,7 @@ void Start() void OnDestroy() { // remove transport hooks - Transport transport = Transport.activeTransport; + Transport transport = Transport.active; if (transport != null) { transport.OnClientDataReceived -= OnClientReceive; @@ -145,8 +147,8 @@ void OnGUI() if (NetworkClient.active || NetworkServer.active) { // create main GUI area - // 105 is below NetworkManager HUD in all cases. - GUILayout.BeginArea(new Rect(10, 105, 215, 300)); + // 120 is below NetworkManager HUD in all cases. + GUILayout.BeginArea(new Rect(10, 120, 215, 300)); // show client / server stats if active if (NetworkClient.active) OnClientGUI(); diff --git a/Assets/Mirror/Components/NetworkStatistics.cs.meta b/Assets/Mirror/Components/NetworkStatistics.cs.meta index 0bf4251..ccce31f 100644 --- a/Assets/Mirror/Components/NetworkStatistics.cs.meta +++ b/Assets/Mirror/Components/NetworkStatistics.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkStatistics.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform.meta b/Assets/Mirror/Components/NetworkTransform.meta new file mode 100644 index 0000000..255a0fa --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 36de72d9255741659bcbd1971ed29822 +timeCreated: 1668358590 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs new file mode 100644 index 0000000..0d6c9ed --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs @@ -0,0 +1,547 @@ +// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ +// +// Base class for NetworkTransform and NetworkTransformChild. +// => simple unreliable sync without any interpolation for now. +// => which means we don't need teleport detection either +// +// NOTE: several functions are virtual in case someone needs to modify a part. +// +// Channel: uses UNRELIABLE at all times. +// -> out of order packets are dropped automatically +// -> it's better than RELIABLE for several reasons: +// * head of line blocking would add delay +// * resending is mostly pointless +// * bigger data race: +// -> if we use a Cmd() at position X over reliable +// -> client gets Cmd() and X at the same time, but buffers X for bufferTime +// -> for unreliable, it would get X before the reliable Cmd(), still +// buffer for bufferTime but end up closer to the original time +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public enum CoordinateSpace { Local, World } + + public abstract class NetworkTransformBase : NetworkBehaviour + { + // target transform to sync. can be on a child. + // TODO this field is kind of unnecessary since we now support child NetworkBehaviours + [Header("Target")] + [Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")] + public Transform target; + + // 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; + + // snapshots with initial capacity to avoid early resizing & allocations: see NetworkRigidbodyBenchmark example. + public readonly SortedList clientSnapshots = new SortedList(16); + public readonly SortedList serverSnapshots = new SortedList(16); + + // selective sync ////////////////////////////////////////////////////// + [Header("Selective Sync\nDon't change these at Runtime")] + public bool syncPosition = true; // do not change at runtime! + public bool syncRotation = true; // do not change at runtime! + public bool syncScale = false; // do not change at runtime! rare. off by default. + + [Header("Bandwidth Savings")] + [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] + public bool onlySyncOnChange = true; + [Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")] + public bool compressRotation = true; + + // interpolation is on by default, but can be disabled to jump to + // the destination immediately. some projects need this. + [Header("Interpolation")] + [Tooltip("Set to false to have a snap-like effect on position movement.")] + public bool interpolatePosition = true; + [Tooltip("Set to false to have a snap-like effect on rotations.")] + public bool interpolateRotation = true; + [Tooltip("Set to false to remove scale smoothing. Example use-case: Instant flipping of sprites that use -X and +X for direction.")] + public bool interpolateScale = true; + + // CoordinateSpace /////////////////////////////////////////////////////////// + [Header("Coordinate Space")] + [Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")] + public CoordinateSpace coordinateSpace = CoordinateSpace.Local; + + // convert syncInterval to sendIntervalMultiplier. + // in the future this can be moved into core to support tick aligned Sync, + public uint sendIntervalMultiplier + { + get + { + if (syncInterval > 0) + { + // if syncInterval is > 0, calculate how many multiples of NetworkManager.sendRate it is + // + // for example: + // NetworkServer.sendInterval is 1/60 = 0.16 + // NetworkTransform.syncInterval is 0.5 (500ms). + // 0.5 / 0.16 = 3.125 + // in other words: 3.125 x sendInterval + // + // note that NetworkServer.sendInterval is usually set on start. + // to make this work in Edit mode, make sure that NetworkManager + // OnValidate sets NetworkServer.sendInterval immediately. + float multiples = syncInterval / NetworkServer.sendInterval; + + // syncInterval is always supposed to sync at a minimum of 1 x sendInterval. + // that's what we do for every other NetworkBehaviour since + // we only sync in Broadcast() which is called @ sendInterval. + return multiples > 1 ? (uint)Mathf.RoundToInt(multiples) : 1; + } + + // if syncInterval is 0, use NetworkManager.sendRate (x1) + return 1; + } + } + + [Header("Timeline Offset")] + [Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")] + public bool timelineOffset = true; + + // Ninja's Notes on offset & mulitplier: + // + // In a no multiplier scenario: + // 1. Snapshots are sent every frame (frame being 1 NM send interval). + // 2. Time Interpolation is set to be 'behind' by 2 frames times. + // In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter. + // + // In a multiplier scenario: + // 1. Snapshots are sent every 10 frames. + // 2. Time Interpolation remains 'behind by 2 frames'. + // When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2. + // Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate. + // + protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1); + protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0; + + // 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; } + + // debugging /////////////////////////////////////////////////////////// + [Header("Debug")] + public bool showGizmos; + public bool showOverlay; + public Color overlayColor = new Color(0, 0, 0, 0.5f); + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + + // configure in awake + Configure(); + } + + // initialization ////////////////////////////////////////////////////// + // forcec configuration of some settings + protected virtual void Configure() + { + // set target to self if none yet + if (target == null) target = transform; + + // Unity doesn't support setting world scale. + // OnValidate force disables syncScale in world mode. + if (coordinateSpace == CoordinateSpace.World) syncScale = false; + } + + // make sure to call this when inheriting too! + protected virtual void Awake() + { + // sometimes OnValidate() doesn't run before launching a project. + // need to guarantee configuration runs. + Configure(); + } + + // snapshot functions ////////////////////////////////////////////////// + // get local/world position + protected virtual Vector3 GetPosition() => + coordinateSpace == CoordinateSpace.Local ? target.localPosition : target.position; + + // get local/world rotation + protected virtual Quaternion GetRotation() => + coordinateSpace == CoordinateSpace.Local ? target.localRotation : target.rotation; + + // get local/world scale + protected virtual Vector3 GetScale() => + coordinateSpace == CoordinateSpace.Local ? target.localScale : target.lossyScale; + + // set local/world position + protected virtual void SetPosition(Vector3 position) + { + if (coordinateSpace == CoordinateSpace.Local) + target.localPosition = position; + else + target.position = position; + } + + // set local/world rotation + protected virtual void SetRotation(Quaternion rotation) + { + if (coordinateSpace == CoordinateSpace.Local) + target.localRotation = rotation; + else + target.rotation = rotation; + } + + // set local/world position + protected virtual void SetScale(Vector3 scale) + { + if (coordinateSpace == CoordinateSpace.Local) + target.localScale = scale; + // Unity doesn't support setting world scale. + // OnValidate disables syncScale in world mode. + // else + // target.lossyScale = scale; // TODO + } + + // construct a snapshot of the current state + // => internal for testing + protected virtual TransformSnapshot Construct() + { + // NetworkTime.localTime for double precision until Unity has it too + return new TransformSnapshot( + // our local time is what the other end uses as remote time + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + 0, // the other end fills out local time itself + GetPosition(), + GetRotation(), + GetScale() + ); + } + + protected void AddSnapshot(SortedList snapshots, double timeStamp, Vector3? position, Quaternion? rotation, Vector3? scale) + { + // 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 (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition(); + if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation(); + if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale(); + + // insert transform snapshot + SnapshotInterpolation.InsertIfNotExists( + snapshots, + NetworkClient.snapshotSettings.bufferLimit, + new TransformSnapshot( + timeStamp, // arrival remote timestamp. NOT remote time. + NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet + position.Value, + rotation.Value, + scale.Value + ) + ); + } + + // 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 Apply(TransformSnapshot interpolated, TransformSnapshot endGoal) + { + // 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. + + // calculate the velocity and angular velocity for the object + // these can be used to drive animations or other behaviours + if (!isOwned && Time.deltaTime > 0) + { + velocity = (transform.localPosition - interpolated.position) / Time.deltaTime; + angularVelocity = (transform.localRotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime; + } + + // interpolate parts + if (syncPosition) SetPosition(interpolatePosition ? interpolated.position : endGoal.position); + if (syncRotation) SetRotation(interpolateRotation ? interpolated.rotation : endGoal.rotation); + if (syncScale) SetScale(interpolateScale ? interpolated.scale : endGoal.scale); + } + + // 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; + + // 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); + } + + // 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; + + // 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); + } + + // 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); + } + + // teleport on server, broadcast to clients. + [Server] + public void ServerTeleport(Vector3 destination, Quaternion rotation) + { + OnTeleport(destination, rotation); + RpcTeleport(destination, rotation); + } + + [ClientRpc] + void RpcResetState() + { + ResetState(); + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination) + { + // set the new position. + // interpolation will automatically continue. + target.position = destination; + + // reset interpolation to immediately jump to the new position. + // do not call Reset() here, this would cause delta compression to + // get out of sync for NetworkTransformReliable because NTReliable's + // 'override Reset()' resets lastDe/SerializedPosition: + // https://github.com/MirrorNetworking/Mirror/issues/3588 + // because client's next OnSerialize() will delta compress, + // but server's last delta will have been reset, causing offsets. + // + // instead, simply clear snapshots. + ResetState(); + + // 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) + { + // set the new position. + // interpolation will automatically continue. + target.position = destination; + target.rotation = rotation; + + // reset interpolation to immediately jump to the new position. + // do not call Reset() here, this would cause delta compression to + // get out of sync for NetworkTransformReliable because NTReliable's + // 'override Reset()' resets lastDe/SerializedPosition: + // https://github.com/MirrorNetworking/Mirror/issues/3588 + // because client's next OnSerialize() will delta compress, + // but server's last delta will have been reset, causing offsets. + // + // instead, simply clear snapshots. + ResetState(); + + // TODO + // what if we still receive a snapshot from before the interpolation? + // it could easily happen over unreliable. + // -> maybe add destination as first entry? + } + + public virtual void ResetState() + { + // disabled objects aren't updated anymore. + // 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() + { + ResetState(); + // default to ClientToServer so this works immediately for users + syncDirection = SyncDirection.ClientToServer; + } + + protected virtual void OnEnable() + { + ResetState(); + + if (NetworkServer.active) + NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged; + } + + protected virtual void OnDisable() + { + ResetState(); + + if (NetworkServer.active) + NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged; + } + + [ServerCallback] + void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity identity, bool authorityState) + { + if (identity != netIdentity) return; + + // If server gets authority or syncdirection is server to client, + // we don't reset buffers. + // This is because if syncdirection is S to C, we will never have + // snapshot issues since there is only ever 1 source. + + if (syncDirection == SyncDirection.ClientToServer) + { + ResetState(); + RpcResetState(); + } + } + + // OnGUI allocates even if it does nothing. avoid in release. +#if UNITY_EDITOR || DEVELOPMENT_BUILD + // debug /////////////////////////////////////////////////////////////// + protected virtual void OnGUI() + { + if (!showOverlay) return; + if (!Camera.main) 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); + + // 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)); + + // 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}"); + + GUILayout.EndArea(); + GUI.color = Color.white; + } + } + + protected virtual void DrawGizmos(SortedList buffer) + { + // 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); + + // 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) + { + // color depends on if old enough or not + TransformSnapshot entry = buffer.Values[i]; + bool oldEnough = entry.localTime <= threshold; + Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; + Gizmos.DrawWireCube(entry.position, Vector3.one); + } + + // 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); + } + + protected virtual void OnDrawGizmos() + { + // This fires in edit mode but that spams NRE's so check isPlaying + if (!Application.isPlaying) return; + if (!showGizmos) return; + + if (isServer) DrawGizmos(serverSnapshots); + if (isClient) DrawGizmos(clientSnapshots); + } +#endif + } +} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs.meta similarity index 51% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta rename to Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs.meta index ab649d9..bc6aac2 100644 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs.meta +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2e77294d8ccbc4e7cb8ca2bd0d3e99ea +guid: 7c44135fde488424eaf28566206ce473 MonoImporter: externalObjects: {} serializedVersion: 2 @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformBase.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs new file mode 100644 index 0000000..a231b88 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs @@ -0,0 +1,717 @@ +// 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; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform Hybrid")] + public class NetworkTransformHybrid : NetworkBehaviourHybrid + { + public bool useFixedUpdate; + TransformSnapshot? pendingSnapshot; + + // 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 + + // 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; + 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; + + // 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; } + + // 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() {} + + protected override void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + base.OnValidate(); + Reset(); + } + + void Reset() + { + // set target to self if none yet + if (target == null) target = transform; + + // we use sendRate for convenience. + // but project it to syncInterval for NetworkTransformHybrid to work properly. + syncInterval = sendInterval; + + // default to ClientToServer so this works immediately for users + syncDirection = SyncDirection.ClientToServer; + } + + // 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. + + // calculate the velocity and angular velocity for the object + // these can be used to drive animations or other behaviours + if (!isOwned && Time.deltaTime > 0) + { + velocity = (transform.localPosition - interpolated.position) / Time.deltaTime; + angularVelocity = (transform.localRotation.eulerAngles - interpolated.rotation.eulerAngles) / Time.deltaTime; + } + + if (syncPosition) target.localPosition = interpolated.position; + if (syncRotation) target.localRotation = interpolated.rotation; + if (syncScale) target.localScale = interpolated.scale; + } + + // store state after baseline sync + protected override void StoreState() + { + 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; + } + } + + if (syncScale) + { + float scaleDelta = Vector3.Distance(scale, lastSerializedBaselineScale); + if (scaleDelta >= scaleSensitivity) + { + return true; + } + } + + return false; + } + + // serialization /////////////////////////////////////////////////////// + // called on server and on client, depending on SyncDirection + protected override void OnSerializeBaseline(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 OnDeserializeBaseline(NetworkReader reader, byte baselineTick) + { + // deserialize + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + if (syncPosition) + { + position = reader.ReadVector3(); + lastDeserializedBaselinePosition = position.Value; + } + if (syncRotation) + { + rotation = reader.ReadQuaternion(); + lastDeserializedBaselineRotation = rotation.Value; + } + if (syncScale) + { + scale = reader.ReadVector3(); + lastDeserializedBaselineScale = scale.Value; + } + + // 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) + { + 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) + { + OnClientToServerDeltaSync(position, rotation, scale); + } + else if (isClient) + { + OnServerToClientDeltaSync(position, rotation, scale); + } + } + + // 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() + { + // apply buffered snapshots IF client authority + // -> in server authority, server moves the object + // so no need to apply any snapshots there. + // -> 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) + { + if (serverSnapshots.Count > 0) + { + // step the transform interpolation without touching time. + // 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 + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + if (useFixedUpdate) + pendingSnapshot = computed; + else + 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); + if (useFixedUpdate) + pendingSnapshot = computed; + else + ApplySnapshot(computed); + } + } + + // Update() without LateUpdate() split: otherwise perf. is cut in half! + protected override void Update() + { + base.Update(); // NetworkBehaviourHybrid + + if (isServer) + { + // interpolate remote clients + UpdateServerInterpolation(); + } + // 'else if' because host mode shouldn't update both. + else if (isClient) + { + // interpolate remote client (and local player if no authority) + if (!IsClientWithAuthority) UpdateClientInterpolation(); + } + } + + void FixedUpdate() + { + if (!useFixedUpdate) return; + + if (pendingSnapshot.HasValue) + { + ApplySnapshot(pendingSnapshot.Value); + pendingSnapshot = null; + } + } + + // common Teleport code for client->server and server->client + protected virtual void OnTeleport(Vector3 destination) + { + // reset any in-progress interpolation & buffers + ResetState(); + + // 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 + ResetState(); + + // 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; + + // 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); + } + + // 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; + + // 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); + } + + [Server] + public void ServerTeleport(Vector3 destination, Quaternion rotation) + { + OnTeleport(destination, rotation); + RpcTeleport(destination, rotation); + } + + public override void ResetState() + { + base.ResetState(); // NetworkBehaviourHybrid + + // disabled objects aren't updated anymore so let's clear the buffers. + serverSnapshots.Clear(); + clientSnapshots.Clear(); + + // reset baseline + lastSerializedBaselinePosition = Vector3.zero; + lastSerializedBaselineRotation = Quaternion.identity; + lastSerializedBaselineScale = Vector3.one; + + lastDeserializedBaselinePosition = Vector3.zero; + lastDeserializedBaselineRotation = Quaternion.identity; + lastDeserializedBaselineScale = Vector3.one; + + // Prevent resistance from CharacterController + // or non-knematic Rigidbodies when teleporting. + Physics.SyncTransforms(); + + // Debug.Log($"[{name}] ResetState to baselineTick=0"); + } + + protected virtual void OnDisable() => ResetState(); + protected virtual void OnEnable() => ResetState(); + + 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. + + base.OnSerialize(writer, initialState); // NetworkBehaviourHybrid + + // 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) + { + // 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); + } + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + base.OnDeserialize(reader, initialState); // NetworkBehaviourHybrid + + // 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) + { + // 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 (syncRotation) + { + rotation = reader.ReadQuaternion(); + lastDeserializedBaselineRotation = rotation; + } + if (syncScale) + { + scale = reader.ReadVector3(); + lastDeserializedBaselineScale = scale; + } + + // if baseline counts as delta, insert it into snapshot buffer too + if (baselineIsDelta) + OnServerToClientDeltaSync(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); + + // 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)); + + // 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}"); + + GUILayout.EndArea(); + GUI.color = Color.white; + } + } + + protected virtual void DrawGizmos(SortedList buffer) + { + // 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); + + // 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) + { + // 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); + } + + // 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); + } + + protected virtual void OnDrawGizmos() + { + // This fires in edit mode but that spams NRE's so check isPlaying + if (!Application.isPlaying) return; + if (!showGizmos) return; + + if (isServer) DrawGizmos(serverSnapshots); + if (isClient) DrawGizmos(clientSnapshots); + } +#endif + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs.meta new file mode 100644 index 0000000..077cb28 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8f63ea2e505fd484193fb31c5c55ca73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformHybrid.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs new file mode 100644 index 0000000..44a48d0 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs @@ -0,0 +1,448 @@ +// NetworkTransform V3 (reliable) by mischa (2022-10) +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Reliable)")] + 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")] + public float rotationSensitivity = 0.01f; + + // 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 scalePrecision = 0.01f; // 1 cm + + // delta compression needs to remember 'last' to compress against + protected Vector3Long lastSerializedPosition = Vector3Long.zero; + protected Vector3Long lastDeserializedPosition = Vector3Long.zero; + + protected Vector3Long lastSerializedScale = Vector3Long.zero; + protected Vector3Long lastDeserializedScale = Vector3Long.zero; + + // Used to store last sent snapshots + protected TransformSnapshot last; + + // update ////////////////////////////////////////////////////////////// + void Update() + { + // 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(); + } + + 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. + // 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)) + { + if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct()))) + SetDirty(); + + CheckLastSendTime(); + } + } + + protected virtual void UpdateServer() + { + // apply buffered snapshots IF client authority + // -> in server authority, server moves the object + // so no need to apply any snapshots there. + // -> 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. + // -> 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) + { + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + Apply(computed, to); + } + } + } + + protected virtual void UpdateClient() + { + if (useFixedUpdate) + { + if (!IsClientWithAuthority && clientSnapshots.Count > 0) + { + SnapshotInterpolation.StepInterpolation( + clientSnapshots, + NetworkTime.time, + out TransformSnapshot from, + out TransformSnapshot 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); + } + } + } + } + + protected virtual void CheckLastSendTime() + { + // timeAsDouble not available in older Unity versions. + if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime)) + { + if (sendIntervalCounter == sendIntervalMultiplier) + sendIntervalCounter = 0; + sendIntervalCounter++; + } + } + + // check if position / rotation / scale changed since last 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) + { + Compression.ScaleToLong(u, precision, out Vector3Long uQuantized); + Compression.ScaleToLong(v, precision, out Vector3Long vQuantized); + return uQuantized != vQuantized; + } + + // NT may be used on client/server/host to Owner/Observers with + // ServerToClient or ClientToServer. + // however, OnSerialize should always delta against last. + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // get current snapshot for broadcasting. + TransformSnapshot snapshot = Construct(); + + // ClientToServer optimization: + // for interpolated client owned identities, + // always broadcast the latest known snapshot so other clients can + // interpolate immediately instead of catching up too + + // 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. + + // initial + if (initialState) + { + // 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; + if (syncPosition) writer.WriteVector3(snapshot.position); + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation)); + else + writer.WriteQuaternion(snapshot.rotation); + } + if (syncScale) writer.WriteVector3(snapshot.scale); + } + // delta + else + { + // int before = 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 + writer.WriteQuaternion(snapshot.rotation); + } + if (syncScale) + { + // quantize -> delta -> varint + Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized); + DeltaCompression.Compress(writer, lastSerializedScale, quantized); + } + } + + // save serialized as 'last' for next delta compression + if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition); + if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale); + + // set 'last' + last = snapshot; + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + Vector3? position = null; + Quaternion? rotation = null; + Vector3? scale = null; + + // initial + if (initialState) + { + if (syncPosition) position = reader.ReadVector3(); + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + else + rotation = reader.ReadQuaternion(); + } + if (syncScale) scale = reader.ReadVector3(); + } + // delta + else + { + // varint -> delta -> quantize + if (syncPosition) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition); + position = Compression.ScaleToFloat(quantized, positionPrecision); + } + if (syncRotation) + { + // (optional) smallest three compression for now. no delta. + if (compressRotation) + rotation = Compression.DecompressQuaternion(reader.ReadUInt()); + else + rotation = reader.ReadQuaternion(); + } + if (syncScale) + { + Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale); + scale = Compression.ScaleToFloat(quantized, scalePrecision); + } + } + + // handle depending on server / client / host. + // server has priority for host mode. + if (isServer) OnClientToServerSync(position, rotation, scale); + else if (isClient) OnServerToClientSync(position, rotation, scale); + + // save deserialized as 'last' for next delta compression + if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition); + if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale); + } + + // sync //////////////////////////////////////////////////////////////// + + // 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; + + // protect against ever growing buffer size attacks + if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return; + + // '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); + } + + // server broadcasts sync message to all clients + protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) + { + // don't apply for local player with authority + if (IsClientWithAuthority) return; + + // 'only sync on change' needs a correction on every new move sequence. + if (onlySyncOnChange && + NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier)) + { + 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()); + } + + // 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); + } + + // 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) + { + // 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; + + lastSerializedScale = Vector3Long.zero; + lastDeserializedScale = Vector3Long.zero; + + // reset 'last' for delta too + last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero); + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs.meta new file mode 100644 index 0000000..c8be862 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 8ff3ba0becae47b8b9381191598957c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformReliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs new file mode 100644 index 0000000..adac721 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs @@ -0,0 +1,462 @@ +// NetworkTransform V2 by mischa (2021-07) +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [AddComponentMenu("Network/Network Transform (Unreliable)")] + 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; + public float rotationSensitivity = 0.01f; + public float scaleSensitivity = 0.01f; + + // Used to store last sent snapshots + protected TransformSnapshot lastSnapshot; + protected Changed cachedChangedComparison; + protected bool hasSentUnchangedPosition; + + // update ////////////////////////////////////////////////////////////// + // Update applies interpolation + void Update() + { + if (isServer) UpdateServerInterpolation(); + // for all other clients (and for local player if !authority), + // we need to apply snapshots from the buffer. + // 'else if' because host mode shouldn't interpolate client + 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. + // otherwise this may run before user update, delaying detection until next frame. + // this could cause visible jitter. + void LateUpdate() + { + // if server then always sync to others. + if (isServer) UpdateServerBroadcast(); + // client authority, and local player (= allowed to move myself)? + // 'else if' because host mode shouldn't send anything to server. + // it is the server. don't overwrite anything there. + else if (isClient && IsClientWithAuthority) UpdateClientBroadcast(); + } + + protected virtual void CheckLastSendTime() + { + // We check interval every frame, and then send if interval is reached. + // So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent, + // thus we reset the counter here. + // This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame, + // because intervalCounter is always = 1 in the previous version. + + // Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571 + + if (sendIntervalCounter >= sendIntervalMultiplier) + sendIntervalCounter = 0; + + // timeAsDouble not available in older Unity versions. + if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime)) + sendIntervalCounter++; + } + + void UpdateServerBroadcast() + { + // 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? + CheckLastSendTime(); + + if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation! + (syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority)) + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + TransformSnapshot snapshot = Construct(); + + cachedChangedComparison = CompareChangedSnapshots(snapshot); + + if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; } + + SyncData syncData = new SyncData(cachedChangedComparison, snapshot); + + RpcServerToClientSync(syncData); + + if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + UpdateLastSentSnapshot(cachedChangedComparison, snapshot); + } + } + } + + void UpdateServerInterpolation() + { + // apply buffered snapshots IF client authority + // -> in server authority, server moves the object + // so no need to apply any snapshots there. + // -> 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. + // -> 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) return; + + // step the transform interpolation without touching time. + // NetworkClient is responsible for time globally. + SnapshotInterpolation.StepInterpolation( + serverSnapshots, + connectionToClient.remoteTimeline, + out TransformSnapshot from, + out TransformSnapshot to, + out double t); + + // interpolate & apply + TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t); + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); + } + } + + void UpdateClientBroadcast() + { + // https://github.com/vis2k/Mirror/pull/2992/ + if (!NetworkClient.ready) return; + + // 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. + CheckLastSendTime(); + if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation! + { + // send snapshot without timestamp. + // receiver gets it from batch timestamp to save bandwidth. + TransformSnapshot snapshot = Construct(); + + cachedChangedComparison = CompareChangedSnapshots(snapshot); + + if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; } + + SyncData syncData = new SyncData(cachedChangedComparison, snapshot); + + CmdClientToServerSync(syncData); + + if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) + { + hasSentUnchangedPosition = true; + } + else + { + hasSentUnchangedPosition = false; + UpdateLastSentSnapshot(cachedChangedComparison, snapshot); + } + } + } + + void UpdateClientInterpolation() + { + // only while we have snapshots + if (clientSnapshots.Count == 0) return; + + // 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); + if (useFixedUpdate) + pendingSnapshot = computed; + else + Apply(computed, to); + } + + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // 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) + { + if (syncPosition) writer.WriteVector3(GetPosition()); + if (syncRotation) writer.WriteQuaternion(GetRotation()); + if (syncScale) writer.WriteVector3(GetScale()); + } + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + // 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) + { + if (syncPosition) SetPosition(reader.ReadVector3()); + if (syncRotation) SetRotation(reader.ReadQuaternion()); + if (syncScale) SetScale(reader.ReadVector3()); + } + } + + protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot) + { + if (change == Changed.None || change == Changed.CompressRot) return; + + if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x; + if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y; + if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z; + + if (compressRotation) + { + if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation; + } + else + { + Vector3 newRotation; + newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x; + newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y; + newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z; + + lastSnapshot.rotation = Quaternion.Euler(newRotation); + } + + if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale; + } + + // Returns true if position, rotation AND scale are unchanged, within given sensitivity range. + // Note the sensitivity comparison are different for pos, rot and scale. + protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot) + { + Changed change = Changed.None; + + if (syncPosition) + { + bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity; + if (positionChanged) + { + if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX; + if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY; + if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ; + } + } + + if (syncRotation) + { + if (compressRotation) + { + bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; + if (rotationChanged) + { + // Here we set all Rot enum flags, to tell us if there was a change in rotation + // when using compression. If no change, we don't write the compressed Quat. + change |= Changed.CompressRot; + change |= Changed.Rot; + } + else + { + change |= Changed.CompressRot; + } + } + else + { + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX; + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY; + if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ; + } + } + + if (syncScale) + { + if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale; + } + + return change; + } + + [Command(channel = Channels.Unreliable)] + void CmdClientToServerSync(SyncData syncData) + { + OnClientToServerSync(syncData); + //For client authority, immediately pass on the client snapshot to all other + //clients instead of waiting for server to send its snapshots. + if (syncDirection == SyncDirection.ClientToServer) + RpcServerToClientSync(syncData); + } + + protected virtual void OnClientToServerSync(SyncData syncData) + { + // 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; + + if (onlySyncOnChange) + { + double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval; + + if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + ResetState(); + } + + UpdateSyncData(ref syncData, serverSnapshots); + + AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale); + } + + [ClientRpc(channel = Channels.Unreliable)] + void RpcServerToClientSync(SyncData syncData) => + OnServerToClientSync(syncData); + + protected virtual void OnServerToClientSync(SyncData syncData) + { + // 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; + + // 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; + + if (onlySyncOnChange) + { + double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval; + + if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp) + ResetState(); + } + + UpdateSyncData(ref syncData, clientSnapshots); + + AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale); + } + + protected virtual void UpdateSyncData(ref SyncData syncData, SortedList snapshots) + { + if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot) + { + syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition(); + syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation(); + syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale(); + } + else + { + // Just going to update these without checking if syncposition or not, + // because if not syncing position, NT will not apply any position data + // to the target during Apply(). + + syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x); + syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y); + syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z); + + // If compressRot is true, we already have the Quat in syncdata. + if ((syncData.changedDataByte & Changed.CompressRot) == 0) + { + syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x); + syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ; + syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z); + + syncData.quatRotation = Quaternion.Euler(syncData.vecRotation); + } + else + { + syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation()); + } + + syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale()); + } + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs.meta b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs.meta new file mode 100644 index 0000000..e5e968f --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a553cb17010b2403e8523b558bffbc14 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/NetworkTransformUnreliable.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs similarity index 78% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs rename to Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs index efd91c0..01b863c 100644 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs +++ b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs @@ -6,7 +6,7 @@ namespace Mirror { // NetworkTransform Snapshot - public struct NTSnapshot : Snapshot + public struct TransformSnapshot : Snapshot { // time or sequence are needed to throw away older snapshots. // @@ -23,30 +23,31 @@ public struct NTSnapshot : Snapshot // // [REMOTE TIME, NOT LOCAL TIME] // => DOUBLE for long term accuracy & batching gives us double anyway - public double remoteTimestamp { get; set; } + public double remoteTime { get; set; } + // the local timestamp (when we received it) // used to know if the first two snapshots are old enough to start. - public double localTimestamp { get; set; } + public double localTime { get; set; } public Vector3 position; public Quaternion rotation; public Vector3 scale; - public NTSnapshot(double remoteTimestamp, double localTimestamp, Vector3 position, Quaternion rotation, Vector3 scale) + public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale) { - this.remoteTimestamp = remoteTimestamp; - this.localTimestamp = localTimestamp; + this.remoteTime = remoteTime; + this.localTime = localTime; this.position = position; this.rotation = rotation; this.scale = scale; } - public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t) + public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSnapshot to, double t) { // NOTE: // Vector3 & Quaternion components are float anyway, so we can // keep using the functions with 't' as float instead of double. - return new NTSnapshot( + return new TransformSnapshot( // interpolated snapshot is applied directly. don't need timestamps. 0, 0, // lerp position/rotation/scale unclamped in case we ever need @@ -60,5 +61,8 @@ public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t) Vector3.LerpUnclamped(from.scale, to.scale, (float)t) ); } + + public override string ToString() => + $"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})"; } } diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs.meta similarity index 59% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta rename to Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs.meta index f43458f..b8e9173 100644 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformSnapshot.cs.meta +++ b/Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/TransformSnapshot.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs new file mode 100644 index 0000000..9b6d51c --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs @@ -0,0 +1,156 @@ +using UnityEngine; +using System; +using Mirror; + +namespace Mirror +{ + [Serializable] + public struct SyncData + { + public Changed changedDataByte; + public Vector3 position; + public Quaternion quatRotation; + public Vector3 vecRotation; + public Vector3 scale; + + public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale) + { + this.changedDataByte = _dataChangedByte; + this.position = _position; + this.quatRotation = _rotation; + this.vecRotation = quatRotation.eulerAngles; + this.scale = _scale; + } + + public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot) + { + this.changedDataByte = _dataChangedByte; + this.position = _snapshot.position; + this.quatRotation = _snapshot.rotation; + this.vecRotation = quatRotation.eulerAngles; + this.scale = _snapshot.scale; + } + + public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale) + { + this.changedDataByte = _dataChangedByte; + this.position = _position; + this.vecRotation = _vecRotation; + this.quatRotation = Quaternion.Euler(vecRotation); + this.scale = _scale; + } + } + + [Flags] + public enum Changed : byte + { + None = 0, + PosX = 1 << 0, + PosY = 1 << 1, + PosZ = 1 << 2, + CompressRot = 1 << 3, + RotX = 1 << 4, + RotY = 1 << 5, + RotZ = 1 << 6, + Scale = 1 << 7, + + Pos = PosX | PosY | PosZ, + Rot = RotX | RotY | RotZ + } + + + public static class SyncDataReaderWriter + { + public static void WriteSyncData(this NetworkWriter writer, SyncData syncData) + { + writer.WriteByte((byte)syncData.changedDataByte); + + // Write position + if ((syncData.changedDataByte & Changed.PosX) > 0) + { + writer.WriteFloat(syncData.position.x); + } + + if ((syncData.changedDataByte & Changed.PosY) > 0) + { + writer.WriteFloat(syncData.position.y); + } + + if ((syncData.changedDataByte & Changed.PosZ) > 0) + { + writer.WriteFloat(syncData.position.z); + } + + // Write rotation + if ((syncData.changedDataByte & Changed.CompressRot) > 0) + { + if((syncData.changedDataByte & Changed.Rot) > 0) + { + writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation)); + } + } + else + { + if ((syncData.changedDataByte & Changed.RotX) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.x); + } + + if ((syncData.changedDataByte & Changed.RotY) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.y); + } + + if ((syncData.changedDataByte & Changed.RotZ) > 0) + { + writer.WriteFloat(syncData.quatRotation.eulerAngles.z); + } + } + + // Write scale + if ((syncData.changedDataByte & Changed.Scale) > 0) + { + writer.WriteVector3(syncData.scale); + } + } + + public static SyncData ReadSyncData(this NetworkReader reader) + { + Changed changedData = (Changed)reader.ReadByte(); + + // If we have nothing to read here, let's say because posX is unchanged, then we can write anything + // for now, but in the NT, we will need to check changedData again, to put the right values of the axis + // back. We don't have it here. + + Vector3 position = + new Vector3( + (changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0 + ); + + Vector3 vecRotation = new Vector3(); + Quaternion quatRotation = new Quaternion(); + + if ((changedData & Changed.CompressRot) > 0) + { + quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion(); + } + else + { + vecRotation = + new Vector3( + (changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0, + (changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0 + ); + } + + Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3(); + + SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale); + + return _syncData; + } + } +} diff --git a/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta new file mode 100644 index 0000000..2f2d647 --- /dev/null +++ b/Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a1c0832ca88e749ff96fe04cebb617ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs deleted file mode 100644 index b7b8e81..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Ê»Oumuamua's light curve, assuming little systematic error, presents its -// motion as tumbling, rather than smoothly rotating, and moving sufficiently -// fast relative to the Sun. -// -// A small number of astronomers suggested that Ê»Oumuamua could be a product of -// alien technology, but evidence in support of this hypothesis is weak. -using UnityEngine; - -namespace Mirror -{ - [DisallowMultipleComponent] - [AddComponentMenu("Network/Network Transform")] - public class NetworkTransform : NetworkTransformBase - { - protected override Transform targetComponent => transform; - } -} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs deleted file mode 100644 index 54e77a7..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformBase.cs +++ /dev/null @@ -1,776 +0,0 @@ -// NetworkTransform V2 aka project Oumuamua by vis2k (2021-07) -// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/ -// -// Base class for NetworkTransform and NetworkTransformChild. -// => simple unreliable sync without any interpolation for now. -// => which means we don't need teleport detection either -// -// NOTE: several functions are virtual in case someone needs to modify a part. -// -// Channel: uses UNRELIABLE at all times. -// -> out of order packets are dropped automatically -// -> it's better than RELIABLE for several reasons: -// * head of line blocking would add delay -// * resending is mostly pointless -// * bigger data race: -// -> if we use a Cmd() at position X over reliable -// -> client gets Cmd() and X at the same time, but buffers X for bufferTime -// -> for unreliable, it would get X before the reliable Cmd(), still -// buffer for bufferTime but end up closer to the original time -// comment out the below line to quickly revert the onlySyncOnChange feature -#define onlySyncOnChange_BANDWIDTH_SAVING -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace Mirror -{ - public abstract class NetworkTransformBase : NetworkBehaviour - { - // TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier? - [Header("Authority")] - [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")] - public bool clientAuthority; - - // 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 => hasAuthority && clientAuthority; - - // target transform to sync. can be on a child. - protected abstract Transform targetComponent { get; } - - [Header("Synchronization")] - [Range(0, 1)] public float sendInterval = 0.050f; - public bool syncPosition = true; - public bool syncRotation = true; - // scale sync is rare. off by default. - public bool syncScale = false; - - double lastClientSendTime; - double lastServerSendTime; - - // not all games need to interpolate. a board game might jump to the - // final position immediately. - [Header("Interpolation")] - public bool interpolatePosition = true; - public bool interpolateRotation = true; - public bool interpolateScale = false; - - // "Experimentally I’ve found that the amount of delay that works best - // at 2-5% packet loss is 3X the packet send rate" - // NOTE: we do NOT use a dyanmically changing buffer size. - // it would come with a lot of complications, e.g. buffer time - // advantages/disadvantages for different connections. - // Glenn Fiedler's recommendation seems solid, and should cover - // the vast majority of connections. - // (a player with 2000ms latency will have issues no matter what) - [Header("Buffering")] - [Tooltip("Snapshots are buffered for sendInterval * multiplier seconds. If your expected client base is to run at non-ideal connection quality (2-5% packet loss), 3x supposedly works best.")] - public int bufferTimeMultiplier = 1; - public float bufferTime => sendInterval * bufferTimeMultiplier; - [Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")] - public int bufferSizeLimit = 64; - - [Tooltip("Start to accelerate interpolation if buffer size is >= threshold. Needs to be larger than bufferTimeMultiplier.")] - public int catchupThreshold = 4; - - [Tooltip("Once buffer is larger catchupThreshold, accelerate by multiplier % per excess entry.")] - [Range(0, 1)] public float catchupMultiplier = 0.10f; - -#if onlySyncOnChange_BANDWIDTH_SAVING - [Header("Sync Only If Changed")] - [Tooltip("When true, changes are not sent unless greater than sensitivity values below.")] - public bool onlySyncOnChange = true; - - // 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching. - [Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")] - public float bufferResetMultiplier = 5; - - [Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")] - public float positionSensitivity = 0.01f; - public float rotationSensitivity = 0.01f; - public float scaleSensitivity = 0.01f; - - protected bool positionChanged; - protected bool rotationChanged; - protected bool scaleChanged; - - // Used to store last sent snapshots - protected NTSnapshot lastSnapshot; - protected bool cachedSnapshotComparison; - protected bool hasSentUnchangedPosition; -#endif - - // snapshots sorted by timestamp - // in the original article, glenn fiedler drops any snapshots older than - // the last received snapshot. - // -> instead, we insert into a sorted buffer - // -> the higher the buffer information density, the better - // -> we still drop anything older than the first element in the buffer - // => internal for testing - // - // IMPORTANT: of explicit 'NTSnapshot' type instead of 'Snapshot' - // interface because List allocates through boxing - internal SortedList serverBuffer = new SortedList(); - internal SortedList clientBuffer = new SortedList(); - - // absolute interpolation time, moved along with deltaTime - // (roughly between [0, delta] where delta is snapshot B - A timestamp) - // (can be bigger than delta when overshooting) - double serverInterpolationTime; - double clientInterpolationTime; - - // only convert the static Interpolation function to Func once to - // avoid allocations - Func Interpolate = NTSnapshot.Interpolate; - - [Header("Debug")] - public bool showGizmos; - public bool showOverlay; - public Color overlayColor = new Color(0, 0, 0, 0.5f); - - // snapshot functions ////////////////////////////////////////////////// - // construct a snapshot of the current state - // => internal for testing - protected virtual NTSnapshot ConstructSnapshot() - { - // NetworkTime.localTime for double precision until Unity has it too - return new NTSnapshot( - // our local time is what the other end uses as remote time - NetworkTime.localTime, - // the other end fills out local time itself - 0, - targetComponent.localPosition, - targetComponent.localRotation, - targetComponent.localScale - ); - } - - // 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(NTSnapshot start, NTSnapshot goal, NTSnapshot 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) - targetComponent.localPosition = interpolatePosition ? interpolated.position : goal.position; - - if (syncRotation) - targetComponent.localRotation = interpolateRotation ? interpolated.rotation : goal.rotation; - - if (syncScale) - targetComponent.localScale = interpolateScale ? interpolated.scale : goal.scale; - } -#if onlySyncOnChange_BANDWIDTH_SAVING - // Returns true if position, rotation AND scale are unchanged, within given sensitivity range. - protected virtual bool CompareSnapshots(NTSnapshot currentSnapshot) - { - positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity; - rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity; - scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity; - - return (!positionChanged && !rotationChanged && !scaleChanged); - } -#endif - // cmd ///////////////////////////////////////////////////////////////// - // only unreliable. see comment above of this file. - [Command(channel = Channels.Unreliable)] - void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale) - { - OnClientToServerSync(position, rotation, scale); - //For client authority, immediately pass on the client snapshot to all other - //clients instead of waiting for server to send its snapshots. - if (clientAuthority) - { - RpcServerToClientSync(position, rotation, scale); - } - } - - // 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 (!clientAuthority) return; - - // protect against ever growing buffer size attacks - if (serverBuffer.Count >= bufferSizeLimit) return; - - // only player owned objects (with a connection) can send to - // server. we can get the timestamp from the connection. - double timestamp = connectionToClient.remoteTimeStamp; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (onlySyncOnChange) - { - double timeIntervalCheck = bufferResetMultiplier * sendInterval; - - if (serverBuffer.Count > 0 && serverBuffer.Values[serverBuffer.Count - 1].remoteTimestamp + timeIntervalCheck < timestamp) - { - Reset(); - } - } -#endif - // 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 (!position.HasValue) position = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].position : targetComponent.localPosition; - if (!rotation.HasValue) rotation = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].rotation : targetComponent.localRotation; - if (!scale.HasValue) scale = serverBuffer.Count > 0 ? serverBuffer.Values[serverBuffer.Count - 1].scale : targetComponent.localScale; - - // construct snapshot with batch timestamp to save bandwidth - NTSnapshot snapshot = new NTSnapshot( - timestamp, - NetworkTime.localTime, - position.Value, rotation.Value, scale.Value - ); - - // add to buffer (or drop if older than first element) - SnapshotInterpolation.InsertIfNewEnough(snapshot, serverBuffer); - } - - // rpc ///////////////////////////////////////////////////////////////// - // only unreliable. see comment above of this file. - [ClientRpc(channel = Channels.Unreliable)] - void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) => - OnServerToClientSync(position, rotation, scale); - - // server broadcasts sync message to all clients - protected virtual void OnServerToClientSync(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; - - // protect against ever growing buffer size attacks - if (clientBuffer.Count >= bufferSizeLimit) return; - - // 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; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (onlySyncOnChange) - { - double timeIntervalCheck = bufferResetMultiplier * sendInterval; - - if (clientBuffer.Count > 0 && clientBuffer.Values[clientBuffer.Count - 1].remoteTimestamp + timeIntervalCheck < timestamp) - { - Reset(); - } - } -#endif - // 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 (!position.HasValue) position = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].position : targetComponent.localPosition; - if (!rotation.HasValue) rotation = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].rotation : targetComponent.localRotation; - if (!scale.HasValue) scale = clientBuffer.Count > 0 ? clientBuffer.Values[clientBuffer.Count - 1].scale : targetComponent.localScale; - - // construct snapshot with batch timestamp to save bandwidth - NTSnapshot snapshot = new NTSnapshot( - timestamp, - NetworkTime.localTime, - position.Value, rotation.Value, scale.Value - ); - - // add to buffer (or drop if older than first element) - SnapshotInterpolation.InsertIfNewEnough(snapshot, clientBuffer); - } - - // update ////////////////////////////////////////////////////////////// - void UpdateServer() - { - // 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. - if (NetworkTime.localTime >= lastServerSendTime + sendInterval && - (!clientAuthority || IsClientWithAuthority)) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - NTSnapshot snapshot = ConstructSnapshot(); -#if onlySyncOnChange_BANDWIDTH_SAVING - cachedSnapshotComparison = CompareSnapshots(snapshot); - if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } -#endif - -#if onlySyncOnChange_BANDWIDTH_SAVING - RpcServerToClientSync( - // only sync what the user wants to sync - syncPosition && positionChanged ? snapshot.position : default(Vector3?), - syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), - syncScale && scaleChanged ? snapshot.scale : default(Vector3?) - ); -#else - RpcServerToClientSync( - // only sync what the user wants to sync - syncPosition ? snapshot.position : default(Vector3?), - syncRotation ? snapshot.rotation : default(Quaternion?), - syncScale ? snapshot.scale : default(Vector3?) - ); -#endif - - lastServerSendTime = NetworkTime.localTime; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (cachedSnapshotComparison) - { - hasSentUnchangedPosition = true; - } - else - { - hasSentUnchangedPosition = false; - lastSnapshot = snapshot; - } -#endif - } - - // apply buffered snapshots IF client authority - // -> in server authority, server moves the object - // so no need to apply any snapshots there. - // -> 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 (clientAuthority && !hasAuthority) - { - // compute snapshot interpolation & apply if any was spit out - // TODO we don't have Time.deltaTime double yet. float is fine. - if (SnapshotInterpolation.Compute( - NetworkTime.localTime, Time.deltaTime, - ref serverInterpolationTime, - bufferTime, serverBuffer, - catchupThreshold, catchupMultiplier, - Interpolate, - out NTSnapshot computed)) - { - NTSnapshot start = serverBuffer.Values[0]; - NTSnapshot goal = serverBuffer.Values[1]; - ApplySnapshot(start, goal, computed); - } - } - } - - void UpdateClient() - { - // client authority, and local player (= allowed to move myself)? - if (IsClientWithAuthority) - { - // https://github.com/vis2k/Mirror/pull/2992/ - if (!NetworkClient.ready) return; - - // 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 (NetworkTime.localTime >= lastClientSendTime + sendInterval) - { - // send snapshot without timestamp. - // receiver gets it from batch timestamp to save bandwidth. - NTSnapshot snapshot = ConstructSnapshot(); -#if onlySyncOnChange_BANDWIDTH_SAVING - cachedSnapshotComparison = CompareSnapshots(snapshot); - if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; } -#endif - -#if onlySyncOnChange_BANDWIDTH_SAVING - CmdClientToServerSync( - // only sync what the user wants to sync - syncPosition && positionChanged ? snapshot.position : default(Vector3?), - syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?), - syncScale && scaleChanged ? snapshot.scale : default(Vector3?) - ); -#else - CmdClientToServerSync( - // only sync what the user wants to sync - syncPosition ? snapshot.position : default(Vector3?), - syncRotation ? snapshot.rotation : default(Quaternion?), - syncScale ? snapshot.scale : default(Vector3?) - ); -#endif - - lastClientSendTime = NetworkTime.localTime; -#if onlySyncOnChange_BANDWIDTH_SAVING - if (cachedSnapshotComparison) - { - hasSentUnchangedPosition = true; - } - else - { - hasSentUnchangedPosition = false; - lastSnapshot = snapshot; - } -#endif - } - } - // for all other clients (and for local player if !authority), - // we need to apply snapshots from the buffer - else - { - // compute snapshot interpolation & apply if any was spit out - // TODO we don't have Time.deltaTime double yet. float is fine. - if (SnapshotInterpolation.Compute( - NetworkTime.localTime, Time.deltaTime, - ref clientInterpolationTime, - bufferTime, clientBuffer, - catchupThreshold, catchupMultiplier, - Interpolate, - out NTSnapshot computed)) - { - NTSnapshot start = clientBuffer.Values[0]; - NTSnapshot goal = clientBuffer.Values[1]; - ApplySnapshot(start, goal, computed); - } - } - } - - void Update() - { - // 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(); - } - - // 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. - targetComponent.position = destination; - - // TODO - // what if we still receive a snapshot from before the interpolation? - // it could easily happen over unreliable. - // -> maybe add destionation 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. - targetComponent.position = destination; - targetComponent.rotation = rotation; - - // TODO - // what if we still receive a snapshot from before the interpolation? - // it could easily happen over unreliable. - // -> maybe add destionation 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); - } - - // Deprecated 2022-01-19 - [Obsolete("Use RpcTeleport(Vector3, Quaternion) instead.")] - [ClientRpc] - public void RpcTeleportAndRotate(Vector3 destination, Quaternion rotation) - { - 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 (!clientAuthority) return; - - // 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 targetComponent.position=pos - RpcTeleport(destination); - } - - // 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 (!clientAuthority) return; - - // 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 targetComponent.position=pos - RpcTeleport(destination, rotation); - } - - // Deprecated 2022-01-19 - [Obsolete("Use CmdTeleport(Vector3, Quaternion) instead.")] - [Command] - public void CmdTeleportAndRotate(Vector3 destination, Quaternion rotation) - { - if (!clientAuthority) return; - OnTeleport(destination, rotation); - RpcTeleport(destination, rotation); - } - - public virtual void Reset() - { - // disabled objects aren't updated anymore. - // so let's clear the buffers. - serverBuffer.Clear(); - clientBuffer.Clear(); - - // reset interpolation time too so we start at t=0 next time - serverInterpolationTime = 0; - clientInterpolationTime = 0; - } - - protected virtual void OnDisable() => Reset(); - protected virtual void OnEnable() => Reset(); - - protected virtual void OnValidate() - { - // make sure that catchup threshold is > buffer multiplier. - // for a buffer multiplier of '3', we usually have at _least_ 3 - // buffered snapshots. often 4-5 even. - // - // catchUpThreshold should be a minimum of bufferTimeMultiplier + 3, - // to prevent clashes with SnapshotInterpolation looking for at least - // 3 old enough buffers, else catch up will be implemented while there - // is not enough old buffers, and will result in jitter. - // (validated with several real world tests by ninja & imer) - catchupThreshold = Mathf.Max(bufferTimeMultiplier + 3, catchupThreshold); - - // buffer limit should be at least multiplier to have enough in there - bufferSizeLimit = Mathf.Max(bufferTimeMultiplier, bufferSizeLimit); - } - - public override bool OnSerialize(NetworkWriter writer, bool initialState) - { - // 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) - { - if (syncPosition) writer.WriteVector3(targetComponent.localPosition); - if (syncRotation) writer.WriteQuaternion(targetComponent.localRotation); - if (syncScale) writer.WriteVector3(targetComponent.localScale); - return true; - } - return false; - } - - public override void OnDeserialize(NetworkReader reader, bool initialState) - { - // 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) - { - if (syncPosition) targetComponent.localPosition = reader.ReadVector3(); - if (syncRotation) targetComponent.localRotation = reader.ReadQuaternion(); - if (syncScale) targetComponent.localScale = reader.ReadVector3(); - } - } - - // OnGUI allocates even if it does nothing. avoid in release. -#if UNITY_EDITOR || DEVELOPMENT_BUILD - // debug /////////////////////////////////////////////////////////////// - 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(targetComponent.position); - - // enough alpha, in front of camera and in screen? - if (point.z >= 0 && Utils.IsPointInScreen(point)) - { - // catchup is useful to show too - int serverBufferExcess = Mathf.Max(serverBuffer.Count - catchupThreshold, 0); - int clientBufferExcess = Mathf.Max(clientBuffer.Count - catchupThreshold, 0); - float serverCatchup = serverBufferExcess * catchupMultiplier; - float clientCatchup = clientBufferExcess * catchupMultiplier; - - GUI.color = overlayColor; - GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100)); - - // always show both client & server buffers so it's super - // obvious if we accidentally populate both. - GUILayout.Label($"Server Buffer:{serverBuffer.Count}"); - if (serverCatchup > 0) - GUILayout.Label($"Server Catchup:{serverCatchup * 100:F2}%"); - - GUILayout.Label($"Client Buffer:{clientBuffer.Count}"); - if (clientCatchup > 0) - GUILayout.Label($"Client Catchup:{clientCatchup * 100:F2}%"); - - GUILayout.EndArea(); - GUI.color = Color.white; - } - } - - protected virtual void DrawGizmos(SortedList buffer) - { - // only draw if we have at least two entries - if (buffer.Count < 2) return; - - // calcluate threshold for 'old enough' snapshots - double threshold = NetworkTime.localTime - bufferTime; - Color oldEnoughColor = new Color(0, 1, 0, 0.5f); - Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f); - - // 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) - { - // color depends on if old enough or not - NTSnapshot entry = buffer.Values[i]; - bool oldEnough = entry.localTimestamp <= threshold; - Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor; - Gizmos.DrawCube(entry.position, Vector3.one); - } - - // extra: lines between start<->position<->goal - Gizmos.color = Color.green; - Gizmos.DrawLine(buffer.Values[0].position, targetComponent.position); - Gizmos.color = Color.white; - Gizmos.DrawLine(targetComponent.position, buffer.Values[1].position); - } - - protected virtual void OnDrawGizmos() - { - // This fires in edit mode but that spams NRE's so check isPlaying - if (!Application.isPlaying) return; - if (!showGizmos) return; - - if (isServer) DrawGizmos(serverBuffer); - if (isClient) DrawGizmos(clientBuffer); - } -#endif - } -} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs deleted file mode 100644 index 8032506..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs +++ /dev/null @@ -1,14 +0,0 @@ -// A component to synchronize the position of child transforms of networked objects. -// There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the recieved values. -using UnityEngine; - -namespace Mirror -{ - [AddComponentMenu("Network/Network Transform Child")] - public class NetworkTransformChild : NetworkTransformBase - { - [Header("Target")] - public Transform target; - protected override Transform targetComponent => target; - } -} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta b/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta deleted file mode 100644 index ae36756..0000000 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransformChild.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 734b48bea0b204338958ee3d885e11f0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Components/PredictedRigidbody.meta b/Assets/Mirror/Components/PredictedRigidbody.meta new file mode 100644 index 0000000..a513fe0 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 09cc6745984c453a8cfb4cf4244d2570 +timeCreated: 1693576410 diff --git a/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat new file mode 100644 index 0000000..ff29a73 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat @@ -0,0 +1,85 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: LocalGhostMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - _ALPHAPREMULTIPLY_ON + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 3000 + stringTagMap: + RenderType: Transparent + disabledShaderPasses: [] + m_LockedProperties: + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 10 + - _GlossMapScale: 1 + - _Glossiness: 0.92 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 3 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 0 + m_Colors: + - _Color: {r: 1, g: 0, b: 0.067070484, a: 0.15686275} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta new file mode 100644 index 0000000..4a9b6eb --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 411a48b4a197d4924bec3e3809bc9320 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/LocalGhostMaterial.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs new file mode 100644 index 0000000..02abda3 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -0,0 +1,1021 @@ +// PredictedRigidbody which stores & indidvidually rewinds history per Rigidbody. +// +// This brings significant performance savings because: +// - if a scene has 1000 objects +// - and a player interacts with say 3 objects at a time +// - Physics.Simulate() would resimulate 1000 objects +// - where as this component only resimulates the 3 changed objects +// +// The downside is that history rewinding is done manually via Vector math, +// instead of real physics. It's not 100% correct - but it sure is fast! +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public enum PredictionMode { Smooth, Fast } + + // [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it. + public class PredictedRigidbody : NetworkBehaviour + { + Transform tf; // this component is performance critical. cache .transform getter! + + // Prediction sometimes moves the Rigidbody to a ghost object. + // .predictedRigidbody is always kept up to date to wherever the RB is. + // other components should use this when accessing Rigidbody. + public Rigidbody predictedRigidbody; + Transform predictedRigidbodyTransform; // predictedRigidbody.transform for performance (Get/SetPositionAndRotation) + + Vector3 lastPosition; + + // motion smoothing happen on-demand, because it requires moving physics components to another GameObject. + // this only starts at a given velocity and ends when stopped moving. + // to avoid constant on/off/on effects, it also stays on for a minimum time. + [Header("Motion Smoothing")] + [Tooltip("Prediction supports two different modes: Smooth and Fast:\n\nSmooth: Physics are separated from the GameObject & applied in the background. Rendering smoothly follows the physics for perfectly smooth interpolation results. Much softer, can be even too soft where sharp collisions won't look as sharp (i.e. Billiard balls avoid the wall before even hitting it).\n\nFast: Physics remain on the GameObject and corrections are applied hard. Much faster since we don't need to update a separate GameObject, a bit harsher, more precise.")] + public PredictionMode mode = PredictionMode.Smooth; + [Tooltip("Smoothing via Ghost-following only happens on demand, while moving with a minimum velocity.")] + public float motionSmoothingVelocityThreshold = 0.1f; + float motionSmoothingVelocityThresholdSqr; // ² cached in Awake + public float motionSmoothingAngularVelocityThreshold = 5.0f; // Billiards demo: 0.1 is way too small, takes forever for IsMoving()==false + float motionSmoothingAngularVelocityThresholdSqr; // ² cached in Awake + public float motionSmoothingTimeTolerance = 0.5f; + double motionSmoothingLastMovedTime; + + // client keeps state history for correction & reconciliation. + // this needs to be a SortedList because we need to be able to insert inbetween. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! + [Header("State History")] + public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough + readonly SortedList stateHistory = new SortedList(); + public float recordInterval = 0.050f; + + [Tooltip("(Optional) performance optimization where FixedUpdate.RecordState() only inserts state into history if the state actually changed.\nThis is generally a good idea.")] + public bool onlyRecordChanges = true; + + [Tooltip("(Optional) performance optimization where received state is compared to the LAST recorded state first, before sampling the whole history.\n\nThis can save significant traversal overhead for idle objects with a tiny chance of missing corrections for objects which revisisted the same position in the recent history twice.")] + public bool compareLastFirst = true; + + [Header("Reconciliation")] + [Tooltip("Correction threshold in meters. For example, 0.1 means that if the client is off by more than 10cm, it gets corrected.")] + public double positionCorrectionThreshold = 0.10; + double positionCorrectionThresholdSqr; // ² cached in Awake + [Tooltip("Correction threshold in degrees. For example, 5 means that if the client is off by more than 5 degrees, it gets corrected.")] + public double rotationCorrectionThreshold = 5; + + [Tooltip("Applying server corrections one frame ahead gives much better results. We don't know why yet, so this is an option for now.")] + public bool oneFrameAhead = true; + + [Header("Smoothing")] + [Tooltip("Snap to the server state directly when velocity is < threshold. This is useful to reduce jitter/fighting effects before coming to rest.\nNote this applies position, rotation and velocity(!) so it's still smooth.")] + public float snapThreshold = 2; // 0.5 has too much fighting-at-rest, 2 seems ideal. + + [Header("Visual Interpolation")] + [Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")] + public bool showGhost = true; + [Tooltip("Physics components are moved onto a ghost object beyond this threshold. Main object visually interpolates to it.")] + public float ghostVelocityThreshold = 0.1f; + + [Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")] + public Material localGhostMaterial; + public Material remoteGhostMaterial; + + [Tooltip("Performance optimization: only create/destroy ghosts every n-th frame is enough.")] + public int checkGhostsEveryNthFrame = 4; + + [Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")] + public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least + public float rotationInterpolationSpeed = 10; + + [Tooltip("Teleport if we are further than 'multiplier x collider size' behind.")] + public float teleportDistanceMultiplier = 10; + + [Header("Bandwidth")] + [Tooltip("Reduce sends while velocity==0. Client's objects may slightly move due to gravity/physics, so we still want to send corrections occasionally even if an object is idle on the server the whole time.")] + public bool reduceSendsWhileIdle = true; + + // Rigidbody & Collider are moved out into a separate object. + // this way the visual object can smoothly follow. + protected GameObject physicsCopy; + // protected Transform physicsCopyTransform; // caching to avoid GetComponent + // protected Rigidbody physicsCopyRigidbody => rb; // caching to avoid GetComponent + // protected Collider physicsCopyCollider; // caching to avoid GetComponent + float smoothFollowThreshold; // caching to avoid calculation in LateUpdate + float smoothFollowThresholdSqr; // caching to avoid calculation in LateUpdate + + // we also create one extra ghost for the exact known server state. + protected GameObject remoteCopy; + + // joints + Vector3 initialPosition; + Quaternion initialRotation; + // Vector3 initialScale; // don't change scale for now. causes issues with parenting. + + Color originalColor; + + protected virtual void Awake() + { + tf = transform; + predictedRigidbody = GetComponent(); + if (predictedRigidbody == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component."); + predictedRigidbodyTransform = predictedRigidbody.transform; + + // in fast mode, we need to force enable Rigidbody.interpolation. + // otherwise there's not going to be any smoothing whatsoever. + if (mode == PredictionMode.Fast) + { + predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate; + } + + // cache some threshold to avoid calculating them in LateUpdate + float colliderSize = GetComponentInChildren().bounds.size.magnitude; + smoothFollowThreshold = colliderSize * teleportDistanceMultiplier; + smoothFollowThresholdSqr = smoothFollowThreshold * smoothFollowThreshold; + + // cache initial position/rotation/scale to be used when moving physics components (configurable joints' range of motion) + initialPosition = tf.position; + initialRotation = tf.rotation; + // initialScale = tf.localScale; + + // cache ² computations + motionSmoothingVelocityThresholdSqr = motionSmoothingVelocityThreshold * motionSmoothingVelocityThreshold; + motionSmoothingAngularVelocityThresholdSqr = motionSmoothingAngularVelocityThreshold * motionSmoothingAngularVelocityThreshold; + positionCorrectionThresholdSqr = positionCorrectionThreshold * positionCorrectionThreshold; + } + + protected virtual void CopyRenderersAsGhost(GameObject destination, Material material) + { + // find the MeshRenderer component, which sometimes is on a child. + MeshRenderer originalMeshRenderer = GetComponentInChildren(true); + MeshFilter originalMeshFilter = GetComponentInChildren(true); + if (originalMeshRenderer != null && originalMeshFilter != null) + { + MeshFilter meshFilter = destination.AddComponent(); + meshFilter.mesh = originalMeshFilter.mesh; + + MeshRenderer meshRenderer = destination.AddComponent(); + meshRenderer.material = originalMeshRenderer.material; + + // renderers often have multiple materials. copy all. + if (originalMeshRenderer.materials != null) + { + Material[] materials = new Material[originalMeshRenderer.materials.Length]; + for (int i = 0; i < materials.Length; ++i) + { + materials[i] = material; + } + meshRenderer.materials = materials; // need to reassign to see it in effect + } + } + // if we didn't find a renderer, show a warning + else Debug.LogWarning($"PredictedRigidbody: {name} found no renderer to copy onto the visual object. If you are using a custom setup, please overwrite PredictedRigidbody.CreateVisualCopy()."); + } + + // instantiate a physics-only copy of the gameobject to apply corrections. + // this way the main visual object can smoothly follow. + // it's best to separate the physics instead of separating the renderers. + // some projects have complex rendering / animation setups which we can't touch. + // besides, Rigidbody+Collider are two components, where as renders may be many. + protected virtual void CreateGhosts() + { + // skip if host mode or already separated + if (isServer || physicsCopy != null) return; + + // Debug.Log($"Separating Physics for {name}"); // logging this allocates too much + + // create an empty GameObject with the same name + _Physical + // it's important to copy world position/rotation/scale, not local! + // because the original object may be a child of another. + // + // for example: + // parent (scale=1.5) + // child (scale=0.5) + // + // if we copy localScale then the copy has scale=0.5, where as the + // original would have a global scale of ~1.0. + physicsCopy = new GameObject($"{name}_Physical"); + + // assign the same Layer for the physics copy. + // games may use a custom physics collision matrix, layer matters. + physicsCopy.layer = gameObject.layer; + + // add the PredictedRigidbodyPhysical component + PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent(); + physicsGhostRigidbody.target = tf; + + // when moving (Configurable)Joints, their range of motion is + // relative to the initial position. if we move them after the + // GameObject rotated, the range of motion is wrong. + // the easiest solution is to move to initial position, + // then move physics components, then move back. + // => remember previous + Vector3 position = tf.position; + Quaternion rotation = tf.rotation; + // Vector3 scale = tf.localScale; // don't change scale for now. causes issues with parenting. + // => reset to initial + physicsGhostRigidbody.transform.position = tf.position = initialPosition; + physicsGhostRigidbody.transform.rotation = tf.rotation = initialRotation; + physicsGhostRigidbody.transform.localScale = tf.lossyScale;// world scale! // = initialScale; // don't change scale for now. causes issues with parenting. + // => move physics components + PredictionUtils.MovePhysicsComponents(gameObject, physicsCopy); + // => reset previous + physicsGhostRigidbody.transform.position = tf.position = position; + physicsGhostRigidbody.transform.rotation = tf.rotation = rotation; + //physicsGhostRigidbody.transform.localScale = tf.lossyScale; // world scale! //= scale; // don't change scale for now. causes issues with parenting. + + // show ghost by copying all renderers / materials with ghost material applied + if (showGhost) + { + // one for the locally predicted rigidbody + CopyRenderersAsGhost(physicsCopy, localGhostMaterial); + + // one for the latest remote state for comparison + // it's important to copy world position/rotation/scale, not local! + // because the original object may be a child of another. + // + // for example: + // parent (scale=1.5) + // child (scale=0.5) + // + // if we copy localScale then the copy has scale=0.5, where as the + // original would have a global scale of ~1.0. + remoteCopy = new GameObject($"{name}_Remote"); + remoteCopy.transform.position = tf.position; // world position! + remoteCopy.transform.rotation = tf.rotation; // world rotation! + remoteCopy.transform.localScale = tf.lossyScale; // world scale! + CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial); + } + + // assign our Rigidbody reference to the ghost + predictedRigidbody = physicsCopy.GetComponent(); + predictedRigidbodyTransform = predictedRigidbody.transform; + } + + protected virtual void DestroyGhosts() + { + // move the copy's Rigidbody back onto self. + // important for scene objects which may be reused for AOI spawn/despawn. + // otherwise next time they wouldn't have a collider anymore. + if (physicsCopy != null) + { + // when moving (Configurable)Joints, their range of motion is + // relative to the initial position. if we move them after the + // GameObject rotated, the range of motion is wrong. + // the easiest solution is to move to initial position, + // then move physics components, then move back. + // => remember previous + Vector3 position = tf.position; + Quaternion rotation = tf.rotation; + Vector3 scale = tf.localScale; + // => reset to initial + physicsCopy.transform.position = tf.position = initialPosition; + physicsCopy.transform.rotation = tf.rotation = initialRotation; + physicsCopy.transform.localScale = tf.lossyScale;// = initialScale; + // => move physics components + PredictionUtils.MovePhysicsComponents(physicsCopy, gameObject); + // => reset previous + tf.position = position; + tf.rotation = rotation; + tf.localScale = scale; + + // when moving components back, we need to undo the joints initial-delta rotation that we added. + Destroy(physicsCopy); + + // reassign our Rigidbody reference + predictedRigidbody = GetComponent(); + predictedRigidbodyTransform = predictedRigidbody.transform; + } + + // simply destroy the remote copy + if (remoteCopy != null) Destroy(remoteCopy); + } + + // this shows in profiler LateUpdates! need to make this as fast as possible! + protected virtual void SmoothFollowPhysicsCopy() + { + // hard follow: + // predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); + // tf.SetPositionAndRotation(physicsPosition, physicsRotation); + + // ORIGINAL VERSION: CLEAN AND SIMPLE + /* + // if we are further than N colliders sizes behind, then teleport + float colliderSize = physicsCopyCollider.bounds.size.magnitude; + float threshold = colliderSize * teleportDistanceMultiplier; + float distance = Vector3.Distance(tf.position, physicsCopyRigidbody.position); + if (distance > threshold) + { + tf.position = physicsCopyRigidbody.position; + tf.rotation = physicsCopyRigidbody.rotation; + Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}"); + return; + } + + // smoothly interpolate to the target position. + // speed relative to how far away we are + float positionStep = distance * positionInterpolationSpeed; + tf.position = Vector3.MoveTowards(tf.position, physicsCopyRigidbody.position, positionStep * Time.deltaTime); + + // smoothly interpolate to the target rotation. + // Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. + tf.rotation = Quaternion.Slerp(tf.rotation, physicsCopyRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime); + */ + + // FAST VERSION: this shows in profiler a lot, so cache EVERYTHING! + tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than tf.position + tf.rotation + predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); // faster than Rigidbody .position and .rotation + float deltaTime = Time.deltaTime; + + // slow and simple version: + // float distance = Vector3.Distance(currentPosition, physicsPosition); + // if (distance > smoothFollowThreshold) + // faster version + Vector3 delta = physicsPosition - currentPosition; + float sqrDistance = Vector3.SqrMagnitude(delta); + float distance = Mathf.Sqrt(sqrDistance); + if (sqrDistance > smoothFollowThresholdSqr) + { + tf.SetPositionAndRotation(physicsPosition, physicsRotation); // faster than .position and .rotation manually + Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {smoothFollowThreshold:F2}"); + return; + } + + // smoothly interpolate to the target position. + // speed relative to how far away we are. + // => speed increases by distance² because the further away, the + // sooner we need to catch the fuck up + // float positionStep = (distance * distance) * interpolationSpeed; + float positionStep = distance * positionInterpolationSpeed; + + Vector3 newPosition = MoveTowardsCustom(currentPosition, physicsPosition, delta, sqrDistance, distance, positionStep * deltaTime); + + // smoothly interpolate to the target rotation. + // Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp. + // Quaternions always need to be normalized in order to be a valid rotation after operations + Quaternion newRotation = Quaternion.Slerp(currentRotation, physicsRotation, rotationInterpolationSpeed * deltaTime).normalized; + + // assign position and rotation together. faster than accessing manually. + tf.SetPositionAndRotation(newPosition, newRotation); + } + + // simple and slow version with MoveTowards, which recalculates delta and delta.sqrMagnitude: + // Vector3 newPosition = Vector3.MoveTowards(currentPosition, physicsPosition, positionStep * deltaTime); + // faster version copied from MoveTowards: + // this increases Prediction Benchmark Client's FPS from 615 -> 640. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector3 MoveTowardsCustom( + Vector3 current, + Vector3 target, + Vector3 _delta, // pass this in since we already calculated it + float _sqrDistance, // pass this in since we already calculated it + float _distance, // pass this in since we already calculated it + float maxDistanceDelta) + { + if (_sqrDistance == 0.0 || maxDistanceDelta >= 0.0 && _sqrDistance <= maxDistanceDelta * maxDistanceDelta) + return target; + + float distFactor = maxDistanceDelta / _distance; // unlike Vector3.MoveTowards, we only calculate this once + return new Vector3( + // current.x + (_delta.x / _distance) * maxDistanceDelta, + // current.y + (_delta.y / _distance) * maxDistanceDelta, + // current.z + (_delta.z / _distance) * maxDistanceDelta); + current.x + _delta.x * distFactor, + current.y + _delta.y * distFactor, + current.z + _delta.z * distFactor); + } + + // destroy visual copy only in OnStopClient(). + // OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed. + public override void OnStopClient() + { + DestroyGhosts(); + } + + void UpdateServer() + { + // bandwidth optimization while idle. + if (reduceSendsWhileIdle) + { + // while moving, always sync every frame for immediate corrections. + // while idle, only sync once per second. + // + // we still need to sync occasionally because objects on client + // may still slide or move slightly due to gravity, physics etc. + // and those still need to get corrected if not moving on server. + // + // TODO + // next round of optimizations: if client received nothing for 1s, + // force correct to last received state. then server doesn't need + // to send once per second anymore. + syncInterval = IsMoving() ? 0 : 1; + } + + // always set dirty to always serialize in next sync interval. + SetDirty(); + } + + // movement detection is virtual, in case projects want to use other methods. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual bool IsMoving() => + // straight forward implementation + // predictedRigidbody.velocity.magnitude >= motionSmoothingVelocityThreshold || + // predictedRigidbody.angularVelocity.magnitude >= motionSmoothingAngularVelocityThreshold; + // faster implementation with cached ² +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity.sqrMagnitude >= motionSmoothingVelocityThresholdSqr || +#else + predictedRigidbody.velocity.sqrMagnitude >= motionSmoothingVelocityThresholdSqr || +#endif + predictedRigidbody.angularVelocity.sqrMagnitude >= motionSmoothingAngularVelocityThresholdSqr; + // TODO maybe merge the IsMoving() checks & callbacks with UpdateState(). + void UpdateGhosting() + { + // perf: enough to check ghosts every few frames. + // PredictionBenchmark: only checking every 4th frame: 585 => 600 FPS + if (Time.frameCount % checkGhostsEveryNthFrame != 0) return; + + // client only uses ghosts on demand while interacting. + // this way 1000 GameObjects don't need +1000 Ghost GameObjects all the time! + + // no ghost at the moment + if (physicsCopy == null) + { + // faster than velocity threshold? then create the ghosts. + // with 10% buffer zone so we don't flip flop all the time. + if (IsMoving()) + { + CreateGhosts(); + OnBeginPrediction(); + } + } + // ghosting at the moment + else + { + // always set last moved time while moving. + // this way we can avoid on/off/oneffects when stopping. + if (IsMoving()) + { + motionSmoothingLastMovedTime = NetworkTime.time; + } + // slower than velocity threshold? then destroy the ghosts. + // with a minimum time since starting to move, to avoid on/off/on effects. + else + { + if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) + { + DestroyGhosts(); + OnEndPrediction(); + physicsCopy = null; // TESTING + } + } + } + } + + // when using Fast mode, we don't create any ghosts. + // but we still want to check IsMoving() in order to support the same + // user callbacks. + bool lastMoving = false; + void UpdateState() + { + // perf: enough to check ghosts every few frames. + // PredictionBenchmark: only checking every 4th frame: 770 => 800 FPS + if (Time.frameCount % checkGhostsEveryNthFrame != 0) return; + + bool moving = IsMoving(); + + // started moving? + if (moving && !lastMoving) + { + OnBeginPrediction(); + lastMoving = true; + } + // stopped moving? + else if (!moving && lastMoving) + { + // ensure a minimum time since starting to move, to avoid on/off/on effects. + if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) + { + OnEndPrediction(); + lastMoving = false; + } + } + } + + void Update() + { + if (isServer) UpdateServer(); + if (isClientOnly) + { + if (mode == PredictionMode.Smooth) + UpdateGhosting(); + else if (mode == PredictionMode.Fast) + UpdateState(); + } + } + + void LateUpdate() + { + // only follow on client-only, not in server or host mode + if (isClientOnly && mode == PredictionMode.Smooth && physicsCopy) SmoothFollowPhysicsCopy(); + } + + void FixedUpdate() + { + // on clients (not host) we record the current state every FixedUpdate. + // this is cheap, and allows us to keep a dense history. + if (!isClientOnly) return; + + // OPTIMIZATION: RecordState() is expensive because it inserts into a SortedList. + // only record if state actually changed! + // risks not having up to date states when correcting, + // but it doesn't matter since we'll always compare with the 'newest' anyway. + // + // we check in here instead of in RecordState() because RecordState() should definitely record if we call it! + if (onlyRecordChanges) + { + // TODO maybe don't reuse the correction thresholds? + tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); + // clean & simple: + // if (Vector3.Distance(lastRecorded.position, position) < positionCorrectionThreshold && + // Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold) + // faster: + if ((lastRecorded.position - position).sqrMagnitude < positionCorrectionThresholdSqr && + Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold) + { + // Debug.Log($"FixedUpdate for {name}: taking optimized early return instead of recording state."); + return; + } + } + + RecordState(); + } + + // manually store last recorded so we can easily check against this + // without traversing the SortedList. + RigidbodyState lastRecorded; + double lastRecordTime; + void RecordState() + { + // performance optimization: only call NetworkTime.time getter once + double networkTime = NetworkTime.time; + + // instead of recording every fixedupdate, let's record in an interval. + // we don't want to record every tiny move and correct too hard. + if (networkTime < lastRecordTime + recordInterval) return; + lastRecordTime = networkTime; + + // NetworkTime.time is always behind by bufferTime. + // prediction aims to be on the exact same server time (immediately). + // use predictedTime to record state, otherwise we would record in the past. + double predictedTime = NetworkTime.predictedTime; + + // FixedUpdate may run twice in the same frame / NetworkTime.time. + // for now, simply don't record if already recorded there. + // previously we checked ContainsKey which is O(logN) for SortedList + // if (stateHistory.ContainsKey(predictedTime)) + // return; + // instead, simply store the last recorded time and don't insert if same. + if (predictedTime == lastRecorded.timestamp) return; + + // keep state history within limit + if (stateHistory.Count >= stateHistoryLimit) + stateHistory.RemoveAt(0); + + // grab current position/rotation/velocity only once. + // this is performance critical, avoid calling .transform multiple times. + tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than accessing .position + .rotation manually +#if UNITY_6000_0_OR_NEWER + Vector3 currentVelocity = predictedRigidbody.linearVelocity; +#else + Vector3 currentVelocity = predictedRigidbody.velocity; +#endif + Vector3 currentAngularVelocity = predictedRigidbody.angularVelocity; + + // calculate delta to previous state (if any) + Vector3 positionDelta = Vector3.zero; + Vector3 velocityDelta = Vector3.zero; + Vector3 angularVelocityDelta = Vector3.zero; + Quaternion rotationDelta = Quaternion.identity; + int stateHistoryCount = stateHistory.Count; // perf: only grab .Count once + if (stateHistoryCount > 0) + { + RigidbodyState last = stateHistory.Values[stateHistoryCount - 1]; + positionDelta = currentPosition - last.position; + velocityDelta = currentVelocity - last.velocity; + // Quaternions always need to be normalized in order to be valid rotations after operations + rotationDelta = (currentRotation * Quaternion.Inverse(last.rotation)).normalized; + angularVelocityDelta = currentAngularVelocity - last.angularVelocity; + + // debug draw the recorded state + // Debug.DrawLine(last.position, currentPosition, Color.red, lineTime); + } + + // create state to insert + RigidbodyState state = new RigidbodyState( + predictedTime, + positionDelta, + currentPosition, + rotationDelta, + currentRotation, + velocityDelta, + currentVelocity, + angularVelocityDelta, + currentAngularVelocity + ); + + // add state to history + stateHistory.Add(predictedTime, state); + + // manually remember last inserted state for faster .Last comparisons + lastRecorded = state; + } + + // optional user callbacks, in case people need to know about events. + protected virtual void OnSnappedIntoPlace() {} + protected virtual void OnBeforeApplyState() {} + protected virtual void OnCorrected() {} + protected virtual void OnBeginPrediction() {} // when the Rigidbody moved above threshold and we created a ghost + protected virtual void OnEndPrediction() {} // when the Rigidbody came to rest and we destroyed the ghost + + void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity) + { + // fix rigidbodies seemingly dancing in place instead of coming to rest. + // hard snap to the position below a threshold velocity. + // this is fine because the visual object still smoothly interpolates to it. + // => consider both velocity and angular velocity (in case of Rigidbodies only rotating with joints etc.) +#if UNITY_6000_0_OR_NEWER + if (predictedRigidbody.linearVelocity.magnitude <= snapThreshold && + predictedRigidbody.angularVelocity.magnitude <= snapThreshold) +#else + if (predictedRigidbody.velocity.magnitude <= snapThreshold && + predictedRigidbody.angularVelocity.magnitude <= snapThreshold) +#endif + { + // Debug.Log($"Prediction: snapped {name} into place because velocity {predictedRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); + + // apply server state immediately. + // important to apply velocity as well, instead of Vector3.zero. + // in case an object is still slightly moving, we don't want it + // to stop and start moving again on client - slide as well here. + predictedRigidbody.position = position; + predictedRigidbody.rotation = rotation; + // projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error + if (!predictedRigidbody.isKinematic) + { +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity = velocity; +#else + predictedRigidbody.velocity = velocity; +#endif + predictedRigidbody.angularVelocity = angularVelocity; + } + + // clear history and insert the exact state we just applied. + // this makes future corrections more accurate. + stateHistory.Clear(); + stateHistory.Add(timestamp, new RigidbodyState( + timestamp, + Vector3.zero, + position, + Quaternion.identity, + rotation, + Vector3.zero, + velocity, + Vector3.zero, + angularVelocity + )); + + // user callback + OnSnappedIntoPlace(); + return; + } + + // we have a callback for snapping into place (above). + // we also need one for corrections without snapping into place. + // call it before applying pos/rot/vel in case we need to set kinematic etc. + OnBeforeApplyState(); + + // apply the state to the Rigidbody + if (mode == PredictionMode.Smooth) + { + // Smooth mode separates Physics from Renderering. + // Rendering smoothly follows Physics in SmoothFollowPhysicsCopy(). + // this allows us to be able to hard teleport to the correction. + // which gives most accurate results since the Rigidbody can't + // be stopped by another object when trying to correct. + predictedRigidbody.position = position; + predictedRigidbody.rotation = rotation; + } + else if (mode == PredictionMode.Fast) + { + // Fast mode doesn't separate physics from rendering. + // The only smoothing we get is from Rigidbody.MovePosition. + predictedRigidbody.MovePosition(position); + predictedRigidbody.MoveRotation(rotation); + } + + // there's only one way to set velocity. + // (projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error) + if (!predictedRigidbody.isKinematic) + { +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity = velocity; +#else + predictedRigidbody.velocity = velocity; +#endif + predictedRigidbody.angularVelocity = angularVelocity; + } + } + + // process a received server state. + // compares it against our history and applies corrections if needed. + void OnReceivedState(double timestamp, RigidbodyState state)//, bool sleeping) + { + // always update remote state ghost + if (remoteCopy != null) + { + Transform remoteCopyTransform = remoteCopy.transform; + remoteCopyTransform.SetPositionAndRotation(state.position, state.rotation); // faster than .position + .rotation setters + remoteCopyTransform.localScale = tf.lossyScale; // world scale! see CreateGhosts comment. + } + + + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // color code remote sleeping objects to debug objects coming to rest + // if (showRemoteSleeping) + // { + // rend.material.color = sleeping ? Color.gray : originalColor; + // } + + // performance: get Rigidbody position & rotation only once, + // and together via its transform + predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); + + // OPTIONAL performance optimization when comparing idle objects. + // even idle objects will have a history of ~32 entries. + // sampling & traversing through them is unnecessarily costly. + // instead, compare directly against the current rigidbody position! + // => this is technically not 100% correct if an object runs in + // circles where it may revisit the same position twice. + // => but practically, objects that didn't move will have their + // whole history look like the last inserted state. + // => comparing against that is free and gives us a significant + // performance saving vs. a tiny chance of incorrect results due + // to objects running in circles. + // => the RecordState() call below is expensive too, so we want to + // do this before even recording the latest state. the only way + // to do this (in case last recorded state is too old), is to + // compare against live rigidbody.position without any recording. + // this is as fast as it gets for skipping idle objects. + // + // if this ever causes issues, feel free to disable it. + float positionToStateDistanceSqr = Vector3.SqrMagnitude(state.position - physicsPosition); + if (compareLastFirst && + // Vector3.Distance(state.position, physicsPosition) < positionCorrectionThreshold && // slow comparison + positionToStateDistanceSqr < positionCorrectionThresholdSqr && // fast comparison + Quaternion.Angle(state.rotation, physicsRotation) < rotationCorrectionThreshold) + { + // Debug.Log($"OnReceivedState for {name}: taking optimized early return!"); + return; + } + + // we only capture state every 'interval' milliseconds. + // so the newest entry in 'history' may be up to 'interval' behind 'now'. + // if there's no latency, we may receive a server state for 'now'. + // sampling would fail, if we haven't recorded anything in a while. + // to solve this, always record the current state when receiving a server state. + RecordState(); + + // correction requires at least 2 existing states for 'before' and 'after'. + // if we don't have two yet, drop this state and try again next time once we recorded more. + if (stateHistory.Count < 2) return; + + RigidbodyState oldest = stateHistory.Values[0]; + RigidbodyState newest = stateHistory.Values[stateHistory.Count - 1]; + + // edge case: is the state older than the oldest state in history? + // this can happen if the client gets so far behind the server + // that it doesn't have a recored history to sample from. + // in that case, we should hard correct the client. + // otherwise it could be out of sync as long as it's too far behind. + if (state.timestamp < oldest.timestamp) + { + // when starting, client may only have 2-3 states in history. + // it's expected that server states would be behind those 2-3. + // only show a warning if it's behind the full history limit! + if (stateHistory.Count >= stateHistoryLimit) + Debug.LogWarning($"Hard correcting client object {name} because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind."); + + // force apply the state + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); + return; + } + + // edge case: is it newer than the newest state in history? + // this can happen if client's predictedTime predicts too far ahead of the server. + // in that case, log a warning for now but still apply the correction. + // otherwise it could be out of sync as long as it's too far ahead. + // + // for example, when running prediction on the same machine with near zero latency. + // when applying corrections here, this looks just fine on the local machine. + if (newest.timestamp < state.timestamp) + { + // the correction is for a state in the future. + // we clamp it to 'now'. + // but only correct if off by threshold. + // TODO maybe we should interpolate this back to 'now'? + // if (Vector3.Distance(state.position, physicsPosition) >= positionCorrectionThreshold) // slow comparison + if (positionToStateDistanceSqr >= positionCorrectionThresholdSqr) // fast comparison + { + // this can happen a lot when latency is ~0. logging all the time allocates too much and is too slow. + // double ahead = state.timestamp - newest.timestamp; + // Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter."); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); + } + return; + } + + // find the two closest client states between timestamp + if (!Prediction.Sample(stateHistory, timestamp, out RigidbodyState before, out RigidbodyState after, out int afterIndex, out double t)) + { + // something went very wrong. sampling should've worked. + // hard correct to recover the error. + Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history."); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); + return; + } + + // interpolate between them to get the best approximation + RigidbodyState interpolated = RigidbodyState.Interpolate(before, after, (float)t); + + // calculate the difference between where we were and where we should be + // TODO only position for now. consider rotation etc. too later + // float positionToInterpolatedDistance = Vector3.Distance(state.position, interpolated.position); // slow comparison + float positionToInterpolatedDistanceSqr = Vector3.SqrMagnitude(state.position - interpolated.position); // fast comparison + float rotationToInterpolatedDistance = Quaternion.Angle(state.rotation, interpolated.rotation); + // Debug.Log($"Sampled history of size={stateHistory.Count} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3} / {correctionThreshold:F3}"); + + // too far off? then correct it + if (positionToInterpolatedDistanceSqr >= positionCorrectionThresholdSqr || // fast comparison + //positionToInterpolatedDistance >= positionCorrectionThreshold || // slow comparison + rotationToInterpolatedDistance >= rotationCorrectionThreshold) + { + // Debug.Log($"CORRECTION NEEDED FOR {name} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3}"); + + // show the received correction position + velocity for debugging. + // helps to compare with the interpolated/applied correction locally. + //Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime); + + // insert the correction and correct the history on top of it. + // returns the final recomputed state after rewinding. + RigidbodyState recomputed = Prediction.CorrectHistory(stateHistory, stateHistoryLimit, state, before, after, afterIndex); + + // log, draw & apply the final position. + // always do this here, not when iterating above, in case we aren't iterating. + // for example, on same machine with near zero latency. + // int correctedAmount = stateHistory.Count - afterIndex; + // Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}"); + //Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime); + ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity, recomputed.angularVelocity); + + // user callback + OnCorrected(); + } + } + + // send state to clients every sendInterval. + // reliable for now. + // TODO we should use the one from FixedUpdate + public override void OnSerialize(NetworkWriter writer, bool initialState) + { + // Time.time was at the beginning of this frame. + // NetworkLateUpdate->Broadcast->OnSerialize is at the end of the frame. + // as result, client should use this to correct the _next_ frame. + // otherwise we see noticeable resets that seem off by one frame. + // + // to solve this, we can send the current deltaTime. + // server is technically supposed to be at a fixed frame rate, but this can vary. + // sending server's current deltaTime is the safest option. + // client then applies it on top of remoteTimestamp. + + + // FAST VERSION: this shows in profiler a lot, so cache EVERYTHING! + tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); // faster than tf.position + tf.rotation. server's rigidbody is on the same transform. + + // simple but slow write: + // writer.WriteFloat(Time.deltaTime); + // writer.WriteVector3(position); + // writer.WriteQuaternion(rotation); + // writer.WriteVector3(predictedRigidbody.velocity); + // writer.WriteVector3(predictedRigidbody.angularVelocity); + + // performance optimization: write a whole struct at once via blittable: + PredictedSyncData data = new PredictedSyncData( + Time.deltaTime, + position, + rotation, +#if UNITY_6000_0_OR_NEWER + predictedRigidbody.linearVelocity, +#else + predictedRigidbody.velocity, +#endif + predictedRigidbody.angularVelocity);//, + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // predictedRigidbody.IsSleeping()); + writer.WritePredictedSyncData(data); + } + + // read the server's state, compare with client state & correct if necessary. + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + // deserialize data + // we want to know the time on the server when this was sent, which is remoteTimestamp. + double timestamp = NetworkClient.connection.remoteTimeStamp; + + // simple but slow read: + // double serverDeltaTime = reader.ReadFloat(); + // Vector3 position = reader.ReadVector3(); + // Quaternion rotation = reader.ReadQuaternion(); + // Vector3 velocity = reader.ReadVector3(); + // Vector3 angularVelocity = reader.ReadVector3(); + + // performance optimization: read a whole struct at once via blittable: + PredictedSyncData data = reader.ReadPredictedSyncData(); + double serverDeltaTime = data.deltaTime; + Vector3 position = data.position; + Quaternion rotation = data.rotation; + Vector3 velocity = data.velocity; + Vector3 angularVelocity = data.angularVelocity; + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // bool sleeping = data.sleeping != 0; + + // server sends state at the end of the frame. + // parse and apply the server's delta time to our timestamp. + // otherwise we see noticeable resets that seem off by one frame. + timestamp += serverDeltaTime; + + // however, adding yet one more frame delay gives much(!) better results. + // we don't know why yet, so keep this as an option for now. + // possibly because client captures at the beginning of the frame, + // with physics happening at the end of the frame? + if (oneFrameAhead) timestamp += serverDeltaTime; + + // process received state + OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, Quaternion.identity, rotation, Vector3.zero, velocity, Vector3.zero, angularVelocity));//, sleeping); + } + + protected override void OnValidate() + { + base.OnValidate(); + + // force syncDirection to be ServerToClient + syncDirection = SyncDirection.ServerToClient; + + // state should be synced immediately for now. + // later when we have prediction fully dialed in, + // then we can maybe relax this a bit. + syncInterval = 0; + } + + // helper function for Physics tests to check if a Rigidbody belongs to + // a PredictedRigidbody component (either on it, or on its ghost). + public static bool IsPredicted(Rigidbody rb, out PredictedRigidbody predictedRigidbody) + { + // by default, Rigidbody is on the PredictedRigidbody GameObject + if (rb.TryGetComponent(out predictedRigidbody)) + return true; + + // it might be on a ghost while interacting + if (rb.TryGetComponent(out PredictedRigidbodyPhysicsGhost ghost)) + { + predictedRigidbody = ghost.target.GetComponent(); + return true; + } + + // otherwise the Rigidbody does not belong to any PredictedRigidbody. + predictedRigidbody = null; + return false; + } + + // helper function for Physics tests to check if a Collider (which may be in children) belongs to + // a PredictedRigidbody component (either on it, or on its ghost). + public static bool IsPredicted(Collider co, out PredictedRigidbody predictedRigidbody) + { + // by default, Collider is on the PredictedRigidbody GameObject or it's children. + predictedRigidbody = co.GetComponentInParent(); + if (predictedRigidbody != null) + return true; + + // it might be on a ghost while interacting + PredictedRigidbodyPhysicsGhost ghost = co.GetComponentInParent(); + if (ghost != null && ghost.target != null && ghost.target.TryGetComponent(out predictedRigidbody)) + return true; + + // otherwise the Rigidbody does not belong to any PredictedRigidbody. + predictedRigidbody = null; + return false; + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta new file mode 100644 index 0000000..1a3ed44 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs.meta @@ -0,0 +1,22 @@ +fileFormatVersion: 2 +guid: d38927cdc6024b9682b5fe9778b9ef99 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, + type: 2} + - remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776, + type: 2} + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs new file mode 100644 index 0000000..f28d49f --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs @@ -0,0 +1,15 @@ +// Prediction moves out the Rigidbody & Collider into a separate object. +// this component simply points back to the owner component. +// in case Raycasts hit it and need to know the owner, etc. +using UnityEngine; + +namespace Mirror +{ + public class PredictedRigidbodyPhysicsGhost : MonoBehaviour + { + // this is performance critical, so store target's .Transform instead of + // PredictedRigidbody, this way we don't need to call the .transform getter. + [Tooltip("The predicted rigidbody owner.")] + public Transform target; + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta new file mode 100644 index 0000000..19f13c2 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 25593abc9bf0d44878a4ad6018204061 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyPhysicsGhost.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs new file mode 100644 index 0000000..636f397 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs @@ -0,0 +1 @@ +// removed 2024-02-09 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta new file mode 100644 index 0000000..22d9cf0 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 62e7e9424c7e48d69b6a3517796142a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbodyRemoteGhost.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs new file mode 100644 index 0000000..fa652fd --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs @@ -0,0 +1,54 @@ +// this struct exists only for OnDe/Serialize performance. +// instead of WriteVector3+Quaternion+Vector3+Vector3, +// we read & write the whole struct as blittable once. +// +// struct packing can cause odd results with blittable on different platforms, +// so this is usually not recommended! +// +// in this case however, we need to squeeze everything we can out of prediction +// to support low even devices / VR. +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Mirror +{ + // struct packing + + [StructLayout(LayoutKind.Sequential)] // explicitly force sequential + public struct PredictedSyncData + { + public float deltaTime; // 4 bytes (word aligned) + public Vector3 position; // 12 bytes (word aligned) + public Quaternion rotation; // 16 bytes (word aligned) + public Vector3 velocity; // 12 bytes (word aligned) + public Vector3 angularVelocity; // 12 bytes (word aligned) + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // public byte sleeping; // 1 byte: bool isn't blittable + + // constructor for convenience + public PredictedSyncData(float deltaTime, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity)//, bool sleeping) + { + this.deltaTime = deltaTime; + this.position = position; + this.rotation = rotation; + this.velocity = velocity; + this.angularVelocity = angularVelocity; + // DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!) + // this.sleeping = sleeping ? (byte)1 : (byte)0; + } + } + + // NetworkReader/Writer extensions to write this struct + public static class PredictedSyncDataReadWrite + { + public static void WritePredictedSyncData(this NetworkWriter writer, PredictedSyncData data) + { + writer.WriteBlittable(data); + } + + public static PredictedSyncData ReadPredictedSyncData(this NetworkReader reader) + { + return reader.ReadBlittable(); + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta new file mode 100644 index 0000000..8a7fe10 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: f595f112a39e4634b670d56991b23823 +timeCreated: 1710387026 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictedSyncData.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs new file mode 100644 index 0000000..82945c9 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs @@ -0,0 +1,430 @@ +// standalone utility functions for PredictedRigidbody component. +using System; +using UnityEngine; + +namespace Mirror +{ + public static class PredictionUtils + { + // rigidbody /////////////////////////////////////////////////////////// + // move a Rigidbody + settings from one GameObject to another. + public static void MoveRigidbody(GameObject source, GameObject destination, bool destroySource = true) + { + // create a new Rigidbody component on destination. + // note that adding a Joint automatically adds a Rigidbody. + // so first check if one was added yet. + Rigidbody original = source.GetComponent(); + if (original == null) throw new Exception($"Prediction: attempted to move {source}'s Rigidbody to the predicted copy, but there was no component."); + Rigidbody rigidbodyCopy; + if (!destination.TryGetComponent(out rigidbodyCopy)) + rigidbodyCopy = destination.AddComponent(); + + // copy all properties + rigidbodyCopy.mass = original.mass; +#if UNITY_6000_0_OR_NEWER + rigidbodyCopy.linearDamping = original.linearDamping; + rigidbodyCopy.angularDamping = original.angularDamping; +#else + rigidbodyCopy.drag = original.drag; + rigidbodyCopy.angularDrag = original.angularDrag; +#endif + rigidbodyCopy.useGravity = original.useGravity; + rigidbodyCopy.isKinematic = original.isKinematic; + rigidbodyCopy.interpolation = original.interpolation; + rigidbodyCopy.collisionDetectionMode = original.collisionDetectionMode; + // fix: need to set freezeRotation before constraints: + // https://github.com/MirrorNetworking/Mirror/pull/3946 + rigidbodyCopy.freezeRotation = original.freezeRotation; + rigidbodyCopy.constraints = original.constraints; + rigidbodyCopy.sleepThreshold = original.sleepThreshold; + + // moving (Configurable)Joints messes up their range of motion unless + // we reset to initial position first (we do this in PredictedRigibody.cs). + // so here we don't set the Rigidbody's physics position at all. + // rigidbodyCopy.position = original.position; + // rigidbodyCopy.rotation = original.rotation; + + // projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error + if (!original.isKinematic) + { +#if UNITY_6000_0_OR_NEWER + rigidbodyCopy.linearVelocity = original.linearVelocity; +#else + rigidbodyCopy.velocity = original.velocity; +#endif + rigidbodyCopy.angularVelocity = original.angularVelocity; + } + + // destroy original + if (destroySource) GameObject.Destroy(original); + } + + // helper function: if a collider is on a child, copy that child first. + // this way child's relative position/rotation/scale are preserved. + public static GameObject CopyRelativeTransform(GameObject source, Transform sourceChild, GameObject destination) + { + // is this on the source root? then we want to put it on the destination root. + if (sourceChild == source.transform) return destination; + + // is this on a child? then create the same child with the same transform on destination. + // note this is technically only correct for the immediate child since + // .localPosition is relative to parent, but this is good enough. + GameObject child = new GameObject(sourceChild.name); + child.transform.SetParent(destination.transform, true); + child.transform.localPosition = sourceChild.localPosition; + child.transform.localRotation = sourceChild.localRotation; + child.transform.localScale = sourceChild.localScale; + + // assign the same Layer for the physics copy. + // games may use a custom physics collision matrix, layer matters. + child.layer = sourceChild.gameObject.layer; + + return child; + } + + // colliders /////////////////////////////////////////////////////////// + // move all BoxColliders + settings from one GameObject to another. + public static void MoveBoxColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + BoxCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (BoxCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + BoxCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.size = sourceCollider.size; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all SphereColliders + settings from one GameObject to another. + public static void MoveSphereColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + SphereCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (SphereCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + SphereCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all CapsuleColliders + settings from one GameObject to another. + public static void MoveCapsuleColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + CapsuleCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (CapsuleCollider sourceCollider in sourceColliders) + { + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + CapsuleCollider colliderCopy = target.AddComponent(); + colliderCopy.center = sourceCollider.center; + colliderCopy.radius = sourceCollider.radius; + colliderCopy.height = sourceCollider.height; + colliderCopy.direction = sourceCollider.direction; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all MeshColliders + settings from one GameObject to another. + public static void MoveMeshColliders(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + MeshCollider[] sourceColliders = source.GetComponentsInChildren(); + foreach (MeshCollider sourceCollider in sourceColliders) + { + // when Models have Mesh->Read/Write disabled, it means that Unity + // uploads the mesh directly to the GPU and erases it on the CPU. + // on some platforms this makes moving a MeshCollider in builds impossible: + // + // "CollisionMeshData couldn't be created because the mesh has been marked as non-accessible." + // + // on other platforms, this works fine. + // let's show an explicit log message so in case collisions don't + // work at runtime, it's obvious why it happens and how to fix it. + if (!sourceCollider.sharedMesh.isReadable) + { + Debug.Log($"[Prediction]: MeshCollider on {sourceCollider.name} isn't readable, which may indicate that the Mesh only exists on the GPU. If {sourceCollider.name} is missing collisions, then please select the model in the Project Area, and enable Mesh->Read/Write so it's also available on the CPU!"); + // don't early return. keep trying, it may work. + } + + // copy the relative transform: + // if collider is on root, it returns destination root. + // if collider is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination); + MeshCollider colliderCopy = target.AddComponent(); + colliderCopy.sharedMesh = sourceCollider.sharedMesh; + colliderCopy.convex = sourceCollider.convex; + colliderCopy.isTrigger = sourceCollider.isTrigger; + colliderCopy.material = sourceCollider.material; + if (destroySource) GameObject.Destroy(sourceCollider); + } + } + + // move all Colliders + settings from one GameObject to another. + public static void MoveAllColliders(GameObject source, GameObject destination, bool destroySource = true) + { + MoveBoxColliders(source, destination, destroySource); + MoveSphereColliders(source, destination, destroySource); + MoveCapsuleColliders(source, destination, destroySource); + MoveMeshColliders(source, destination, destroySource); + } + + // joints ////////////////////////////////////////////////////////////// + // move all CharacterJoints + settings from one GameObject to another. + public static void MoveCharacterJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + CharacterJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (CharacterJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + CharacterJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.enableProjection = sourceJoint.enableProjection; + jointCopy.highTwistLimit = sourceJoint.highTwistLimit; + jointCopy.lowTwistLimit = sourceJoint.lowTwistLimit; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.projectionAngle = sourceJoint.projectionAngle; + jointCopy.projectionDistance = sourceJoint.projectionDistance; + jointCopy.swing1Limit = sourceJoint.swing1Limit; + jointCopy.swing2Limit = sourceJoint.swing2Limit; + jointCopy.swingAxis = sourceJoint.swingAxis; + jointCopy.swingLimitSpring = sourceJoint.swingLimitSpring; + jointCopy.twistLimitSpring = sourceJoint.twistLimitSpring; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all ConfigurableJoints + settings from one GameObject to another. + public static void MoveConfigurableJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + ConfigurableJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (ConfigurableJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + ConfigurableJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.angularXLimitSpring = sourceJoint.angularXLimitSpring; + jointCopy.angularXDrive = sourceJoint.angularXDrive; + jointCopy.angularXMotion = sourceJoint.angularXMotion; + jointCopy.angularYLimit = sourceJoint.angularYLimit; + jointCopy.angularYMotion = sourceJoint.angularYMotion; + jointCopy.angularYZDrive = sourceJoint.angularYZDrive; + jointCopy.angularYZLimitSpring = sourceJoint.angularYZLimitSpring; + jointCopy.angularZLimit = sourceJoint.angularZLimit; + jointCopy.angularZMotion = sourceJoint.angularZMotion; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.configuredInWorldSpace = sourceJoint.configuredInWorldSpace; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.highAngularXLimit = sourceJoint.highAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs + jointCopy.linearLimitSpring = sourceJoint.linearLimitSpring; + jointCopy.linearLimit = sourceJoint.linearLimit; + jointCopy.lowAngularXLimit = sourceJoint.lowAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs + jointCopy.massScale = sourceJoint.massScale; + jointCopy.projectionAngle = sourceJoint.projectionAngle; + jointCopy.projectionDistance = sourceJoint.projectionDistance; + jointCopy.projectionMode = sourceJoint.projectionMode; + jointCopy.rotationDriveMode = sourceJoint.rotationDriveMode; + jointCopy.secondaryAxis = sourceJoint.secondaryAxis; + jointCopy.slerpDrive = sourceJoint.slerpDrive; + jointCopy.swapBodies = sourceJoint.swapBodies; + jointCopy.targetAngularVelocity = sourceJoint.targetAngularVelocity; + jointCopy.targetPosition = sourceJoint.targetPosition; + jointCopy.targetRotation = sourceJoint.targetRotation; + jointCopy.targetVelocity = sourceJoint.targetVelocity; + jointCopy.xDrive = sourceJoint.xDrive; + jointCopy.xMotion = sourceJoint.xMotion; + jointCopy.yDrive = sourceJoint.yDrive; + jointCopy.yMotion = sourceJoint.yMotion; + jointCopy.zDrive = sourceJoint.zDrive; + jointCopy.zMotion = sourceJoint.zMotion; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all FixedJoints + settings from one GameObject to another. + public static void MoveFixedJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + FixedJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (FixedJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + FixedJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.massScale = sourceJoint.massScale; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all HingeJoints + settings from one GameObject to another. + public static void MoveHingeJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + HingeJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (HingeJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + HingeJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.limits = sourceJoint.limits; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.motor = sourceJoint.motor; + jointCopy.spring = sourceJoint.spring; + jointCopy.useLimits = sourceJoint.useLimits; + jointCopy.useMotor = sourceJoint.useMotor; + jointCopy.useSpring = sourceJoint.useSpring; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif +#if UNITY_2022_3_OR_NEWER + jointCopy.extendedLimits = sourceJoint.extendedLimits; + jointCopy.useAcceleration = sourceJoint.useAcceleration; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all SpringJoints + settings from one GameObject to another. + public static void MoveSpringJoints(GameObject source, GameObject destination, bool destroySource = true) + { + // colliders may be on children + SpringJoint[] sourceJoints = source.GetComponentsInChildren(); + foreach (SpringJoint sourceJoint in sourceJoints) + { + // copy the relative transform: + // if joint is on root, it returns destination root. + // if joint is on a child, it creates and returns a child on destination. + GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination); + SpringJoint jointCopy = target.AddComponent(); + // apply settings, in alphabetical order + jointCopy.anchor = sourceJoint.anchor; + jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor; + jointCopy.axis = sourceJoint.axis; + jointCopy.breakForce = sourceJoint.breakForce; + jointCopy.breakTorque = sourceJoint.breakTorque; + jointCopy.connectedAnchor = sourceJoint.connectedAnchor; + jointCopy.connectedBody = sourceJoint.connectedBody; + jointCopy.connectedMassScale = sourceJoint.connectedMassScale; + jointCopy.damper = sourceJoint.damper; + jointCopy.enableCollision = sourceJoint.enableCollision; + jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing; + jointCopy.massScale = sourceJoint.massScale; + jointCopy.maxDistance = sourceJoint.maxDistance; + jointCopy.minDistance = sourceJoint.minDistance; + jointCopy.spring = sourceJoint.spring; + jointCopy.tolerance = sourceJoint.tolerance; +#if UNITY_2020_3_OR_NEWER + jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody; +#endif + + if (destroySource) GameObject.Destroy(sourceJoint); + } + } + + // move all Joints + settings from one GameObject to another. + public static void MoveAllJoints(GameObject source, GameObject destination, bool destroySource = true) + { + MoveCharacterJoints(source, destination, destroySource); + MoveConfigurableJoints(source, destination, destroySource); + MoveFixedJoints(source, destination, destroySource); + MoveHingeJoints(source, destination, destroySource); + MoveSpringJoints(source, destination, destroySource); + } + + // all ///////////////////////////////////////////////////////////////// + // move all physics components from one GameObject to another. + public static void MovePhysicsComponents(GameObject source, GameObject destination, bool destroySource = true) + { + // need to move joints first, otherwise we might see: + // 'can't move Rigidbody because a Joint depends on it' + MoveAllJoints(source, destination, destroySource); + MoveAllColliders(source, destination, destroySource); + MoveRigidbody(source, destination, destroySource); + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta new file mode 100644 index 0000000..0c3ac22 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 17cfe1beb3f94a69b94bf60afc37ef7a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat new file mode 100644 index 0000000..d652a50 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat @@ -0,0 +1,85 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: RemoteGhostMaterial + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: + - _ALPHAPREMULTIPLY_ON + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: 3000 + stringTagMap: + RenderType: Transparent + disabledShaderPasses: [] + m_LockedProperties: + 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_Ints: [] + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 10 + - _GlossMapScale: 1 + - _Glossiness: 0.92 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 3 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 0 + m_Colors: + - _Color: {r: 0.09849727, g: 1, b: 0, a: 0.15686275} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] diff --git a/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta new file mode 100644 index 0000000..ec597ae --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 04f0b2088c857414393bab3b80356776 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/RemoteGhostMaterial.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs new file mode 100644 index 0000000..c30ce4a --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs @@ -0,0 +1,60 @@ +// PredictedRigidbody stores a history of its rigidbody states. +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + // inline everything because this is performance critical! + public struct RigidbodyState : PredictedState + { + public double timestamp { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } + + // we want to store position delta (last + delta = current), and current. + // this way we can apply deltas on top of corrected positions to get the corrected final position. + public Vector3 positionDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this position + public Vector3 position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public Quaternion rotationDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this rotation + public Quaternion rotation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public Vector3 velocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity + public Vector3 velocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public Vector3 angularVelocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity + public Vector3 angularVelocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } + + public RigidbodyState( + double timestamp, + Vector3 positionDelta, + Vector3 position, + Quaternion rotationDelta, + Quaternion rotation, + Vector3 velocityDelta, + Vector3 velocity, + Vector3 angularVelocityDelta, + Vector3 angularVelocity) + { + this.timestamp = timestamp; + this.positionDelta = positionDelta; + this.position = position; + this.rotationDelta = rotationDelta; + this.rotation = rotation; + this.velocityDelta = velocityDelta; + this.velocity = velocity; + this.angularVelocityDelta = angularVelocityDelta; + this.angularVelocity = angularVelocity; + } + + public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t) + { + return new RigidbodyState + { + position = Vector3.Lerp(a.position, b.position, t), + // Quaternions always need to be normalized in order to be a valid rotation after operations + rotation = Quaternion.Slerp(a.rotation, b.rotation, t).normalized, + velocity = Vector3.Lerp(a.velocity, b.velocity, t), + angularVelocity = Vector3.Lerp(a.angularVelocity, b.angularVelocity, t) + }; + } + } +} diff --git a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs.meta b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs.meta new file mode 100644 index 0000000..593ce27 --- /dev/null +++ b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ed0e1c0c874c4c9db6be2d5885bb7bee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling.meta b/Assets/Mirror/Components/Profiling.meta new file mode 100644 index 0000000..043fbfe --- /dev/null +++ b/Assets/Mirror/Components/Profiling.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4b799e47369048fcb1604e5b3e7d71cf +timeCreated: 1724245584 diff --git a/Assets/Mirror/Components/Profiling/BaseUIGraph.cs b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs new file mode 100644 index 0000000..94e84e9 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs @@ -0,0 +1,217 @@ +using System; +using UnityEngine; +using UnityEngine.Serialization; +using UnityEngine.UI; +namespace Mirror +{ + public enum GraphAggregationMode + { + Sum, + Average, + PerSecond, + Min, + Max + } + + public abstract class BaseUIGraph : MonoBehaviour + { + static readonly int MaxValue = Shader.PropertyToID("_MaxValue"); + static readonly int GraphData = Shader.PropertyToID("_GraphData"); + static readonly int CategoryCount = Shader.PropertyToID("_CategoryCount"); + static readonly int Colors = Shader.PropertyToID("_CategoryColors"); + static readonly int Width = Shader.PropertyToID("_Width"); + static readonly int DataStart = Shader.PropertyToID("_DataStart"); + + public Material Material; + public Graphic Renderer; + [Range(1, 64)] + public int Points = 64; + public float SecondsPerPoint = 1; + public Color[] CategoryColors = new[] { Color.cyan }; + public bool IsStacked; + + public Text[] LegendTexts; + [Header("Diagnostics")] + [ReadOnly, SerializeField] + Material runtimeMaterial; + + float[] graphData; + // graphData is a circular buffer, this is the offset to get the 0-index + int graphDataStartIndex; + // Is graphData dirty and needs to be set to the material + bool isGraphDataDirty; + // currently aggregating data to be added to the graph soon + float[] aggregatingData; + GraphAggregationMode[] aggregatingModes; + // Counts for avg aggregation mode + int[] aggregatingDataCounts; + // How much time has elapsed since the last aggregation finished + float aggregatingTime; + + int DataLastIndex => (graphDataStartIndex - 1 + Points) % Points; + + void Awake() + { + Renderer.material = runtimeMaterial = Instantiate(Material); + graphData = new float[Points * CategoryColors.Length]; + aggregatingData = new float[CategoryColors.Length]; + aggregatingDataCounts = new int[CategoryColors.Length]; + aggregatingModes = new GraphAggregationMode[CategoryColors.Length]; + isGraphDataDirty = true; + } + + protected virtual void OnValidate() + { + if (Renderer == null) + Renderer = GetComponent(); + } + + protected virtual void Update() + { + for (int i = 0; i < CategoryColors.Length; i++) + { + CollectData(i, out float value, out GraphAggregationMode mode); + // we probably don't need negative values, so lets skip supporting it + if (value < 0) + { + Debug.LogWarning("Graphing negative values is not supported."); + value = 0; + } + + if (mode != aggregatingModes[i]) + { + aggregatingModes[i] = mode; + ResetCurrent(i); + } + + switch (mode) + { + case GraphAggregationMode.Average: + case GraphAggregationMode.Sum: + case GraphAggregationMode.PerSecond: + aggregatingData[i] += value; + aggregatingDataCounts[i]++; + break; + case GraphAggregationMode.Min: + if (aggregatingData[i] > value) + aggregatingData[i] = value; + break; + case GraphAggregationMode.Max: + if (value > aggregatingData[i]) + aggregatingData[i] = value; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + aggregatingTime += Time.deltaTime; + if (aggregatingTime > SecondsPerPoint) + { + graphDataStartIndex = (graphDataStartIndex + 1) % Points; + ClearDataAt(DataLastIndex); + for (int i = 0; i < CategoryColors.Length; i++) + { + float value = aggregatingData[i]; + switch (aggregatingModes[i]) + { + case GraphAggregationMode.Sum: + case GraphAggregationMode.Min: + case GraphAggregationMode.Max: + // do nothing! + break; + case GraphAggregationMode.Average: + value /= aggregatingDataCounts[i]; + break; + case GraphAggregationMode.PerSecond: + value /= aggregatingTime; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + SetCurrentGraphData(i, value); + ResetCurrent(i); + } + + aggregatingTime = 0; + } + } + + void ResetCurrent(int i) + { + switch (aggregatingModes[i]) + { + case GraphAggregationMode.Min: + aggregatingData[i] = float.MaxValue; + break; + default: + aggregatingData[i] = 0; + break; + } + + aggregatingDataCounts[i] = 0; + } + + protected virtual string FormatValue(float value) => $"{value:N1}"; + + protected abstract void CollectData(int category, out float value, out GraphAggregationMode mode); + + void SetCurrentGraphData(int c, float value) + { + graphData[DataLastIndex * CategoryColors.Length + c] = value; + isGraphDataDirty = true; + } + + void ClearDataAt(int i) + { + for (int c = 0; c < CategoryColors.Length; c++) + graphData[i * CategoryColors.Length + c] = 0; + + isGraphDataDirty = true; + } + + public void LateUpdate() + { + if (isGraphDataDirty) + { + runtimeMaterial.SetInt(Width, Points); + runtimeMaterial.SetInt(DataStart, graphDataStartIndex); + float max = 1; + if (IsStacked) + for (int x = 0; x < Points; x++) + { + float total = 0; + for (int c = 0; c < CategoryColors.Length; c++) + total += graphData[x * CategoryColors.Length + c]; + + if (total > max) + max = total; + } + else + for (int i = 0; i < graphData.Length; i++) + { + float v = graphData[i]; + if (v > max) + max = v; + } + + max = AdjustMaxValue(max); + for (int i = 0; i < LegendTexts.Length; i++) + { + Text legendText = LegendTexts[i]; + float pct = (float)i / (LegendTexts.Length - 1); + legendText.text = FormatValue(max * pct); + } + + runtimeMaterial.SetFloat(MaxValue, max); + runtimeMaterial.SetFloatArray(GraphData, graphData); + runtimeMaterial.SetInt(CategoryCount, CategoryColors.Length); + runtimeMaterial.SetColorArray(Colors, CategoryColors); + isGraphDataDirty = false; + } + } + + protected virtual float AdjustMaxValue(float max) => Mathf.Ceil(max); + } +} diff --git a/Assets/Mirror/Components/Profiling/BaseUIGraph.cs.meta b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs.meta new file mode 100644 index 0000000..51dab56 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/BaseUIGraph.cs.meta @@ -0,0 +1,21 @@ +fileFormatVersion: 2 +guid: 0f7dbe0fe96842f3b3ec76d05d2bb24a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + - Renderer: {instanceID: 0} + - runtimeMaterial: {instanceID: 0} + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/BaseUIGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs new file mode 100644 index 0000000..d0f1a86 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs @@ -0,0 +1,40 @@ +using System; +using UnityEngine; +namespace Mirror +{ + public class FpsMinMaxAvgGraph : BaseUIGraph + { + protected override void CollectData(int category, out float value, out GraphAggregationMode mode) + { + value = 1 / Time.deltaTime; + switch (category) + { + case 0: + mode = GraphAggregationMode.Average; + break; + case 1: + mode = GraphAggregationMode.Min; + break; + case 2: + mode = GraphAggregationMode.Max; + break; + default: + throw new ArgumentOutOfRangeException($"{category} is not valid."); + } + } + + protected override void OnValidate() + { + base.OnValidate(); + if (CategoryColors.Length != 3) + CategoryColors = new[] + { + Color.cyan, // avg + Color.red, // min + Color.green // max + }; + + IsStacked = false; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs.meta b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs.meta new file mode 100644 index 0000000..25af560 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 73bc3b929ec94537a8dbd67eb9d0c2c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/FpsMinMaxAvgGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/LineGraph.mat b/Assets/Mirror/Components/Profiling/LineGraph.mat new file mode 100644 index 0000000..e42a0ce --- /dev/null +++ b/Assets/Mirror/Components/Profiling/LineGraph.mat @@ -0,0 +1,89 @@ +%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: LineGraph + m_Shader: {fileID: 4800000, guid: e9fd6820072746bbaa3a83a449c31709, type: 3} + 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 + - _CategoryCount: 0 + - _ColorMask: 15 + - _Cutoff: 0.5 + - _DataStart: 0 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _LineWidth: 1 + - _MaxValue: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _Stencil: 0 + - _StencilComp: 8 + - _StencilOp: 0 + - _StencilReadMask: 255 + - _StencilWriteMask: 255 + - _UVSec: 0 + - _UseUIAlphaClip: 0 + - _Width: 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/Components/Profiling/LineGraph.mat.meta b/Assets/Mirror/Components/Profiling/LineGraph.mat.meta new file mode 100644 index 0000000..10ff58a --- /dev/null +++ b/Assets/Mirror/Components/Profiling/LineGraph.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 5f77111e39fad6240bbf2a93d735b648 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/LineGraph.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs new file mode 100644 index 0000000..ac2b1fd --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs @@ -0,0 +1,85 @@ +using System; +using UnityEngine; +namespace Mirror +{ + public class NetworkBandwidthGraph : BaseUIGraph + { + int dataIn; + int dataOut; + + void Start() + { + // Ordering, Awake happens before NetworkDiagnostics reset + NetworkDiagnostics.InMessageEvent += OnReceive; + NetworkDiagnostics.OutMessageEvent += OnSend; + } + + void OnEnable() + { + // If we've been inactive, clear counter + dataIn = 0; + dataOut = 0; + } + + void OnDestroy() + { + NetworkDiagnostics.InMessageEvent -= OnReceive; + NetworkDiagnostics.OutMessageEvent -= OnSend; + } + + void OnSend(NetworkDiagnostics.MessageInfo obj) => dataOut += obj.bytes; + + void OnReceive(NetworkDiagnostics.MessageInfo obj) => dataIn += obj.bytes; + + protected override void CollectData(int category, out float value, out GraphAggregationMode mode) + { + mode = GraphAggregationMode.PerSecond; + switch (category) + { + case 0: + value = dataIn; + dataIn = 0; + break; + case 1: + value = dataOut; + dataOut = 0; + break; + default: + throw new ArgumentOutOfRangeException($"{category} is not valid."); + } + } + + static readonly string[] Units = new[] { "B/s", "KiB/s", "MiB/s" }; + const float UnitScale = 1024; + + protected override string FormatValue(float value) + { + string selectedUnit = null; + for (int i = 0; i < Units.Length; i++) + { + string unit = Units[i]; + selectedUnit = unit; + if (i > 0) + value /= UnitScale; + + if (value < UnitScale) + break; + } + + return $"{value:N0} {selectedUnit}"; + } + + protected override void OnValidate() + { + base.OnValidate(); + if (CategoryColors.Length != 2) + CategoryColors = new[] + { + Color.red, // min + Color.green // max + }; + + IsStacked = false; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs.meta b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs.meta new file mode 100644 index 0000000..adcf9b7 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e1ae19b97f0e4a5eb8cf5158d97506f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkBandwidthGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader new file mode 100644 index 0000000..c491953 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader @@ -0,0 +1,178 @@ +Shader "Mirror/NetworkGraphLines" +{ + Properties + { + [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} + _Color ("Tint", Color) = (1,1,1,1) + _Width ("Width", Int) = 0 + _LineWidth ("Line Width", Float) = 0.005 + _CategoryCount ("CategoryCount", Int) = 0 + _MaxValue ("MaxValue", Float) = 1 + _DataStart ("DataStart", Int) = 0 + + _StencilComp ("Stencil Comparison", Float) = 8 + _Stencil ("Stencil ID", Float) = 0 + _StencilOp ("Stencil Operation", Float) = 0 + _StencilWriteMask ("Stencil Write Mask", Float) = 255 + _StencilReadMask ("Stencil Read Mask", Float) = 255 + + _ColorMask ("Color Mask", Float) = 15 + + [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 + } + + SubShader + { + Tags + { + "Queue"="Transparent" + "IgnoreProjector"="True" + "RenderType"="Transparent" + "PreviewType"="Plane" + "CanUseSpriteAtlas"="True" + } + + Stencil + { + Ref [_Stencil] + Comp [_StencilComp] + Pass [_StencilOp] + ReadMask [_StencilReadMask] + WriteMask [_StencilWriteMask] + } + + Cull Off + Lighting Off + ZWrite Off + ZTest [unity_GUIZTestMode] + Blend SrcAlpha OneMinusSrcAlpha + ColorMask [_ColorMask] + + Pass + { + Name "Default" + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 2.0 + + #include "UnityCG.cginc" + #include "UnityUI.cginc" + + #pragma multi_compile_local _ UNITY_UI_CLIP_RECT + #pragma multi_compile_local _ UNITY_UI_ALPHACLIP + + struct appdata_t + { + float4 vertex : POSITION; + float4 color : COLOR; + float2 texcoord : TEXCOORD0; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + fixed4 color : COLOR; + float2 texcoord : TEXCOORD0; + float4 worldPosition : TEXCOORD1; + UNITY_VERTEX_OUTPUT_STEREO + }; + + sampler2D _MainTex; // we dont use this, but unitys ui library expects the shader to have a texture + fixed4 _Color; + fixed4 _TextureSampleAdd; + float4 _ClipRect; + float4 _MainTex_ST; + + uint _Width; + half _LineWidth; + uint _CategoryCount; + uint _MaxValue; + uint _DataStart; + half _GraphData[64 /* max. 128 points */ * 8 /* max 8 categories */]; + half4 _CategoryColors[8 /* max 8 categories */]; + + v2f vert(appdata_t v) + { + v2f OUT; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); + OUT.worldPosition = v.vertex; + OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); + + OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); + + OUT.color = v.color * _Color; + return OUT; + } + + // Helper function to calculate the shortest distance from a point (p) to a line segment (from a to b) + float distanceToLineSegment(float2 p, float2 a, float2 b) + { + float2 ab = b - a; + float2 ap = p - a; + float t = saturate(dot(ap, ab) / dot(ab, ab)); + // Clamp t between 0 and 1 to ensure we stay within the segment + float2 closestPoint = a + t * ab; // Find the closest point on the line segment + return length(p - closestPoint); // Return the distance from p to the closest point on the line + } + + fixed4 frag(v2f IN) : SV_Target + { + uint wCur = (uint)(IN.texcoord.x * _Width); + uint wMin = wCur == 0 ? 0 : wCur - 1; + uint wMax = wCur == _Width - 1 ? wCur : wCur + 1; + float2 screenSize = _ScreenParams.xy; + // this scaling only works if the object is flat and not rotated - but thats fine + float2 pixelScale = float2(1 / ddx(IN.texcoord.x), 1 / ddy(IN.texcoord.y)); + float2 screenSpaceUV = IN.texcoord * pixelScale; + half4 color = half4(0, 0, 0, 0); + // Loop through the graph's points + bool colored = false; + for (uint wNonOffset = wMin; wNonOffset < wMax && !colored; wNonOffset++) + { + uint w = (wNonOffset + _DataStart) % _Width; + // previous entry, unless it's the start, then we clamp to start + uint nextW = (w + 1) % _Width; + + float texPosCurrentX = float(wNonOffset) / _Width; + float texPosPrevX = texPosCurrentX + 1.0f / _Width; + + + for (uint c = 0; c < _CategoryCount; c++) + { + float categoryValueCurrent = _GraphData[w * _CategoryCount + c] / _MaxValue; + float categoryValueNext = _GraphData[nextW * _CategoryCount + c] / _MaxValue; + + float2 pointCurrent = float2(texPosCurrentX, categoryValueCurrent); + float2 pointNext = float2(texPosPrevX, categoryValueNext); + + float distance = distanceToLineSegment(screenSpaceUV, pointCurrent * pixelScale, + pointNext * pixelScale); + + if (distance < _LineWidth) + { + color = _CategoryColors[c]; + colored = true; + break; + } + } + } + + color *= IN.color; + + #ifdef UNITY_UI_CLIP_RECT + color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); + #endif + + #ifdef UNITY_UI_ALPHACLIP + clip (color.a - 0.001); + #endif + + return color; + } + ENDCG + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader.meta b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader.meta new file mode 100644 index 0000000..0fcbede --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphLines.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: e9fd6820072746bbaa3a83a449c31709 +timeCreated: 1725809505 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkGraphLines.shader + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader new file mode 100644 index 0000000..e666d41 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader @@ -0,0 +1,138 @@ +Shader "Mirror/NetworkGraphStacked" +{ + Properties + { + [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} + _Color ("Tint", Color) = (1,1,1,1) + _Width ("Width", Int) = 0 + _CategoryCount ("CategoryCount", Int) = 0 + _MaxValue ("MaxValue", Float) = 1 + _DataStart ("DataStart", Int) = 0 + + _StencilComp ("Stencil Comparison", Float) = 8 + _Stencil ("Stencil ID", Float) = 0 + _StencilOp ("Stencil Operation", Float) = 0 + _StencilWriteMask ("Stencil Write Mask", Float) = 255 + _StencilReadMask ("Stencil Read Mask", Float) = 255 + + _ColorMask ("Color Mask", Float) = 15 + + [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 + } + + SubShader + { + Tags + { + "Queue"="Transparent" + "IgnoreProjector"="True" + "RenderType"="Transparent" + "PreviewType"="Plane" + "CanUseSpriteAtlas"="True" + } + + Stencil + { + Ref [_Stencil] + Comp [_StencilComp] + Pass [_StencilOp] + ReadMask [_StencilReadMask] + WriteMask [_StencilWriteMask] + } + + Cull Off + Lighting Off + ZWrite Off + ZTest [unity_GUIZTestMode] + Blend SrcAlpha OneMinusSrcAlpha + ColorMask [_ColorMask] + + Pass + { + Name "Default" + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 2.0 + + #include "UnityCG.cginc" + #include "UnityUI.cginc" + + #pragma multi_compile_local _ UNITY_UI_CLIP_RECT + #pragma multi_compile_local _ UNITY_UI_ALPHACLIP + + struct appdata_t + { + float4 vertex : POSITION; + float4 color : COLOR; + float2 texcoord : TEXCOORD0; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct v2f + { + float4 vertex : SV_POSITION; + fixed4 color : COLOR; + float2 texcoord : TEXCOORD0; + float4 worldPosition : TEXCOORD1; + UNITY_VERTEX_OUTPUT_STEREO + }; + + sampler2D _MainTex; // we dont use this, but unitys ui library expects the shader to have a texture + fixed4 _Color; + fixed4 _TextureSampleAdd; + float4 _ClipRect; + float4 _MainTex_ST; + + uint _Width; + uint _CategoryCount; + uint _MaxValue; + uint _DataStart; + half _GraphData[64 /* max. 64 points */ * 8 /* max 8 categories */]; + half4 _CategoryColors[8 /* max 8 categories */]; + + v2f vert(appdata_t v) + { + v2f OUT; + UNITY_SETUP_INSTANCE_ID(v); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); + OUT.worldPosition = v.vertex; + OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); + + OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); + + OUT.color = v.color * _Color; + return OUT; + } + + fixed4 frag(v2f IN) : SV_Target + { + uint w = ((uint)(IN.texcoord.x * _Width) + _DataStart) % _Width; + half4 color = half4(0, 0, 0, 0); + float totalValue = 0; + for (uint c = 0; c < _CategoryCount; c++) + { + float categoryValue = _GraphData[w * _CategoryCount + c] / _MaxValue; + totalValue += categoryValue; + if (totalValue >= IN.texcoord.y) + { + color = _CategoryColors[c]; + break; + } + } + color *= IN.color; + + #ifdef UNITY_UI_CLIP_RECT + color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); + #endif + + #ifdef UNITY_UI_ALPHACLIP + clip (color.a - 0.001); + #endif + + return color; + } + ENDCG + } + } +} \ No newline at end of file diff --git a/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader.meta b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader.meta new file mode 100644 index 0000000..2559cf6 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: b5b24284f35f4992bcd4cc43919267d7 +timeCreated: 1724246251 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkGraphStacked.shader + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs new file mode 100644 index 0000000..bcc1639 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs @@ -0,0 +1,34 @@ +using System; +using UnityEngine; +namespace Mirror +{ + public class NetworkPingGraph : BaseUIGraph + { + protected override void CollectData(int category, out float value, out GraphAggregationMode mode) + { + mode = GraphAggregationMode.Average; + switch (category) + { + case 0: + value = (float)NetworkTime.rtt * 1000f; + break; + case 1: + value = (float)NetworkTime.rttVariance * 1000f; + break; + default: + throw new ArgumentOutOfRangeException($"{category} is not valid."); + } + } + + protected override string FormatValue(float value) => $"{value:N0}ms"; + + protected override void OnValidate() + { + base.OnValidate(); + if (CategoryColors.Length != 2) + CategoryColors = new[] { Color.cyan, Color.yellow }; + + IsStacked = false; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs.meta b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs.meta new file mode 100644 index 0000000..4ec56aa --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkPingGraph.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 14a68afedbbf4568b0decc5c3fe6dfd9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkPingGraph.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs new file mode 100644 index 0000000..fe78918 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Mirror.RemoteCalls; +using UnityEngine; + +namespace Mirror +{ + public class NetworkRuntimeProfiler : MonoBehaviour + { + [Serializable] + public class Sorter : IComparer + { + public SortBy Order; + + public int Compare(Stat a, Stat b) + { + if (a == null) + return 1; + + if (b == null) + return -1; + + // Compare B to A for desc order + switch (Order) + { + case SortBy.RecentBytes: + return b.RecentBytes.CompareTo(a.RecentBytes); + case SortBy.RecentCount: + return b.RecentCount.CompareTo(a.RecentCount); + case SortBy.TotalBytes: + return b.TotalBytes.CompareTo(a.TotalBytes); + case SortBy.TotalCount: + return b.TotalCount.CompareTo(a.TotalCount); + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public enum SortBy + { + RecentBytes, + RecentCount, + TotalBytes, + TotalCount + } + + public class Stat + { + public string Name; + public long TotalCount; + public long TotalBytes; + + public long RecentCount; + public long RecentBytes; + + public void ResetRecent() + { + RecentCount = 0; + RecentBytes = 0; + } + + public void Add(int count, int bytes) + { + TotalBytes += bytes; + TotalCount += count; + + RecentBytes += bytes; + RecentCount += count; + } + } + + class MessageStats + { + public readonly Dictionary MessageByType = new Dictionary(); + public readonly Dictionary RpcByHash = new Dictionary(); + + public void Record(NetworkDiagnostics.MessageInfo info) + { + Type type = info.message.GetType(); + if (!MessageByType.TryGetValue(type, out Stat stat)) + { + stat = new Stat + { + Name = type.ToString(), + TotalCount = 0, + TotalBytes = 0, + RecentCount = 0, + RecentBytes = 0 + }; + + MessageByType[type] = stat; + } + + stat.Add(info.count, info.bytes * info.count); + + if (info.message is CommandMessage cmd) + RecordRpc(cmd.functionHash, info); + else if (info.message is RpcMessage rpc) + RecordRpc(rpc.functionHash, info); + } + + void RecordRpc(ushort hash, NetworkDiagnostics.MessageInfo info) + { + if (!RpcByHash.TryGetValue(hash, out Stat stat)) + { + string name = "n/a"; + RemoteCallDelegate rpcDelegate = RemoteProcedureCalls.GetDelegate(hash); + if (rpcDelegate != null) + name = $"{rpcDelegate.Method.DeclaringType}.{rpcDelegate.GetMethodName().Replace(RemoteProcedureCalls.InvokeRpcPrefix, "")}"; + + stat = new Stat + { + Name = name, + TotalCount = 0, + TotalBytes = 0, + RecentCount = 0, + RecentBytes = 0 + }; + + RpcByHash[hash] = stat; + } + + stat.Add(info.count, info.bytes * info.count); + } + + public void ResetRecent() + { + foreach (Stat stat in MessageByType.Values) + stat.ResetRecent(); + + foreach (Stat stat in RpcByHash.Values) + stat.ResetRecent(); + } + } + + [Tooltip("How many seconds to accumulate 'recent' stats for, this is also the output interval")] + public float RecentDuration = 5; + public Sorter Sort = new Sorter(); + + public enum OutputType + { + UnityLog, + StdOut, + File + } + + public OutputType Output; + [Tooltip("If Output is set to 'File', where to the path of that file")] + public string OutputFilePath = "network-stats.log"; + + readonly MessageStats inStats = new MessageStats(); + readonly MessageStats outStats = new MessageStats(); + readonly StringBuilder printBuilder = new StringBuilder(); + float elapsedSinceReset; + + void Start() + { + // Ordering, Awake happens before NetworkDiagnostics reset + NetworkDiagnostics.InMessageEvent += HandleMessageIn; + NetworkDiagnostics.OutMessageEvent += HandleMessageOut; + } + + void OnDestroy() + { + NetworkDiagnostics.InMessageEvent -= HandleMessageIn; + NetworkDiagnostics.OutMessageEvent -= HandleMessageOut; + } + + void HandleMessageOut(NetworkDiagnostics.MessageInfo info) => outStats.Record(info); + + void HandleMessageIn(NetworkDiagnostics.MessageInfo info) => inStats.Record(info); + + void LateUpdate() + { + elapsedSinceReset += Time.deltaTime; + if (elapsedSinceReset > RecentDuration) + { + elapsedSinceReset = 0; + Print(); + inStats.ResetRecent(); + outStats.ResetRecent(); + } + } + + void Print() + { + printBuilder.Clear(); + printBuilder.AppendLine($"Stats for {DateTime.Now} ({RecentDuration:N1}s interval)"); + int nameMaxLength = "OUT Message".Length; + + foreach (Stat stat in inStats.MessageByType.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + foreach (Stat stat in outStats.MessageByType.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + foreach (Stat stat in inStats.RpcByHash.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + foreach (Stat stat in outStats.RpcByHash.Values) + if (stat.Name.Length > nameMaxLength) + nameMaxLength = stat.Name.Length; + + string recentBytes = "Recent Bytes"; + string recentCount = "Recent Count"; + string totalBytes = "Total Bytes"; + string totalCount = "Total Count"; + int maxBytesLength = FormatBytes(999999).Length; + int maxCountLength = FormatCount(999999).Length; + + int recentBytesPad = Mathf.Max(recentBytes.Length, maxBytesLength); + int recentCountPad = Mathf.Max(recentCount.Length, maxCountLength); + int totalBytesPad = Mathf.Max(totalBytes.Length, maxBytesLength); + int totalCountPad = Mathf.Max(totalCount.Length, maxCountLength); + string header = $"| {"IN Message".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + string sep = "".PadLeft(header.Length, '-'); + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + + foreach (Stat stat in inStats.MessageByType.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + header = $"| {"IN RPCs".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + foreach (Stat stat in inStats.RpcByHash.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + header = $"| {"OUT Message".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + foreach (Stat stat in outStats.MessageByType.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + header = $"| {"OUT RPCs".PadLeft(nameMaxLength)} | {recentBytes.PadLeft(recentBytesPad)} | {recentCount.PadLeft(recentCountPad)} | {totalBytes.PadLeft(totalBytesPad)} | {totalCount.PadLeft(totalCountPad)} |"; + printBuilder.AppendLine(sep); + printBuilder.AppendLine(header); + printBuilder.AppendLine(sep); + + foreach (Stat stat in outStats.RpcByHash.Values.OrderBy(stat => stat, Sort)) + printBuilder.AppendLine($"| {stat.Name.PadLeft(nameMaxLength)} | {FormatBytes(stat.RecentBytes).PadLeft(recentBytesPad)} | {FormatCount(stat.RecentCount).PadLeft(recentCountPad)} | {FormatBytes(stat.TotalBytes).PadLeft(totalBytesPad)} | {FormatCount(stat.TotalCount).PadLeft(totalCountPad)} |"); + + printBuilder.AppendLine(sep); + + switch (Output) + { + case OutputType.UnityLog: + Debug.Log(printBuilder.ToString()); + break; + case OutputType.StdOut: + Console.Write(printBuilder); + break; + case OutputType.File: + File.AppendAllText(OutputFilePath, printBuilder.ToString()); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + static string FormatBytes(long bytes) + { + const double KiB = 1024; + const double MiB = KiB * 1024; + const double GiB = MiB * 1024; + const double TiB = GiB * 1024; + + if (bytes < KiB) + return $"{bytes:N0} B"; + + if (bytes < MiB) + return $"{bytes / KiB:N2} KiB"; + + if (bytes < GiB) + return $"{bytes / MiB:N2} MiB"; + + if (bytes < TiB) + return $"{bytes / GiB:N2} GiB"; + + return $"{bytes / TiB:N2} TiB"; + } + + string FormatCount(long count) + { + const double K = 1000; + const double M = K * 1000; + const double G = M * 1000; + const double T = G * 1000; + + if (count < K) + return $"{count:N0}"; + + if (count < M) + return $"{count / K:N2} K"; + + if (count < G) + return $"{count / M:N2} M"; + + if (count < T) + return $"{count / G:N2} G"; + + return $"{count / T:N2} T"; + } + } +} diff --git a/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs.meta b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs.meta new file mode 100644 index 0000000..6fc7aed --- /dev/null +++ b/Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ef8db82aeb77400bb9e80850e39065a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/NetworkRuntimeProfiler.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Empty.meta b/Assets/Mirror/Components/Profiling/Prefabs.meta similarity index 77% rename from Assets/Mirror/Editor/Empty.meta rename to Assets/Mirror/Components/Profiling/Prefabs.meta index ee87976..e81bce5 100644 --- a/Assets/Mirror/Editor/Empty.meta +++ b/Assets/Mirror/Components/Profiling/Prefabs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 62c8dc5bb12bbc6428bb66ccbac57000 +guid: 083c6613a11cad746bb252bc7748947f folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab new file mode 100644 index 0000000..5aff497 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab @@ -0,0 +1,1776 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &119935549420283780 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8824290184142807954} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8824290184142807954 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 119935549420283780} + 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: + - {fileID: 8397968435809926716} + - {fileID: 8383045724353990353} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &215824790723025810 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8473536528693599174} + - component: {fileID: 8359308210431883230} + m_Layer: 5 + m_Name: LegendOUT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8473536528693599174 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + 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: + - {fileID: 5970650343794535896} + - {fileID: 2927000160895229417} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8359308210431883230 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + m_CullTransparentMesh: 1 +--- !u!1 &230788547622420441 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968396119680197966} + - component: {fileID: 5129519199212594243} + - component: {fileID: 5123833194418803548} + m_Layer: 5 + m_Name: GraphContent + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &968396119680197966 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + 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: 842792608162333385} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 10, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5129519199212594243 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_CullTransparentMesh: 1 +--- !u!114 &5123833194418803548 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &969091221012082297 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8397968435809926716} + - component: {fileID: 5852067364008015930} + - component: {fileID: 4136376136871219296} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8397968435809926716 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + 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: 8824290184142807954} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5852067364008015930 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_CullTransparentMesh: 1 +--- !u!114 &4136376136871219296 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1694800138959048866 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5970650343794535896} + - component: {fileID: 3299375740833370906} + - component: {fileID: 6202027950733238712} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5970650343794535896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + 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: 8473536528693599174} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3299375740833370906 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_CullTransparentMesh: 1 +--- !u!114 &6202027950733238712 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1711370235648153955 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5455569689841791731} + - component: {fileID: 8443939090520853786} + - component: {fileID: 1177453892628721408} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5455569689841791731 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + 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: 5885248672530866080} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8443939090520853786 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_CullTransparentMesh: 1 +--- !u!114 &1177453892628721408 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &2002525035342291879 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1750250400045347475} + - component: {fileID: 8835486359842310823} + m_Layer: 5 + m_Name: LegendIN + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1750250400045347475 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + 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: + - {fileID: 5157207313487273501} + - {fileID: 3683314132040949185} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8835486359842310823 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + m_CullTransparentMesh: 1 +--- !u!1 &3357588547163078786 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2288491264626255212} + m_Layer: 5 + m_Name: LegendsY + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2288491264626255212 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3357588547163078786} + 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: + - {fileID: 5885248672530866080} + - {fileID: 5389867435348264296} + - {fileID: 8455135462272940247} + - {fileID: 6309157798075429021} + - {fileID: 8824290184142807954} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 10, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3534129496521135982 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 842792608162333385} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &842792608162333385 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3534129496521135982} + 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: + - {fileID: 968396119680197966} + - {fileID: 2288491264626255212} + - {fileID: 2581675890115619580} + - {fileID: 5071673759804795070} + m_Father: {fileID: 2138752905301465689} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3670180597793129839 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2138752905301465689} + - component: {fileID: 8024388048541391361} + m_Layer: 5 + m_Name: BandwidthGraph + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2138752905301465689 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + 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: + - {fileID: 3363869356760374277} + - {fileID: 842792608162333385} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &8024388048541391361 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e1ae19b97f0e4a5eb8cf5158d97506f5, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 5123833194418803548} + Points: 64 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 1, g: 0, b: 0, a: 1} + - {r: 0, g: 1, b: 0, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 142216866270345907} + - {fileID: 4330748233085529081} + - {fileID: 2248143582364066648} + - {fileID: 97403263534861610} + - {fileID: 4349487955187298716} + runtimeMaterial: {fileID: 0} +--- !u!1 &3678091663373353095 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5732919540871248940} + - component: {fileID: 6719434420897279190} + - component: {fileID: 142216866270345907} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5732919540871248940 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + 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: 5885248672530866080} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6719434420897279190 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_CullTransparentMesh: 1 +--- !u!114 &142216866270345907 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3714570775115803475 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8383045724353990353} + - component: {fileID: 1355767179239727018} + - component: {fileID: 4349487955187298716} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8383045724353990353 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + 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: 8824290184142807954} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1355767179239727018 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_CullTransparentMesh: 1 +--- !u!114 &4349487955187298716 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &4375453557142464801 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2581675890115619580} + - component: {fileID: 5374880666536229423} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2581675890115619580 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + 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: + - {fileID: 1750250400045347475} + - {fileID: 8473536528693599174} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &5374880666536229423 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &4917723141099053105 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3363869356760374277} + - component: {fileID: 4476095696555559496} + - component: {fileID: 5567867155054281268} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3363869356760374277 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + 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: 2138752905301465689} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4476095696555559496 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_CullTransparentMesh: 1 +--- !u!114 &5567867155054281268 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5274949478677654638 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5885248672530866080} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5885248672530866080 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5274949478677654638} + 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: + - {fileID: 5455569689841791731} + - {fileID: 5732919540871248940} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5352407663590911692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 9183933987449881395} + - component: {fileID: 7256530256701424070} + - component: {fileID: 4330748233085529081} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &9183933987449881395 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + 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: 5389867435348264296} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7256530256701424070 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_CullTransparentMesh: 1 +--- !u!114 &4330748233085529081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &5693887164122654533 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6309157798075429021} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6309157798075429021 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5693887164122654533} + 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: + - {fileID: 7988638580994470440} + - {fileID: 3076841666317101811} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5810998373083543261 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5071673759804795070} + - component: {fileID: 1370951599340238612} + - component: {fileID: 2160324143502055429} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5071673759804795070 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + 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: 842792608162333385} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &1370951599340238612 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_CullTransparentMesh: 1 +--- !u!114 &2160324143502055429 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: BANDWIDTH +--- !u!1 &6326536691934325288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6145669649686337282} + - component: {fileID: 968747920458182069} + - component: {fileID: 4680362740251307526} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6145669649686337282 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + 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: 8455135462272940247} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &968747920458182069 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_CullTransparentMesh: 1 +--- !u!114 &4680362740251307526 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6354099053443266201 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3127105718844619361} + - component: {fileID: 6920956740542454426} + - component: {fileID: 2248143582364066648} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3127105718844619361 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + 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: 8455135462272940247} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6920956740542454426 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_CullTransparentMesh: 1 +--- !u!114 &2248143582364066648 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6498744302665369373 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2927000160895229417} + - component: {fileID: 3795603901461861956} + - component: {fileID: 3504239642613663755} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2927000160895229417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + 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: 8473536528693599174} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3795603901461861956 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_CullTransparentMesh: 1 +--- !u!114 &3504239642613663755 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: out +--- !u!1 &6501576574154348994 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8455135462272940247} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8455135462272940247 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6501576574154348994} + 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: + - {fileID: 6145669649686337282} + - {fileID: 3127105718844619361} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6868069573715158019 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5389867435348264296} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5389867435348264296 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6868069573715158019} + 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: + - {fileID: 8254041970903625228} + - {fileID: 9183933987449881395} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &7002258827437049681 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5157207313487273501} + - component: {fileID: 5222763877749258818} + - component: {fileID: 1845491664005460411} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5157207313487273501 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + 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: 1750250400045347475} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5222763877749258818 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_CullTransparentMesh: 1 +--- !u!114 &1845491664005460411 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7189887428595669623 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7988638580994470440} + - component: {fileID: 7267528306069058291} + - component: {fileID: 4777559063183442036} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7988638580994470440 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + 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: 6309157798075429021} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7267528306069058291 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_CullTransparentMesh: 1 +--- !u!114 &4777559063183442036 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7203666293600016388 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3683314132040949185} + - component: {fileID: 263296843630000285} + - component: {fileID: 5681678021283145729} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3683314132040949185 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + 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: 1750250400045347475} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &263296843630000285 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_CullTransparentMesh: 1 +--- !u!114 &5681678021283145729 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: in +--- !u!1 &8334833703202091165 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8254041970903625228} + - component: {fileID: 9068658072675687366} + - component: {fileID: 3509666831540494135} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8254041970903625228 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + 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: 5389867435348264296} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &9068658072675687366 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_CullTransparentMesh: 1 +--- !u!114 &3509666831540494135 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &8757401605356351412 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3076841666317101811} + - component: {fileID: 3104114958221384706} + - component: {fileID: 97403263534861610} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3076841666317101811 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + 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: 6309157798075429021} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &3104114958221384706 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_CullTransparentMesh: 1 +--- !u!114 &97403263534861610 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab.meta new file mode 100644 index 0000000..440e669 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 3c7a97355c25a2b4da731b53876f8a8b +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/BandwidthGraph.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab new file mode 100644 index 0000000..b29376f --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab @@ -0,0 +1,1976 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &24878003039380082 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1738718049027005141} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1738718049027005141 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 24878003039380082} + 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: + - {fileID: 343546206307382570} + - {fileID: 343546206776216637} + - {fileID: 343546206294766220} + - {fileID: 343546206649983697} + m_Father: {fileID: 343546206339761112} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546204850636262 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546204850636257} + - component: {fileID: 343546204850636259} + - component: {fileID: 343546204850636256} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546204850636257 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204850636262} + 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: 343546206877774020} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546204850636259 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204850636262} + m_CullTransparentMesh: 1 +--- !u!114 &343546204850636256 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204850636262} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546204890306331 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546204890306330} + - component: {fileID: 343546204890306340} + - component: {fileID: 343546204890306341} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546204890306330 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204890306331} + 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: 343546206506823101} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546204890306340 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204890306331} + m_CullTransparentMesh: 1 +--- !u!114 &343546204890306341 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546204890306331} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: max +--- !u!1 &343546205136049349 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205136049348} + - component: {fileID: 343546205136049350} + - component: {fileID: 343546205136049351} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205136049348 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205136049349} + 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: 343546205806955365} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546205136049350 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205136049349} + m_CullTransparentMesh: 1 +--- !u!114 &343546205136049351 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205136049349} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546205212282750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205212282745} + - component: {fileID: 343546205212282747} + m_Layer: 5 + m_Name: LegendAVG + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205212282745 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205212282750} + 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: + - {fileID: 343546205253700114} + - {fileID: 343546206801970585} + m_Father: {fileID: 343546206294766220} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205212282747 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205212282750} + m_CullTransparentMesh: 1 +--- !u!1 &343546205253700115 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205253700114} + - component: {fileID: 343546205253700124} + - component: {fileID: 343546205253700125} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205253700114 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205253700115} + 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: 343546205212282745} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205253700124 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205253700115} + m_CullTransparentMesh: 1 +--- !u!114 &343546205253700125 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205253700115} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205281663119 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205281663118} + - component: {fileID: 343546205281663113} + m_Layer: 5 + m_Name: LegendMIN + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205281663118 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205281663119} + 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: + - {fileID: 343546205496339525} + - {fileID: 343546206359818246} + m_Father: {fileID: 343546206294766220} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205281663113 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205281663119} + m_CullTransparentMesh: 1 +--- !u!1 &343546205394542240 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205394542243} + - component: {fileID: 343546205394542253} + - component: {fileID: 343546205394542242} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205394542243 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205394542240} + 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: 343546205508279401} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546205394542253 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205394542240} + m_CullTransparentMesh: 1 +--- !u!114 &343546205394542242 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205394542240} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546205496339642 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205496339525} + - component: {fileID: 343546205496339527} + - component: {fileID: 343546205496339524} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205496339525 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205496339642} + 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: 343546205281663118} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205496339527 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205496339642} + m_CullTransparentMesh: 1 +--- !u!114 &343546205496339524 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205496339642} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205500504725 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205500504724} + - component: {fileID: 343546205500504726} + - component: {fileID: 343546205500504727} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205500504724 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205500504725} + 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: 343546206877774020} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205500504726 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205500504725} + m_CullTransparentMesh: 1 +--- !u!114 &343546205500504727 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205500504725} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205508279406 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205508279401} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205508279401 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205508279406} + 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: + - {fileID: 343546206740227476} + - {fileID: 343546205394542243} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205523578647 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205523578646} + - component: {fileID: 343546205523578640} + - component: {fileID: 343546205523578641} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205523578646 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205523578647} + 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: 343546205634725308} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205523578640 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205523578647} + m_CullTransparentMesh: 1 +--- !u!114 &343546205523578641 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205523578647} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546205634725309 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205634725308} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205634725308 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205634725309} + 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: + - {fileID: 343546205523578646} + - {fileID: 343546206440079829} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205685920600 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205685920603} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205685920603 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205685920600} + 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: + - {fileID: 343546206488145202} + - {fileID: 343546206326199842} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205806955354 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205806955365} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205806955365 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205806955354} + 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: + - {fileID: 343546206737158417} + - {fileID: 343546205136049348} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546205859589119 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546205859589118} + - component: {fileID: 343546205859589112} + - component: {fileID: 343546205859589113} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546205859589118 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205859589119} + 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: 343546206339761112} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546205859589112 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205859589119} + m_CullTransparentMesh: 1 +--- !u!114 &343546205859589113 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546205859589119} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206294766221 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206294766220} + - component: {fileID: 343546206294766223} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206294766220 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206294766221} + 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: + - {fileID: 343546205212282745} + - {fileID: 343546205281663118} + - {fileID: 343546206506823101} + m_Father: {fileID: 1738718049027005141} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &343546206294766223 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206294766221} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &343546206307382571 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206307382570} + - component: {fileID: 343546206307382580} + - component: {fileID: 343546206307382581} + m_Layer: 5 + m_Name: GraphContent + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206307382570 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206307382571} + 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: 1738718049027005141} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206307382580 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206307382571} + m_CullTransparentMesh: 1 +--- !u!114 &343546206307382581 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206307382571} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206326199843 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206326199842} + - component: {fileID: 343546206326199852} + - component: {fileID: 343546206326199853} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206326199842 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206326199843} + 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: 343546205685920603} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546206326199852 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206326199843} + m_CullTransparentMesh: 1 +--- !u!114 &343546206326199853 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206326199843} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546206339761113 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206339761112} + - component: {fileID: 4901013733809370033} + m_Layer: 5 + m_Name: FPSMinMaxAvg + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206339761112 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206339761113} + 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: + - {fileID: 343546205859589118} + - {fileID: 1738718049027005141} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &4901013733809370033 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206339761113} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 73bc3b929ec94537a8dbd67eb9d0c2c6, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 343546206307382581} + Points: 16 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 0, g: 1, b: 1, a: 1} + - {r: 1, g: 0, b: 0, a: 1} + - {r: 0, g: 1, b: 0, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 343546205394542242} + - {fileID: 343546204850636256} + - {fileID: 343546206440079828} + - {fileID: 343546206326199853} + - {fileID: 343546205136049351} + runtimeMaterial: {fileID: 0} +--- !u!1 &343546206359818247 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206359818246} + - component: {fileID: 343546206359818240} + - component: {fileID: 343546206359818241} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206359818246 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206359818247} + 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: 343546205281663118} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206359818240 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206359818247} + m_CullTransparentMesh: 1 +--- !u!114 &343546206359818241 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206359818247} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: min +--- !u!1 &343546206440079818 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206440079829} + - component: {fileID: 343546206440079831} + - component: {fileID: 343546206440079828} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206440079829 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206440079818} + 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: 343546205634725308} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &343546206440079831 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206440079818} + m_CullTransparentMesh: 1 +--- !u!114 &343546206440079828 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206440079818} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &343546206488145203 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206488145202} + - component: {fileID: 343546206488145212} + - component: {fileID: 343546206488145213} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206488145202 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206488145203} + 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: 343546205685920603} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206488145212 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206488145203} + m_CullTransparentMesh: 1 +--- !u!114 &343546206488145213 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206488145203} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206506823090 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206506823101} + - component: {fileID: 343546206506823100} + m_Layer: 5 + m_Name: LegendMAX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206506823101 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206506823090} + 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: + - {fileID: 343546206516923994} + - {fileID: 343546204890306330} + m_Father: {fileID: 343546206294766220} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206506823100 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206506823090} + m_CullTransparentMesh: 1 +--- !u!1 &343546206516923995 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206516923994} + - component: {fileID: 343546206516924004} + - component: {fileID: 343546206516924005} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206516923994 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206516923995} + 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: 343546206506823101} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206516924004 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206516923995} + m_CullTransparentMesh: 1 +--- !u!114 &343546206516924005 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206516923995} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206649983702 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206649983697} + - component: {fileID: 343546206649983699} + - component: {fileID: 343546206649983696} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206649983697 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206649983702} + 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: 1738718049027005141} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &343546206649983699 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206649983702} + m_CullTransparentMesh: 1 +--- !u!114 &343546206649983696 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206649983702} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: FPS +--- !u!1 &343546206737158422 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206737158417} + - component: {fileID: 343546206737158419} + - component: {fileID: 343546206737158416} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206737158417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206737158422} + 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: 343546205806955365} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206737158419 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206737158422} + m_CullTransparentMesh: 1 +--- !u!114 &343546206737158416 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206737158422} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206740227477 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206740227476} + - component: {fileID: 343546206740227478} + - component: {fileID: 343546206740227479} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206740227476 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206740227477} + 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: 343546205508279401} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206740227478 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206740227477} + m_CullTransparentMesh: 1 +--- !u!114 &343546206740227479 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206740227477} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &343546206776216626 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206776216637} + m_Layer: 5 + m_Name: LegendsY + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206776216637 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206776216626} + 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: + - {fileID: 343546205508279401} + - {fileID: 343546206877774020} + - {fileID: 343546205634725308} + - {fileID: 343546205685920603} + - {fileID: 343546205806955365} + m_Father: {fileID: 1738718049027005141} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &343546206801970590 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206801970585} + - component: {fileID: 343546206801970587} + - component: {fileID: 343546206801970584} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206801970585 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206801970590} + 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: 343546205212282745} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &343546206801970587 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206801970590} + m_CullTransparentMesh: 1 +--- !u!114 &343546206801970584 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206801970590} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: avg +--- !u!1 &343546206877774021 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 343546206877774020} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &343546206877774020 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 343546206877774021} + 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: + - {fileID: 343546205500504724} + - {fileID: 343546204850636257} + m_Father: {fileID: 343546206776216637} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab.meta new file mode 100644 index 0000000..4e2d104 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 9bdc42bca9b7109428d00fe33bdb5102 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/FPSMinMaxAvg.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab new file mode 100644 index 0000000..b90a863 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab @@ -0,0 +1,765 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &2777084101886578567 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3864348419064627986} + - component: {fileID: 4699359553083341963} + m_Layer: 5 + m_Name: Graphs + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &3864348419064627986 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2777084101886578567} + 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: + - {fileID: 4509006437822490170} + - {fileID: 522813179161291686} + - {fileID: 7802229218722708586} + m_Father: {fileID: 5551289487596275721} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &4699359553083341963 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2777084101886578567} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 10 + m_Right: 0 + m_Top: 10 + m_Bottom: 0 + m_ChildAlignment: 2 + m_Spacing: 0 + m_ChildForceExpandWidth: 0 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 0 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &4512081604627528395 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5551289487596275721} + - component: {fileID: 5641452096830013034} + - component: {fileID: 4961392508020504993} + - component: {fileID: 1349218098020237989} + - component: {fileID: 8415974033864006777} + m_Layer: 5 + m_Name: GraphCanvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5551289487596275721 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_Children: + - {fileID: 3864348419064627986} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!223 &5641452096830013034 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!114 &4961392508020504993 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1280, y: 720} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 1 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!114 &1349218098020237989 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &8415974033864006777 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4512081604627528395} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a27b133d890c41828d3b01ffa12fe440, type: 3} + m_Name: + m_EditorClassIdentifier: + Key: 291 + ToToggle: {fileID: 2777084101886578567} +--- !u!1001 &4204022305993221602 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 3864348419064627986} + m_Modifications: + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 86.666664 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 43.333332 + objectReference: {fileID: 0} + - target: {fileID: 343546205212282745, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 86.666664 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 343546205281663118, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 150 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 1668.6208 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -85 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 343546206339761113, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_Name + value: FPSMinMaxAvg + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.x + value: 86.666664 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 216.66666 + objectReference: {fileID: 0} + - target: {fileID: 343546206506823101, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 9bdc42bca9b7109428d00fe33bdb5102, type: 3} +--- !u!224 &4509006437822490170 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 343546206339761112, guid: 9bdc42bca9b7109428d00fe33bdb5102, + type: 3} + m_PrefabInstance: {fileID: 4204022305993221602} + m_PrefabAsset: {fileID: 0} +--- !u!1001 &7469102913200609509 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 3864348419064627986} + m_Modifications: + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 195 + objectReference: {fileID: 0} + - target: {fileID: 942441613928011978, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 2997867828362567431, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_Name + value: PingGraph + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 65 + objectReference: {fileID: 0} + - target: {fileID: 6899184289734897349, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_RootOrder + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_SizeDelta.y + value: 150 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 1968.6208 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -85 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: ed3b4e27086dcc64b8c6605011a321e2, type: 3} +--- !u!224 &522813179161291686 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 6982534731248530243, guid: ed3b4e27086dcc64b8c6605011a321e2, + type: 3} + m_PrefabInstance: {fileID: 7469102913200609509} + m_PrefabAsset: {fileID: 0} +--- !u!1001 &8208221870570858035 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 3864348419064627986} + m_Modifications: + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 65 + objectReference: {fileID: 0} + - target: {fileID: 1750250400045347475, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + - target: {fileID: 1986485545668258777, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: Points + value: 32 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_RootOrder + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.x + value: 300 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.y + value: 150 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 2268.6208 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -85 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3670180597793129839, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_Name + value: NetworkGraph + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMax.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchorMin.y + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.x + value: 130 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_SizeDelta.y + value: 20 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 195 + objectReference: {fileID: 0} + - target: {fileID: 8473536528693599174, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -10 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 3c7a97355c25a2b4da731b53876f8a8b, type: 3} +--- !u!224 &7802229218722708586 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2138752905301465689, guid: 3c7a97355c25a2b4da731b53876f8a8b, + type: 3} + m_PrefabInstance: {fileID: 8208221870570858035} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab.meta new file mode 100644 index 0000000..4e4dab5 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: e87e1847def3c1f41b19b7df4f0920b3 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/GraphCanvas.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab new file mode 100644 index 0000000..bb77911 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab @@ -0,0 +1,2888 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &119935549420283780 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8824290184142807954} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8824290184142807954 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 119935549420283780} + 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: + - {fileID: 8397968435809926716} + - {fileID: 8383045724353990353} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &215824790723025810 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8473536528693599174} + - component: {fileID: 8359308210431883230} + m_Layer: 5 + m_Name: LegendOUT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8473536528693599174 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + 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: + - {fileID: 5970650343794535896} + - {fileID: 2927000160895229417} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8359308210431883230 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 215824790723025810} + m_CullTransparentMesh: 1 +--- !u!1 &230788547622420441 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 968396119680197966} + - component: {fileID: 5129519199212594243} + - component: {fileID: 5123833194418803548} + m_Layer: 5 + m_Name: GraphContent - Bandwidth + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &968396119680197966 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + 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: 842792608162333385} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10.1066, y: 0} + m_SizeDelta: {x: -60.2131, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5129519199212594243 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_CullTransparentMesh: 1 +--- !u!114 &5123833194418803548 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 230788547622420441} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &969091221012082297 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8397968435809926716} + - component: {fileID: 5852067364008015930} + - component: {fileID: 4136376136871219296} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8397968435809926716 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + 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: 8824290184142807954} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5852067364008015930 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_CullTransparentMesh: 1 +--- !u!114 &4136376136871219296 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 969091221012082297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1144042303701158085 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2770498519712146759} + - component: {fileID: 1909324844259712510} + - component: {fileID: 6801190371405574669} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2770498519712146759 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1144042303701158085} + 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: 6118054815374899758} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1909324844259712510 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1144042303701158085} + m_CullTransparentMesh: 1 +--- !u!114 &6801190371405574669 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1144042303701158085} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: jitter +--- !u!1 &1194010084318017500 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3962977762644453858} + m_Layer: 5 + m_Name: LegendsY - Ping + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3962977762644453858 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1194010084318017500} + 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: + - {fileID: 8467240887132563218} + - {fileID: 2900620391526308637} + - {fileID: 1423636040061542291} + - {fileID: 3701477534332925528} + - {fileID: 5515203252368553261} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 109.89, y: 0} + m_SizeDelta: {x: -199.79, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1649716185402538073 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7375623566472823448} + - component: {fileID: 7280101622563951250} + - component: {fileID: 1772663036056914474} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7375623566472823448 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649716185402538073} + 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: 6118054815374899758} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7280101622563951250 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649716185402538073} + m_CullTransparentMesh: 1 +--- !u!114 &1772663036056914474 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649716185402538073} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1694800138959048866 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5970650343794535896} + - component: {fileID: 3299375740833370906} + - component: {fileID: 6202027950733238712} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5970650343794535896 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + 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: 8473536528693599174} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3299375740833370906 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_CullTransparentMesh: 1 +--- !u!114 &6202027950733238712 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1694800138959048866} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1711370235648153955 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5455569689841791731} + - component: {fileID: 8443939090520853786} + - component: {fileID: 1177453892628721408} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5455569689841791731 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + 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: 5885248672530866080} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8443939090520853786 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_CullTransparentMesh: 1 +--- !u!114 &1177453892628721408 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1711370235648153955} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &2002525035342291879 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1750250400045347475} + - component: {fileID: 8835486359842310823} + m_Layer: 5 + m_Name: LegendIN + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1750250400045347475 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + 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: + - {fileID: 5157207313487273501} + - {fileID: 3683314132040949185} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8835486359842310823 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2002525035342291879} + m_CullTransparentMesh: 1 +--- !u!1 &3357588547163078786 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2288491264626255212} + m_Layer: 5 + m_Name: LegendsY - Bandwidth + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2288491264626255212 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3357588547163078786} + 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: + - {fileID: 5885248672530866080} + - {fileID: 5389867435348264296} + - {fileID: 8455135462272940247} + - {fileID: 6309157798075429021} + - {fileID: 8824290184142807954} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10.107, y: 0} + m_SizeDelta: {x: -60.213, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3534129496521135982 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 842792608162333385} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &842792608162333385 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3534129496521135982} + 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: + - {fileID: 968396119680197966} + - {fileID: 5267541382525022770} + - {fileID: 2288491264626255212} + - {fileID: 3962977762644453858} + - {fileID: 2581675890115619580} + - {fileID: 5071673759804795070} + m_Father: {fileID: 2138752905301465689} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3569519790910280589 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1259420167673541393} + - component: {fileID: 4579425422591491119} + - component: {fileID: 8580294978504068442} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1259420167673541393 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3569519790910280589} + 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: 1423636040061542291} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &4579425422591491119 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3569519790910280589} + m_CullTransparentMesh: 1 +--- !u!114 &8580294978504068442 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3569519790910280589} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3670180597793129839 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2138752905301465689} + - component: {fileID: 8024388048541391361} + - component: {fileID: 6891156770396053448} + m_Layer: 5 + m_Name: NetworkGraph + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2138752905301465689 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + 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: + - {fileID: 3363869356760374277} + - {fileID: 842792608162333385} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &8024388048541391361 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e1ae19b97f0e4a5eb8cf5158d97506f5, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 5123833194418803548} + Points: 64 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 1, g: 0, b: 0, a: 1} + - {r: 0, g: 1, b: 0, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 142216866270345907} + - {fileID: 4330748233085529081} + - {fileID: 2248143582364066648} + - {fileID: 97403263534861610} + - {fileID: 4349487955187298716} + runtimeMaterial: {fileID: 0} +--- !u!114 &6891156770396053448 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3670180597793129839} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 14a68afedbbf4568b0decc5c3fe6dfd9, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 3880664398471042142} + Points: 64 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 0, g: 1, b: 1, a: 1} + - {r: 1, g: 0.92156863, b: 0.015686275, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 2117081375211614847} + - {fileID: 7864316414649457688} + - {fileID: 8580294978504068442} + - {fileID: 1639235332378233126} + - {fileID: 7668278254153150358} + runtimeMaterial: {fileID: 0} +--- !u!1 &3678091663373353095 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5732919540871248940} + - component: {fileID: 6719434420897279190} + - component: {fileID: 142216866270345907} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5732919540871248940 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + 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: 5885248672530866080} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6719434420897279190 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_CullTransparentMesh: 1 +--- !u!114 &142216866270345907 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3678091663373353095} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3714570775115803475 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8383045724353990353} + - component: {fileID: 1355767179239727018} + - component: {fileID: 4349487955187298716} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8383045724353990353 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + 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: 8824290184142807954} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1355767179239727018 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_CullTransparentMesh: 1 +--- !u!114 &4349487955187298716 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3714570775115803475} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &4375453557142464801 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2581675890115619580} + - component: {fileID: 5374880666536229423} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2581675890115619580 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + 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: + - {fileID: 1750250400045347475} + - {fileID: 8473536528693599174} + - {fileID: 4635932635135080026} + - {fileID: 6118054815374899758} + m_Father: {fileID: 842792608162333385} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &5374880666536229423 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4375453557142464801} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &4490397563866728061 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8036502514150168521} + - component: {fileID: 3374747859601888581} + - component: {fileID: 460765458322436772} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8036502514150168521 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4490397563866728061} + 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: 4635932635135080026} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3374747859601888581 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4490397563866728061} + m_CullTransparentMesh: 1 +--- !u!114 &460765458322436772 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4490397563866728061} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &4492650180689071546 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7595841946320721050} + - component: {fileID: 186511036850520057} + - component: {fileID: 7292537739385968087} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7595841946320721050 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4492650180689071546} + 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: 4635932635135080026} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &186511036850520057 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4492650180689071546} + m_CullTransparentMesh: 1 +--- !u!114 &7292537739385968087 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4492650180689071546} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: ping +--- !u!1 &4671836082000350783 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1224777268200255961} + - component: {fileID: 6711771988030203581} + - component: {fileID: 7864316414649457688} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1224777268200255961 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4671836082000350783} + 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: 2900620391526308637} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6711771988030203581 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4671836082000350783} + m_CullTransparentMesh: 1 +--- !u!114 &7864316414649457688 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4671836082000350783} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &4723476562664894706 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3701477534332925528} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3701477534332925528 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4723476562664894706} + 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: + - {fileID: 6198365725618080772} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &4917723141099053105 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3363869356760374277} + - component: {fileID: 4476095696555559496} + - component: {fileID: 5567867155054281268} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3363869356760374277 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + 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: 2138752905301465689} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4476095696555559496 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_CullTransparentMesh: 1 +--- !u!114 &5567867155054281268 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4917723141099053105} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5075371807985313428 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4635932635135080026} + - component: {fileID: 7147312742844013158} + m_Layer: 5 + m_Name: LegendPING + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4635932635135080026 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5075371807985313428} + 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: + - {fileID: 8036502514150168521} + - {fileID: 7595841946320721050} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7147312742844013158 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5075371807985313428} + m_CullTransparentMesh: 1 +--- !u!1 &5274949478677654638 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5885248672530866080} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5885248672530866080 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5274949478677654638} + 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: + - {fileID: 5455569689841791731} + - {fileID: 5732919540871248940} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5352407663590911692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 9183933987449881395} + - component: {fileID: 7256530256701424070} + - component: {fileID: 4330748233085529081} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &9183933987449881395 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + 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: 5389867435348264296} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7256530256701424070 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_CullTransparentMesh: 1 +--- !u!114 &4330748233085529081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5352407663590911692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &5597103007742484545 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1423636040061542291} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1423636040061542291 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5597103007742484545} + 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: + - {fileID: 1259420167673541393} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5628951584669016692 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5267541382525022770} + - component: {fileID: 3449685404681157396} + - component: {fileID: 3880664398471042142} + m_Layer: 5 + m_Name: GraphContent - Ping + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5267541382525022770 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5628951584669016692} + 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: 842792608162333385} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10.1066, y: 0} + m_SizeDelta: {x: -60.2131, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3449685404681157396 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5628951584669016692} + m_CullTransparentMesh: 1 +--- !u!114 &3880664398471042142 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5628951584669016692} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5693887164122654533 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6309157798075429021} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6309157798075429021 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5693887164122654533} + 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: + - {fileID: 7988638580994470440} + - {fileID: 3076841666317101811} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &5810998373083543261 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5071673759804795070} + - component: {fileID: 1370951599340238612} + - component: {fileID: 2160324143502055429} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5071673759804795070 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + 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: 842792608162333385} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &1370951599340238612 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_CullTransparentMesh: 1 +--- !u!114 &2160324143502055429 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5810998373083543261} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: netgraph +--- !u!1 &5949791456184720458 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 808931863454774844} + - component: {fileID: 3262084368540049565} + - component: {fileID: 7668278254153150358} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &808931863454774844 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5949791456184720458} + 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: 5515203252368553261} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &3262084368540049565 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5949791456184720458} + m_CullTransparentMesh: 1 +--- !u!114 &7668278254153150358 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5949791456184720458} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6299708506135008504 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6198365725618080772} + - component: {fileID: 4666306959683799360} + - component: {fileID: 1639235332378233126} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6198365725618080772 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6299708506135008504} + 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: 3701477534332925528} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &4666306959683799360 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6299708506135008504} + m_CullTransparentMesh: 1 +--- !u!114 &1639235332378233126 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6299708506135008504} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6326536691934325288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6145669649686337282} + - component: {fileID: 968747920458182069} + - component: {fileID: 4680362740251307526} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6145669649686337282 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + 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: 8455135462272940247} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &968747920458182069 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_CullTransparentMesh: 1 +--- !u!114 &4680362740251307526 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6326536691934325288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6354099053443266201 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3127105718844619361} + - component: {fileID: 6920956740542454426} + - component: {fileID: 2248143582364066648} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3127105718844619361 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + 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: 8455135462272940247} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6920956740542454426 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_CullTransparentMesh: 1 +--- !u!114 &2248143582364066648 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6354099053443266201} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &6473252433622726265 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5515203252368553261} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5515203252368553261 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6473252433622726265} + 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: + - {fileID: 808931863454774844} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6498744302665369373 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2927000160895229417} + - component: {fileID: 3795603901461861956} + - component: {fileID: 3504239642613663755} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2927000160895229417 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + 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: 8473536528693599174} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3795603901461861956 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_CullTransparentMesh: 1 +--- !u!114 &3504239642613663755 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6498744302665369373} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: out +--- !u!1 &6501576574154348994 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8455135462272940247} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8455135462272940247 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6501576574154348994} + 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: + - {fileID: 6145669649686337282} + - {fileID: 3127105718844619361} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6697282991844538288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8467240887132563218} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8467240887132563218 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6697282991844538288} + 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: + - {fileID: 8045971123728951344} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6868069573715158019 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5389867435348264296} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5389867435348264296 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6868069573715158019} + 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: + - {fileID: 8254041970903625228} + - {fileID: 9183933987449881395} + m_Father: {fileID: 2288491264626255212} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &7002258827437049681 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5157207313487273501} + - component: {fileID: 5222763877749258818} + - component: {fileID: 1845491664005460411} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5157207313487273501 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + 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: 1750250400045347475} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5222763877749258818 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_CullTransparentMesh: 1 +--- !u!114 &1845491664005460411 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7002258827437049681} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0, b: 0, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7189887428595669623 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7988638580994470440} + - component: {fileID: 7267528306069058291} + - component: {fileID: 4777559063183442036} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7988638580994470440 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + 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: 6309157798075429021} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7267528306069058291 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_CullTransparentMesh: 1 +--- !u!114 &4777559063183442036 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7189887428595669623} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7203666293600016388 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3683314132040949185} + - component: {fileID: 263296843630000285} + - component: {fileID: 5681678021283145729} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3683314132040949185 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + 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: 1750250400045347475} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &263296843630000285 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_CullTransparentMesh: 1 +--- !u!114 &5681678021283145729 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7203666293600016388} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: in +--- !u!1 &8077720310600692407 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6118054815374899758} + - component: {fileID: 3202099948270756252} + m_Layer: 5 + m_Name: LegendJITTER + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6118054815374899758 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8077720310600692407} + 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: + - {fileID: 7375623566472823448} + - {fileID: 2770498519712146759} + m_Father: {fileID: 2581675890115619580} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &3202099948270756252 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8077720310600692407} + m_CullTransparentMesh: 1 +--- !u!1 &8334833703202091165 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8254041970903625228} + - component: {fileID: 9068658072675687366} + - component: {fileID: 3509666831540494135} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8254041970903625228 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + 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: 5389867435348264296} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &9068658072675687366 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_CullTransparentMesh: 1 +--- !u!114 &3509666831540494135 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8334833703202091165} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &8757401605356351412 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3076841666317101811} + - component: {fileID: 3104114958221384706} + - component: {fileID: 97403263534861610} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3076841666317101811 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + 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: 6309157798075429021} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &3104114958221384706 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_CullTransparentMesh: 1 +--- !u!114 &97403263534861610 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8757401605356351412} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &8983883851149759884 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8045971123728951344} + - component: {fileID: 1946944942956829893} + - component: {fileID: 2117081375211614847} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8045971123728951344 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8983883851149759884} + 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: 8467240887132563218} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -45, y: 0} + m_SizeDelta: {x: 40, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1946944942956829893 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8983883851149759884} + m_CullTransparentMesh: 1 +--- !u!114 &2117081375211614847 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8983883851149759884} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &9092185120150146117 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2900620391526308637} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2900620391526308637 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9092185120150146117} + 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: + - {fileID: 1224777268200255961} + m_Father: {fileID: 3962977762644453858} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab.meta new file mode 100644 index 0000000..1e2c86c --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: a4ee3092d7c7f4a38a70b11014175ebd +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/NetworkGraph.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab new file mode 100644 index 0000000..c35ae4d --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab @@ -0,0 +1,1776 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &512166646911724149 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5569695547238921462} + - component: {fileID: 7180649172285861411} + - component: {fileID: 3698306165924257989} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5569695547238921462 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 512166646911724149} + 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: 4620809971601425299} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7180649172285861411 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 512166646911724149} + m_CullTransparentMesh: 1 +--- !u!114 &3698306165924257989 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 512166646911724149} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &693974290553253915 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2454008239292490697} + - component: {fileID: 6928098193469425262} + - component: {fileID: 9151821596118048258} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2454008239292490697 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 693974290553253915} + 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: 7987015295734336241} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &6928098193469425262 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 693974290553253915} + m_CullTransparentMesh: 1 +--- !u!114 &9151821596118048258 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 693974290553253915} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &724734797682004671 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 216369457880424682} + - component: {fileID: 6446260028879253612} + m_Layer: 5 + m_Name: LegendsX + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &216369457880424682 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724734797682004671} + 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: + - {fileID: 6899184289734897349} + - {fileID: 942441613928011978} + m_Father: {fileID: 5294687292649652416} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 5, y: -5} + m_SizeDelta: {x: 10, y: 20} + m_Pivot: {x: 0.5, y: 1} +--- !u!114 &6446260028879253612 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 724734797682004671} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 +--- !u!1 &1093758345687749999 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7987015295734336241} + m_Layer: 5 + m_Name: Legend25 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7987015295734336241 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1093758345687749999} + 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: + - {fileID: 911697406546784338} + - {fileID: 2454008239292490697} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.25} + m_AnchorMax: {x: 1, y: 0.25} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1549744656742816016 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5294687292649652416} + m_Layer: 5 + m_Name: Container + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5294687292649652416 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1549744656742816016} + 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: + - {fileID: 1768608949435380201} + - {fileID: 1469817193488350550} + - {fileID: 216369457880424682} + - {fileID: 6975151376875653975} + m_Father: {fileID: 6982534731248530243} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 15, y: 0} + m_SizeDelta: {x: -50, y: -60} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1723664113960925886 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6658247662285819325} + - component: {fileID: 1965012378351765188} + - component: {fileID: 4336023028329030949} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6658247662285819325 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1723664113960925886} + 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: 7590176880698012387} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1965012378351765188 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1723664113960925886} + m_CullTransparentMesh: 1 +--- !u!114 &4336023028329030949 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1723664113960925886} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1751972084552208902 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7870100887797750244} + m_Layer: 5 + m_Name: Legend50 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7870100887797750244 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1751972084552208902} + 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: + - {fileID: 7889663160516621925} + - {fileID: 3705598884744263366} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1934697115913144479 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2203074428927526136} + - component: {fileID: 2023856609668669680} + - component: {fileID: 2206720168024543330} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2203074428927526136 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1934697115913144479} + 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: 942441613928011978} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2023856609668669680 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1934697115913144479} + m_CullTransparentMesh: 1 +--- !u!114 &2206720168024543330 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1934697115913144479} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0.9215687, b: 0.015686275, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &2810967667379106609 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1217973610716945855} + - component: {fileID: 1127519584872800040} + - component: {fileID: 362765526694760680} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1217973610716945855 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2810967667379106609} + 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: 7590176880698012387} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1127519584872800040 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2810967667379106609} + m_CullTransparentMesh: 1 +--- !u!114 &362765526694760680 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2810967667379106609} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &2969139731012847270 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6899184289734897349} + - component: {fileID: 5775259578045717352} + m_Layer: 5 + m_Name: LegendPing + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6899184289734897349 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2969139731012847270} + 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: + - {fileID: 908890067499602259} + - {fileID: 8083971487120669529} + m_Father: {fileID: 216369457880424682} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5775259578045717352 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2969139731012847270} + m_CullTransparentMesh: 1 +--- !u!1 &2997867828362567431 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6982534731248530243} + - component: {fileID: 5865401507776993270} + m_Layer: 5 + m_Name: PingGraph + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6982534731248530243 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2997867828362567431} + 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: + - {fileID: 1056662130267328870} + - {fileID: 5294687292649652416} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &5865401507776993270 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2997867828362567431} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 14a68afedbbf4568b0decc5c3fe6dfd9, type: 3} + m_Name: + m_EditorClassIdentifier: + Material: {fileID: 2100000, guid: 5f77111e39fad6240bbf2a93d735b648, type: 2} + Renderer: {fileID: 5857741690380071345} + Points: 32 + SecondsPerPoint: 0.5 + CategoryColors: + - {r: 0, g: 1, b: 1, a: 1} + - {r: 1, g: 0.92156863, b: 0.015686275, a: 1} + IsStacked: 0 + LegendTexts: + - {fileID: 3160579670834603183} + - {fileID: 9151821596118048258} + - {fileID: 2754244171796987324} + - {fileID: 362765526694760680} + - {fileID: 8382168573226188986} + runtimeMaterial: {fileID: 0} +--- !u!1 &3105901817953974653 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4652558859609211497} + - component: {fileID: 1400924857942257598} + - component: {fileID: 3160579670834603183} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4652558859609211497 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3105901817953974653} + 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: 2684192023408130911} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &1400924857942257598 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3105901817953974653} + m_CullTransparentMesh: 1 +--- !u!114 &3160579670834603183 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3105901817953974653} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &3205747331435468870 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 942441613928011978} + - component: {fileID: 6327993025711354549} + m_Layer: 5 + m_Name: LegendJitter + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &942441613928011978 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3205747331435468870} + 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: + - {fileID: 2203074428927526136} + - {fileID: 5372474103490645384} + m_Father: {fileID: 216369457880424682} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6327993025711354549 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3205747331435468870} + m_CullTransparentMesh: 1 +--- !u!1 &3640500073563500983 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1056662130267328870} + - component: {fileID: 8437258848603187620} + - component: {fileID: 4311061136952486782} + m_Layer: 5 + m_Name: Backdrop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1056662130267328870 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3640500073563500983} + 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: 6982534731248530243} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8437258848603187620 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3640500073563500983} + m_CullTransparentMesh: 1 +--- !u!114 &4311061136952486782 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3640500073563500983} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.22745098} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &3668972322997488969 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4620809971601425299} + m_Layer: 5 + m_Name: Legend100 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4620809971601425299 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3668972322997488969} + 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: + - {fileID: 5569695547238921462} + - {fileID: 4579137679805290761} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &3867562142981469492 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2684192023408130911} + m_Layer: 5 + m_Name: Legend0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2684192023408130911 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3867562142981469492} + 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: + - {fileID: 2990412341526050050} + - {fileID: 4652558859609211497} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &4001027587005364764 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1469817193488350550} + m_Layer: 5 + m_Name: LegendsY + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1469817193488350550 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4001027587005364764} + 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: + - {fileID: 2684192023408130911} + - {fileID: 7987015295734336241} + - {fileID: 7870100887797750244} + - {fileID: 7590176880698012387} + - {fileID: 4620809971601425299} + m_Father: {fileID: 5294687292649652416} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &4037112855574815760 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5372474103490645384} + - component: {fileID: 767451476397572077} + - component: {fileID: 419353126541061517} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5372474103490645384 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4037112855574815760} + 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: 942441613928011978} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &767451476397572077 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4037112855574815760} + m_CullTransparentMesh: 1 +--- !u!114 &419353126541061517 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4037112855574815760} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Jitter +--- !u!1 &5326465489219711421 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7889663160516621925} + - component: {fileID: 8263199366839964538} + - component: {fileID: 4067121774068232129} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7889663160516621925 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5326465489219711421} + 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: 7870100887797750244} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8263199366839964538 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5326465489219711421} + m_CullTransparentMesh: 1 +--- !u!114 &4067121774068232129 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5326465489219711421} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5506933360427255673 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 911697406546784338} + - component: {fileID: 6011329830179878330} + - component: {fileID: 2951732881719804678} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &911697406546784338 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5506933360427255673} + 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: 7987015295734336241} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6011329830179878330 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5506933360427255673} + m_CullTransparentMesh: 1 +--- !u!114 &2951732881719804678 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5506933360427255673} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &5788823770659353615 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1768608949435380201} + - component: {fileID: 8205076076307683200} + - component: {fileID: 5857741690380071345} + m_Layer: 5 + m_Name: GraphContent + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1768608949435380201 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5788823770659353615} + 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: 5294687292649652416} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8205076076307683200 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5788823770659353615} + m_CullTransparentMesh: 1 +--- !u!114 &5857741690380071345 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5788823770659353615} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6335670163093277870 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2990412341526050050} + - component: {fileID: 8490552629736366217} + - component: {fileID: 7762443815874078549} + m_Layer: 5 + m_Name: Line + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2990412341526050050 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6335670163093277870} + 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: 2684192023408130911} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8490552629736366217 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6335670163093277870} + m_CullTransparentMesh: 1 +--- !u!114 &7762443815874078549 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6335670163093277870} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.40392157} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6425321695431688699 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7590176880698012387} + m_Layer: 5 + m_Name: Legend75 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7590176880698012387 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6425321695431688699} + 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: + - {fileID: 6658247662285819325} + - {fileID: 1217973610716945855} + m_Father: {fileID: 1469817193488350550} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.75} + m_AnchorMax: {x: 1, y: 0.75} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6673247198283208466 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6975151376875653975} + - component: {fileID: 1474497204505854206} + - component: {fileID: 3859801130584468475} + m_Layer: 5 + m_Name: Title + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6975151376875653975 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6673247198283208466} + 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: 5294687292649652416} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 30} + m_Pivot: {x: 0.5, y: 0} +--- !u!222 &1474497204505854206 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6673247198283208466} + m_CullTransparentMesh: 1 +--- !u!114 &3859801130584468475 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6673247198283208466} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 4 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: PING +--- !u!1 &6858241430425100238 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4579137679805290761} + - component: {fileID: 7750660140099561029} + - component: {fileID: 8382168573226188986} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4579137679805290761 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6858241430425100238} + 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: 4620809971601425299} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7750660140099561029 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6858241430425100238} + m_CullTransparentMesh: 1 +--- !u!114 &8382168573226188986 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6858241430425100238} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &7921722240077587436 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3705598884744263366} + - component: {fileID: 7268291769466718766} + - component: {fileID: 2754244171796987324} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3705598884744263366 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7921722240077587436} + 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: 7870100887797750244} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: -1, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -5, y: 50} + m_Pivot: {x: 0, y: 0.5} +--- !u!222 &7268291769466718766 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7921722240077587436} + m_CullTransparentMesh: 1 +--- !u!114 &2754244171796987324 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7921722240077587436} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 12 + m_FontStyle: 1 + m_BestFit: 0 + m_MinSize: 1 + m_MaxSize: 40 + m_Alignment: 5 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 10 +--- !u!1 &8434725759024132407 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8083971487120669529} + - component: {fileID: 2625528277965073533} + - component: {fileID: 1427747543481707907} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8083971487120669529 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8434725759024132407} + 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: 6899184289734897349} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 4, y: 0} + m_SizeDelta: {x: -8, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2625528277965073533 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8434725759024132407} + m_CullTransparentMesh: 1 +--- !u!114 &1427747543481707907 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8434725759024132407} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 14 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 10 + m_MaxSize: 40 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: Ping +--- !u!1 &8835429319627613495 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 908890067499602259} + - component: {fileID: 8735215443978414631} + - component: {fileID: 2493539334624522905} + m_Layer: 5 + m_Name: Color + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &908890067499602259 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8835429319627613495} + 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: 6899184289734897349} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 10, y: -10} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8735215443978414631 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8835429319627613495} + m_CullTransparentMesh: 1 +--- !u!114 &2493539334624522905 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8835429319627613495} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 diff --git a/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab.meta b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab.meta new file mode 100644 index 0000000..57c4881 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: ed3b4e27086dcc64b8c6605011a321e2 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/Prefabs/PingGraph.prefab + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/StackedGraph.mat b/Assets/Mirror/Components/Profiling/StackedGraph.mat new file mode 100644 index 0000000..a4c4b44 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/StackedGraph.mat @@ -0,0 +1,88 @@ +%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: StackedGraph + m_Shader: {fileID: 4800000, guid: b5b24284f35f4992bcd4cc43919267d7, type: 3} + 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 + - _CategoryCount: 0 + - _ColorMask: 15 + - _Cutoff: 0.5 + - _DataStart: 0 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _MaxValue: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _Stencil: 0 + - _StencilComp: 8 + - _StencilOp: 0 + - _StencilReadMask: 255 + - _StencilWriteMask: 255 + - _UVSec: 0 + - _UseUIAlphaClip: 0 + - _Width: 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/Components/Profiling/StackedGraph.mat.meta b/Assets/Mirror/Components/Profiling/StackedGraph.mat.meta new file mode 100644 index 0000000..6c9422c --- /dev/null +++ b/Assets/Mirror/Components/Profiling/StackedGraph.mat.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 14fba9d19cfe7f346bfb595965558722 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/StackedGraph.mat + uploadId: 736421 diff --git a/Assets/Mirror/Components/Profiling/ToggleHotkey.cs b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs new file mode 100644 index 0000000..130e452 --- /dev/null +++ b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs @@ -0,0 +1,15 @@ +using UnityEngine; +namespace Mirror +{ + public class ToggleHotkey : MonoBehaviour + { + public KeyCode Key = KeyCode.F10; + public GameObject ToToggle; + + void Update() + { + if (Input.GetKeyDown(Key)) + ToToggle.SetActive(!ToToggle.activeSelf); + } + } +} diff --git a/Assets/Mirror/Components/Profiling/ToggleHotkey.cs.meta b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs.meta new file mode 100644 index 0000000..cdcf3aa --- /dev/null +++ b/Assets/Mirror/Components/Profiling/ToggleHotkey.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a27b133d890c41828d3b01ffa12fe440 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/Profiling/ToggleHotkey.cs + uploadId: 736421 diff --git a/Assets/Mirror/Components/RemoteStatistics.cs b/Assets/Mirror/Components/RemoteStatistics.cs new file mode 100644 index 0000000..5b3ede9 --- /dev/null +++ b/Assets/Mirror/Components/RemoteStatistics.cs @@ -0,0 +1,441 @@ +// remote statistics panel from Mirror II to show connections, load, etc. +// server syncs statistics to clients if authenticated. +// +// attach this to a player. +// requires NetworkStatistics component on the Network object. +// +// Unity's OnGUI is the easiest to use solution at the moment. +// * playfab is super complex to set up +// * http servers would be nice, but still need to open ports, live refresh, etc +// +// for safety reasons, let's keep this read-only. +// at least until there's safe authentication. +using System; +using System.IO; +using UnityEngine; + +namespace Mirror +{ + // server -> client + struct Stats + { + // general + public int connections; + public double uptime; + public int configuredTickRate; + public int actualTickRate; + + // traffic + public long sentBytesPerSecond; + public long receiveBytesPerSecond; + + // cpu + public float serverTickInterval; + public double fullUpdateAvg; + public double serverEarlyAvg; + public double serverLateAvg; + public double transportEarlyAvg; + public double transportLateAvg; + + // C# boilerplate + public Stats( + // general + int connections, + double uptime, + int configuredTickRate, + int actualTickRate, + // traffic + long sentBytesPerSecond, + long receiveBytesPerSecond, + // cpu + float serverTickInterval, + double fullUpdateAvg, + double serverEarlyAvg, + double serverLateAvg, + double transportEarlyAvg, + double transportLateAvg + ) + { + // general + this.connections = connections; + this.uptime = uptime; + this.configuredTickRate = configuredTickRate; + this.actualTickRate = actualTickRate; + + // traffic + this.sentBytesPerSecond = sentBytesPerSecond; + this.receiveBytesPerSecond = receiveBytesPerSecond; + + // cpu + this.serverTickInterval = serverTickInterval; + this.fullUpdateAvg = fullUpdateAvg; + this.serverEarlyAvg = serverEarlyAvg; + this.serverLateAvg = serverLateAvg; + this.transportEarlyAvg = transportEarlyAvg; + this.transportLateAvg = transportLateAvg; + } + } + + // [RequireComponent(typeof(NetworkStatistics))] <- needs to be on Network GO, not on NI + public class RemoteStatistics : NetworkBehaviour + { + // components ("fake statics" for similar API) + protected NetworkStatistics NetworkStatistics; + + // broadcast to client. + // stats are quite huge, let's only send every few seconds via TargetRpc. + // instead of sending multiple times per second via NB.OnSerialize. + [Tooltip("Send stats every 'interval' seconds to client.")] + public float sendInterval = 1; + double lastSendTime; + + [Header("GUI")] + public bool showGui; + public KeyCode hotKey = KeyCode.BackQuote; + Rect windowRect = new Rect(0, 0, 400, 400); + + // password can't be stored in code or in Unity project. + // it would be available in clients otherwise. + // this is not perfectly secure. that's why RemoteStatistics is read-only. + [Header("Authentication")] + public string passwordFile = "remote_statistics.txt"; + protected bool serverAuthenticated; // client needs to authenticate + protected bool clientAuthenticated; // show GUI until authenticated + protected string serverPassword = null; // null means not found, auth impossible + protected string clientPassword = ""; // for GUI + + // statistics synced to client + Stats stats; + + void LoadPassword() + { + // TODO only load once, not for all players? + // let's avoid static state for now. + + // load the password + string path = Path.GetFullPath(passwordFile); + if (File.Exists(path)) + { + // don't spam the server logs for every player's loaded file + // Debug.Log($"RemoteStatistics: loading password file: {path}"); + try + { + serverPassword = File.ReadAllText(path); + } + catch (Exception exception) + { + Debug.LogWarning($"RemoteStatistics: failed to read password file: {exception}"); + } + } + else + { + Debug.LogWarning($"RemoteStatistics: password file has not been created. Authentication will be impossible. Please save the password in: {path}"); + } + } + + protected override void OnValidate() + { + base.OnValidate(); + syncMode = SyncMode.Owner; + } + + // make sure to call base function when overwriting! + // public so it can also be called from tests (and be overwritten by users) + public override void OnStartServer() + { + NetworkStatistics = NetworkManager.singleton.GetComponent(); + if (NetworkStatistics == null) throw new Exception($"RemoteStatistics requires a NetworkStatistics component on {NetworkManager.singleton.name}!"); + + // server needs to load the password + LoadPassword(); + } + + public override void OnStartLocalPlayer() + { + // center the window initially + windowRect.x = Screen.width / 2 - windowRect.width / 2; + windowRect.y = Screen.height / 2 - windowRect.height / 2; + } + + [TargetRpc] + void TargetRpcSync(Stats v) + { + // store stats and flag as authenticated + clientAuthenticated = true; + stats = v; + } + + [Command] + public void CmdAuthenticate(string v) + { + // was a valid password loaded on the server, + // and did the client send the correct one? + if (!string.IsNullOrWhiteSpace(serverPassword) && + serverPassword.Equals(v)) + { + serverAuthenticated = true; + Debug.Log($"RemoteStatistics: connectionId {connectionToClient.connectionId} authenticated with player {name}"); + } + } + + void UpdateServer() + { + // only sync if client has authenticated on the server + if (!serverAuthenticated) return; + + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= lastSendTime + sendInterval) + { + lastSendTime = NetworkTime.localTime; + + // target rpc to owner client + TargetRpcSync(new Stats( + // general + NetworkServer.connections.Count, + NetworkTime.time, + NetworkServer.tickRate, + NetworkServer.actualTickRate, + + // traffic + NetworkStatistics.serverSentBytesPerSecond, + NetworkStatistics.serverReceivedBytesPerSecond, + + // cpu + NetworkServer.tickInterval, + NetworkServer.fullUpdateDuration.average, + NetworkServer.earlyUpdateDuration.average, + NetworkServer.lateUpdateDuration.average, + 0, // TODO ServerTransport.earlyUpdateDuration.average, + 0 // TODO ServerTransport.lateUpdateDuration.average + )); + } + } + + void UpdateClient() + { + if (Input.GetKeyDown(hotKey)) + showGui = !showGui; + } + + void Update() + { + if (isServer) UpdateServer(); + if (isLocalPlayer) UpdateClient(); + } + + void OnGUI() + { + if (!isLocalPlayer) return; + if (!showGui) return; + + windowRect = GUILayout.Window(0, windowRect, OnWindow, "Remote Statistics"); + windowRect = Utils.KeepInScreen(windowRect); + } + + // Text: value + void GUILayout_TextAndValue(string text, string value) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(text); + GUILayout.FlexibleSpace(); + GUILayout.Label(value); + GUILayout.EndHorizontal(); + } + + // fake a progress bar via horizontal scroll bar with ratio as width + void GUILayout_ProgressBar(double ratio, int width) + { + // clamp ratio, otherwise >1 would make it extremely large + ratio = Mathd.Clamp01(ratio); + GUILayout.HorizontalScrollbar(0, (float)ratio, 0, 1, GUILayout.Width(width)); + } + + // need to specify progress bar & caption width, + // otherwise differently sized captions would always misalign the + // progress bars. + void GUILayout_TextAndProgressBar(string text, double ratio, int progressbarWidth, string caption, int captionWidth, Color captionColor) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(text); + GUILayout.FlexibleSpace(); + GUILayout_ProgressBar(ratio, progressbarWidth); + + // coloring the caption is enough. otherwise it's too much. + GUI.color = captionColor; + GUILayout.Label(caption, GUILayout.Width(captionWidth)); + GUI.color = Color.white; + + GUILayout.EndHorizontal(); + } + + void GUI_Authenticate() + { + GUILayout.BeginVertical("Box"); // start general + GUILayout.Label("Authentication"); + + // warning if insecure connection + // if (ClientTransport.IsEncrypted()) + // { + // GUILayout.Label("Connection is encrypted!"); + // } + // else + // { + GUILayout.Label("Connection is not encrypted. Use with care!"); + // } + + // input + clientPassword = GUILayout.PasswordField(clientPassword, '*'); + + // button + GUI.enabled = !string.IsNullOrWhiteSpace(clientPassword); + if (GUILayout.Button("Authenticate")) + { + CmdAuthenticate(clientPassword); + } + GUI.enabled = true; + + GUILayout.EndVertical(); // end general + } + + void GUI_General( + int connections, + double uptime, + int configuredTickRate, + int actualTickRate) + { + GUILayout.BeginVertical("Box"); // start general + GUILayout.Label("General"); + + // connections + GUILayout_TextAndValue("Connections:", $"{connections}"); + + // uptime + GUILayout_TextAndValue("Uptime:", $"{Utils.PrettySeconds(uptime)}"); // TODO + + // tick rate + // might be lower under heavy load. + // might be higher in editor if targetFrameRate can't be set. + GUI.color = actualTickRate < configuredTickRate ? Color.red : Color.green; + GUILayout_TextAndValue("Tick Rate:", $"{actualTickRate} Hz / {configuredTickRate} Hz"); + GUI.color = Color.white; + + GUILayout.EndVertical(); // end general + } + + void GUI_Traffic( + long serverSentBytesPerSecond, + long serverReceivedBytesPerSecond) + { + GUILayout.BeginVertical("Box"); + GUILayout.Label("Network"); + + GUILayout_TextAndValue("Outgoing:", $"{Utils.PrettyBytes(serverSentBytesPerSecond) }/s"); + GUILayout_TextAndValue("Incoming:", $"{Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s"); + + GUILayout.EndVertical(); + } + + void GUI_Cpu( + float serverTickInterval, + double fullUpdateAvg, + double serverEarlyAvg, + double serverLateAvg, + double transportEarlyAvg, + double transportLateAvg) + { + const int barWidth = 120; + const int captionWidth = 90; + + GUILayout.BeginVertical("Box"); + GUILayout.Label("CPU"); + + // unity update + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + double fullRatio = fullUpdateAvg / serverTickInterval; + GUILayout_TextAndProgressBar( + "World Update Avg:", + fullRatio, + barWidth, $"{fullUpdateAvg * 1000:F1} ms", + captionWidth, + fullRatio <= 0.9 ? Color.green : Color.red); + + // server update + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + double serverRatio = (serverEarlyAvg + serverLateAvg) / serverTickInterval; + GUILayout_TextAndProgressBar( + "Server Update Avg:", + serverRatio, + barWidth, $"{serverEarlyAvg * 1000:F1} + {serverLateAvg * 1000:F1} ms", + captionWidth, + serverRatio <= 0.9 ? Color.green : Color.red); + + // transport: early + late update milliseconds. + // for threaded transport, this is the thread's update time. + // happens every 'tickInterval'. progress bar shows it in relation. + // <= 90% load is green, otherwise red + // double transportRatio = (transportEarlyAvg + transportLateAvg) / serverTickInterval; + // GUILayout_TextAndProgressBar( + // "Transport Avg:", + // transportRatio, + // barWidth, + // $"{transportEarlyAvg * 1000:F1} + {transportLateAvg * 1000:F1} ms", + // captionWidth, + // transportRatio <= 0.9 ? Color.green : Color.red); + + GUILayout.EndVertical(); + } + + void GUI_Notice() + { + // for security reasons, let's keep this read-only for now. + + // single line keeps input & visuals simple + // GUILayout.BeginVertical("Box"); + // GUILayout.Label("Global Notice"); + // notice = GUILayout.TextField(notice); + // if (GUILayout.Button("Send")) + // { + // // TODO + // } + // GUILayout.EndVertical(); + } + + void OnWindow(int windowID) + { + if (!clientAuthenticated) + { + GUI_Authenticate(); + } + else + { + GUI_General( + stats.connections, + stats.uptime, + stats.configuredTickRate, + stats.actualTickRate + ); + + GUI_Traffic( + stats.sentBytesPerSecond, + stats.receiveBytesPerSecond + ); + + GUI_Cpu( + stats.serverTickInterval, + stats.fullUpdateAvg, + stats.serverEarlyAvg, + stats.serverLateAvg, + stats.transportEarlyAvg, + stats.transportLateAvg + ); + + GUI_Notice(); + } + + // dragable window in any case + GUI.DragWindow(new Rect(0, 0, 10000, 10000)); + } + } +} diff --git a/Assets/Mirror/Components/RemoteStatistics.cs.meta b/Assets/Mirror/Components/RemoteStatistics.cs.meta new file mode 100644 index 0000000..8754194 --- /dev/null +++ b/Assets/Mirror/Components/RemoteStatistics.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ba360e4ff6b44fc6898f56322b90c6c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Components/RemoteStatistics.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime.meta b/Assets/Mirror/Core.meta similarity index 100% rename from Assets/Mirror/Runtime.meta rename to Assets/Mirror/Core.meta diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs b/Assets/Mirror/Core/AssemblyInfo.cs similarity index 92% rename from Assets/Mirror/Runtime/AssemblyInfo.cs rename to Assets/Mirror/Core/AssemblyInfo.cs index f342716..a9c6442 100644 --- a/Assets/Mirror/Runtime/AssemblyInfo.cs +++ b/Assets/Mirror/Core/AssemblyInfo.cs @@ -10,3 +10,4 @@ [assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")] [assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")] [assembly: InternalsVisibleTo("Mirror.Editor")] +[assembly: InternalsVisibleTo("Mirror.Components")] diff --git a/Assets/Mirror/Runtime/AssemblyInfo.cs.meta b/Assets/Mirror/Core/AssemblyInfo.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/AssemblyInfo.cs.meta rename to Assets/Mirror/Core/AssemblyInfo.cs.meta index 50cc028..879e6d0 100644 --- a/Assets/Mirror/Runtime/AssemblyInfo.cs.meta +++ b/Assets/Mirror/Core/AssemblyInfo.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Attributes.cs b/Assets/Mirror/Core/Attributes.cs similarity index 60% rename from Assets/Mirror/Runtime/Attributes.cs rename to Assets/Mirror/Core/Attributes.cs index 39b06fd..53114b4 100644 --- a/Assets/Mirror/Runtime/Attributes.cs +++ b/Assets/Mirror/Core/Attributes.cs @@ -4,8 +4,12 @@ namespace Mirror { /// - /// SyncVars are used to synchronize a variable from the server to all clients automatically. - /// Value must be changed on server, not directly by clients. Hook parameter allows you to define a client-side method to be invoked when the client gets an update from the server. + /// SyncVars are used to automatically synchronize a variable between the server and all clients. The direction of synchronization depends on the Sync Direction property, ServerToClient by default. + /// + /// When Sync Direction is equal to ServerToClient, the value should be changed on the server side and synchronized to all clients. + /// Otherwise, the value should be changed on the client side and synchronized to server and other clients. + /// + /// Hook parameter allows you to define a method to be invoked when gets an value update. Notice that the hook method will not be called on the change side. /// [AttributeUsage(AttributeTargets.Field)] public class SyncVarAttribute : PropertyAttribute @@ -44,28 +48,28 @@ public class TargetRpcAttribute : Attribute } /// - /// Prevents clients from running this method. - /// Prints a warning if a client tries to execute this method. + /// Only an active server will run this method. + /// Prints a warning if a client or in-active server tries to execute this method. /// [AttributeUsage(AttributeTargets.Method)] public class ServerAttribute : Attribute {} /// - /// Prevents clients from running this method. + /// Only an active server will run this method. /// No warning is thrown. /// [AttributeUsage(AttributeTargets.Method)] public class ServerCallbackAttribute : Attribute {} /// - /// Prevents the server from running this method. - /// Prints a warning if the server tries to execute this method. + /// Only an active client will run this method. + /// Prints a warning if the server or in-active client tries to execute this method. /// [AttributeUsage(AttributeTargets.Method)] public class ClientAttribute : Attribute {} /// - /// Prevents the server from running this method. + /// Only an active client will run this method. /// No warning is printed. /// [AttributeUsage(AttributeTargets.Method)] @@ -82,4 +86,16 @@ public class SceneAttribute : PropertyAttribute {} /// [AttributeUsage(AttributeTargets.Field)] public class ShowInInspectorAttribute : Attribute {} + + /// + /// Used to make a field readonly in the inspector + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class ReadOnlyAttribute : PropertyAttribute {} + + /// + /// When defining multiple Readers/Writers for the same type, indicate which one Weaver must use. + /// + [AttributeUsage(AttributeTargets.Method)] + public class WeaverPriorityAttribute : Attribute {} } diff --git a/Assets/Mirror/Runtime/Attributes.cs.meta b/Assets/Mirror/Core/Attributes.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/Attributes.cs.meta rename to Assets/Mirror/Core/Attributes.cs.meta index c50a489..09a8b6f 100644 --- a/Assets/Mirror/Runtime/Attributes.cs.meta +++ b/Assets/Mirror/Core/Attributes.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Attributes.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Batching.meta b/Assets/Mirror/Core/Batching.meta similarity index 100% rename from Assets/Mirror/Runtime/Batching.meta rename to Assets/Mirror/Core/Batching.meta diff --git a/Assets/Mirror/Runtime/Batching/Batcher.cs b/Assets/Mirror/Core/Batching/Batcher.cs similarity index 54% rename from Assets/Mirror/Runtime/Batching/Batcher.cs rename to Assets/Mirror/Core/Batching/Batcher.cs index 3a8d457..6f7cad9 100644 --- a/Assets/Mirror/Runtime/Batching/Batcher.cs +++ b/Assets/Mirror/Core/Batching/Batcher.cs @@ -29,8 +29,17 @@ public class Batcher // they would not contain a timestamp readonly int threshold; - // TimeStamp header size for those who need it - public const int HeaderSize = sizeof(double); + // TimeStamp header size. each batch has one. + public const int TimestampSize = sizeof(double); + + // Message header size. each message has one. + public static int MessageHeaderSize(int messageSize) => + Compression.VarUIntSize((ulong)messageSize); + + // maximum overhead for a single message. + // useful for the outside to calculate max message sizes. + public static int MaxMessageOverhead(int messageSize) => + TimestampSize + MessageHeaderSize(messageSize); // full batches ready to be sent. // DO NOT queue NetworkMessage, it would box. @@ -38,10 +47,18 @@ public class Batcher // it would allocate too many writers. // https://github.com/vis2k/Mirror/pull/3127 // => best to build batches on the fly. - Queue batches = new Queue(); + readonly Queue batches = new Queue(); - // current batch in progress + // current batch in progress. + // we also store the timestamp to ensure we don't add a message from another frame, + // as this would introduce subtle jitter! + // + // for example: + // - a batch is started at t=1, another message is added at t=2 and then it's flushed + // - NetworkTransform uses remoteTimestamp which is t=1 + // - snapshot interpolation would off by one (or multiple) frames! NetworkWriterPooled batch; + double batchTimestamp; public Batcher(int threshold) { @@ -53,16 +70,47 @@ public Batcher(int threshold) // caller needs to make sure they are within max packet size. public void AddMessage(ArraySegment message, double timeStamp) { + // safety: message timestamp is only written once. + // make sure all messages in this batch are from the same timestamp. + // otherwise it could silently introduce jitter. + // + // this happened before: + // - NetworkEarlyUpdate @ t=1 processes transport messages + // - a handler replies by sending a message + // - a new batch is started @ t=1, timestamp is encoded + // - NetworkLateUpdate @ t=2 decides it's time to broadcast + // - NetworkTransform sends @ t=2 + // - we add to the above batch which already encoded t=1 + // - Client receives the batch which timestamp t=1 + // - NetworkTransform uses remoteTime for interpolation + // remoteTime is the batch timestamp which is t=1 + // - the NetworkTransform message is actually t=2 + // => smooth interpolation would be impossible! + // NT thinks the position was @ t=1 but actually it was @ t=2 ! + // + // the solution: if timestamp changed, enqueue the existing batch + if (batch != null && batchTimestamp != timeStamp) + { + batches.Enqueue(batch); + batch = null; + batchTimestamp = 0; + } + + // predict the needed size, which is varint(size) + content + int headerSize = Compression.VarUIntSize((ulong)message.Count); + int neededSize = headerSize + message.Count; + // when appending to a batch in progress, check final size. // if it expands beyond threshold, then we should finalize it first. // => less than or exactly threshold is fine. // GetBatch() will finalize it. // => see unit tests. if (batch != null && - batch.Position + message.Count > threshold) + batch.Position + neededSize > threshold) { batches.Enqueue(batch); batch = null; + batchTimestamp = 0; } // initialize a new batch if necessary @@ -76,12 +124,25 @@ public void AddMessage(ArraySegment message, double timeStamp) // -> batches are per-frame, it doesn't matter which message's // timestamp we use. batch.WriteDouble(timeStamp); + + // remember the encoded timestamp, see safety check below. + batchTimestamp = timeStamp; } // add serialization to current batch. even if > threshold. // -> we do allow > threshold sized messages as single batch // -> WriteBytes instead of WriteSegment because the latter // would add a size header. we want to write directly. + // + // include size prefix as varint! + // -> fixes NetworkMessage serialization mismatch corrupting the + // next message in a batch. + // -> a _lot_ of time was wasted debugging corrupt batches. + // no easy way to figure out which NetworkMessage has a mismatch. + // -> this is worth everyone's sanity. + // -> varint means we prefix with 1 byte most of the time. + // -> the same issue in NetworkIdentity was why Mirror started! + Compression.CompressVarUInt(batch, (ulong)message.Count); batch.WriteBytes(message.Array, message.Offset, message.Count); } @@ -123,5 +184,23 @@ public bool GetBatch(NetworkWriter writer) // nothing was written return false; } + + // return all batches to the pool for cleanup + public void Clear() + { + // return batch in progress + if (batch != null) + { + NetworkWriterPool.Return(batch); + batch = null; + batchTimestamp = 0; + } + + // return all queued batches + foreach (NetworkWriterPooled queued in batches) + NetworkWriterPool.Return(queued); + + batches.Clear(); + } } } diff --git a/Assets/Mirror/Runtime/Batching/Batcher.cs.meta b/Assets/Mirror/Core/Batching/Batcher.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/Batching/Batcher.cs.meta rename to Assets/Mirror/Core/Batching/Batcher.cs.meta index a774908..2449b26 100644 --- a/Assets/Mirror/Runtime/Batching/Batcher.cs.meta +++ b/Assets/Mirror/Core/Batching/Batcher.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Batching/Batcher.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs b/Assets/Mirror/Core/Batching/Unbatcher.cs similarity index 72% rename from Assets/Mirror/Runtime/Batching/Unbatcher.cs rename to Assets/Mirror/Core/Batching/Unbatcher.cs index 495ada9..6b2c405 100644 --- a/Assets/Mirror/Runtime/Batching/Unbatcher.cs +++ b/Assets/Mirror/Core/Batching/Unbatcher.cs @@ -14,13 +14,13 @@ public class Unbatcher { // supporting adding multiple batches before GetNextMessage is called. // just in case. - Queue batches = new Queue(); + readonly Queue batches = new Queue(); public int BatchesCount => batches.Count; // NetworkReader is only created once, // then pointed to the first batch. - NetworkReader reader = new NetworkReader(new byte[0]); + readonly NetworkReader reader = new NetworkReader(new byte[0]); // timestamp that was written into the batch remotely. // for the batch that our reader is currently pointed at. @@ -48,7 +48,7 @@ public bool AddBatch(ArraySegment batch) // don't need to check against that. // make sure we have at least 8 bytes to read for tick timestamp - if (batch.Count < Batcher.HeaderSize) + if (batch.Count < Batcher.TimestampSize) return false; // put into a (pooled) writer @@ -69,43 +69,22 @@ public bool AddBatch(ArraySegment batch) } // get next message, unpacked from batch (if any) + // message ArraySegment is only valid until the next call. // timestamp is the REMOTE time when the batch was created remotely. - public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp) + public bool GetNextMessage(out ArraySegment message, out double remoteTimeStamp) { - // getting messages would be easy via - // <> - // but to save A LOT of bandwidth, we use - // < - // in other words, we don't know where the current message ends - // - // BUT: it doesn't matter! - // -> we simply return the reader - // * if we have one yet - // * and if there's more to read - // -> the caller can then read one message from it - // -> when the end is reached, we retire the batch! - // - // for example: - // while (GetNextMessage(out message)) - // ProcessMessage(message); - // - message = null; + message = default; + remoteTimeStamp = 0; // do nothing if we don't have any batches. // otherwise the below queue.Dequeue() would throw an // InvalidOperationException if operating on empty queue. if (batches.Count == 0) - { - remoteTimeStamp = 0; return false; - } // was our reader pointed to anything yet? - if (reader.Length == 0) - { - remoteTimeStamp = 0; + if (reader.Capacity == 0) return false; - } // no more data to read? if (reader.Remaining == 0) @@ -123,19 +102,27 @@ public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp StartReadingBatch(next); } // otherwise there's nothing more to read - else - { - remoteTimeStamp = 0; - return false; - } + else return false; } // use the current batch's remote timestamp // AFTER potentially moving to the next batch ABOVE! remoteTimeStamp = readerRemoteTimeStamp; - // if we got here, then we have more data to read. - message = reader; + // enough data to read the size prefix? + if (reader.Remaining == 0) + return false; + + // read the size prefix as varint + // see Batcher.AddMessage comments for explanation. + int size = (int)Compression.DecompressVarUInt(reader); + + // validate size prefix, in case attackers send malicious data + if (reader.Remaining < size) + return false; + + // return the message of size + message = reader.ReadBytesSegment(size); return true; } } diff --git a/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta b/Assets/Mirror/Core/Batching/Unbatcher.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta rename to Assets/Mirror/Core/Batching/Unbatcher.cs.meta index 26038b0..a2d8dfa 100644 --- a/Assets/Mirror/Runtime/Batching/Unbatcher.cs.meta +++ b/Assets/Mirror/Core/Batching/Unbatcher.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Batching/Unbatcher.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/ConnectionQuality.cs b/Assets/Mirror/Core/ConnectionQuality.cs new file mode 100644 index 0000000..9989f87 --- /dev/null +++ b/Assets/Mirror/Core/ConnectionQuality.cs @@ -0,0 +1,74 @@ +// standalone, Unity-independent connection-quality algorithm & enum. +// don't need to use this directly, it's built into Mirror's NetworkClient. +using UnityEngine; + +namespace Mirror +{ + public enum ConnectionQuality : byte + { + ESTIMATING, // still estimating + POOR, // unplayable + FAIR, // very noticeable latency, not very enjoyable anymore + GOOD, // very playable for everyone but high level competitors + EXCELLENT // ideal experience for high level competitors + } + + public enum ConnectionQualityMethod : byte + { + Simple, // simple estimation based on rtt and jitter + Pragmatic // based on snapshot interpolation adjustment + } + + // provide different heuristics for users to choose from. + // simple heuristics to get started. + // this will be iterated on over time based on user feedback. + public static class ConnectionQualityHeuristics + { + // convenience extension to color code Connection Quality + public static Color ColorCode(this ConnectionQuality quality) + { + switch (quality) + { + case ConnectionQuality.POOR: return Color.red; + case ConnectionQuality.FAIR: return new Color(1.0f, 0.647f, 0.0f); + case ConnectionQuality.GOOD: return Color.yellow; + case ConnectionQuality.EXCELLENT: return Color.green; + default: return Color.gray; // ESTIMATING + } + } + + // straight forward estimation + // rtt: average round trip time in seconds. + // jitter: average latency variance. + public static ConnectionQuality Simple(double rtt, double jitter) + { + if (rtt <= 0.100 && jitter <= 0.10) return ConnectionQuality.EXCELLENT; + if (rtt <= 0.200 && jitter <= 0.20) return ConnectionQuality.GOOD; + if (rtt <= 0.400 && jitter <= 0.50) return ConnectionQuality.FAIR; + return ConnectionQuality.POOR; + } + + // snapshot interpolation based estimation. + // snap. interp. adjusts buffer time based on connection quality. + // based on this, we can measure how far away we are from the ideal. + // the returned quality will always directly correlate with gameplay. + // => requires SnapshotInterpolation dynamicAdjustment to be enabled! + public static ConnectionQuality Pragmatic(double targetBufferTime, double currentBufferTime) + { + // buffer time is set by the game developer. + // estimating in multiples is a great way to be game independent. + // for example, a fast paced shooter and a slow paced RTS will both + // have poor connection if the multiplier is >10. + double multiplier = currentBufferTime / targetBufferTime; + + // empirically measured with Tanks demo + LatencySimulation. + // it's not obvious to estimate on paper. + if (multiplier <= 1.15) return ConnectionQuality.EXCELLENT; + if (multiplier <= 1.25) return ConnectionQuality.GOOD; + if (multiplier <= 1.50) return ConnectionQuality.FAIR; + + // anything else is poor + return ConnectionQuality.POOR; + } + } +} diff --git a/Assets/Mirror/Core/ConnectionQuality.cs.meta b/Assets/Mirror/Core/ConnectionQuality.cs.meta new file mode 100644 index 0000000..50eddb3 --- /dev/null +++ b/Assets/Mirror/Core/ConnectionQuality.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ff663b880e33e4606b545c8b497041c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/ConnectionQuality.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/HostMode.cs b/Assets/Mirror/Core/HostMode.cs new file mode 100644 index 0000000..3054b3c --- /dev/null +++ b/Assets/Mirror/Core/HostMode.cs @@ -0,0 +1,44 @@ +// host mode related helper functions. +// usually they set up both server & client. +// it's cleaner to keep them in one place, instead of only in server / client. +using System; + +namespace Mirror +{ + public static class HostMode + { + // keep the local connections setup in one function. + // makes host setup easier to follow. + internal static void SetupConnections() + { + // create local connections pair, both are connected + Utils.CreateLocalConnections( + out LocalConnectionToClient connectionToClient, + out LocalConnectionToServer connectionToServer); + + // set client connection + NetworkClient.connection = connectionToServer; + + // set server connection + NetworkServer.SetLocalConnection(connectionToClient); + } + + // call OnConnected on server & client. + // public because NetworkClient.ConnectLocalServer was public before too. + public static void InvokeOnConnected() + { + // call server OnConnected with server's connection to client + NetworkServer.OnConnected(NetworkServer.localConnection); + + // call client OnConnected with client's connection to server + // => previously we used to send a ConnectMessage to + // NetworkServer.localConnection. this would queue the message + // until NetworkClient.Update processes it. + // => invoking the client's OnConnected event directly here makes + // tests fail. so let's do it exactly the same order as before by + // queueing the event for next Update! + //OnConnectedEvent?.Invoke(connection); + ((LocalConnectionToServer)NetworkClient.connection).QueueConnectedEvent(); + } + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs.meta b/Assets/Mirror/Core/HostMode.cs.meta similarity index 55% rename from Assets/Mirror/Components/Experimental/NetworkRigidbody.cs.meta rename to Assets/Mirror/Core/HostMode.cs.meta index 1610f0a..6e10676 100644 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody.cs.meta +++ b/Assets/Mirror/Core/HostMode.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 83392ae5c1b731446909f252fd494ae4 +guid: d27175a08d5341fc97645b49ee533d5a MonoImporter: externalObjects: {} serializedVersion: 2 @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/HostMode.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/InterestManagement.cs b/Assets/Mirror/Core/InterestManagement.cs new file mode 100644 index 0000000..88ead0f --- /dev/null +++ b/Assets/Mirror/Core/InterestManagement.cs @@ -0,0 +1,146 @@ +// interest management component for custom solutions like +// distance based, spatial hashing, raycast based, etc. + +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public abstract class InterestManagement : InterestManagementBase + { + // allocate newObservers helper HashSet + readonly HashSet newObservers = + new HashSet(); + + // rebuild observers for the given NetworkIdentity. + // Server will automatically spawn/despawn added/removed ones. + // newObservers: cached hashset to put the result into + // initialize: true if being rebuilt for the first time + // + // IMPORTANT: + // => global rebuild would be more simple, BUT + // => local rebuild is way faster for spawn/despawn because we can + // simply rebuild a select NetworkIdentity only + // => having both .observers and .observing is necessary for local + // rebuilds + // + // in other words, this is the perfect solution even though it's not + // completely simple (due to .observers & .observing). + // + // Mirror maintains .observing automatically in the background. best of + // both worlds without any worrying now! + public abstract void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers); + + // helper function to trigger a full rebuild. + // most implementations should call this in a certain interval. + // some might call this all the time, or only on team changes or + // scene changes and so on. + // + // IMPORTANT: check if NetworkServer.active when using Update()! + [ServerCallback] + protected void RebuildAll() + { + foreach (NetworkIdentity identity in NetworkServer.spawned.Values) + { + NetworkServer.RebuildObservers(identity, false); + } + } + + public override void Rebuild(NetworkIdentity identity, bool initialize) + { + // clear newObservers hashset before using it + newObservers.Clear(); + + // not force hidden? + if (identity.visibility != Visibility.ForceHidden) + { + OnRebuildObservers(identity, newObservers); + } + + // IMPORTANT: AFTER rebuilding add own player connection in any case + // to ensure player always sees himself no matter what. + // -> OnRebuildObservers might clear observers, so we need to add + // the player's own connection AFTER. 100% fail safe. + // -> fixes https://github.com/vis2k/Mirror/issues/692 where a + // player might teleport out of the ProximityChecker's cast, + // losing the own connection as observer. + if (identity.connectionToClient != null) + { + newObservers.Add(identity.connectionToClient); + } + + bool changed = false; + + // add all newObservers that aren't in .observers yet + foreach (NetworkConnectionToClient conn in newObservers) + { + // only add ready connections. + // otherwise the player might not be in the world yet or anymore + if (conn != null && conn.isReady) + { + if (initialize || !identity.observers.ContainsKey(conn.connectionId)) + { + // new observer + conn.AddToObserving(identity); + // Debug.Log($"New Observer for {gameObject} {conn}"); + changed = true; + } + } + } + + // remove all old .observers that aren't in newObservers anymore + foreach (NetworkConnectionToClient conn in identity.observers.Values) + { + if (!newObservers.Contains(conn)) + { + // removed observer + conn.RemoveFromObserving(identity, false); + // Debug.Log($"Removed Observer for {gameObject} {conn}"); + changed = true; + } + } + + // copy new observers to observers + if (changed) + { + identity.observers.Clear(); + foreach (NetworkConnectionToClient conn in newObservers) + { + if (conn != null && conn.isReady) + identity.observers.Add(conn.connectionId, conn); + } + } + + // special case for host mode: we use SetHostVisibility to hide + // NetworkIdentities that aren't in observer range from host. + // this is what games like Dota/Counter-Strike do too, where a host + // does NOT see all players by default. they are in memory, but + // hidden to the host player. + // + // this code is from UNET, it's a bit strange but it works: + // * it hides newly connected identities in host mode + // => that part was the intended behaviour + // * it hides ALL NetworkIdentities in host mode when the host + // connects but hasn't selected a character yet + // => this only works because we have no .localConnection != null + // check. at this stage, localConnection is null because + // StartHost starts the server first, then calls this code, + // then starts the client and sets .localConnection. so we can + // NOT add a null check without breaking host visibility here. + // * it hides ALL NetworkIdentities in server-only mode because + // observers never contain the 'null' .localConnection + // => that was not intended, but let's keep it as it is so we + // don't break anything in host mode. it's way easier than + // iterating all identities in a special function in StartHost. + if (initialize) + { + if (!newObservers.Contains(NetworkServer.localConnection)) + { + SetHostVisibility(identity, false); + } + } + } + } +} diff --git a/Assets/Mirror/Runtime/InterestManagement.cs.meta b/Assets/Mirror/Core/InterestManagement.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/InterestManagement.cs.meta rename to Assets/Mirror/Core/InterestManagement.cs.meta index bfabf6b..a13677b 100644 --- a/Assets/Mirror/Runtime/InterestManagement.cs.meta +++ b/Assets/Mirror/Core/InterestManagement.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/InterestManagement.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/InterestManagementBase.cs b/Assets/Mirror/Core/InterestManagementBase.cs new file mode 100644 index 0000000..1a3cc1b --- /dev/null +++ b/Assets/Mirror/Core/InterestManagementBase.cs @@ -0,0 +1,103 @@ +// interest management component for custom solutions like +// distance based, spatial hashing, raycast based, etc. +// low level base class allows for low level spatial hashing etc., which is 3-5x faster. +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] + public abstract class InterestManagementBase : MonoBehaviour + { + // initialize NetworkServer/Client .aoi. + // previously we did this in Awake(), but that's called for disabled + // components too. if we do it OnEnable(), then it's not set for + // disabled components. + protected virtual void OnEnable() + { + // do not check if == null or error if already set. + // users may enabled/disable components randomly, + // causing this to be called multiple times. + NetworkServer.aoi = this; + NetworkClient.aoi = this; + } + + [ServerCallback] + public virtual void ResetState() {} + + // Callback used by the visibility system to determine if an observer + // (player) can see the NetworkIdentity. If this function returns true, + // the network connection will be added as an observer. + // conn: Network connection of a player. + // returns True if the player can see this object. + public abstract bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver); + + + // Callback used by the visibility system for objects on a host. + // Objects on a host (with a local client) cannot be disabled or + // destroyed when they are not visible to the local client. So this + // function is called to allow custom code to hide these objects. A + // typical implementation will disable renderer components on the + // object. This is only called on local clients on a host. + // => need the function in here and virtual so people can overwrite! + // => not everyone wants to hide renderers! + [ServerCallback] + public virtual void SetHostVisibility(NetworkIdentity identity, bool visible) + { + foreach (Renderer rend in identity.GetComponentsInChildren()) + rend.enabled = visible; + + // reason to also set lights/audio/terrain/etc.: + // Let's say players were holding a flashlight or magic wand with a particle effect. Without this, + // host client would see the light / particles for all players in all subscenes because we don't + // hide lights and particles. Host client would hear ALL audio sources in all subscenes too. We + // hide the renderers, which covers basic objects and UI, but we don't hide anything else that may + // be a child of a networked object. Same idea for cars with lights and sounds in other subscenes + // that host client shouldn't see or hear...host client wouldn't see the car itself, but sees the + // lights moving around and hears all of their engines / horns / etc. + foreach (Light light in identity.GetComponentsInChildren()) + light.enabled = visible; + + foreach (AudioSource audio in identity.GetComponentsInChildren()) + audio.enabled = visible; + + foreach (Terrain terrain in identity.GetComponentsInChildren()) + { + terrain.drawHeightmap = visible; + terrain.drawTreesAndFoliage = visible; + } + + foreach (ParticleSystem particle in identity.GetComponentsInChildren()) + { + ParticleSystem.EmissionModule emission = particle.emission; + emission.enabled = visible; + } + } + + /// Called on the server when a new networked object is spawned. + // (useful for 'only rebuild if changed' interest management algorithms) + [ServerCallback] + public virtual void OnSpawned(NetworkIdentity identity) {} + + /// Called on the server when a networked object is destroyed. + // (useful for 'only rebuild if changed' interest management algorithms) + [ServerCallback] + public virtual void OnDestroyed(NetworkIdentity identity) {} + + public abstract void Rebuild(NetworkIdentity identity, bool initialize); + + /// Adds the specified connection to the observers of identity + protected void AddObserver(NetworkConnectionToClient connection, NetworkIdentity identity) + { + connection.AddToObserving(identity); + identity.observers.Add(connection.connectionId, connection); + } + + /// Removes the specified connection from the observers of identity + protected void RemoveObserver(NetworkConnectionToClient connection, NetworkIdentity identity) + { + connection.RemoveFromObserving(identity, false); + identity.observers.Remove(connection.connectionId); + } + } +} diff --git a/Assets/Mirror/Core/InterestManagementBase.cs.meta b/Assets/Mirror/Core/InterestManagementBase.cs.meta new file mode 100644 index 0000000..257afc4 --- /dev/null +++ b/Assets/Mirror/Core/InterestManagementBase.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 18bd2ffe65a444f3b13d59bdac7f2228 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/InterestManagementBase.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation.meta b/Assets/Mirror/Core/LagCompensation.meta new file mode 100644 index 0000000..3c2b3f9 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d2656015ded44e83a24f4c4776bafd40 +timeCreated: 1687920405 diff --git a/Assets/Mirror/Core/LagCompensation/Capture.cs b/Assets/Mirror/Core/LagCompensation/Capture.cs new file mode 100644 index 0000000..e4fdabe --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/Capture.cs @@ -0,0 +1,13 @@ +namespace Mirror +{ + public interface Capture + { + // server timestamp at time of capture. + double timestamp { get; set; } + + // optional gizmo drawing for visual debugging. + // history is only known on the server, which usually doesn't render. + // showing Gizmos in the Editor is enough. + void DrawGizmo(); + } +} diff --git a/Assets/Mirror/Core/LagCompensation/Capture.cs.meta b/Assets/Mirror/Core/LagCompensation/Capture.cs.meta new file mode 100644 index 0000000..a9cc6cc --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/Capture.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 347e831952e943a49095cadd39a5aeb2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/Capture.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs new file mode 100644 index 0000000..29ebc2e --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs @@ -0,0 +1,139 @@ +// HistoryBounds keeps a bounding box of all the object's bounds in the past N seconds. +// useful to decide which objects to rollback, instead of rolling back all of them. +// https://www.youtube.com/watch?v=zrIY0eIyqmI (37:00) +// standalone C# implementation to be engine (and language) agnostic. + +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // FakeByte: gather bounds in smaller buckets. + // for example, bucket(t0,t1,t2), bucket(t3,t4,t5), ... + // instead of removing old bounds t0, t1, ... + // we remove a whole bucket every 3 times: bucket(t0,t1,t2) + // and when building total bounds, we encapsulate a few larger buckets + // instead of many smaller bounds. + // + // => a bucket is encapsulate(bounds0, bounds1, bounds2) so we don't + // need a custom struct, simply reuse bounds but remember that each + // entry includes N timestamps. + // + // => note that simply reducing capture interval is _not_ the same. + // we want to capture in detail in case players run in zig-zag. + // but still grow larger buckets internally. + public class HistoryBounds + { + // mischa: use MinMaxBounds to avoid Unity Bounds.Encapsulate conversions. + readonly int boundsPerBucket; + readonly Queue fullBuckets; + + // full bucket limit. older ones will be removed. + readonly int bucketLimit; + + // bucket in progress, contains 0..boundsPerBucket bounds encapsulated. + MinMaxBounds? currentBucket; + int currentBucketSize; + + // amount of total bounds, including bounds in full buckets + current + public int boundsCount { get; private set; } + + // total bounds encapsulating all of the bounds history. + // totalMinMax is used for internal calculations. + // public total is used for Unity representation. + MinMaxBounds totalMinMax; + public Bounds total + { + get + { + Bounds bounds = new Bounds(); + bounds.SetMinMax(totalMinMax.min, totalMinMax.max); + return bounds; + } + } + + public HistoryBounds(int boundsLimit, int boundsPerBucket) + { + // bucketLimit via '/' cuts off remainder. + // that's what we want, since we always have a 'currentBucket'. + this.boundsPerBucket = boundsPerBucket; + this.bucketLimit = (boundsLimit / boundsPerBucket); + + // initialize queue with maximum capacity to avoid runtime resizing + // capacity +1 because it makes the code easier if we insert first, and then remove. + fullBuckets = new Queue(bucketLimit + 1); + } + + // insert new bounds into history. calculates new total bounds. + // Queue.Dequeue() always has the oldest bounds. + public void Insert(Bounds bounds) + { + // convert to MinMax representation for faster .Encapsulate() + MinMaxBounds minmax = new MinMaxBounds + { + min = bounds.min, + max = bounds.max + }; + + // initialize 'total' if not initialized yet. + // we don't want to call (0,0).Encapsulate(bounds). + if (boundsCount == 0) + { + totalMinMax = minmax; + } + + // add to current bucket: + // either initialize new one, or encapsulate into existing one + if (currentBucket == null) + { + currentBucket = minmax; + } + else + { + currentBucket.Value.Encapsulate(minmax); + } + + // current bucket has one more bounds. + // total bounds increased as well. + currentBucketSize += 1; + boundsCount += 1; + + // always encapsulate into total immediately. + // this is free. + totalMinMax.Encapsulate(minmax); + + // current bucket full? + if (currentBucketSize == boundsPerBucket) + { + // move it to full buckets + fullBuckets.Enqueue(currentBucket.Value); + currentBucket = null; + currentBucketSize = 0; + + // full bucket capacity reached? + if (fullBuckets.Count > bucketLimit) + { + // remove oldest bucket + fullBuckets.Dequeue(); + boundsCount -= boundsPerBucket; + + // recompute total bounds + // instead of iterating N buckets, we iterate N / boundsPerBucket buckets. + // TODO technically we could reuse 'currentBucket' before clearing instead of encapsulating again + totalMinMax = minmax; + foreach (MinMaxBounds bucket in fullBuckets) + totalMinMax.Encapsulate(bucket); + } + } + } + + public void Reset() + { + fullBuckets.Clear(); + currentBucket = null; + currentBucketSize = 0; + boundsCount = 0; + totalMinMax = new MinMaxBounds(); + } + } +} diff --git a/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta new file mode 100644 index 0000000..290df33 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ca9ea58b98a34f73801b162cd5de724e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/HistoryBounds.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensation.cs b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs new file mode 100644 index 0000000..ae37a3f --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs @@ -0,0 +1,144 @@ +// standalone lag compensation algorithm +// based on the Valve Networking Model: +// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking +using System.Collections.Generic; + +namespace Mirror +{ + public static class LagCompensation + { + // history is of . + // Queue allows for fast 'remove first' and 'append last'. + // + // make sure to always insert in order. + // inserting out of order like [1,2,4,3] would cause issues. + // can't safeguard this because Queue doesn't have .Last access. + public static void Insert( + Queue> history, + int historyLimit, + double timestamp, + T capture) + where T : Capture + { + // make space according to history limit. + // do this before inserting, to avoid resizing past capacity. + if (history.Count >= historyLimit) + history.Dequeue(); + + // insert + history.Enqueue(new KeyValuePair(timestamp, capture)); + } + + // get the two snapshots closest to a given timestamp. + // those can be used to interpolate the exact snapshot at that time. + // if timestamp is newer than the newest history entry, then we extrapolate. + // 't' will be between 1 and 2, before is second last, after is last. + // callers should Lerp(before, after, t=1.5) to extrapolate the hit. + // see comments below for extrapolation. + public static bool Sample( + Queue> history, + double timestamp, // current server time + double interval, // capture interval + out T before, + out T after, + out double t) // interpolation factor + where T : Capture + { + before = default; + after = default; + t = 0; + + // can't sample an empty history + // interpolation needs at least one entry. + // extrapolation needs at least two entries. + // can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0. + if(history.Count < 2) { + return false; + } + + // older than oldest + if (timestamp < history.Peek().Key) { + return false; + } + + // iterate through the history + // TODO faster version: guess start index by how many 'intervals' we are behind. + // search around that area. + // should be O(1) most of the time, unless sampling was off. + KeyValuePair prev = new KeyValuePair(); + KeyValuePair prevPrev = new KeyValuePair(); + foreach(KeyValuePair entry in history) { + // exact match? + if (timestamp == entry.Key) { + before = entry.Value; + after = entry.Value; + t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp); + return true; + } + + // did we check beyond timestamp? then return the previous two. + if (entry.Key > timestamp) { + before = prev.Value; + after = entry.Value; + t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp); + return true; + } + + // remember the last two for extrapolation. + // Queue doesn't have access to .Last. + prevPrev = prev; + prev = entry; + } + + // newer than newest: extrapolate up to one interval. + // let's say we capture every 100 ms: + // 100, 200, 300, 400 + // and the server is at 499 + // if a client sends CmdFire at time 480, then there's no history entry. + // => adding the current entry every time would be too expensive. + // worst case we would capture at 401, 402, 403, 404, ... 100 times + // => not extrapolating isn't great. low latency clients would be + // punished by missing their targets since no entry at 'time' was found. + // => extrapolation is the best solution. make sure this works as + // expected and within limits. + if (prev.Key < timestamp && timestamp <= prev.Key + interval) { + // return the last two valid snapshots. + // can't just return (after, after) because we can't extrapolate + // if their distance is 0. + before = prevPrev.Value; + after = prev.Value; + + // InverseLerp will give [after, after+interval]. + // but we return [before, after, t]. + // so add +1 for the distance from before->after + t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp); + return true; + } + + return false; + } + + // never trust the client. + // we estimate when a message was sent. + // don't trust the client to tell us the time. + // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking + // Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation + // => lag compensation demo estimation is off by only ~6ms + public static double EstimateTime(double serverTime, double rtt, double bufferTime) + { + // packet latency is one trip from client to server, so rtt / 2 + // client view interpolation is the snapshot interpolation buffer time + double latency = rtt / 2; + return serverTime - latency - bufferTime; + } + + // convenience function to draw all history gizmos. + // this should be called from OnDrawGizmos. + public static void DrawGizmos(Queue> history) + where T : Capture + { + foreach (KeyValuePair entry in history) + entry.Value.DrawGizmo(); + } + } +} diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta new file mode 100644 index 0000000..c697593 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ad53cc7d12144d0ba3a8b0a4515e5d17 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/LagCompensation.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs new file mode 100644 index 0000000..a7ec944 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs @@ -0,0 +1,19 @@ +// snapshot interpolation settings struct. +// can easily be exposed in Unity inspectors. +using System; +using UnityEngine; + +namespace Mirror +{ + // class so we can define defaults easily + [Serializable] + public class LagCompensationSettings + { + [Header("Buffering")] + [Tooltip("Keep this many past snapshots in the buffer. The larger this is, the further we can rewind into the past.\nMaximum rewind time := historyAmount * captureInterval")] + public int historyLimit = 6; + + [Tooltip("Capture state every 'captureInterval' seconds. Larger values will space out the captures more, which gives a longer history but with possible gaps inbetween.\nSmaller values will have fewer gaps, with shorter history.")] + public float captureInterval = 0.100f; // 100 ms + } +} diff --git a/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs.meta b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs.meta new file mode 100644 index 0000000..d57904c --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fa80bec245f94bf8a28ec78777992a1c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs new file mode 100644 index 0000000..b1b4874 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs @@ -0,0 +1,73 @@ +// Unity's Bounds struct is represented as (center, extents). +// HistoryBounds make heavy use of .Encapsulate(), which has to convert +// Unity's (center, extents) to (min, max) every time, and then convert back. +// +// It's faster to use a (min, max) representation directly instead. +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public struct MinMaxBounds: IEquatable + { + public Vector3 min; + public Vector3 max; + + // encapsulate a single point + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Encapsulate(Vector3 point) + { + min = Vector3.Min(this.min, point); + max = Vector3.Max(this.max, point); + } + + // encapsulate another bounds + public void Encapsulate(MinMaxBounds bounds) + { + Encapsulate(bounds.min); + Encapsulate(bounds.max); + } + + // convenience comparison with Unity's bounds, for unit tests etc. + public static bool operator ==(MinMaxBounds lhs, Bounds rhs) => + lhs.min == rhs.min && + lhs.max == rhs.max; + + public static bool operator !=(MinMaxBounds lhs, Bounds rhs) => + !(lhs == rhs); + + public override bool Equals(object obj) => + obj is MinMaxBounds other && + min == other.min && + max == other.max; + + public bool Equals(MinMaxBounds other) => + min.Equals(other.min) && max.Equals(other.max); + + public bool Equals(Bounds other) => + min.Equals(other.min) && max.Equals(other.max); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + public override int GetHashCode() => HashCode.Combine(min, max); +#else + public override int GetHashCode() + { + // return HashCode.Combine(min, max); without using .Combine for older Unity versions + unchecked + { + int hash = 17; + hash = hash * 23 + min.GetHashCode(); + hash = hash * 23 + max.GetHashCode(); + return hash; + } + } +#endif + + // tostring + public override string ToString() => $"({min}, {max})"; + } +} diff --git a/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta new file mode 100644 index 0000000..04b5ed9 --- /dev/null +++ b/Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4372b1e1a1cc4c669cc7bf0925f59d29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/LocalConnectionToClient.cs b/Assets/Mirror/Core/LocalConnectionToClient.cs new file mode 100644 index 0000000..3ceba29 --- /dev/null +++ b/Assets/Mirror/Core/LocalConnectionToClient.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; + +namespace Mirror +{ + // a server's connection TO a LocalClient. + // sending messages on this connection causes the client's handler function to be invoked directly + public class LocalConnectionToClient : NetworkConnectionToClient + { + internal LocalConnectionToServer connectionToServer; + + // packet queue + internal readonly Queue queue = new Queue(); + + public LocalConnectionToClient() : base(LocalConnectionId) {} + + internal override void Send(ArraySegment segment, int channelId = Channels.Reliable) + { + // instead of invoking it directly, we enqueue and process next update. + // this way we can simulate a similar call flow as with remote clients. + // the closer we get to simulating host as remote, the better! + // both directions do this, so [Command] and [Rpc] behave the same way. + + //Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}"); + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.WriteBytes(segment.Array, segment.Offset, segment.Count); + connectionToServer.queue.Enqueue(writer); + } + + // true because local connections never timeout + internal override bool IsAlive(float timeout) => true; + + // don't ping host client in host mode + protected override void UpdatePing() {} + + internal override void Update() + { + base.Update(); + + // process internal messages so they are applied at the correct time + while (queue.Count > 0) + { + // call receive on queued writer's content, return to pool + NetworkWriterPooled writer = queue.Dequeue(); + ArraySegment message = writer.ToArraySegment(); + + // OnTransportData assumes a proper batch with timestamp etc. + // let's make a proper batch and pass it to OnTransportData. + Batcher batcher = GetBatchForChannelId(Channels.Reliable); + batcher.AddMessage(message, NetworkTime.localTime); + + using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get()) + { + // make a batch with our local time (double precision) + if (batcher.GetBatch(batchWriter)) + { + NetworkServer.OnTransportData(connectionId, batchWriter.ToArraySegment(), Channels.Reliable); + } + } + + NetworkWriterPool.Return(writer); + } + } + + internal void DisconnectInternal() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + isReady = false; + RemoveFromObservingsObservers(); + } + + /// Disconnects this connection. + public override void Disconnect() + { + DisconnectInternal(); + connectionToServer.DisconnectInternal(); + } + } +} diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta b/Assets/Mirror/Core/LocalConnectionToClient.cs.meta similarity index 53% rename from Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta rename to Assets/Mirror/Core/LocalConnectionToClient.cs.meta index 42243ed..e910e0d 100644 --- a/Assets/Mirror/Runtime/LocalConnectionToClient.cs.meta +++ b/Assets/Mirror/Core/LocalConnectionToClient.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e79d1be9a9a54e240ab239f687376c8e +guid: a88758df7db2043d6a9d926e0b6d4191 MonoImporter: externalObjects: {} serializedVersion: 2 @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LocalConnectionToClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs b/Assets/Mirror/Core/LocalConnectionToServer.cs similarity index 82% rename from Assets/Mirror/Runtime/LocalConnectionToServer.cs rename to Assets/Mirror/Core/LocalConnectionToServer.cs index 378ffdb..15669c7 100644 --- a/Assets/Mirror/Runtime/LocalConnectionToServer.cs +++ b/Assets/Mirror/Core/LocalConnectionToServer.cs @@ -13,8 +13,6 @@ public class LocalConnectionToServer : NetworkConnectionToServer // packet queue internal readonly Queue queue = new Queue(); - public override string address => "localhost"; - // see caller for comments on why we need this bool connectedEventPending; bool disconnectedEventPending; @@ -30,22 +28,15 @@ internal override void Send(ArraySegment segment, int channelId = Channels return; } - // OnTransportData assumes batching. - // so let's make a batch with proper timestamp prefix. - Batcher batcher = GetBatchForChannelId(channelId); - batcher.AddMessage(segment, NetworkTime.localTime); + // instead of invoking it directly, we enqueue and process next update. + // this way we can simulate a similar call flow as with remote clients. + // the closer we get to simulating host as remote, the better! + // both directions do this, so [Command] and [Rpc] behave the same way. - // flush it to the server's OnTransportData immediately. - // local connection to server always invokes immediately. - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // make a batch with our local time (double precision) - if (batcher.GetBatch(writer)) - { - NetworkServer.OnTransportData(connectionId, writer.ToArraySegment(), channelId); - } - else Debug.LogError("Local connection failed to make batch. This should never happen."); - } + //Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}"); + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.WriteBytes(segment.Array, segment.Offset, segment.Count); + connectionToClient.queue.Enqueue(writer); } internal override void Update() diff --git a/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta b/Assets/Mirror/Core/LocalConnectionToServer.cs.meta similarity index 61% rename from Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta rename to Assets/Mirror/Core/LocalConnectionToServer.cs.meta index 856b255..4a1ee42 100644 --- a/Assets/Mirror/Runtime/LocalConnectionToServer.cs.meta +++ b/Assets/Mirror/Core/LocalConnectionToServer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/LocalConnectionToServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Messages.cs b/Assets/Mirror/Core/Messages.cs new file mode 100644 index 0000000..899b0ea --- /dev/null +++ b/Assets/Mirror/Core/Messages.cs @@ -0,0 +1,186 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + // need to send time every sendInterval. + // batching automatically includes remoteTimestamp. + // all we need to do is ensure that an empty message is sent. + // and react to it. + // => we don't want to insert a snapshot on every batch. + // => do it exactly every sendInterval on every TimeSnapshotMessage. + public struct TimeSnapshotMessage : NetworkMessage {} + + public struct ReadyMessage : NetworkMessage {} + + public struct NotReadyMessage : NetworkMessage {} + + public struct AddPlayerMessage : NetworkMessage {} + + public struct SceneMessage : NetworkMessage + { + public string sceneName; + // Normal = 0, LoadAdditive = 1, UnloadAdditive = 2 + public SceneOperation sceneOperation; + public bool customHandling; + } + + public enum SceneOperation : byte + { + Normal, + LoadAdditive, + UnloadAdditive + } + + public struct CommandMessage : NetworkMessage + { + public uint netId; + public byte componentIndex; + public ushort functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + } + + public struct RpcMessage : NetworkMessage + { + public uint netId; + public byte componentIndex; + public ushort functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + 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; + // 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; + // Local position + public Vector3 position; + // Local rotation + public Quaternion rotation; + // Local scale + public Vector3 scale; + // 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; + // 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 {} + + public struct ObjectSpawnFinishedMessage : NetworkMessage {} + + public struct ObjectDestroyMessage : NetworkMessage + { + public uint netId; + } + + public struct ObjectHideMessage : NetworkMessage + { + public uint netId; + } + + public struct EntityStateMessage : NetworkMessage + { + 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 + { + // local time is used to calculate round trip time, + // and to calculate the predicted time offset. + public double localTime; + + // predicted time is sent to compare the final error, for debugging only + public double predictedTimeAdjusted; + + public NetworkPingMessage(double localTime, double predictedTimeAdjusted) + { + this.localTime = localTime; + this.predictedTimeAdjusted = predictedTimeAdjusted; + } + } + + // the other end responds with this message. + // we can use this to calculate rtt. + public struct NetworkPongMessage : NetworkMessage + { + // local time is used to calculate round trip time. + public double localTime; + + // predicted error is used to adjust the predicted timeline. + public double predictionErrorUnadjusted; + public double predictionErrorAdjusted; // for debug purposes + + public NetworkPongMessage(double localTime, double predictionErrorUnadjusted, double predictionErrorAdjusted) + { + this.localTime = localTime; + this.predictionErrorUnadjusted = predictionErrorUnadjusted; + this.predictionErrorAdjusted = predictionErrorAdjusted; + } + } +} diff --git a/Assets/Mirror/Runtime/Messages.cs.meta b/Assets/Mirror/Core/Messages.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/Messages.cs.meta rename to Assets/Mirror/Core/Messages.cs.meta index 5d119e2..3411dc8 100644 --- a/Assets/Mirror/Runtime/Messages.cs.meta +++ b/Assets/Mirror/Core/Messages.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Messages.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Mirror.asmdef b/Assets/Mirror/Core/Mirror.asmdef similarity index 59% rename from Assets/Mirror/Runtime/Mirror.asmdef rename to Assets/Mirror/Core/Mirror.asmdef index 0f38055..2fa8d95 100644 --- a/Assets/Mirror/Runtime/Mirror.asmdef +++ b/Assets/Mirror/Core/Mirror.asmdef @@ -1,16 +1,16 @@ { "name": "Mirror", + "rootNamespace": "", "references": [ - "Mirror.CompilerSymbols", - "Telepathy", - "kcp2k" + "GUID:325984b52e4128546bc7558552f8b1d2" ], - "optionalUnityReferences": [], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": true, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, - "defineConstraints": [] + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false } \ No newline at end of file diff --git a/Assets/Mirror/Core/Mirror.asmdef.meta b/Assets/Mirror/Core/Mirror.asmdef.meta new file mode 100644 index 0000000..69f6cee --- /dev/null +++ b/Assets/Mirror/Core/Mirror.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 30817c1a0e6d646d99c048fc403f5979 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Mirror.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs b/Assets/Mirror/Core/NetworkAuthenticator.cs similarity index 93% rename from Assets/Mirror/Runtime/NetworkAuthenticator.cs rename to Assets/Mirror/Core/NetworkAuthenticator.cs index 9f99b50..aa1e7f7 100644 --- a/Assets/Mirror/Runtime/NetworkAuthenticator.cs +++ b/Assets/Mirror/Core/NetworkAuthenticator.cs @@ -25,7 +25,7 @@ public virtual void OnStartServer() {} /// Called when server stops, used to unregister message handlers if needed. public virtual void OnStopServer() {} - /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// Called on server from OnServerConnectInternal when a client needs to authenticate public virtual void OnServerAuthenticate(NetworkConnectionToClient conn) {} protected void ServerAccept(NetworkConnectionToClient conn) @@ -44,7 +44,7 @@ public virtual void OnStartClient() {} /// Called when client stops, used to unregister message handlers if needed. public virtual void OnStopClient() {} - /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// Called on client from OnClientConnectInternal when a client needs to authenticate public virtual void OnClientAuthenticate() {} protected void ClientAccept() diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta b/Assets/Mirror/Core/NetworkAuthenticator.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta rename to Assets/Mirror/Core/NetworkAuthenticator.cs.meta index d37db68..be59e4a 100644 --- a/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta +++ b/Assets/Mirror/Core/NetworkAuthenticator.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkAuthenticator.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs similarity index 63% rename from Assets/Mirror/Runtime/NetworkBehaviour.cs rename to Assets/Mirror/Core/NetworkBehaviour.cs index 94cd930..f887f47 100644 --- a/Assets/Mirror/Runtime/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -6,14 +6,28 @@ namespace Mirror { + // SyncMode decides if a component is synced to all observers, or only owner public enum SyncMode { Observers, Owner } + // SyncDirection decides if a component is synced from: + // * server to all clients + // * owner client, to server, to all other clients + // + // naming: 'ClientToServer' etc. instead of 'ClientAuthority', because + // that wouldn't be accurate. server's OnDeserialize can still validate + // client data before applying. it's really about direction, not authority. + public enum SyncDirection { ServerToClient, ClientToServer } + /// Base class for networked components. + // [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours [AddComponentMenu("")] - [RequireComponent(typeof(NetworkIdentity))] [HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")] public abstract class NetworkBehaviour : MonoBehaviour { + /// 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.")] + [HideInInspector] public SyncDirection syncDirection = SyncDirection.ServerToClient; + /// sync mode for OnSerialize // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. [Tooltip("By default synced data is sent from the server to all Observers of the object.\nChange this to Owner to only have the server update the client that has ownership authority for this object")] @@ -22,9 +36,14 @@ public abstract class NetworkBehaviour : MonoBehaviour /// sync interval for OnSerialize (in seconds) // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. // [0,2] should be enough. anything >2s is too laggy anyway. + // + // NetworkServer & NetworkClient broadcast() are behind a sendInterval timer now. + // it makes sense to keep every component's syncInterval setting at '0' by default. + // otherwise, the overlapping timers could introduce unexpected latency. + // careful: default of '0.1' may [Tooltip("Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)")] [Range(0, 2)] - [HideInInspector] public float syncInterval = 0.1f; + [HideInInspector] public float syncInterval = 0; internal double lastSyncTime; /// True if this object is on the server and has been spawned. @@ -44,8 +63,39 @@ public abstract class NetworkBehaviour : MonoBehaviour /// True if this object is on the client-only, not host. public bool isClientOnly => netIdentity.isClientOnly; - /// True on client if that component has been assigned to the client. E.g. player, pets, henchmen. - public bool hasAuthority => netIdentity.hasAuthority; + /// isOwned is true on the client if this NetworkIdentity is one of the .owned entities of our connection on the server. + // for example: main player & pets are owned. monsters & npcs aren't. + public bool isOwned => netIdentity.isOwned; + + /// authority is true if we are allowed to modify this component's state. On server, it's true if SyncDirection is ServerToClient. On client, it's true if SyncDirection is ClientToServer and(!) if this object is owned by the client. + // on the client: if Client->Server SyncDirection and owned + // on the server: if Server->Client SyncDirection + // on the host: if Server->Client SyncDirection (= server owns it), or if Client->Server and owned (=host client owns it) + // in host mode: always true because either server or client always has authority, and host is both. + // + // for example, NetworkTransform: + // client may modify position if ClientAuthority mode and owned + // server may modify position only if server authority + // + // note that in original Mirror, hasAuthority only meant 'isOwned'. + // there was no syncDirection to check. + // + // also note that this is a per-NetworkBehaviour flag. + // another component may not be client authoritative, etc. + public bool authority + { + get + { + // host mode needs to be checked explicitly + if (isClient && isServer) return syncDirection == SyncDirection.ServerToClient || isOwned; + + // client-only + if (isClient) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // server-only + return syncDirection == SyncDirection.ServerToClient; + } + } /// The unique network Id of this object (unique at runtime). public uint netId => netIdentity.netId; @@ -71,7 +121,7 @@ public abstract class NetworkBehaviour : MonoBehaviour public NetworkIdentity netIdentity { get; internal set; } /// Returns the index of the component on this object - public int ComponentIndex { get; internal set; } + public byte ComponentIndex { get; internal set; } // to avoid fully serializing entities every time, we have two options: // * run a delta compression algorithm @@ -85,8 +135,9 @@ public abstract class NetworkBehaviour : MonoBehaviour // -> still supports dynamically sized types // // 64 bit mask, tracking up to 64 SyncVars. - protected ulong syncVarDirtyBits { get; private set; } - // 64 bit mask, tracking up to 64 sync collections (internal for tests). + // protected since NB child classes read this field in the weaver generated SerializeSyncVars method + protected ulong syncVarDirtyBits; + // 64 bit mask, tracking up to 64 sync collections. // internal for tests, field for faster access (instead of property) // TODO 64 SyncLists are too much. consider smaller mask later. internal ulong syncObjectDirtyBits; @@ -98,14 +149,39 @@ public abstract class NetworkBehaviour : MonoBehaviour // hook guard prevents that. ulong syncVarHookGuard; + protected virtual void OnValidate() + { + // Skip if Editor is in Play mode + if (Application.isPlaying) return; + + // we now allow child NetworkBehaviours. + // we can not [RequireComponent(typeof(NetworkIdentity))] anymore. + // instead, we need to ensure a NetworkIdentity is somewhere in the + // parents. + // only run this in Editor. don't add more runtime overhead. + + // GetComponentInParent(includeInactive) is needed because Prefabs are not + // considered active, so this check requires to scan inactive. +#if UNITY_2021_3_OR_NEWER // 2021 has GetComponentInParent(bool includeInactive = false) + if (GetComponent() == null && + GetComponentInParent(true) == null) + { + 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 its parents.", this); + } +#endif + } + // USED BY WEAVER to set syncvars in host mode without deadlocking protected bool GetSyncVarHookGuard(ulong dirtyBit) => (syncVarHookGuard & dirtyBit) != 0UL; - // Deprecated 2021-09-16 (old weavers used it) - [Obsolete("Renamed to GetSyncVarHookGuard (uppercase)")] - protected bool getSyncVarHookGuard(ulong dirtyBit) => GetSyncVarHookGuard(dirtyBit); - // USED BY WEAVER to set syncvars in host mode without deadlocking protected void SetSyncVarHookGuard(ulong dirtyBit, bool value) { @@ -117,33 +193,42 @@ protected void SetSyncVarHookGuard(ulong dirtyBit, bool value) syncVarHookGuard &= ~dirtyBit; } - // Deprecated 2021-09-16 (old weavers used it) - [Obsolete("Renamed to SetSyncVarHookGuard (uppercase)")] - protected void setSyncVarHookGuard(ulong dirtyBit, bool value) => SetSyncVarHookGuard(dirtyBit, value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void SetSyncObjectDirtyBit(ulong dirtyBit) + { + syncObjectDirtyBits |= dirtyBit; + } /// Set as dirty so that it's synced to clients again. // these are masks, not bit numbers, ie. 110011b not '2' for 2nd bit. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetSyncVarDirtyBit(ulong dirtyBit) { syncVarDirtyBits |= dirtyBit; } - // Deprecated 2021-09-19 - [Obsolete("SetDirtyBit was renamed to SetSyncVarDirtyBit because that's what it does")] - public void SetDirtyBit(ulong dirtyBit) => SetSyncVarDirtyBit(dirtyBit); + /// Set as dirty to trigger OnSerialize & send. Dirty bits are cleared after the send. + // previously one had to use SetSyncVarDirtyBit(1), which is confusing. + // simply reuse SetSyncVarDirtyBit for now. + // instead of adding another field. + // syncVarDirtyBits does trigger OnSerialize as well. + // + // it's important to set _all_ bits as dirty. + // for example, server needs to broadcast ClientToServer components. + // if we only set the first bit, only that SyncVar would be broadcast. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetDirty() => SetSyncVarDirtyBit(ulong.MaxValue); // true if syncInterval elapsed and any SyncVar or SyncObject is dirty - public bool IsDirty() - { - if (NetworkTime.localTime - lastSyncTime >= syncInterval) - { - // OR both bitmasks. != 0 if either was dirty. - return (syncVarDirtyBits | syncObjectDirtyBits) != 0UL; - } - return false; - } + // OR both bitmasks. != 0 if either was dirty. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDirty() => + // 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; - /// Clears all the dirty bits that were set by SetDirtyBits() + /// 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() @@ -177,25 +262,84 @@ protected void InitSyncObject(SyncObject syncObject) // OnDirty needs to set nth bit in our dirty mask ulong nthBit = 1UL << index; - syncObject.OnDirty = () => syncObjectDirtyBits |= nthBit; - - // only record changes while we have observers. - // prevents ever growing .changes lists: - // if a monster has no observers but we keep modifing a SyncObject, - // then the changes would never be flushed and keep growing, - // because OnSerialize isn't called without observers. - syncObject.IsRecording = () => netIdentity.observers?.Count > 0; + syncObject.OnDirty = () => SetSyncObjectDirtyBit(nthBit); + + // who is allowed to modify SyncList/SyncSet/etc.: + // on client: only if owned ClientToserver + // on server: only if ServerToClient. + // but also for initial state when spawning. + // need to set a lambda because 'isClient' isn't available in + // InitSyncObject yet, which is called from the constructor. + syncObject.IsWritable = () => + { + // carefully check each mode separately to ensure correct results. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3342 + + // normally we would check isServer / isClient here. + // users may add to SyncLists before the object was spawned. + // isServer / isClient would still be false. + // so we need to check NetworkServer/Client.active here instead. + + // host mode: any ServerToClient and any local client owned + if (NetworkServer.active && NetworkClient.active) + return syncDirection == SyncDirection.ServerToClient || isOwned; + + // server only: any ServerToClient + if (NetworkServer.active) + return syncDirection == SyncDirection.ServerToClient; + + // client only: only ClientToServer and owned + if (NetworkClient.active) + { + // spawned: only ClientToServer and owned + if (netId != 0) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // not spawned (character selection previews, etc.): always allow + // fixes https://github.com/MirrorNetworking/Mirror/issues/3343 + return true; + } + + // undefined behaviour should throw to make it very obvious + throw new Exception("InitSyncObject: IsWritable: neither NetworkServer nor NetworkClient are active."); + }; + + // when do we record changes: + // on client: only if owned ClientToServer + // on server: only if we have observers. + // prevents ever growing .changes lists: + // if a monster has no observers but we keep modifing a SyncObject, + // then the changes would never be flushed and keep growing, + // because OnSerialize isn't called without observers. + syncObject.IsRecording = () => + { + // carefully check each mode separately to ensure correct results. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3342 + + // host mode: only if observed + if (isServer && isClient) return netIdentity.observers.Count > 0; + + // server only: only if observed + if (isServer) return netIdentity.observers.Count > 0; + + // client only: only ClientToServer and owned + if (isClient) return syncDirection == SyncDirection.ClientToServer && isOwned; + + // users may add to SyncLists before the object was spawned. + // isServer / isClient would still be false. + // in that case, allow modifying but don't record changes yet. + return false; + }; } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions - protected void SendCommandInternal(string functionFullName, NetworkWriter writer, int channelId, bool requiresAuthority = true) + protected void SendCommandInternal(string functionFullName, int functionHashCode, NetworkWriter writer, int channelId, bool requiresAuthority = true) { // this was in Weaver before // NOTE: we could remove this later to allow calling Cmds on Server // to avoid Wrapper functions. a lot of people requested this. if (!NetworkClient.active) { - Debug.LogError($"Command Function {functionFullName} called without an active client."); + Debug.LogError($"Command {functionFullName} called on {name} without an active client.", gameObject); return; } @@ -207,14 +351,15 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer // or client may have been set NotReady intentionally, so // only warn if on the reliable channel. if (channelId == Channels.Reliable) - Debug.LogWarning("Send command attempted while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady."); + Debug.LogWarning($"Command {functionFullName} called on {name} while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady.", gameObject); return; } - // local players can always send commands, regardless of authority, other objects must have authority. - if (!(!requiresAuthority || isLocalPlayer || hasAuthority)) + // local players can always send commands, regardless of authority, + // other objects must have authority. + if (!(!requiresAuthority || isLocalPlayer || isOwned)) { - Debug.LogWarning($"Trying to send command for object without authority. {functionFullName}"); + Debug.LogWarning($"Command {functionFullName} called on {name} without authority.", gameObject); return; } @@ -225,7 +370,13 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer // => see also: https://github.com/vis2k/Mirror/issues/2629 if (NetworkClient.connection == null) { - Debug.LogError("Send command attempted with no client running."); + Debug.LogError($"Command {functionFullName} called on {name} with no client running.", gameObject); + return; + } + + if (netId == 0) + { + Debug.LogWarning($"Command {functionFullName} called on {name} with netId=0. Maybe it wasn't spawned yet?", gameObject); return; } @@ -233,9 +384,9 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer CommandMessage message = new CommandMessage { netId = netId, - componentIndex = (byte)ComponentIndex, + componentIndex = ComponentIndex, // type+func so Inventory.RpcUse != Equipment.RpcUse - functionHash = functionFullName.GetStableHashCode(), + functionHash = (ushort)functionHashCode, // segment to avoid reader allocations payload = writer.ToArraySegment() }; @@ -245,23 +396,25 @@ protected void SendCommandInternal(string functionFullName, NetworkWriter writer // false. other objects don't have a .connectionToServer. // => so we always need to use NetworkClient.connection instead. // => see also: https://github.com/vis2k/Mirror/issues/2629 + // This bypasses the null check in NetworkClient.Send but we have + // a null check above with a detailed error log. NetworkClient.connection.Send(message, channelId); } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions - protected void SendRPCInternal(string functionFullName, NetworkWriter writer, int channelId, bool includeOwner) + protected void SendRPCInternal(string functionFullName, int functionHashCode, NetworkWriter writer, int channelId, bool includeOwner) { // this was in Weaver before if (!NetworkServer.active) { - Debug.LogError($"RPC Function {functionFullName} called on Client."); + Debug.LogError($"RPC Function {functionFullName} called without an active server.", gameObject); return; } // This cannot use NetworkServer.active, as that is not specific to this object. if (!isServer) { - Debug.LogWarning($"ClientRpc {functionFullName} called on un-spawned object: {name}"); + Debug.LogWarning($"ClientRpc {functionFullName} called on un-spawned object: {name}", gameObject); return; } @@ -269,28 +422,51 @@ protected void SendRPCInternal(string functionFullName, NetworkWriter writer, in RpcMessage message = new RpcMessage { netId = netId, - componentIndex = (byte)ComponentIndex, + componentIndex = ComponentIndex, // type+func so Inventory.RpcUse != Equipment.RpcUse - functionHash = functionFullName.GetStableHashCode(), + functionHash = (ushort)functionHashCode, // segment to avoid reader allocations payload = writer.ToArraySegment() }; - NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId); + // serialize it to each ready observer's connection's rpc buffer. + // send them all at once, instead of sending one message per rpc. + // NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId); + + // safety check used to be in SendToReadyObservers. keep it for now. + if (netIdentity.observers == null || netIdentity.observers.Count == 0) + return; + + // serialize the message only once + using (NetworkWriterPooled serialized = NetworkWriterPool.Get()) + { + serialized.Write(message); + + // send to every observer. + // batching buffers this automatically. + foreach (NetworkConnectionToClient conn in netIdentity.observers.Values) + { + bool isOwner = conn == netIdentity.connectionToClient; + if ((!isOwner || includeOwner) && conn.isReady) + { + conn.Send(message, channelId); + } + } + } } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions - protected void SendTargetRPCInternal(NetworkConnection conn, string functionFullName, NetworkWriter writer, int channelId) + protected void SendTargetRPCInternal(NetworkConnection conn, string functionFullName, int functionHashCode, NetworkWriter writer, int channelId) { if (!NetworkServer.active) { - Debug.LogError($"TargetRPC {functionFullName} called when server not active"); + Debug.LogError($"TargetRPC {functionFullName} was called on {name} when server not active.", gameObject); return; } if (!isServer) { - Debug.LogWarning($"TargetRpc {functionFullName} called on {name} but that object has not been spawned or has been unspawned"); + Debug.LogWarning($"TargetRpc {functionFullName} called on {name} but that object has not been spawned or has been unspawned.", gameObject); return; } @@ -303,13 +479,14 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull // if still null if (conn is null) { - Debug.LogError($"TargetRPC {functionFullName} was given a null connection, make sure the object has an owner or you pass in the target connection"); + Debug.LogError($"TargetRPC {functionFullName} can't be sent because it was given a null connection. Make sure {name} is owned by a connection, or if you pass a connection manually then make sure it's not null. For example, TargetRpcs can be called on Player/Pet which are owned by a connection. However, they can not be called on Monsters/Npcs which don't have an owner connection.", gameObject); return; } - if (!(conn is NetworkConnectionToClient)) + // TODO change conn type to NetworkConnectionToClient to begin with. + if (!(conn is NetworkConnectionToClient connToClient)) { - Debug.LogError($"TargetRPC {functionFullName} requires a NetworkConnectionToClient but was given {conn.GetType().Name}"); + Debug.LogError($"TargetRPC {functionFullName} called on {name} requires a NetworkConnectionToClient but was given {conn.GetType().Name}", gameObject); return; } @@ -317,13 +494,15 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull RpcMessage message = new RpcMessage { netId = netId, - componentIndex = (byte)ComponentIndex, + componentIndex = ComponentIndex, // type+func so Inventory.RpcUse != Equipment.RpcUse - functionHash = functionFullName.GetStableHashCode(), + functionHash = (ushort)functionHashCode, // segment to avoid reader allocations payload = writer.ToArraySegment() }; + // send it to the connection. + // batching buffers this automatically. conn.Send(message, channelId); } @@ -344,7 +523,7 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull // { // int oldValue = health; // SetSyncVar(value, ref health, 1uL); - // if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL)) + // if (NetworkServer.activeHost && !GetSyncVarHookGuard(1uL)) // { // SetSyncVarHookGuard(1uL, value: true); // OnChanged(oldValue, value); @@ -368,7 +547,7 @@ public void GeneratedSyncVarSetter(T value, ref T field, ulong dirtyBit, Acti // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -395,7 +574,7 @@ public void GeneratedSyncVarSetter_GameObject(GameObject value, ref GameObject f // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -422,7 +601,7 @@ public void GeneratedSyncVarSetter_NetworkIdentity(NetworkIdentity value, ref Ne // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -450,7 +629,7 @@ public void GeneratedSyncVarSetter_NetworkBehaviour(T value, ref T field, ulo // in client-only mode, OnDeserialize would call it. // we use hook guard to protect against deadlock where hook // changes syncvar, calling hook again. - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(dirtyBit)) + if (NetworkServer.activeHost && !GetSyncVarHookGuard(dirtyBit)) { SetSyncVarHookGuard(dirtyBit, true); OnChanged(oldValue, value); @@ -469,8 +648,7 @@ public static bool SyncVarGameObjectEqual(GameObject newGameObject, uint netIdFi uint newNetId = 0; if (newGameObject != null) { - NetworkIdentity identity = newGameObject.GetComponent(); - if (identity != null) + if (newGameObject.TryGetComponent(out NetworkIdentity identity)) { newNetId = identity.netId; if (newNetId == 0) @@ -493,8 +671,7 @@ protected void SetSyncVarGameObject(GameObject newGameObject, ref GameObject gam uint newNetId = 0; if (newGameObject != null) { - NetworkIdentity identity = newGameObject.GetComponent(); - if (identity != null) + if (newGameObject.TryGetComponent(out NetworkIdentity identity)) { newNetId = identity.netId; if (newNetId == 0) @@ -516,7 +693,9 @@ protected void SetSyncVarGameObject(GameObject newGameObject, ref GameObject gam protected GameObject GetSyncVarGameObject(uint netId, ref GameObject gameObjectField) { // server always uses the field - if (isServer) + // if neither, fallback to original field + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447 + if (isServer || !isClient) { return gameObjectField; } @@ -592,7 +771,6 @@ public static bool SyncVarNetworkIdentityEqual(NetworkIdentity newIdentity, uint // GeneratedSyncVarDeserialize(reader, ref health, null, reader.ReadInt()); // } // } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, T value) { T previous = field; @@ -650,7 +828,6 @@ public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, // GeneratedSyncVarDeserialize_GameObject(reader, ref target, OnChangedNB, ref ___targetNetId); // } // } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action OnChanged, NetworkReader reader, ref uint netIdField) { uint previousNetId = netIdField; @@ -713,7 +890,6 @@ public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action< // GeneratedSyncVarDeserialize_NetworkIdentity(reader, ref target, OnChangedNI, ref ___targetNetId); // } // } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity field, Action OnChanged, NetworkReader reader, ref uint netIdField) { uint previousNetId = netIdField; @@ -777,7 +953,6 @@ public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity fiel // GeneratedSyncVarDeserialize_NetworkBehaviour(reader, ref target, OnChangedNB, ref ___targetNetId); // } // } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GeneratedSyncVarDeserialize_NetworkBehaviour(ref T field, Action OnChanged, NetworkReader reader, ref NetworkBehaviourSyncVar netIdField) where T : NetworkBehaviour { @@ -824,7 +999,9 @@ protected void SetSyncVarNetworkIdentity(NetworkIdentity newIdentity, ref Networ protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdentity identityField) { // server always uses the field - if (isServer) + // if neither, fallback to original field + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447 + if (isServer || !isClient) { return identityField; } @@ -838,7 +1015,7 @@ protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdent protected static bool SyncVarNetworkBehaviourEqual(T newBehaviour, NetworkBehaviourSyncVar syncField) where T : NetworkBehaviour { uint newNetId = 0; - int newComponentIndex = 0; + byte newComponentIndex = 0; if (newBehaviour != null) { newNetId = newBehaviour.netId; @@ -861,7 +1038,7 @@ protected void SetSyncVarNetworkBehaviour(T newBehaviour, ref T behaviourFiel return; uint newNetId = 0; - int componentIndex = 0; + byte componentIndex = 0; if (newBehaviour != null) { newNetId = newBehaviour.netId; @@ -882,12 +1059,14 @@ protected void SetSyncVarNetworkBehaviour(T newBehaviour, ref T behaviourFiel // Debug.Log($"SetSyncVarNetworkBehaviour NetworkIdentity {GetType().Name} bit [{dirtyBit}] netIdField:{oldField}->{syncField}"); } - // helper function for [SyncVar] NetworkIdentities. + // helper function for [SyncVar] NetworkBehaviours. // -> ref GameObject as second argument makes OnDeserialize processing easier protected T GetSyncVarNetworkBehaviour(NetworkBehaviourSyncVar syncNetBehaviour, ref T behaviourField) where T : NetworkBehaviour { // server always uses the field - if (isServer) + // if neither, fallback to original field + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447 + if (isServer || !isClient) { return behaviourField; } @@ -898,38 +1077,18 @@ protected T GetSyncVarNetworkBehaviour(NetworkBehaviourSyncVar syncNetBehavio { return null; } - - behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T; - return behaviourField; - } - - // backing field for sync NetworkBehaviour - public struct NetworkBehaviourSyncVar : IEquatable - { - public uint netId; - // limited to 255 behaviours per identity - public byte componentIndex; - - public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this() + + // 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. + if (syncNetBehaviour.componentIndex >= identity.NetworkBehaviours.Length) { - this.netId = netId; - this.componentIndex = (byte)componentIndex; - } - - public bool Equals(NetworkBehaviourSyncVar other) - { - return other.netId == netId && other.componentIndex == componentIndex; - } - - public bool Equals(uint netId, int componentIndex) - { - return this.netId == netId && this.componentIndex == componentIndex; + Debug.LogError($"[SyncVar] {typeof(T)} on {name}'s {GetType()}: can't access {identity.name} NetworkBehaviour[{syncNetBehaviour.componentIndex}] because it only has {identity.NetworkBehaviours.Length} components.\nWas a NetworkBeahviour accidentally destroyed at runtime?"); + return null; } - public override string ToString() - { - return $"[netId:{netId} compIndex:{componentIndex}]"; - } + behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T; + return behaviourField; } protected static bool SyncVarEqual(T value, ref T fieldValue) @@ -955,35 +1114,44 @@ protected void SetSyncVar(T value, ref T fieldValue, ulong dirtyBit) // // initialState is true for full spawns, false for delta syncs. // note: SyncVar hooks are only called when inital=false - public virtual bool OnSerialize(NetworkWriter writer, bool initialState) + public virtual void OnSerialize(NetworkWriter writer, bool initialState) { - // if initialState: write all SyncVars. - // otherwise write dirtyBits+dirty SyncVars - bool objectWritten = initialState ? SerializeObjectsAll(writer) : SerializeObjectsDelta(writer); - bool syncVarWritten = SerializeSyncVars(writer, initialState); - return objectWritten || syncVarWritten; + SerializeSyncObjects(writer, initialState); + SerializeSyncVars(writer, initialState); } /// Override to do custom deserialization (instead of SyncVars/SyncLists). Use OnSerialize too. public virtual void OnDeserialize(NetworkReader reader, bool initialState) + { + DeserializeSyncObjects(reader, initialState); + DeserializeSyncVars(reader, initialState); + } + + void SerializeSyncObjects(NetworkWriter writer, bool initialState) + { + // if initialState: write all SyncObjects (SyncList/Set/etc) + // otherwise write dirtyBits+dirty SyncVars + if (initialState) + SerializeObjectsAll(writer); + else + SerializeObjectsDelta(writer); + } + + void DeserializeSyncObjects(NetworkReader reader, bool initialState) { if (initialState) { - DeSerializeObjectsAll(reader); + DeserializeObjectsAll(reader); } else { - DeSerializeObjectsDelta(reader); + DeserializeObjectsDelta(reader); } - - DeserializeSyncVars(reader, initialState); } // USED BY WEAVER - protected virtual bool SerializeSyncVars(NetworkWriter writer, bool initialState) + protected virtual void SerializeSyncVars(NetworkWriter writer, bool initialState) { - return false; - // SyncVar are written here in subclass // if initialState @@ -1005,23 +1173,20 @@ protected virtual void DeserializeSyncVars(NetworkReader reader, bool initialSta // read dirty SyncVars } - public bool SerializeObjectsAll(NetworkWriter writer) + public void SerializeObjectsAll(NetworkWriter writer) { - bool dirty = false; for (int i = 0; i < syncObjects.Count; i++) { SyncObject syncObject = syncObjects[i]; syncObject.OnSerializeAll(writer); - dirty = true; } - return dirty; } - public bool SerializeObjectsDelta(NetworkWriter writer) + public void SerializeObjectsDelta(NetworkWriter writer) { - bool dirty = false; // write the mask writer.WriteULong(syncObjectDirtyBits); + // serializable objects, such as synclists for (int i = 0; i < syncObjects.Count; i++) { @@ -1030,13 +1195,11 @@ public bool SerializeObjectsDelta(NetworkWriter writer) if ((syncObjectDirtyBits & (1UL << i)) != 0) { syncObject.OnSerializeDelta(writer); - dirty = true; } } - return dirty; } - internal void DeSerializeObjectsAll(NetworkReader reader) + internal void DeserializeObjectsAll(NetworkReader reader) { for (int i = 0; i < syncObjects.Count; i++) { @@ -1045,7 +1208,7 @@ internal void DeSerializeObjectsAll(NetworkReader reader) } } - internal void DeSerializeObjectsDelta(NetworkReader reader) + internal void DeserializeObjectsDelta(NetworkReader reader) { ulong dirty = reader.ReadULong(); for (int i = 0; i < syncObjects.Count; i++) @@ -1059,6 +1222,130 @@ internal void DeSerializeObjectsDelta(NetworkReader reader) } } + // safely serialize each component in a way that one reading too much or + // too few bytes will show obvious, easy to resolve error messages. + // + // prevents the original UNET bug which started Mirror: + // https://github.com/vis2k/Mirror/issues/2617 + // where one component would read too much, and then all following reads + // on other entities would be mismatched, causing the weirdest errors. + // + // reads <> for 100% safety. + internal void Serialize(NetworkWriter writer, bool initialState) + { + // reserve length header to ensure the correct amount will be read. + // originally we used a 4 byte header (too bandwidth heavy). + // instead, let's "& 0xFF" the size. + // + // this is cleaner than barriers at the end of payload, because: + // - ensures the correct safety is read _before_ payload. + // - it's quite hard to break the check. + // a component would need to read/write the intented amount + // multiplied by 255 in order to miss the check. + // with barriers, reading 1 byte too much may still succeed if the + // next component's first byte matches the expected barrier. + // - we can still attempt to correct the invalid position via the + // safety length byte (we know that one is correct). + // + // it's just overall cleaner, and still low on bandwidth. + + // write placeholder length byte + // (jumping back later is WAY faster than allocating a temporary + // writer for the payload, then writing payload.size, payload) + int headerPosition = writer.Position; + writer.WriteByte(0); + int contentPosition = writer.Position; + + // write payload + try + { + // note this may not write anything if no syncIntervals elapsed + OnSerialize(writer, initialState); + } + catch (Exception e) + { + // show a detailed error and let the user know what went wrong + Debug.LogError($"OnSerialize failed for: object={name} component={GetType()} sceneId={netIdentity.sceneId:X}\n\n{e}"); + } + int endPosition = writer.Position; + + // fill in length hash as the last byte of the 4 byte length + writer.Position = headerPosition; + int size = endPosition - contentPosition; + byte safety = (byte)(size & 0xFF); + writer.WriteByte(safety); + writer.Position = endPosition; + + //Debug.Log($"OnSerializeSafely written for object {name} component:{GetType()} sceneId:{sceneId:X} header:{headerPosition} content:{contentPosition} end:{endPosition} contentSize:{endPosition - contentPosition}"); + } + + // correct the read size with the 1 byte length hash (by mischa). + // -> the component most likely read a few too many/few bytes. + // -> we know the correct last byte of the expected size (=the safety). + // -> attempt to reconstruct the size via safety byte. + // it will be correct unless someone wrote way way too much, + // as in > 255 bytes worth too much. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int ErrorCorrection(int size, byte safety) + { + // clear the last byte which most likely contains the error + uint cleared = (uint)size & 0xFFFFFF00; + + // insert the safety which we know to be correct + return (int)(cleared | safety); + } + + // returns false in case of errors. + // server needs to know in order to disconnect on error. + internal bool Deserialize(NetworkReader reader, bool initialState) + { + // detect errors, but attempt to correct before returning + bool result = true; + + // read 1 byte length hash safety & capture beginning for size check + byte safety = reader.ReadByte(); + int chunkStart = reader.Position; + + // call OnDeserialize and wrap it in a try-catch block so there's no + // way to mess up another component's deserialization + try + { + //Debug.Log($"OnDeserializeSafely: {name} component:{GetType()} sceneId:{sceneId:X} length:{contentSize}"); + OnDeserialize(reader, initialState); + } + catch (Exception e) + { + // show a detailed error and let the user know what went wrong + Debug.LogError($"OnDeserialize failed Exception={e.GetType()} (see below) object={name} component={GetType()} netId={netId}. Possible Reasons:\n" + + $" * Do {GetType()}'s OnSerialize and OnDeserialize calls write the same amount of data? \n" + + $" * Was there an exception in {GetType()}'s OnSerialize/OnDeserialize code?\n" + + $" * Are the server and client the exact same project?\n" + + $" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" + + $"Exception {e}"); + result = false; + } + + // compare bytes read with length hash + int size = reader.Position - chunkStart; + byte sizeHash = (byte)(size & 0xFF); + if (sizeHash != safety) + { + // warn the user. + Debug.LogWarning($"{name} (netId={netId}): {GetType()} OnDeserialize size mismatch. It read {size} bytes, which caused a size hash mismatch of {sizeHash:X2} vs. {safety:X2}. Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases."); + + // attempt to fix the position, so the following components + // don't all fail. this is very likely to work, unless the user + // read more than 255 bytes too many / too few. + // + // see test: SerializationSizeMismatch. + int correctedSize = ErrorCorrection(size, safety); + reader.Position = chunkStart + correctedSize; + result = false; + } + + return result; + } + internal void ResetSyncObjects() { foreach (SyncObject syncObject in syncObjects) @@ -1090,5 +1377,10 @@ public virtual void OnStartAuthority() {} /// Stop event, only called for objects the client has authority over. public virtual void OnStopAuthority() {} + + // Weaver injects this into inheriting classes to return true. + // allows runtime & tests to check if a type was weaved. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool Weaved() => false; } } diff --git a/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta b/Assets/Mirror/Core/NetworkBehaviour.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkBehaviour.cs.meta rename to Assets/Mirror/Core/NetworkBehaviour.cs.meta index f0bc195..3c6e6f5 100644 --- a/Assets/Mirror/Runtime/NetworkBehaviour.cs.meta +++ b/Assets/Mirror/Core/NetworkBehaviour.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkBehaviour.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkBehaviourHybrid.cs b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs new file mode 100644 index 0000000..988c5cc --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs @@ -0,0 +1,483 @@ +// 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 ResetState() + { + 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 clients + // server moves to A2, unreliabe delta sent to clients + // server moves back to A1, nothing is sent to clients because last baseline position == position + // => clients 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: + // client start in A1, reliable baseline sent to server and other clients + // client moves to A2, unreliabe delta sent to server and other clients + // client moves back to A1, nothing is sent to server because last baseline position == position + // => server / other clients 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/NetworkBehaviourHybrid.cs.meta b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta new file mode 100644 index 0000000..bfa737d --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourHybrid.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 657535a722c74173bdaa18a4394ce016 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkBehaviourHybrid.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs new file mode 100644 index 0000000..e9ed726 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs @@ -0,0 +1,33 @@ +using System; + +namespace Mirror +{ + // backing field for sync NetworkBehaviour + public struct NetworkBehaviourSyncVar : IEquatable + { + public uint netId; + // limited to 255 behaviours per identity + public byte componentIndex; + + public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this() + { + this.netId = netId; + this.componentIndex = (byte)componentIndex; + } + + public bool Equals(NetworkBehaviourSyncVar other) + { + return other.netId == netId && other.componentIndex == componentIndex; + } + + public bool Equals(uint netId, int componentIndex) + { + return this.netId == netId && this.componentIndex == componentIndex; + } + + public override string ToString() + { + return $"[netId:{netId} compIndex:{componentIndex}]"; + } + } +} diff --git a/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta new file mode 100644 index 0000000..0a925a7 --- /dev/null +++ b/Assets/Mirror/Core/NetworkBehaviourSyncVar.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: b04fe7518657486089dfaf811db0b3ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkBehaviourSyncVar.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs similarity index 62% rename from Assets/Mirror/Runtime/NetworkClient.cs rename to Assets/Mirror/Core/NetworkClient.cs index e5dabe3..d9cce5a 100644 --- a/Assets/Mirror/Runtime/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; @@ -18,8 +18,30 @@ public enum ConnectState } /// NetworkClient with connection to server. - public static class NetworkClient + public static partial class NetworkClient { + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + // + // additionally, server & client need to use the same send interval. + // otherwise it's too easy to accidentally cause interpolation issues if + // a component sends with client.interval but interpolates with + // server.interval, etc. + public static int sendRate => NetworkServer.sendRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; + + // 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 + // exploits. + // + // However, some games may want to allow exceptions in order to not + // interrupt the player's experience. + public static bool exceptionsDisconnect = true; // security by default + // message handlers by messageId internal static readonly Dictionary handlers = new Dictionary(); @@ -30,7 +52,7 @@ public static class NetworkClient new Dictionary(); /// Client's NetworkConnection to server. - public static NetworkConnection connection { get; internal set; } + public static NetworkConnectionToServer connection { get; internal set; } /// True if client is ready (= joined world). // TODO redundant state. point it to .connection.isReady instead (& test) @@ -46,24 +68,21 @@ public static class NetworkClient // NetworkClient state internal static ConnectState connectState = ConnectState.None; - /// IP address of the connection to server. - // empty if the client has not connected yet. - public static string serverIp => connection.address; - - /// active is true while a client is connecting/connected + /// active is true while a client is connecting/connected either as standalone or as host client. // (= while the network is active) public static bool active => connectState == ConnectState.Connecting || connectState == ConnectState.Connected; + /// active is true while the client is connected in host mode. + // naming consistent with NetworkServer.activeHost. + public static bool activeHost => connection is LocalConnectionToServer; + /// Check if client is connecting (before connected). public static bool isConnecting => connectState == ConnectState.Connecting; /// Check if client is connected (after connecting). public static bool isConnected => connectState == ConnectState.Connected; - /// True if client is running in host mode. - public static bool isHostClient => connection is LocalConnectionToServer; - // OnConnected / OnDisconnected used to be NetworkMessages that were // invoked. this introduced a bug where external clients could send // Connected/Disconnected messages over the network causing undefined @@ -71,19 +90,20 @@ public static class NetworkClient // => public so that custom NetworkManagers can hook into it public static Action OnConnectedEvent; public static Action OnDisconnectedEvent; - public static Action OnErrorEvent; + public static Action OnErrorEvent; + public static Action OnTransportExceptionEvent; /// Registered spawnable prefabs by assetId. - public static readonly Dictionary prefabs = - new Dictionary(); + public static readonly Dictionary prefabs = + new Dictionary(); - // custom spawn / unspawn handlers. + // custom spawn / unspawn handlers by assetId. // useful to support prefab pooling etc.: // https://mirror-networking.gitbook.io/docs/guides/gameobjects/custom-spawnfunctions - internal static readonly Dictionary spawnHandlers = - new Dictionary(); - internal static readonly Dictionary unspawnHandlers = - new Dictionary(); + internal static readonly Dictionary spawnHandlers = + new Dictionary(); + internal static readonly Dictionary unspawnHandlers = + new Dictionary(); // spawning // internal for tests @@ -93,98 +113,106 @@ public static class NetworkClient internal static readonly Dictionary spawnableObjects = new Dictionary(); - static Unbatcher unbatcher = new Unbatcher(); + internal static Unbatcher unbatcher = new Unbatcher(); // interest management component (optional) // only needed for SetHostVisibility - public static InterestManagement aoi; + public static InterestManagementBase aoi; // scene loading public static bool isLoadingScene; + // connection quality + // this is set by a virtual function in NetworkManager, + // which allows users to overwrite it with their own estimations. + public static ConnectionQuality connectionQuality = ConnectionQuality.ESTIMATING; + public static ConnectionQuality lastConnectionQuality = ConnectionQuality.ESTIMATING; + public static ConnectionQualityMethod connectionQualityMethod = ConnectionQualityMethod.Simple; + public static float connectionQualityInterval = 3; + static double lastConnectionQualityUpdate; + + /// + /// Invoked when connection quality changes. + /// First argument is the old quality, second argument is the new quality. + /// + public static event Action onConnectionQualityChanged; + // initialization ////////////////////////////////////////////////////// static void AddTransportHandlers() { + // community Transports may forget to call OnDisconnected. + // which could cause handlers to be added twice with +=. + // ensure we always clear the old ones first. + // fixes: https://github.com/vis2k/Mirror/issues/3152 + RemoveTransportHandlers(); + // += so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnClientConnected += OnTransportConnected; - Transport.activeTransport.OnClientDataReceived += OnTransportData; - Transport.activeTransport.OnClientDisconnected += OnTransportDisconnected; - Transport.activeTransport.OnClientError += OnError; + Transport.active.OnClientConnected += OnTransportConnected; + Transport.active.OnClientDataReceived += OnTransportData; + Transport.active.OnClientDisconnected += OnTransportDisconnected; + Transport.active.OnClientError += OnTransportError; + Transport.active.OnClientTransportException += OnTransportException; } static void RemoveTransportHandlers() { // -= so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnClientConnected -= OnTransportConnected; - Transport.activeTransport.OnClientDataReceived -= OnTransportData; - Transport.activeTransport.OnClientDisconnected -= OnTransportDisconnected; - Transport.activeTransport.OnClientError -= OnError; + Transport.active.OnClientConnected -= OnTransportConnected; + Transport.active.OnClientDataReceived -= OnTransportData; + Transport.active.OnClientDisconnected -= OnTransportDisconnected; + Transport.active.OnClientError -= OnTransportError; + Transport.active.OnClientTransportException -= OnTransportException; } - internal static void RegisterSystemHandlers(bool hostMode) + // connect ///////////////////////////////////////////////////////////// + // initialize is called before every connect + static void Initialize(bool hostMode) { - // host mode client / remote client react to some messages differently. - // but we still need to add handlers for all of them to avoid - // 'message id not found' errors. - if (hostMode) - { - RegisterHandler(OnHostClientObjectDestroy); - RegisterHandler(OnHostClientObjectHide); - RegisterHandler(_ => {}, false); - RegisterHandler(OnHostClientSpawn); - // host mode doesn't need spawning - RegisterHandler(_ => {}); - // host mode doesn't need spawning - RegisterHandler(_ => {}); - // host mode doesn't need state updates - RegisterHandler(_ => {}); - } - else + // safety: ensure Weaving succeded. + // if it silently failed, we would get lots of 'writer not found' + // and other random errors at runtime instead. this is cleaner. + if (!WeaverFuse.Weaved()) { - RegisterHandler(OnObjectDestroy); - RegisterHandler(OnObjectHide); - RegisterHandler(NetworkTime.OnClientPong, false); - RegisterHandler(OnSpawn); - RegisterHandler(OnObjectSpawnStarted); - RegisterHandler(OnObjectSpawnFinished); - RegisterHandler(OnEntityStateMessage); + // if it failed, throw an exception to early exit all Connect calls. + throw new Exception("NetworkClient won't start because Weaving failed or didn't run."); } - // These handlers are the same for host and remote clients - RegisterHandler(OnChangeOwner); - RegisterHandler(OnRPCMessage); + // Debug.Log($"Client Connect: {address}"); + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.active' first"); + + // reset unbatcher in case any batches from last session remain. + // need to do this in Initialize() so it runs for the host as well. + // fixes host mode scene transition receiving data from previous scene. + // credits: BigBoxVR + unbatcher = new Unbatcher(); + + // reset time interpolation on every new connect. + // ensures last sessions' state is cleared before starting again. + InitTimeInterpolation(); + + RegisterMessageHandlers(hostMode); + Transport.active.enabled = true; } - // connect ///////////////////////////////////////////////////////////// /// Connect client to a NetworkServer by address. public static void Connect(string address) { - // Debug.Log($"Client Connect: {address}"); - Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); + Initialize(false); - RegisterSystemHandlers(false); - Transport.activeTransport.enabled = true; AddTransportHandlers(); - connectState = ConnectState.Connecting; - Transport.activeTransport.ClientConnect(address); - + Transport.active.ClientConnect(address); connection = new NetworkConnectionToServer(); } /// Connect client to a NetworkServer by Uri. public static void Connect(Uri uri) { - // Debug.Log($"Client Connect: {uri}"); - Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); + Initialize(false); - RegisterSystemHandlers(false); - Transport.activeTransport.enabled = true; AddTransportHandlers(); - connectState = ConnectState.Connecting; - Transport.activeTransport.ClientConnect(uri); - + Transport.active.ClientConnect(uri); connection = new NetworkConnectionToServer(); } @@ -192,41 +220,9 @@ public static void Connect(Uri uri) // called from NetworkManager.FinishStartHost() public static void ConnectHost() { - //Debug.Log("Client Connect Host to Server"); - - RegisterSystemHandlers(true); - + Initialize(true); connectState = ConnectState.Connected; - - // create local connection objects and connect them - LocalConnectionToServer connectionToServer = new LocalConnectionToServer(); - LocalConnectionToClient connectionToClient = new LocalConnectionToClient(); - connectionToServer.connectionToClient = connectionToClient; - connectionToClient.connectionToServer = connectionToServer; - - connection = connectionToServer; - - // create server connection to local client - NetworkServer.SetLocalConnection(connectionToClient); - } - - /// Connect host mode - // called from NetworkManager.StartHostClient - // TODO why are there two connect host methods? - public static void ConnectLocalServer() - { - // call server OnConnected with server's connection to client - NetworkServer.OnConnected(NetworkServer.localConnection); - - // call client OnConnected with client's connection to server - // => previously we used to send a ConnectMessage to - // NetworkServer.localConnection. this would queue the message - // until NetworkClient.Update processes it. - // => invoking the client's OnConnected event directly here makes - // tests fail. so let's do it exactly the same order as before by - // queueing the event for next Update! - //OnConnectedEvent?.Invoke(connection); - ((LocalConnectionToServer)connection).QueueConnectedEvent(); + HostMode.SetupConnections(); } // disconnect ////////////////////////////////////////////////////////// @@ -265,13 +261,11 @@ static void OnTransportConnected() // reset network time stats NetworkTime.ResetStatics(); - // reset unbatcher in case any batches from last session remain. - unbatcher = new Unbatcher(); - // the handler may want to send messages to the client // thus we should set the connected state before calling the handler connectState = ConnectState.Connected; - NetworkTime.UpdateClient(); + // ping right away after connecting so client gets new time asap + NetworkTime.SendPing(); OnConnectedEvent?.Invoke(); } else Debug.LogError("Skipped Connect message handling because connection is null."); @@ -280,7 +274,7 @@ static void OnTransportConnected() // helper function static bool UnpackAndInvoke(NetworkReader reader, int channelId) { - if (MessagePacking.Unpack(reader, out ushort msgType)) + if (NetworkMessages.UnpackId(reader, out ushort msgType)) { // try to invoke the handler for that message if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) @@ -328,8 +322,14 @@ internal static void OnTransportData(ArraySegment data, int channelId) // always process all messages in the batch. if (!unbatcher.AddBatch(data)) { - Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting."); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: failed to add batch, disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkClient: failed to add batch."); + return; } @@ -344,38 +344,51 @@ internal static void OnTransportData(ArraySegment data, int channelId) // the next time. // => consider moving processing to NetworkEarlyUpdate. while (!isLoadingScene && - unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) + unbatcher.GetNextMessage(out ArraySegment message, out double remoteTimestamp)) { - // enough to read at least header size? - if (reader.Remaining >= MessagePacking.HeaderSize) + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message)) { - // make remoteTimeStamp available to the user - connection.remoteTimeStamp = remoteTimestamp; + // enough to read at least header size? + if (reader.Remaining >= NetworkMessages.IdSize) + { + // make remoteTimeStamp available to the user + connection.remoteTimeStamp = remoteTimestamp; - // handle message - if (!UnpackAndInvoke(reader, channelId)) + // handle message + if (!UnpackAndInvoke(reader, channelId)) + { + // warn, disconnect and return if failed + // -> warning because attackers might send random data + // -> messages in a batch aren't length prefixed. + // failing to read one would cause undefined + // behaviour for every message afterwards. + // so we need to disconnect. + // -> return to avoid the below unbatches.count error. + // we already disconnected and handled it. + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: failed to unpack and invoke message. Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkClient: failed to unpack and invoke message."); + + return; + } + } + // otherwise disconnect + else { - // warn, disconnect and return if failed - // -> warning because attackers might send random data - // -> messages in a batch aren't length prefixed. - // failing to read one would cause undefined - // behaviour for every message afterwards. - // so we need to disconnect. - // -> return to avoid the below unbatches.count error. - // we already disconnected and handled it. - Debug.LogWarning($"NetworkClient: failed to unpack and invoke message. Disconnecting."); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkClient: received Message was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning("NetworkClient: received Message was too short (messages should start with message id)"); return; } } - // otherwise disconnect - else - { - // WARNING, not error. can happen if attacker sends random data. - Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)"); - connection.Disconnect(); - return; - } } // if we weren't interrupted by a scene change, @@ -417,14 +430,23 @@ internal static void OnTransportDisconnected() // Raise the event before changing ConnectState // because 'active' depends on this during shutdown - if (connection != null) OnDisconnectedEvent?.Invoke(); + // + // previously OnDisconnected was only invoked if connection != null. + // however, if DNS resolve fails in Transport.Connect(), + // OnDisconnected would never be called because 'connection' is only + // created after the Transport.Connect() call. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3365 + OnDisconnectedEvent?.Invoke(); connectState = ConnectState.Disconnected; ready = false; + snapshots.Clear(); + localTimeline = 0; // now that everything was handled, clear the connection. // previously this was done in Disconnect() already, but we still // need it for the above OnDisconnectedEvent. + connection?.Cleanup(); connection = null; // transport handlers are only added when connecting. @@ -432,10 +454,21 @@ internal static void OnTransportDisconnected() RemoveTransportHandlers(); } - static void OnError(Exception exception) + // transport errors are forwarded to high level + static void OnTransportError(TransportError error, string reason) { - Debug.LogException(exception); - OnErrorEvent?.Invoke(exception); + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Client Transport Error: {error}: {reason}. This is fine."); + OnErrorEvent?.Invoke(error, reason); + } + + static void OnTransportException(Exception exception) + { + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Client Transport Exception: {exception}. This is fine."); + OnTransportExceptionEvent?.Invoke(exception); } // send //////////////////////////////////////////////////////////////// @@ -455,39 +488,118 @@ public static void Send(T message, int channelId = Channels.Reliable) } // message handlers //////////////////////////////////////////////////// + internal static void RegisterMessageHandlers(bool hostMode) + { + // host mode client / remote client react to some messages differently. + // but we still need to add handlers for all of them to avoid + // 'message id not found' errors. + if (hostMode) + { + // host mode doesn't need destroy messages (see NetworkServer::UnSpawnInternal) + RegisterHandler(_ => { }); + RegisterHandler(OnHostClientObjectHide); + RegisterHandler(_ => { }, false); + RegisterHandler(OnHostClientSpawn); + // host mode doesn't need spawning + RegisterHandler(_ => { }); + // host mode doesn't need spawning + RegisterHandler(_ => { }); + // host mode doesn't need state updates + RegisterHandler(_ => { }); + } + else + { + RegisterHandler(OnObjectDestroy); + RegisterHandler(OnObjectHide); + RegisterHandler(NetworkTime.OnClientPong, false); + RegisterHandler(NetworkTime.OnClientPing, false); + RegisterHandler(OnSpawn); + RegisterHandler(OnObjectSpawnStarted); + RegisterHandler(OnObjectSpawnFinished); + RegisterHandler(OnEntityStateMessage); + } + + // These handlers are the same for host and remote clients + RegisterHandler(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through + RegisterHandler(OnChangeOwner); + RegisterHandler(OnRPCMessage); + } + /// Register a handler for a message type T. Most should require authentication. public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessageId.Id; if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + // we use the same WrapHandler function for server and client. // so let's wrap it to ignore the NetworkConnection parameter. // it's not needed on client. it's always NetworkClient.connection. void HandlerWrapped(NetworkConnection _, T value) => handler(value); - handlers[msgType] = MessagePacking.WrapHandler((Action) HandlerWrapped, requireAuthentication); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); + } + + /// Register a handler for a message type T. Most should require authentication. + // This version passes channelId to the handler. + public static void RegisterHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = NetworkMessageId.Id; + if (handlers.ContainsKey(msgType)) + { + Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); + } + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + // we use the same WrapHandler function for server and client. + // so let's wrap it to ignore the NetworkConnection parameter. + // it's not needed on client. it's always NetworkClient.connection. + void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } /// Replace a handler for a particular message type. Should require authentication by default. // RegisterHandler throws a warning (as it should) if a handler is assigned twice // Use of ReplaceHandler makes it clear the user intended to replace the handler - public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + // we use the same WrapHandler function for server and client. + // so let's wrap it to ignore the NetworkConnection parameter. + // it's not needed on client. it's always NetworkClient.connection. + ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + void HandlerWrapped(NetworkConnection _, T value) => handler(value); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } - /// Replace a handler for a particular message type. Should require authentication by default. + /// Replace a handler for a particular message type. Should require authentication by default. This version passes channelId to the handler. // RegisterHandler throws a warning (as it should) if a handler is assigned twice // Use of ReplaceHandler makes it clear the user intended to replace the handler - public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication); + // we use the same WrapHandler function for server and client. + // so let's wrap it to ignore the NetworkConnection parameter. + // it's not needed on client. it's always NetworkClient.connection. + ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId); + handlers[msgType] = NetworkMessages.WrapHandler((Action)HandlerWrapped, requireAuthentication, exceptionsDisconnect); } /// Unregister a message handler of type T. @@ -495,24 +607,25 @@ public static bool UnregisterHandler() where T : struct, NetworkMessage { // use int to minimize collisions - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessageId.Id; return handlers.Remove(msgType); } // spawnable prefabs /////////////////////////////////////////////////// /// Find the registered prefab for this asset id. // Useful for debuggers - public static bool GetPrefab(Guid assetId, out GameObject prefab) + public static bool GetPrefab(uint assetId, out GameObject prefab) { prefab = null; - return assetId != Guid.Empty && - prefabs.TryGetValue(assetId, out prefab) && prefab != null; + return assetId != 0 && + prefabs.TryGetValue(assetId, out prefab) && + prefab != null; } /// Validates Prefab then adds it to prefabs dictionary. static void RegisterPrefabIdentity(NetworkIdentity prefab) { - if (prefab.assetId == Guid.Empty) + if (prefab.assetId == 0) { Debug.LogError($"Can not Register '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; @@ -524,6 +637,9 @@ static void RegisterPrefabIdentity(NetworkIdentity prefab) return; } + // disallow child NetworkIdentities. + // TODO likely not necessary anymore due to the new check in + // NetworkIdentity.OnValidate. NetworkIdentity[] identities = prefab.GetComponentsInChildren(); if (identities.Length > 1) { @@ -550,7 +666,7 @@ static void RegisterPrefabIdentity(NetworkIdentity prefab) // Note: newAssetId can not be set on GameObjects that already have an assetId // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. - public static void RegisterPrefab(GameObject prefab, Guid newAssetId) + public static void RegisterPrefab(GameObject prefab, uint newAssetId) { if (prefab == null) { @@ -558,20 +674,19 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId) return; } - if (newAssetId == Guid.Empty) + if (newAssetId == 0) { Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty"); return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); return; } - if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) + if (identity.assetId != 0 && identity.assetId != newAssetId) { Debug.LogError($"Could not register '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); return; @@ -591,8 +706,7 @@ public static void RegisterPrefab(GameObject prefab) return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); return; @@ -606,7 +720,7 @@ public static void RegisterPrefab(GameObject prefab) // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? - public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterPrefab(GameObject prefab, uint newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) @@ -628,8 +742,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); return; @@ -641,9 +754,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, return; } - Guid assetId = identity.assetId; - - if (assetId == Guid.Empty) + if (identity.assetId == 0) { Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; @@ -652,7 +763,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) { - Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); + Debug.LogError($"Can not Register null SpawnHandler for {identity.assetId}"); return; } @@ -664,9 +775,9 @@ public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? - public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterPrefab(GameObject prefab, uint newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { - if (newAssetId == Guid.Empty) + if (newAssetId == 0) { Debug.LogError($"Could not register handler for '{prefab.name}' with new assetId because the new assetId was empty"); return; @@ -678,14 +789,13 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandl return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); return; } - if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) + if (identity.assetId != 0 && identity.assetId != newAssetId) { Debug.LogError($"Could not register Handler for '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); return; @@ -698,7 +808,7 @@ public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandl } identity.assetId = newAssetId; - Guid assetId = identity.assetId; + uint assetId = identity.assetId; if (spawnHandler == null) { @@ -745,8 +855,7 @@ public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnH return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component"); return; @@ -758,9 +867,9 @@ public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnH return; } - Guid assetId = identity.assetId; + uint assetId = identity.assetId; - if (assetId == Guid.Empty) + if (assetId == 0) { Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; @@ -810,14 +919,13 @@ public static void UnregisterPrefab(GameObject prefab) return; } - NetworkIdentity identity = prefab.GetComponent(); - if (identity == null) + if (!prefab.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Could not unregister '{prefab.name}' since it contains no NetworkIdentity component"); return; } - Guid assetId = identity.assetId; + uint assetId = identity.assetId; prefabs.Remove(assetId); spawnHandlers.Remove(assetId); @@ -831,7 +939,7 @@ public static void UnregisterPrefab(GameObject prefab) // prefab. This should be used when no prefab exists for the spawned // objects - such as when they are constructed dynamically at runtime // from configuration data. - public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterSpawnHandler(uint assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) @@ -849,7 +957,7 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler // prefab. This should be used when no prefab exists for the spawned // objects - such as when they are constructed dynamically at runtime // from configuration data. - public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + public static void RegisterSpawnHandler(uint assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { if (spawnHandler == null) { @@ -863,9 +971,9 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawn return; } - if (assetId == Guid.Empty) + if (assetId == 0) { - Debug.LogError("Can not Register SpawnHandler for empty Guid"); + Debug.LogError("Can not Register SpawnHandler for empty assetId"); return; } @@ -887,7 +995,7 @@ public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawn } /// Removes a registered spawn handler function that was registered with NetworkClient.RegisterHandler(). - public static void UnregisterSpawnHandler(Guid assetId) + public static void UnregisterSpawnHandler(uint assetId) { spawnHandlers.Remove(assetId); unspawnHandlers.Remove(assetId); @@ -901,7 +1009,7 @@ public static void ClearSpawners() unspawnHandlers.Clear(); } - internal static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj) + internal static bool InvokeUnSpawnHandler(uint assetId, GameObject obj) { if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null) { @@ -964,7 +1072,7 @@ internal static void InternalAddPlayer(NetworkIdentity identity) { connection.identity = identity; } - else Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer"); + else Debug.LogWarning("NetworkClient can't AddPlayer before being ready. Please call NetworkClient.Ready() first. Clients are considered ready after joining the game world."); } /// Sends AddPlayer message to the server, indicating that we want to join the world. @@ -999,36 +1107,51 @@ public static bool AddPlayer() // spawning //////////////////////////////////////////////////////////// internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message) { - if (message.assetId != Guid.Empty) + // add to spawned first because DeserializeClient may need it for SyncVars + spawned[message.netId] = identity; + + if (message.assetId != 0) identity.assetId = message.assetId; if (!identity.gameObject.activeSelf) - { identity.gameObject.SetActive(true); - } // apply local values for VR support identity.transform.localPosition = message.position; identity.transform.localRotation = message.rotation; identity.transform.localScale = message.scale; - identity.hasAuthority = message.isOwner; + + // configure flags + // the below DeserializeClient call invokes SyncVarHooks. + // flags always need to be initialized before that. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3259 identity.netId = message.netId; + identity.isOwned = message.isOwner; + + if (identity.isOwned) + connection?.owned.Add(identity); if (message.isLocalPlayer) InternalAddPlayer(identity); + // configure isClient/isLocalPlayer flags. + // => after InternalAddPlayer. can't initialize .isLocalPlayer + // before InternalAddPlayer sets .localPlayer + // => before DeserializeClient, otherwise SyncVar hooks wouldn't + // have isClient/isLocalPlayer set yet. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3259 + InitializeIdentityFlags(identity); + // deserialize components if any payload // (Count is 0 if there were no components) if (message.payload.Count > 0) { using (NetworkReaderPooled payloadReader = NetworkReaderPool.Get(message.payload)) { - identity.OnDeserializeAllSafely(payloadReader, true); + identity.DeserializeClient(payloadReader, true); } } - spawned[message.netId] = identity; - // the initial spawn with OnObjectSpawnStarted/Finished calls all // object's OnStartClient/OnStartLocalPlayer after they were all // spawned. @@ -1037,9 +1160,7 @@ internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage me // here immediately since there won't be another OnObjectSpawnFinished. if (isSpawnFinished) { - identity.NotifyAuthority(); - identity.OnStartClient(); - CheckForLocalPlayer(identity); + InvokeIdentityCallbacks(identity); } } @@ -1055,7 +1176,7 @@ internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity return true; } - if (message.assetId == Guid.Empty && message.sceneId == 0) + if (message.assetId == 0 && message.sceneId == 0) { Debug.LogError($"OnSpawn message with netId '{message.netId}' has no AssetId or sceneId"); return false; @@ -1074,8 +1195,8 @@ internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity static NetworkIdentity GetExistingObject(uint netid) { - spawned.TryGetValue(netid, out NetworkIdentity localObject); - return localObject; + spawned.TryGetValue(netid, out NetworkIdentity identity); + return identity; } static NetworkIdentity SpawnPrefab(SpawnMessage message) @@ -1095,12 +1216,13 @@ static NetworkIdentity SpawnPrefab(SpawnMessage message) Debug.LogError($"Spawn Handler returned null, Handler assetId '{message.assetId}'"); return null; } - NetworkIdentity identity = obj.GetComponent(); - if (identity == null) + + if (!obj.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"Object Spawned by handler did not have a NetworkIdentity, Handler assetId '{message.assetId}'"); return null; } + return identity; } @@ -1141,16 +1263,6 @@ static NetworkIdentity GetAndRemoveSceneObject(ulong sceneId) return null; } - // Checks if identity is not spawned yet, not hidden and has sceneId - static bool ConsiderForSpawning(NetworkIdentity identity) - { - // not spawned yet, not hidden, etc.? - return !identity.gameObject.activeSelf && - identity.gameObject.hideFlags != HideFlags.NotEditable && - identity.gameObject.hideFlags != HideFlags.HideAndDontSave && - identity.sceneId != 0; - } - /// Call this after loading/unloading a scene in the client after connection to register the spawnable objects public static void PrepareToSpawnSceneObjects() { @@ -1162,9 +1274,24 @@ public static void PrepareToSpawnSceneObjects() foreach (NetworkIdentity identity in allIdentities) { // add all unspawned NetworkIdentities to spawnable objects - if (ConsiderForSpawning(identity)) + // need to check netId to make sure object is not spawned + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3541 + // PrepareToSpawnSceneObjects may be called multiple times in case + // the ObjectSpawnStarted message is received multiple times. + if (Utils.IsSceneObject(identity) && + identity.netId == 0) { - spawnableObjects.Add(identity.sceneId, identity); + if (spawnableObjects.TryGetValue(identity.sceneId, out NetworkIdentity existingIdentity)) + { + string msg = $"NetworkClient: Duplicate sceneId {identity.sceneId} detected on {identity.gameObject.name} and {existingIdentity.gameObject.name}\n" + + $"This can happen if a networked object is persisted in DontDestroyOnLoad through loading / changing to the scene where it originated,\n" + + $"otherwise you may need to open and re-save the {identity.gameObject.scene} to reset scene id's."; + Debug.LogWarning(msg, identity.gameObject); + } + else + { + spawnableObjects.Add(identity.sceneId, identity); + } } } } @@ -1173,65 +1300,54 @@ internal static void OnObjectSpawnStarted(ObjectSpawnStartedMessage _) { // Debug.Log("SpawnStarted"); PrepareToSpawnSceneObjects(); + pendingSpawns.Clear(); isSpawnFinished = false; } + static readonly Dictionary pendingSpawns = new Dictionary(); + internal static void OnObjectSpawnFinished(ObjectSpawnFinishedMessage _) { - //Debug.Log("SpawnFinished"); - ClearNullFromSpawned(); - // paul: Initialize the objects in the same order as they were // initialized in the server. This is important if spawned objects // use data from scene objects foreach (NetworkIdentity identity in spawned.Values.OrderBy(uv => uv.netId)) { - identity.NotifyAuthority(); - identity.OnStartClient(); - CheckForLocalPlayer(identity); - } - isSpawnFinished = true; - } - - static readonly List removeFromSpawned = new List(); - static void ClearNullFromSpawned() - { - // spawned has null objects after changing scenes on client using - // NetworkManager.ServerChangeScene remove them here so that 2nd - // loop below does not get NullReferenceException - // see https://github.com/vis2k/Mirror/pull/2240 - // TODO fix scene logic so that client scene doesn't have null objects - foreach (KeyValuePair kvp in spawned) - { - if (kvp.Value == null) + // NetworkIdentities should always be removed from .spawned when + // they are destroyed. for safety, let's double check here. + if (identity != null) { - removeFromSpawned.Add(kvp.Key); + // We may have deferred ApplySpawnPayload in OnSpawn + // to avoid cross-reference race conditions with SyncVars. + // Apply payload before invoking OnStartClient etc. callbacks + // so that all data is there when they are invoked. + // Note that Interest Management may not have updated spawned + // dictionary yet, so not all identities may be in pendingSpawns. + // Generally that's user error in their code, so we don't throw + // a warning here, but keep the warning code for debugging if needed. + if (pendingSpawns.TryGetValue(identity, out SpawnMessage message)) + ApplySpawnPayload(identity, message); + //else + // Debug.LogWarning($"Expected pendingSpawns to contain {identity}: {identity.netId} but didn't"); + + BootstrapIdentity(identity); } + else Debug.LogWarning("Found null entry in NetworkClient.spawned. This is unexpected. Was the NetworkIdentity not destroyed properly?"); } - // can't modify NetworkIdentity.spawned inside foreach so need 2nd loop to remove - foreach (uint id in removeFromSpawned) - { - spawned.Remove(id); - } - removeFromSpawned.Clear(); + pendingSpawns.Clear(); + isSpawnFinished = true; } // host mode callbacks ///////////////////////////////////////////////// - static void OnHostClientObjectDestroy(ObjectDestroyMessage message) - { - //Debug.Log($"NetworkClient.OnLocalObjectObjDestroy netId:{message.netId}"); - spawned.Remove(message.netId); - } - static void OnHostClientObjectHide(ObjectHideMessage message) { //Debug.Log($"ClientScene::OnLocalObjectObjHide netId:{message.netId}"); - if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && - localObject != null) + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && + identity != null) { if (aoi != null) - aoi.SetHostVisibility(localObject, false); + aoi.SetHostVisibility(identity, false); } } @@ -1239,45 +1355,86 @@ internal static void OnHostClientSpawn(SpawnMessage message) { // on host mode, the object already exist in NetworkServer.spawned. // simply add it to NetworkClient.spawned too. - if (NetworkServer.spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) + if (NetworkServer.spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) { - spawned[message.netId] = localObject; + spawned[message.netId] = identity; + if (message.isOwner) connection.owned.Add(identity); // now do the actual 'spawning' on host mode if (message.isLocalPlayer) - InternalAddPlayer(localObject); - - localObject.hasAuthority = message.isOwner; - localObject.NotifyAuthority(); - localObject.OnStartClient(); + InternalAddPlayer(identity); + // set visibility before invoking OnStartClient etc. callbacks if (aoi != null) - aoi.SetHostVisibility(localObject, true); + aoi.SetHostVisibility(identity, true); - CheckForLocalPlayer(localObject); + identity.isOwned = message.isOwner; + BootstrapIdentity(identity); } } + // configure flags & invoke callbacks + static void BootstrapIdentity(NetworkIdentity identity) + { + InitializeIdentityFlags(identity); + InvokeIdentityCallbacks(identity); + } + + // set up NetworkIdentity flags on the client. + // needs to be separate from invoking callbacks. + // cleaner, and some places need to set flags first. + static void InitializeIdentityFlags(NetworkIdentity identity) + { + // initialize flags before invoking callbacks. + // this way isClient/isLocalPlayer is correct during callbacks. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3362 + identity.isClient = true; + identity.isLocalPlayer = localPlayer == identity; + + // .connectionToServer is only available for local players. + // set it here, before invoking any callbacks. + // this way it's available in _all_ callbacks. + if (identity.isLocalPlayer) + identity.connectionToServer = connection; + } + + // invoke NetworkIdentity callbacks on the client. + // needs to be separate from configuring flags. + // cleaner, and some places need to set flags first. + static void InvokeIdentityCallbacks(NetworkIdentity identity) + { + // invoke OnStartClient + identity.OnStartClient(); + + // invoke OnStartAuthority + identity.NotifyAuthority(); + + // invoke OnStartLocalPlayer + if (identity.isLocalPlayer) + identity.OnStartLocalPlayer(); + } + // client-only mode callbacks ////////////////////////////////////////// static void OnEntityStateMessage(EntityStateMessage message) { // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); - if (spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) { - using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload)) - localObject.OnDeserializeAllSafely(networkReader, false); + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + identity.DeserializeClient(reader, false); } - 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."); + 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:{msg.functionHash} netId:{msg.netId}"); + // Debug.Log($"NetworkClient.OnRPCMessage hash:{message.functionHash} netId:{message.netId}"); if (spawned.TryGetValue(message.netId, out NetworkIdentity identity)) { - using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(message.payload)) - identity.HandleRemoteCall(message.componentIndex, message.functionHash, RemoteCallType.ClientRpc, networkReader); + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + identity.HandleRemoteCall(message.componentIndex, message.functionHash, RemoteCallType.ClientRpc, reader); } + // Rpcs often can't be applied if interest management unspawned them } static void OnObjectHide(ObjectHideMessage message) => DestroyObject(message.netId); @@ -1289,7 +1446,42 @@ internal static void OnSpawn(SpawnMessage message) // Debug.Log($"Client spawn handler instantiating netId={msg.netId} assetID={msg.assetId} sceneId={msg.sceneId:X} pos={msg.position}"); if (FindOrSpawnObject(message, out NetworkIdentity identity)) { - ApplySpawnPayload(identity, message); + if (isSpawnFinished) + { + ApplySpawnPayload(identity, message); + } + else + { + // 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. + 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] = messageCopy; + } } } @@ -1315,7 +1507,15 @@ internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage me } // set ownership flag (aka authority) - identity.hasAuthority = message.isOwner; + identity.isOwned = message.isOwner; + + // Add / Remove to client's connectionToServer.owned hashset. + if (identity.isOwned) + connection?.owned.Add(identity); + else + connection?.owned.Remove(identity); + + // Call OnStartAuthority / OnStopAuthority identity.NotifyAuthority(); // set localPlayer flag @@ -1325,66 +1525,17 @@ internal static void ChangeOwner(NetworkIdentity identity, ChangeOwnerMessage me if (identity.isLocalPlayer) { localPlayer = identity; + identity.connectionToServer = connection; + identity.OnStartLocalPlayer(); } // identity's isLocalPlayer was set to false. // clear our static localPlayer IF (and only IF) it was that one before. else if (localPlayer == identity) { localPlayer = null; + // TODO set .connectionToServer to null for old local player? + // since we set it in the above 'if' case too. } - - // call OnStartLocalPlayer if it's the local player now. - CheckForLocalPlayer(identity); - } - - internal static void CheckForLocalPlayer(NetworkIdentity identity) - { - if (identity == localPlayer) - { - // Set isLocalPlayer to true on this NetworkIdentity and trigger - // OnStartLocalPlayer in all scripts on the same GO - identity.connectionToServer = connection; - identity.OnStartLocalPlayer(); - // Debug.Log($"NetworkClient.OnOwnerMessage player:{identity.name}"); - } - } - - // destroy ///////////////////////////////////////////////////////////// - static void DestroyObject(uint netId) - { - // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}"); - if (spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null) - { - if (localObject.isLocalPlayer) - localObject.OnStopLocalPlayer(); - - localObject.OnStopClient(); - - // custom unspawn handler for this prefab? (for prefab pools etc.) - if (InvokeUnSpawnHandler(localObject.assetId, localObject.gameObject)) - { - // reset object after user's handler - localObject.Reset(); - } - // otherwise fall back to default Destroy - else if (localObject.sceneId == 0) - { - // don't call reset before destroy so that values are still set in OnDestroy - GameObject.Destroy(localObject.gameObject); - } - // scene object.. disable it in scene instead of destroying - else - { - localObject.gameObject.SetActive(false); - spawnableObjects[localObject.sceneId] = localObject; - // reset for scene objects - localObject.Reset(); - } - - // remove from dictionary no matter how it is unspawned - spawned.Remove(netId); - } - //else Debug.LogWarning($"Did not find target for destroy message for {netId}"); } // update ////////////////////////////////////////////////////////////// @@ -1393,14 +1544,75 @@ static void DestroyObject(uint netId) internal static void NetworkEarlyUpdate() { // process all incoming messages first before updating the world - if (Transport.activeTransport != null) - Transport.activeTransport.ClientEarlyUpdate(); + if (Transport.active != null) + Transport.active.ClientEarlyUpdate(); + + // time snapshot interpolation + UpdateTimeInterpolation(); } // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkLateUpdate() { + // broadcast ClientToServer components while active + if (active) + { + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + // + // Unity 2019 doesn't have Time.timeAsDouble yet + bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + if (!Application.isPlaying || sendIntervalElapsed) + { + Broadcast(); + } + + UpdateConnectionQuality(); + } + + // Connection Quality ////////////////////////////////////////////////// + // uses 'pragmatic' version based on snapshot interpolation by default. + void UpdateConnectionQuality() + { + // only recalculate every few seconds + // we don't want to fire Good->Bad->Good->Bad dozens of times per second. + if (connectionQualityInterval > 0 && NetworkTime.time > lastConnectionQualityUpdate + connectionQualityInterval) + { + lastConnectionQualityUpdate = NetworkTime.time; + + switch (connectionQualityMethod) + { + case ConnectionQualityMethod.Simple: + connectionQuality = ConnectionQualityHeuristics.Simple(NetworkTime.rtt, NetworkTime.rttVariance); + break; + case ConnectionQualityMethod.Pragmatic: + connectionQuality = ConnectionQualityHeuristics.Pragmatic(initialBufferTime, bufferTime); + break; + } + + if (lastConnectionQuality != connectionQuality) + { + // Invoke the event before assigning the new value so + // the event handler can compare old and new values. + onConnectionQualityChanged?.Invoke(lastConnectionQuality, connectionQuality); + lastConnectionQuality = connectionQuality; + } + } + } + + // update connections to flush out messages _after_ broadcast // local connection? if (connection is LocalConnectionToServer localConnection) { @@ -1421,11 +1633,66 @@ internal static void NetworkLateUpdate() } // process all outgoing messages after updating the world - if (Transport.activeTransport != null) - Transport.activeTransport.ClientLateUpdate(); + if (Transport.active != null) + Transport.active.ClientLateUpdate(); } - // shutdown //////////////////////////////////////////////////////////// + // broadcast /////////////////////////////////////////////////////////// + // make sure Broadcast() is only called every sendInterval. + // calling it every update() would require too much bandwidth. + static void Broadcast() + { + // joined the world yet? + if (!connection.isReady) return; + + // nothing to do in host mode. server already knows the state. + if (NetworkServer.active) return; + + // send time snapshot every sendInterval. + Send(new TimeSnapshotMessage(), Channels.Unreliable); + + // broadcast client state to server + BroadcastToServer(); + } + + // NetworkServer has BroadcastToConnection. + // NetworkClient has BroadcastToServer. + static void BroadcastToServer() + { + // for each entity that the client owns + foreach (NetworkIdentity identity in connection.owned) + { + // make sure it's not null or destroyed. + // (which can happen if someone uses + // GameObject.Destroy instead of + // NetworkServer.Destroy) + if (identity != null) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // get serialization for this entity viewed by this connection + // (if anything was serialized this time) + identity.SerializeClient(writer); + if (writer.Position > 0) + { + // send state update message + EntityStateMessage message = new EntityStateMessage + { + netId = identity.netId, + payload = writer.ToArraySegment() + }; + Send(message); + } + } + } + // spawned list should have no null entries because we + // always call Remove in OnObjectDestroy everywhere. + // if it does have null then we missed something. + else Debug.LogWarning($"Found 'null' entry in owned list for client. This is unexpected behaviour."); + } + } + + // destroy ///////////////////////////////////////////////////////////// /// Destroys all networked objects on the client. // Note: NetworkServer.CleanupNetworkIdentities does the same on server. public static void DestroyAllClientObjects() @@ -1460,7 +1727,7 @@ public static void DestroyAllClientObjects() // unspawned objects should be reset for reuse later. if (wasUnspawned) { - identity.Reset(); + identity.ResetState(); } // without unspawn handler, we need to disable/destroy. else @@ -1469,7 +1736,7 @@ public static void DestroyAllClientObjects() // they always stay in the scene, we don't destroy them. if (identity.sceneId != 0) { - identity.Reset(); + identity.ResetState(); identity.gameObject.SetActive(false); } // spawned objects are destroyed @@ -1482,6 +1749,7 @@ public static void DestroyAllClientObjects() } } spawned.Clear(); + connection?.owned.Clear(); } catch (InvalidOperationException e) { @@ -1490,6 +1758,45 @@ public static void DestroyAllClientObjects() } } + static void DestroyObject(uint netId) + { + // Debug.Log($"NetworkClient.OnObjDestroy netId: {netId}"); + if (spawned.TryGetValue(netId, out NetworkIdentity identity) && identity != null) + { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + + identity.OnStopClient(); + + // custom unspawn handler for this prefab? (for prefab pools etc.) + if (InvokeUnSpawnHandler(identity.assetId, identity.gameObject)) + { + // reset object after user's handler + identity.ResetState(); + } + // otherwise fall back to default Destroy + else if (identity.sceneId == 0) + { + // don't call reset before destroy so that values are still set in OnDestroy + GameObject.Destroy(identity.gameObject); + } + // scene object.. disable it in scene instead of destroying + else + { + identity.gameObject.SetActive(false); + spawnableObjects[identity.sceneId] = identity; + // reset for scene objects + identity.ResetState(); + } + + // remove from dictionary no matter how it is unspawned + connection.owned.Remove(identity); // if any + spawned.Remove(netId); + } + //else Debug.LogWarning($"Did not find target for destroy message for {netId}"); + } + + // shutdown //////////////////////////////////////////////////////////// /// Shutdown the client. // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] @@ -1497,15 +1804,17 @@ public static void Shutdown() { //Debug.Log("Shutting down client."); + // objects need to be destroyed before spawners are cleared + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3334 + DestroyAllClientObjects(); + // calls prefabs.Clear(); // calls spawnHandlers.Clear(); // calls unspawnHandlers.Clear(); ClearSpawners(); - // calls spawned.Clear() if no exception occurs - DestroyAllClientObjects(); - spawned.Clear(); + connection?.owned.Clear(); handlers.Clear(); spawnableObjects.Clear(); @@ -1521,8 +1830,8 @@ public static void Shutdown() // we do NOT call Transport.Shutdown, because someone only called // NetworkClient.Shutdown. we can't assume that the server is // supposed to be shut down too! - if (Transport.activeTransport != null) - Transport.activeTransport.ClientDisconnect(); + if (Transport.active != null) + Transport.active.ClientDisconnect(); // reset statics connectState = ConnectState.None; @@ -1531,6 +1840,7 @@ public static void Shutdown() ready = false; isSpawnFinished = false; isLoadingScene = false; + lastSendTime = 0; unbatcher = new Unbatcher(); @@ -1539,6 +1849,37 @@ public static void Shutdown() OnConnectedEvent = null; OnDisconnectedEvent = null; OnErrorEvent = null; + OnTransportExceptionEvent = null; + } + + // GUI ///////////////////////////////////////////////////////////////// + // called from NetworkManager to display timeline interpolation status. + // useful to indicate catchup / slowdown / dynamic adjustment etc. + public static void OnGUI() + { + // only if in world + if (!ready) return; + + GUILayout.BeginArea(new Rect(10, 5, 1020, 50)); + + GUILayout.BeginHorizontal("Box"); + GUILayout.Label("Snapshot Interp.:"); + // color while catching up / slowing down + if (localTimescale > 1) GUI.color = Color.green; // green traffic light = go fast + else if (localTimescale < 1) GUI.color = Color.red; // red traffic light = go slow + else GUI.color = Color.white; + GUILayout.Box($"timeline: {localTimeline:F2}"); + GUILayout.Box($"buffer: {snapshots.Count}"); + GUILayout.Box($"DriftEMA: {NetworkClient.driftEma.Value:F2}"); + GUILayout.Box($"DelTimeEMA: {NetworkClient.deliveryTimeEma.Value:F2}"); + GUILayout.Box($"timescale: {localTimescale:F2}"); + GUILayout.Box($"BTM: {NetworkClient.bufferTimeMultiplier:F2}"); // current dynamically adjusted multiplier + GUILayout.Box($"RTT: {NetworkTime.rtt * 1000:F0}ms"); + GUILayout.Box($"PredErrUNADJ: {NetworkTime.predictionErrorUnadjusted * 1000:F0}ms"); + GUILayout.Box($"PredErrADJ: {NetworkTime.predictionErrorAdjusted * 1000:F0}ms"); + GUILayout.EndHorizontal(); + + GUILayout.EndArea(); } } } diff --git a/Assets/Mirror/Runtime/NetworkClient.cs.meta b/Assets/Mirror/Core/NetworkClient.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkClient.cs.meta rename to Assets/Mirror/Core/NetworkClient.cs.meta index 20cb211..33080e4 100644 --- a/Assets/Mirror/Runtime/NetworkClient.cs.meta +++ b/Assets/Mirror/Core/NetworkClient.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs new file mode 100644 index 0000000..2578034 --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + public static partial class NetworkClient + { + // snapshot interpolation settings ///////////////////////////////////// + // TODO expose the settings to the user later. + // via NetMan or NetworkClientConfig or NetworkClient as component etc. + public static SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings(); + + // snapshot interpolation runtime data ///////////////////////////////// + // buffer time is dynamically adjusted. + // store the current multiplier here, without touching the original in settings. + // this way we can easily reset to or compare with original where needed. + public static double bufferTimeMultiplier; + + // original buffer time based on the settings + // dynamically adjusted buffer time based on dynamically adjusted multiplier + public static double initialBufferTime => NetworkServer.sendInterval * snapshotSettings.bufferTimeMultiplier; + public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + + // + public static SortedList snapshots = new SortedList(); + + // for smooth interpolation, we need to interpolate along server time. + // any other time (arrival on client, client local time, etc.) is not + // going to give smooth results. + // in other words, this is the remote server's time, but adjusted. + // + // internal for use from NetworkTime. + // double for long running servers, see NetworkTime comments. + internal static double localTimeline; + + // catchup / slowdown adjustments are applied to timescale, + // to be adjusted in every update instead of when receiving messages. + internal static double localTimescale = 1; + + // catchup ///////////////////////////////////////////////////////////// + // we use EMA to average the last second worth of snapshot time diffs. + // manually averaging the last second worth of values with a for loop + // would be the same, but a moving average is faster because we only + // ever add one value. + static ExponentialMovingAverage driftEma; + + // dynamic buffer time adjustment ////////////////////////////////////// + // DEPRECATED 2024-10-08 + [Obsolete("NeworkClient.dynamicAdjustment was moved to NetworkClient.snapshotSettings.dynamicAdjustment")] + public static bool dynamicAdjustment => snapshotSettings.dynamicAdjustment; + // DEPRECATED 2024-10-08 + [Obsolete("NeworkClient.dynamicAdjustmentTolerance was moved to NetworkClient.snapshotSettings.dynamicAdjustmentTolerance")] + public static float dynamicAdjustmentTolerance => snapshotSettings.dynamicAdjustmentTolerance; + // DEPRECATED 2024-10-08 + [Obsolete("NeworkClient.dynamicAdjustment was moved to NetworkClient.snapshotSettings.dynamicAdjustment")] + public static int deliveryTimeEmaDuration => snapshotSettings.deliveryTimeEmaDuration; + + static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + + // OnValidate: see NetworkClient.cs + // add snapshot & initialize client interpolation time if needed + + // initialization called from Awake + static void InitTimeInterpolation() + { + // reset timeline, localTimescale & snapshots from last session (if any) + bufferTimeMultiplier = snapshotSettings.bufferTimeMultiplier; + localTimeline = 0; + localTimescale = 1; + snapshots.Clear(); + + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.deliveryTimeEmaDuration); + } + + // server sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. + + // Unity 2019 doesn't have Time.timeAsDouble yet + OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); + } + + // see comments at the top of this file + public static void OnTimeSnapshot(TimeSnapshot snap) + { + // Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}"); + + // (optional) dynamic adjustment + if (snapshotSettings.dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + snapshotSettings.dynamicAdjustmentTolerance + ); + } + + // insert into the buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + snapshotSettings.bufferLimit, + snap, + ref localTimeline, + ref localTimescale, + NetworkServer.sendInterval, + bufferTime, + snapshotSettings.catchupSpeed, + snapshotSettings.slowdownSpeed, + ref driftEma, + snapshotSettings.catchupNegativeThreshold, + snapshotSettings.catchupPositiveThreshold, + ref deliveryTimeEma); + + // Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}"); + } + + // call this from early update, so the timeline is safe to use in update + static void UpdateTimeInterpolation() + { + // only while we have snapshots. + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + // NetworkTime uses unscaled time and ignores Time.timeScale. + // fixes Time.timeScale getting server & client time out of sync: + // https://github.com/MirrorNetworking/Mirror/issues/3409 + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale); + + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } + } +} diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta new file mode 100644 index 0000000..4e836ff --- /dev/null +++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ad039071a9cc487b9f7831d28bbe8e83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs b/Assets/Mirror/Core/NetworkConnection.cs similarity index 68% rename from Assets/Mirror/Runtime/NetworkConnection.cs rename to Assets/Mirror/Core/NetworkConnection.cs index 14729c6..1268ca2 100644 --- a/Assets/Mirror/Runtime/NetworkConnection.cs +++ b/Assets/Mirror/Core/NetworkConnection.cs @@ -10,16 +10,6 @@ public abstract class NetworkConnection { public const int LocalConnectionId = 0; - /// NetworkIdentities that this connection can see - // DEPRECATED 2022-02-05 - [Obsolete("Cast to NetworkConnectionToClient to access .observing")] - public HashSet observing => ((NetworkConnectionToClient)this).observing; - - /// 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; @@ -32,9 +22,6 @@ public abstract class NetworkConnection // state. public bool isReady; - /// IP address of the connection. Can be useful for game master IP bans etc. - public abstract string address { get; } - /// Last time a message was received for this connection. Includes system and user messages. public float lastMessageTime; @@ -42,13 +29,12 @@ public abstract class NetworkConnection public NetworkIdentity identity { get; internal set; } /// All NetworkIdentities owned by this connection. Can be main player, pets, etc. + // .owned is now valid both on server and on client. // IMPORTANT: this needs to be , not . // fixes a bug where DestroyOwnedObjects wouldn't find the // netId anymore: https://github.com/vis2k/Mirror/issues/1380 // Works fine with NetworkIdentity pointers though. - // DEPRECATED 2022-02-05 - [Obsolete("Cast to NetworkConnectionToClient to access .clientOwnedObjects")] - public HashSet clientOwnedObjects => ((NetworkConnectionToClient)this).clientOwnedObjects; + public readonly HashSet owned = new HashSet(); // batching from server to client & client to server. // fewer transport calls give us significantly better performance/scale. @@ -77,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) @@ -91,7 +72,7 @@ protected Batcher GetBatchForChannelId(int channelId) if (!batches.TryGetValue(channelId, out batch)) { // get max batch size for this channel - int threshold = Transport.activeTransport.GetBatchThreshold(channelId); + int threshold = Transport.active.GetBatchThreshold(channelId); // create batcher batch = new Batcher(threshold); @@ -100,41 +81,29 @@ protected Batcher GetBatchForChannelId(int channelId) return batch; } - // validate packet size before sending. show errors if too big/small. - // => it's best to check this here, we can't assume that all transports - // would check max size and show errors internally. best to do it - // in one place in Mirror. - // => it's important to log errors, so the user knows what went wrong. - protected static bool ValidatePacketSize(ArraySegment segment, int channelId) - { - int max = Transport.activeTransport.GetMaxPacketSize(channelId); - if (segment.Count > max) - { - Debug.LogError($"NetworkConnection.ValidatePacketSize: cannot send packet larger than {max} bytes, was {segment.Count} bytes"); - return false; - } - - if (segment.Count == 0) - { - // zero length packets getting into the packet queues are bad. - Debug.LogError("NetworkConnection.ValidatePacketSize: cannot send zero bytes"); - return false; - } - - // good size - return true; - } - // Send stage one: NetworkMessage /// Send a NetworkMessage to this connection over the given channel. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Send(T message, int channelId = Channels.Reliable) where T : struct, NetworkMessage { using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { - // pack message and send allocation free - MessagePacking.Pack(message, writer); + // pack message + NetworkMessages.Pack(message, writer); + + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkConnection.Send: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + + // send allocation free NetworkDiagnostics.OnSend(message, channelId, writer.Position, 1); Send(writer.ToArraySegment(), channelId); } @@ -143,6 +112,7 @@ public void Send(T message, int channelId = Channels.Reliable) // Send stage two: serialized NetworkMessage as ArraySegment // internal because no one except Mirror should send bytes directly to // the client. they would be detected as a message. send messages instead. + // => make sure to validate message size before calling Send! [MethodImpl(MethodImplOptions.AggressiveInlining)] internal virtual void Send(ArraySegment segment, int channelId = Channels.Reliable) { @@ -168,38 +138,32 @@ internal virtual void Send(ArraySegment segment, int channelId = Channels. } // Send stage three: hand off to transport - [MethodImpl(MethodImplOptions.AggressiveInlining)] protected abstract void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable); // flush batched messages at the end of every Update. internal virtual void Update() { // go through batches for all channels + // foreach ((int key, Batcher batcher) in batches) // Unity 2020 doesn't support deconstruct yet foreach (KeyValuePair kvp in batches) { // make and send as many batches as necessary from the stored // messages. - Batcher batcher = kvp.Value; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // make a batch with our local time (double precision) - while (batcher.GetBatch(writer)) + while (kvp.Value.GetBatch(writer)) { - // validate packet before handing the batch to the - // transport. this guarantees that we always stay - // within transport's max message size limit. - // => just in case transport forgets to check it - // => just in case mirror miscalulated it etc. + // message size is validated in Send, with test coverage. + // we can send directly without checking again. ArraySegment segment = writer.ToArraySegment(); - if (ValidatePacketSize(segment, kvp.Key)) - { - // send to transport - SendToTransport(segment, kvp.Key); - //UnityEngine.Debug.Log($"sending batch of {writer.Position} bytes for channel={kvp.Key} connId={connectionId}"); - - // reset writer for each new batch - writer.Position = 0; - } + + // send to transport + SendToTransport(segment, kvp.Key); + //UnityEngine.Debug.Log($"sending batch of {writer.Position} bytes for channel={kvp.Key} connId={connectionId}"); + + // reset writer for each new batch + writer.Position = 0; } } } @@ -228,6 +192,16 @@ internal virtual void Update() // then later the transport events will do the clean up. public abstract void Disconnect(); - public override string ToString() => $"connection({connectionId})"; + // cleanup is called before the connection is removed. + // return any batches' pooled writers before the connection disappears. + // otherwise if a connection disappears before flushing, writers would + // never be returned to the pool. + public virtual void Cleanup() + { + foreach (Batcher batcher in batches.Values) + { + batcher.Clear(); + } + } } } diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs.meta b/Assets/Mirror/Core/NetworkConnection.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkConnection.cs.meta rename to Assets/Mirror/Core/NetworkConnection.cs.meta index 32c4ba2..9516827 100644 --- a/Assets/Mirror/Runtime/NetworkConnection.cs.meta +++ b/Assets/Mirror/Core/NetworkConnection.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkConnection.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkConnectionToClient.cs b/Assets/Mirror/Core/NetworkConnectionToClient.cs new file mode 100644 index 0000000..d80749c --- /dev/null +++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public class NetworkConnectionToClient : NetworkConnection + { + // rpcs are collected in a buffer, and then flushed out together. + // this way we don't need one NetworkMessage per rpc. + // => prepares for LocalWorldState as well. + // ensure max size when adding! + readonly NetworkWriter reliableRpcs = new NetworkWriter(); + readonly NetworkWriter unreliableRpcs = new NetworkWriter(); + + 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(); + + // unbatcher + public Unbatcher unbatcher = new Unbatcher(); + + // server runs a time snapshot interpolation for each client's local time. + // this is necessary for client auth movement to still be smooth on the + // server for host mode. + // TODO move them along server's timeline in the future. + // perhaps with an offset. + // for now, keep compatibility by manually constructing a timeline. + ExponentialMovingAverage driftEma; + ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + public double remoteTimeline; + public double remoteTimescale; + double bufferTimeMultiplier = 2; + double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + + // + readonly SortedList snapshots = new SortedList(); + + // Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients. + public int snapshotBufferSizeLimit = 64; + + // ping for rtt (round trip time) + // useful for statistics, lag compensation, etc. + double lastPingTime = 0; + internal ExponentialMovingAverage _rtt = new ExponentialMovingAverage(NetworkTime.PingWindowSize); + + /// Round trip time (in seconds) that it takes a message to go server->client->server. + public double rtt => _rtt.Value; + + internal NetworkConnectionToClient() : base() { } + + public NetworkConnectionToClient(int networkConnectionId, string clientAddress = "localhost") : base() + { + connectionId = networkConnectionId; + address = clientAddress; + + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.deliveryTimeEmaDuration); + + // buffer limit should be at least multiplier to have enough in there + snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.snapshotSettings.bufferTimeMultiplier, snapshotBufferSizeLimit); + } + + public override string ToString() => $"connection({connectionId})"; + + public void OnTimeSnapshot(TimeSnapshot snapshot) + { + // protect against ever growing buffer size attacks + if (snapshots.Count >= snapshotBufferSizeLimit) return; + + // (optional) dynamic adjustment + if (NetworkClient.snapshotSettings.dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + NetworkClient.snapshotSettings.dynamicAdjustmentTolerance + ); + // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} "); + } + + // insert into the server buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + NetworkClient.snapshotSettings.bufferLimit, + snapshot, + ref remoteTimeline, + ref remoteTimescale, + NetworkServer.sendInterval, + bufferTime, + NetworkClient.snapshotSettings.catchupSpeed, + NetworkClient.snapshotSettings.slowdownSpeed, + ref driftEma, + NetworkClient.snapshotSettings.catchupNegativeThreshold, + NetworkClient.snapshotSettings.catchupPositiveThreshold, + ref deliveryTimeEma + ); + } + + public void UpdateTimeInterpolation() + { + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale); + + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } + + // Send stage three: hand off to transport + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => + Transport.active.ServerSend(connectionId, segment, channelId); + + protected virtual void UpdatePing() + { + // localTime (double) instead of Time.time for accuracy over days + if (NetworkTime.localTime >= lastPingTime + NetworkTime.PingInterval) + { + // TODO it would be safer for the server to store the last N + // messages' timestamp and only send a message number. + // This way client's can't just modify the timestamp. + // predictedTime parameter is 0 because the server doesn't predict. + NetworkPingMessage pingMessage = new NetworkPingMessage(NetworkTime.localTime, 0); + Send(pingMessage, Channels.Unreliable); + lastPingTime = NetworkTime.localTime; + } + } + + internal override void Update() + { + UpdatePing(); + base.Update(); + } + + /// Disconnects this connection. + public override void Disconnect() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + isReady = false; + reliableRpcs.Position = 0; + unreliableRpcs.Position = 0; + Transport.active.ServerDisconnect(connectionId); + + // IMPORTANT: NetworkConnection.Disconnect() is NOT called for + // voluntary disconnects from the other end. + // -> so all 'on disconnect' cleanup code needs to be in + // OnTransportDisconnect, where it's called for both voluntary + // and involuntary disconnects! + } + + internal void AddToObserving(NetworkIdentity netIdentity) + { + observing.Add(netIdentity); + + // spawn identity for this conn + NetworkServer.ShowForConnection(netIdentity, this); + } + + internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed) + { + observing.Remove(netIdentity); + + if (!isDestroyed) + { + // hide identity for this conn + NetworkServer.HideForConnection(netIdentity, this); + } + } + + internal void RemoveFromObservingsObservers() + { + foreach (NetworkIdentity netIdentity in observing) + { + netIdentity.RemoveObserver(this); + } + observing.Clear(); + } + + internal void AddOwnedObject(NetworkIdentity obj) + { + owned.Add(obj); + } + + internal void RemoveOwnedObject(NetworkIdentity obj) + { + owned.Remove(obj); + } + + internal void DestroyOwnedObjects() + { + // create a copy because the list might be modified when destroying + HashSet tmp = new HashSet(owned); + foreach (NetworkIdentity netIdentity in tmp) + { + if (netIdentity != null) + { + // disown scene objects, destroy instantiated objects. + if (netIdentity.sceneId != 0) + NetworkServer.RemovePlayerForConnection(this, RemovePlayerOptions.KeepActive); + else + NetworkServer.Destroy(netIdentity.gameObject); + } + } + + // clear the hashset because we destroyed them all + owned.Clear(); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta b/Assets/Mirror/Core/NetworkConnectionToClient.cs.meta similarity index 61% rename from Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta rename to Assets/Mirror/Core/NetworkConnectionToClient.cs.meta index 6001a71..d00cd5f 100644 --- a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs.meta +++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkConnectionToClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs b/Assets/Mirror/Core/NetworkConnectionToServer.cs similarity index 81% rename from Assets/Mirror/Runtime/NetworkConnectionToServer.cs rename to Assets/Mirror/Core/NetworkConnectionToServer.cs index a1ebc5f..496813d 100644 --- a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs +++ b/Assets/Mirror/Core/NetworkConnectionToServer.cs @@ -5,12 +5,10 @@ namespace Mirror { public class NetworkConnectionToServer : NetworkConnection { - public override string address => ""; - // Send stage three: hand off to transport [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => - Transport.activeTransport.ClientSend(segment, channelId); + Transport.active.ClientSend(segment, channelId); /// Disconnects this connection. public override void Disconnect() @@ -20,7 +18,7 @@ public override void Disconnect() // TODO remove redundant state. have one source of truth for .ready! isReady = false; NetworkClient.ready = false; - Transport.activeTransport.ClientDisconnect(); + Transport.active.ClientDisconnect(); } } } diff --git a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta b/Assets/Mirror/Core/NetworkConnectionToServer.cs.meta similarity index 61% rename from Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta rename to Assets/Mirror/Core/NetworkConnectionToServer.cs.meta index 3424b58..6f95398 100644 --- a/Assets/Mirror/Runtime/NetworkConnectionToServer.cs.meta +++ b/Assets/Mirror/Core/NetworkConnectionToServer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkConnectionToServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkDiagnostics.cs b/Assets/Mirror/Core/NetworkDiagnostics.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkDiagnostics.cs rename to Assets/Mirror/Core/NetworkDiagnostics.cs diff --git a/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta b/Assets/Mirror/Core/NetworkDiagnostics.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta rename to Assets/Mirror/Core/NetworkDiagnostics.cs.meta index fe37316..efefe99 100644 --- a/Assets/Mirror/Runtime/NetworkDiagnostics.cs.meta +++ b/Assets/Mirror/Core/NetworkDiagnostics.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkDiagnostics.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs similarity index 65% rename from Assets/Mirror/Runtime/NetworkIdentity.cs rename to Assets/Mirror/Core/NetworkIdentity.cs index 6c3c122..af60ef3 100644 --- a/Assets/Mirror/Runtime/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -1,17 +1,18 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Mirror.RemoteCalls; using UnityEngine; using UnityEngine.Serialization; #if UNITY_EDITOR - using UnityEditor; +using UnityEditor; - #if UNITY_2021_2_OR_NEWER - using UnityEditor.SceneManagement; - #elif UNITY_2018_3_OR_NEWER - using UnityEditor.Experimental.SceneManagement; - #endif +#if UNITY_2021_2_OR_NEWER +using UnityEditor.SceneManagement; +#else +using UnityEditor.Experimental.SceneManagement; +#endif #endif namespace Mirror @@ -28,6 +29,12 @@ public struct NetworkIdentitySerialization public int tick; public NetworkWriter ownerWriter; public NetworkWriter observersWriter; + + public void ResetWriters() + { + ownerWriter.Position = 0; + observersWriter.Position = 0; + } } /// NetworkIdentity identifies objects across the network. @@ -88,13 +95,16 @@ public sealed class NetworkIdentity : MonoBehaviour /// True if this object exists on a client that is not also acting as a server. public bool isClientOnly => isClient && !isServer; - /// True on client if that component has been assigned to the client. E.g. player, pets, henchmen. - public bool hasAuthority { get; internal set; } + /// isOwned is true on the client if this NetworkIdentity is one of the .owned entities of our connection on the server. + // for example: main player & pets are owned. monsters & npcs aren't. + public bool isOwned { get; internal set; } + + // internal so NetworkManager can reset it from StopClient. + internal bool clientStarted; /// The set of network connections (players) that can see this object. - // note: null until OnStartServer was called. this is necessary for - // SendTo* to work properly in server-only mode. - public Dictionary observers; + public readonly Dictionary observers = + new Dictionary(); /// The unique network Id of this object (unique at runtime). public uint netId { get; internal set; } @@ -104,6 +114,55 @@ public sealed class NetworkIdentity : MonoBehaviour [FormerlySerializedAs("m_SceneId"), HideInInspector] public ulong sceneId; + // assetId used to spawn prefabs across the network. + // originally a Guid, but a 4 byte uint is sufficient + // (as suggested by james) + // + // it's also easier to work with for serialization etc. + // serialized and visible in inspector for easier debugging + [SerializeField, HideInInspector] uint _assetId; + + // The AssetId trick: + // Ideally we would have a serialized 'Guid m_AssetId' but Unity can't + // serialize it because Guid's internal bytes are private + // + // Using just the Guid string would work, but it's 32 chars long and + // would then be sent over the network as 64 instead of 16 bytes + // + // => The solution is to serialize the string internally here and then + // use the real 'Guid' type for everything else via .assetId + public uint assetId + { + get + { +#if UNITY_EDITOR + // old UNET comment: + // This is important because sometimes OnValidate does not run + // (like when adding NetworkIdentity to prefab with no child links) + if (_assetId == 0) + SetupIDs(); +#endif + return _assetId; + } + // assetId is set internally when creating or duplicating a prefab + internal set + { + // should never be empty + if (value == 0) + { + Debug.LogError($"Can not set AssetId to empty guid on NetworkIdentity '{name}', old assetId '{_assetId}'"); + return; + } + + // always set it otherwise. + // for new prefabs, it will set from 0 to N. + // for duplicated prefabs, it will set from N to M. + // either way, it's always set to a valid GUID. + _assetId = value; + // Debug.Log($"Setting AssetId on NetworkIdentity '{name}', new assetId '{value:X4}'"); + } + } + /// Make this object only exist when the game is running as a server (or host). [FormerlySerializedAs("m_ServerOnly")] [Tooltip("Prevents this object from being spawned / enabled on clients")] @@ -130,45 +189,23 @@ internal set } NetworkConnectionToClient _connectionToClient; - /// All spawned NetworkIdentities by netId. Available on server and client. - // server sees ALL spawned ones. - // client sees OBSERVED spawned ones. - // => split into NetworkServer.spawned and NetworkClient.spawned to - // reduce shared state between server & client. - // => prepares for NetworkServer/Client as component & better host mode. - [Obsolete("NetworkIdentity.spawned is now NetworkServer.spawned on server, NetworkClient.spawned on client.\nPrepares for NetworkServer/Client as component, better host mode, better testing.")] - public static Dictionary spawned - { - get - { - // server / host mode: use the one from server. - // host mode has access to all spawned. - if (NetworkServer.active) return NetworkServer.spawned; - - // client - if (NetworkClient.active) return NetworkClient.spawned; - - // neither: then we are testing. - // we could default to NetworkServer.spawned. - // but from the outside, that's not obvious. - // better to throw an exception to make it obvious. - throw new Exception("NetworkIdentity.spawned was accessed before NetworkServer/NetworkClient were active."); - } - } - // get all NetworkBehaviour components public NetworkBehaviour[] NetworkBehaviours { get; private set; } + // to save bandwidth, we send one 64 bit dirty mask + // instead of 1 byte index per dirty component. + // which means we can't allow > 64 components (it's enough). + const int MaxNetworkBehaviours = 64; + // current visibility // // Default = use interest management // ForceHidden = useful to hide monsters while they respawn etc. // ForceShown = useful to have score NetworkIdentities that always broadcast // to everyone etc. - // - // TODO rename to 'visibility' after removing .visibility some day! [Tooltip("Visibility can overwrite interest management. ForceHidden can be useful to hide monsters while they respawn. ForceShown can be useful for score NetworkIdentities that should always broadcast to everyone in the world.")] - public Visibility visible = Visibility.Default; + [FormerlySerializedAs("visible")] + public Visibility visibility = Visibility.Default; // broadcasting serializes all entities around a player for each player. // we don't want to serialize one entity twice in the same tick. @@ -183,68 +220,43 @@ public static Dictionary spawned observersWriter = new NetworkWriter() }; - /// Prefab GUID used to spawn prefabs across the network. - // - // The AssetId trick: - // Ideally we would have a serialized 'Guid m_AssetId' but Unity can't - // serialize it because Guid's internal bytes are private - // - // UNET used 'NetworkHash128' originally, with byte0, ..., byte16 - // which works, but it just unnecessary extra code - // - // Using just the Guid string would work, but it's 32 chars long and - // would then be sent over the network as 64 instead of 16 bytes - // - // => The solution is to serialize the string internally here and then - // use the real 'Guid' type for everything else via .assetId - public Guid assetId + // Keep track of all sceneIds to detect scene duplicates + static readonly Dictionary sceneIds = + new Dictionary(); + + // Helper function to handle Command/Rpc + internal void HandleRemoteCall(byte componentIndex, ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkConnectionToClient senderConnection = null) { - get + // check if unity object has been destroyed + if (this == null) { -#if UNITY_EDITOR - // This is important because sometimes OnValidate does not run (like when adding view to prefab with no child links) - if (string.IsNullOrWhiteSpace(m_AssetId)) - SetupIDs(); -#endif - // convert string to Guid and use .Empty to avoid exception if - // we would use 'new Guid("")' - return string.IsNullOrWhiteSpace(m_AssetId) ? Guid.Empty : new Guid(m_AssetId); + Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]"); + return; } - internal set - { - string newAssetIdString = value == Guid.Empty ? string.Empty : value.ToString("N"); - string oldAssetIdString = m_AssetId; - - // they are the same, do nothing - if (oldAssetIdString == newAssetIdString) - { - return; - } - - // new is empty - if (string.IsNullOrWhiteSpace(newAssetIdString)) - { - Debug.LogError($"Can not set AssetId to empty guid on NetworkIdentity '{name}', old assetId '{oldAssetIdString}'"); - return; - } - // old not empty - if (!string.IsNullOrWhiteSpace(oldAssetIdString)) - { - Debug.LogError($"Can not Set AssetId on NetworkIdentity '{name}' because it already had an assetId, current assetId '{oldAssetIdString}', attempted new assetId '{newAssetIdString}'"); - return; - } + // find the right component to invoke the function on + if (componentIndex >= NetworkBehaviours.Length) + { + Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]"); + return; + } - // old is empty - m_AssetId = newAssetIdString; - // Debug.Log($"Settings AssetId on NetworkIdentity '{name}', new assetId '{newAssetIdString}'"); + NetworkBehaviour invokeComponent = NetworkBehaviours[componentIndex]; + if (!RemoteProcedureCalls.Invoke(functionHash, remoteCallType, reader, invokeComponent, senderConnection)) + { + Debug.LogError($"Found no receiver for incoming {remoteCallType} [{functionHash}] on {gameObject.name}, the server and client should have the same NetworkBehaviour instances [netId={netId}]."); } } - [SerializeField, HideInInspector] string m_AssetId; - // Keep track of all sceneIds to detect scene duplicates - static readonly Dictionary sceneIds = - new Dictionary(); + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + // internal so it can be called from NetworkServer & NetworkClient + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + internal static void ResetStatics() + { + // reset ALL statics + ResetClientStatics(); + ResetServerStatics(); + } // reset only client sided statics. // don't touch server statics when calling StopClient in host mode. @@ -260,33 +272,9 @@ internal static void ResetServerStatics() nextNetworkId = 1; } - // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload - // internal so it can be called from NetworkServer & NetworkClient - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - internal static void ResetStatics() - { - // reset ALL statics - ResetClientStatics(); - ResetServerStatics(); - } - /// Gets the NetworkIdentity from the sceneIds dictionary with the corresponding id public static NetworkIdentity GetSceneIdentity(ulong id) => sceneIds[id]; - // used when adding players - internal void SetClientOwner(NetworkConnectionToClient conn) - { - // do nothing if it already has an owner - if (connectionToClient != null && conn != connectionToClient) - { - Debug.LogError($"Object {this} netId={netId} already has an owner. Use RemoveClientAuthority() first", this); - return; - } - - // otherwise set the owner connection - connectionToClient = conn; - } - static uint nextNetworkId = 1; internal static uint GetNextNetworkId() => nextNetworkId++; @@ -308,18 +296,35 @@ internal void SetClientOwner(NetworkConnectionToClient conn) // BUT internal so tests can add them after creating the NetworkIdentity internal void InitializeNetworkBehaviours() { - // Get all NetworkBehaviours - // (never null. GetComponents returns [] if none found) - NetworkBehaviours = GetComponents(); - if (NetworkBehaviours.Length > byte.MaxValue) - Debug.LogError($"Only {byte.MaxValue} NetworkBehaviour components are allowed for NetworkIdentity: {name} because we send the index as byte.", this); + // Get all NetworkBehaviour components, including children. + // Some users need NetworkTransform on child bones, etc. + // => Deterministic: https://forum.unity.com/threads/getcomponentsinchildren.4582/#post-33983 + // => Never null. GetComponents returns [] if none found. + // => Include inactive. We need all child components. + NetworkBehaviours = GetComponentsInChildren(true); + ValidateComponents(); // initialize each one for (int i = 0; i < NetworkBehaviours.Length; ++i) { NetworkBehaviour component = NetworkBehaviours[i]; component.netIdentity = this; - component.ComponentIndex = i; + component.ComponentIndex = (byte)i; + } + } + + void ValidateComponents() + { + if (NetworkBehaviours == null) + { + Debug.LogError($"NetworkBehaviours array is null on {gameObject.name}!\n" + + $"Typically this can happen when a networked object is a child of a " + + $"non-networked parent that's disabled, preventing Awake on the networked object " + + $"from being invoked, where the NetworkBehaviours array is initialized.", gameObject); + } + else if (NetworkBehaviours.Length > MaxNetworkBehaviours) + { + Debug.LogError($"NetworkIdentity {name} has too many NetworkBehaviour components: only {MaxNetworkBehaviours} NetworkBehaviour components are allowed in order to save bandwidth.", this); } } @@ -349,16 +354,57 @@ void OnValidate() hasSpawned = false; #if UNITY_EDITOR + DisallowChildNetworkIdentities(); SetupIDs(); #endif } + // expose our AssetId Guid to uint mapping code in case projects need to map Guids to uint as well. + // this way their projects won't break if we change our mapping algorithm. + // needs to be available at runtime / builds, don't wrap in #if UNITY_EDITOR + public static uint AssetGuidToUint(Guid guid) => (uint)guid.GetHashCode(); // deterministic + #if UNITY_EDITOR + // child NetworkIdentities are not supported. + // Disallow them and show an error for the user to fix. + // This needs to work for Prefabs & Scene objects, so the previous check + // in NetworkClient.RegisterPrefab is not enough. + void DisallowChildNetworkIdentities() + { +#if UNITY_2020_3_OR_NEWER + NetworkIdentity[] identities = GetComponentsInChildren(true); +#else + NetworkIdentity[] identities = GetComponentsInChildren(); +#endif + if (identities.Length > 1) + { + // always log the next child component so it's easy to fix. + // if there are multiple, then after removing it'll log the next. + Debug.LogError($"'{name}' has another NetworkIdentity component on '{identities[1].name}'. There should only be one NetworkIdentity, and it must be on the root object. Please remove the other one.", this); + } + } + void AssignAssetID(string path) { // only set if not empty. fixes https://github.com/vis2k/Mirror/issues/2765 if (!string.IsNullOrWhiteSpace(path)) - m_AssetId = AssetDatabase.AssetPathToGUID(path); + { + // if we generate the assetId then we MUST be sure to set dirty + // in order to save the prefab object properly. otherwise it + // would be regenerated every time we reopen the prefab. + // -> Undo.RecordObject is the new EditorUtility.SetDirty! + // -> we need to call it before changing. + // + // to verify this, duplicate a prefab and double click to open it. + // add a log message if "_assetId != before_". + // without RecordObject, it'll log every time because it's not saved. + Undo.RecordObject(this, "Assigned AssetId"); + + // uint before = _assetId; + Guid guid = new Guid(AssetDatabase.AssetPathToGUID(path)); + assetId = AssetGuidToUint(guid); + // if (_assetId != before) Debug.Log($"Assigned assetId={assetId} to {name}"); + } } void AssignAssetID(GameObject prefab) => AssignAssetID(AssetDatabase.GetAssetPath(prefab)); @@ -557,7 +603,7 @@ void SetupIDs() // anymore because assetId was cleared if (!EditorApplication.isPlaying) { - m_AssetId = ""; + _assetId = 0; } // don't log. would show a lot when pressing play in uMMORPG/uSurvival/etc. //else Debug.Log($"Avoided clearing assetId at runtime for {name} after (probably) clicking any of the NetworkIdentity properties."); @@ -612,55 +658,47 @@ void OnDestroy() if (NetworkClient.localPlayer == this) NetworkClient.localPlayer = null; } - } - - internal void OnStartServer() - { - // do nothing if already spawned - if (isServer) - return; - - // set isServer flag - isServer = true; - - // set isLocalPlayer earlier, in case OnStartLocalplayer is called - // AFTER OnStartClient, in which case it would still be falsse here. - // many projects will check isLocalPlayer in OnStartClient though. - // TODO ideally set isLocalPlayer when NetworkClient.localPlayer is set? - if (NetworkClient.localPlayer == this) - { - isLocalPlayer = true; - } - // If the instance/net ID is invalid here then this is an object instantiated from a prefab and the server should assign a valid ID - // NOTE: this might not be necessary because the above m_IsServer - // check already checks netId. BUT this case here checks only - // netId, so it would still check cases where isServer=false - // but netId!=0. - if (netId != 0) + if (isClient) { - // This object has already been spawned, this method might be called again - // if we try to respawn all objects. This can happen when we add a scene - // in that case there is nothing else to do. - return; + // ServerChangeScene doesn't send destroy messages. + // some identities may persist in DDOL. + // some are destroyed by scene change. + // if an identity is still in .owned remove it. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3308 + if (NetworkClient.connection != null) + NetworkClient.connection.owned.Remove(this); + + // if an identity is still in .spawned, remove it too. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3324 + // + // however, verify that spawned[netId] is this NetworkIdentity + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3785 + // - server: netId=42 walks out of and back into AOI range in same frame + // - client frame 1: + // on_destroymsg(42) -> NetworkClient.DestroyObject -> GameObject.Destroy(42) // next frame + // on_spawnmsg(42) -> NetworkClient.SpawnPrefab -> Instantiate(42) -> spawned[42]=new_identity + // - client frame 2: + // Unity destroys the old 42 + // NetworkIdentity.OnDestroy removes .spawned[42] which is new_identity not old_identity + // new_identity becomes orphaned + // + // solution: only remove if spawned[netId] is this NetworkIdentity or null + if (NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity entry)) + { + if (entry == this || entry == null) + NetworkClient.spawned.Remove(netId); + } } - netId = GetNextNetworkId(); - observers = new Dictionary(); - - //Debug.Log($"OnStartServer {this} NetId:{netId} SceneId:{sceneId:X}"); - - // add to spawned (note: the original EnableIsServer isn't needed - // because we already set m_isServer=true above) - NetworkServer.spawned[netId] = this; - - // in host mode we set isClient true before calling OnStartServer, - // otherwise isClient is false in OnStartServer. - if (NetworkClient.active) - { - isClient = true; - } + // workaround for cyclid NI<->NB reference causing memory leaks + // after Destroy. [Credits: BigBoxVR/R.S.] + // TODO report this to Unity! + this.NetworkBehaviours = null; + } + internal void OnStartServer() + { foreach (NetworkBehaviour comp in NetworkBehaviours) { // an exception in OnStartServer should be caught, so that one @@ -699,23 +737,11 @@ internal void OnStopServer() } } - bool clientStarted; internal void OnStartClient() { - if (clientStarted) - return; - clientStarted = true; - - isClient = true; + if (clientStarted) return; - // set isLocalPlayer earlier, in case OnStartLocalplayer is called - // AFTER OnStartClient, in which case it would still be falsse here. - // many projects will check isLocalPlayer in OnStartClient though. - // TODO ideally set isLocalPlayer when NetworkClient.localPlayer is set? - if (NetworkClient.localPlayer == this) - { - isLocalPlayer = true; - } + clientStarted = true; // Debug.Log($"OnStartClient {gameObject} netId:{netId}"); foreach (NetworkBehaviour comp in NetworkBehaviours) @@ -739,6 +765,10 @@ internal void OnStartClient() internal void OnStopClient() { + // In case this object was destroyed already don't call + // OnStopClient if OnStartClient hasn't been called. + if (!clientStarted) return; + foreach (NetworkBehaviour comp in NetworkBehaviours) { // an exception in OnStopClient should be caught, so that @@ -757,26 +787,33 @@ internal void OnStopClient() } } - // TODO any way to make this not static? - // introduced in https://github.com/vis2k/Mirror/commit/c7530894788bb843b0f424e8f25029efce72d8ca#diff-dc8b7a5a67840f75ccc884c91b9eb76ab7311c9ca4360885a7e41d980865bdc2 - // for PR https://github.com/vis2k/Mirror/pull/1263 - // - // explanation: - // we send the spawn message multiple times. Whenever an object changes - // authority, we send the spawn message again for the object. This is - // necessary because we need to reinitialize all variables when - // ownership change due to sync to owner feature. - // Without this static, the second time we get the spawn message we - // would call OnStartLocalPlayer again on the same object internal static NetworkIdentity previousLocalPlayer = null; internal void OnStartLocalPlayer() { + // ensure OnStartLocalPlayer is only called once. + // Room demo would call it multiple times: + // - once from ApplySpawnPayload + // - once from OnObjectSpawnFinished + // + // to reproduce: + // - open room demo, add the 3 scenes to build settings + // - add OnStartLocalPlayer log to RoomPlayer prefab + // - build, run server-only + // - in editor, connect, press ready + // - in server, start game + // - notice multiple OnStartLocalPlayer logs in editor client + // + // explanation: + // we send the spawn message multiple times. Whenever an object changes + // authority, we send the spawn message again for the object. This is + // necessary because we need to reinitialize all variables when + // ownership change due to sync to owner feature. + // Without this static, the second time we get the spawn message we + // would call OnStartLocalPlayer again on the same object if (previousLocalPlayer == this) return; previousLocalPlayer = this; - isLocalPlayer = true; - foreach (NetworkBehaviour comp in NetworkBehaviours) { // an exception in OnStartLocalPlayer should be caught, so that @@ -815,293 +852,320 @@ internal void OnStopLocalPlayer() } } - bool hadAuthority; - internal void NotifyAuthority() + // 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) { - if (!hadAuthority && hasAuthority) - OnStartAuthority(); - if (hadAuthority && !hasAuthority) - OnStopAuthority(); - hadAuthority = hasAuthority; - } + ulong ownerMask = 0; + ulong observerMask = 0; - internal void OnStartAuthority() - { - foreach (NetworkBehaviour comp in NetworkBehaviours) + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) { - // an exception in OnStartAuthority should be caught, so that one - // component's exception doesn't stop all other components from - // being initialized - // => this is what Unity does for Start() etc. too. - // one exception doesn't stop all the other Start() calls! - try - { - comp.OnStartAuthority(); - } - catch (Exception e) + 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; + + // observers need to be considered only in Observers mode, + // otherwise they receive no sync data of this component ever. + if (component.syncMode == SyncMode.Observers) { - Debug.LogException(e, comp); + // for initial, it should always sync to observers. + // for delta, only if dirty. + // SyncDirection is irrelevant, as both are broadcast to + // observers which aren't the owner. + if (initialState || dirty) + observerMask |= nthBit; } } + + return (ownerMask, observerMask); } - internal void OnStopAuthority() + // build dirty mask for client. + // server always knows initialState, so we don't need it here. + ulong ClientDirtyMask() { - foreach (NetworkBehaviour comp in NetworkBehaviours) + ulong mask = 0; + + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) { - // an exception in OnStopAuthority should be caught, so that one - // component's exception doesn't stop all other components from - // being initialized - // => this is what Unity does for Start() etc. too. - // one exception doesn't stop all the other Start() calls! - try - { - comp.OnStopAuthority(); - } - catch (Exception e) + // on the client, we need to consider different sync scenarios: + // + // ServerToClient SyncDirection: + // do nothing. + // ClientToServer SyncDirection: + // serialize only if owned. + + // on client, only consider owned components with SyncDirection to server + NetworkBehaviour component = components[i]; + ulong nthBit = 1ul << i; + + if (isOwned && component.syncDirection == SyncDirection.ClientToServer) { - Debug.LogException(e, comp); + // set the n-th bit if dirty + // shifting from small to large numbers is varint-efficient. + if (component.IsDirty()) mask |= nthBit; } } + + return mask; } - // vis2k: readstring bug prevention: https://github.com/vis2k/Mirror/issues/2617 - // -> OnSerialize writes length,componentData,length,componentData,... - // -> OnDeserialize carefully extracts each data, then deserializes each component with separate readers - // -> it will be impossible to read too many or too few bytes in OnDeserialize - // -> we can properly track down errors - bool OnSerializeSafely(NetworkBehaviour comp, NetworkWriter writer, bool initialState) + // check if n-th component is dirty. + // in other words, if it has the n-th bit set in the dirty mask. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirty(ulong mask, int index) { - // write placeholder length bytes - // (jumping back later is WAY faster than allocating a temporary - // writer for the payload, then writing payload.size, payload) - int headerPosition = writer.Position; - // no varint because we don't know the final size yet - writer.WriteInt(0); - int contentPosition = writer.Position; - - // write payload - bool result = false; - try - { - result = comp.OnSerialize(writer, initialState); - } - catch (Exception e) - { - // show a detailed error and let the user know what went wrong - Debug.LogError($"OnSerialize failed for: object={name} component={comp.GetType()} sceneId={sceneId:X}\n\n{e}"); - } - int endPosition = writer.Position; - - // fill in length now - writer.Position = headerPosition; - writer.WriteInt(endPosition - contentPosition); - writer.Position = endPosition; - - //Debug.Log($"OnSerializeSafely written for object {comp.name} component:{comp.GetType()} sceneId:{sceneId:X} header:{headerPosition} content:{contentPosition} end:{endPosition} contentSize:{endPosition - contentPosition}"); - - return result; + ulong nthBit = 1ul << index; + return (mask & nthBit) != 0; } - // serialize all components using dirtyComponentsMask + // serialize components into writer on the server. // check ownerWritten/observersWritten to know if anything was written - // We pass dirtyComponentsMask into this function so that we can check - // if any Components are dirty before creating writers - internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) + internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, NetworkWriter observersWriter) { - // check if components are in byte.MaxRange just to be 100% sure - // that we avoid overflows + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); NetworkBehaviour[] components = NetworkBehaviours; - if (components.Length > byte.MaxValue) - throw new IndexOutOfRangeException($"{name} has more than {byte.MaxValue} components. This is not supported."); + + // 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. + (ulong ownerMask, ulong observerMask) = ServerDirtyMasks(initialState); + + // if nothing dirty, then don't even write the mask. + // otherwise, every unchanged object would send a 1 byte dirty mask! + if (ownerMask != 0) Compression.CompressVarUInt(ownerWriter, ownerMask); + if (observerMask != 0) Compression.CompressVarUInt(observersWriter, observerMask); // serialize all components - for (int i = 0; i < components.Length; ++i) + // perf: only iterate if either dirty mask has dirty bits. + if ((ownerMask | observerMask) != 0) { - // is this component dirty? - // -> always serialize if initialState so all components are included in spawn packet - // -> note: IsDirty() is false if the component isn't dirty or sendInterval isn't elapsed yet - NetworkBehaviour comp = components[i]; - if (initialState || comp.IsDirty()) + for (int i = 0; i < components.Length; ++i) { - //Debug.Log($"OnSerializeAllSafely: {name} -> {comp.GetType()} initial:{ initialState}"); - - // remember start position in case we need to copy it into - // observers writer too - int startPosition = ownerWriter.Position; - - // write index as byte [0..255] - ownerWriter.WriteByte((byte)i); - - // serialize into ownerWriter first - // (owner always gets everything!) - OnSerializeSafely(comp, ownerWriter, initialState); - - // copy into observersWriter too if SyncMode.Observers - // -> we copy instead of calling OnSerialize again because - // we don't know what magic the user does in OnSerialize. - // -> it's not guaranteed that calling it twice gets the - // same result - // -> it's not guaranteed that calling it twice doesn't mess - // with the user's OnSerialize timing code etc. - // => so we just copy the result without touching - // OnSerialize again - if (comp.syncMode == SyncMode.Observers) + 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 ownerDirty = IsDirty(ownerMask, i); + bool observersDirty = IsDirty(observerMask, i); + if (ownerDirty || observersDirty) { - ArraySegment segment = ownerWriter.ToArraySegment(); - int length = ownerWriter.Position - startPosition; - observersWriter.WriteBytes(segment.Array, startPosition, length); + // serialize into helper writer + using (NetworkWriterPooled temp = NetworkWriterPool.Get()) + { + comp.Serialize(temp, initialState); + ArraySegment segment = temp.ToArraySegment(); + + // copy to owner / observers as needed + if (ownerDirty) ownerWriter.WriteBytes(segment.Array, segment.Offset, segment.Count); + 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(); } } } } - // get cached serialization for this tick (or serialize if none yet) - // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - internal NetworkIdentitySerialization GetSerializationAtTick(int tick) + // serialize components into writer on the client. + internal void SerializeClient(NetworkWriter writer) { - // only rebuild serialization once per tick. reuse otherwise. - // except for tests, where Time.frameCount never increases. - // so during tests, we always rebuild. - // (otherwise [SyncVar] changes would never be serialized in tests) + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // check which components are dirty. + // this is quite complicated with SyncMode + SyncDirection. + // see the function for explanation. // - // NOTE: != instead of < because int.max+1 overflows at some point. - if (lastSerialization.tick != tick || !Application.isPlaying) - { - // reset - lastSerialization.ownerWriter.Position = 0; - lastSerialization.observersWriter.Position = 0; + // 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(); + + // varint compresses the mask to 1 byte in most cases. + // instead of writing an 8 byte ulong. + // 7 components fit into 1 byte. (previously 7 bytes) + // 11 components fit into 2 bytes. (previously 11 bytes) + // 16 components fit into 3 bytes. (previously 16 bytes) + // TODO imer: server knows amount of comps, write N bytes instead + + // 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); - // serialize - OnSerializeAllSafely(false, - lastSerialization.ownerWriter, - lastSerialization.observersWriter); - - // clear dirty bits for the components that we serialized. - // previously we did this in NetworkServer.BroadcastToConnection - // for every connection, for every entity. - // but we only serialize each entity once, right here in this - // 'lastSerialization.tick != tick' scope. - // so only do it once. - // - // NOTE: not in OnSerializeAllSafely as that should only do one - // thing: serialize data. - // - // - // NOTE: DO NOT clear ALL component's dirty bits, because - // components can have different syncIntervals and we - // don't want to reset dirty bits for the ones that were - // not synced yet. - // - // NOTE: this used to be very important to avoid ever growing - // SyncList changes if they had no observers, but we've - // added SyncObject.isRecording since. - ClearDirtyComponentsDirtyBits(); + // serialize all components + // perf: only iterate if dirty mask has dirty bits. + if (dirtyMask != 0) + { + // serialize all components + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour comp = components[i]; - // set tick - lastSerialization.tick = tick; - //Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}"); + // is this component dirty? + // reuse the mask instead of calling comp.IsDirty() again here. + if (IsDirty(dirtyMask, i)) + // if (isOwned && component.syncDirection == SyncDirection.ClientToServer) + { + // serialize into writer. + // server always knows initialState, we never need to send it + comp.Serialize(writer, false); + + // 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. + comp.ClearAllDirtyBits(); + } + } } - - // return it - return lastSerialization; } - void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState) + // deserialize components from the client on the server. + // there's no 'initialState'. server always knows the initial state. + internal bool DeserializeServer(NetworkReader reader) { - // read header as 4 bytes and calculate this chunk's start+end - int contentSize = reader.ReadInt(); - int chunkStart = reader.Position; - int chunkEnd = reader.Position + contentSize; - - // call OnDeserialize and wrap it in a try-catch block so there's no - // way to mess up another component's deserialization - try - { - //Debug.Log($"OnDeserializeSafely: {comp.name} component:{comp.GetType()} sceneId:{sceneId:X} length:{contentSize}"); - comp.OnDeserialize(reader, initialState); - } - catch (Exception e) - { - // show a detailed error and let the user know what went wrong - Debug.LogError($"OnDeserialize failed Exception={e.GetType()} (see below) object={name} component={comp.GetType()} sceneId={sceneId:X} length={contentSize}. Possible Reasons:\n" + - $" * Do {comp.GetType()}'s OnSerialize and OnDeserialize calls write the same amount of data({contentSize} bytes)? \n" + - $" * Was there an exception in {comp.GetType()}'s OnSerialize/OnDeserialize code?\n" + - $" * Are the server and client the exact same project?\n" + - $" * Maybe this OnDeserialize call was meant for another GameObject? The sceneIds can easily get out of sync if the Hierarchy was modified only in the client OR the server. Try rebuilding both.\n\n" + - $"Exception {e}"); - } + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); + NetworkBehaviour[] components = NetworkBehaviours; + + // first we deserialize the varinted dirty mask + ulong mask = Compression.DecompressVarUInt(reader); - // now the reader should be EXACTLY at 'before + size'. - // otherwise the component read too much / too less data. - if (reader.Position != chunkEnd) + // now deserialize every dirty component + for (int i = 0; i < components.Length; ++i) { - // warn the user - int bytesRead = reader.Position - chunkStart; - Debug.LogWarning($"OnDeserialize was expected to read {contentSize} instead of {bytesRead} bytes for object:{name} component={comp.GetType()} sceneId={sceneId:X}. Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases."); + // was this one dirty? + if (IsDirty(mask, i)) + { + NetworkBehaviour comp = components[i]; - // fix the position, so the following components don't all fail - reader.Position = chunkEnd; + // safety check to ensure clients can only modify their own + // ClientToServer components, nothing else. + if (comp.syncDirection == SyncDirection.ClientToServer) + { + // 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; + + // server received state from the owner client. + // set dirty so it's broadcast to other clients too. + // + // note that we set the _whole_ component as dirty. + // everything will be broadcast to others. + // SetSyncVarDirtyBits() would be nicer, but not all + // components use [SyncVar]s. + comp.SetDirty(); + } + } } + + // successfully deserialized everything + return true; } - internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState) + // deserialize components from server on the client. + internal void DeserializeClient(NetworkReader reader, bool initialState) { - if (NetworkBehaviours == null) - { - Debug.LogError($"NetworkBehaviours array is null on {gameObject.name}!\n" + - $"Typically this can happen when a networked object is a child of a " + - $"non-networked parent that's disabled, preventing Awake on the networked object " + - $"from being invoked, where the NetworkBehaviours array is initialized.", gameObject); - return; - } - - // deserialize all components that were received + // ensure NetworkBehaviours are valid before usage + ValidateComponents(); NetworkBehaviour[] components = NetworkBehaviours; - while (reader.Remaining > 0) + + // first we deserialize the varinted dirty mask + ulong mask = Compression.DecompressVarUInt(reader); + + // now deserialize every dirty component + for (int i = 0; i < components.Length; ++i) { - // read & check index [0..255] - byte index = reader.ReadByte(); - if (index < components.Length) + // was this one dirty? + if (IsDirty(mask, i)) { // deserialize this component - OnDeserializeSafely(components[index], reader, initialState); + NetworkBehaviour comp = components[i]; + comp.Deserialize(reader, initialState); } } } - // Helper function to handle Command/Rpc - internal void HandleRemoteCall(byte componentIndex, int functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkConnectionToClient senderConnection = null) + // 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. + internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick) { - // check if unity object has been destroyed - if (this == null) + // only rebuild serialization once per tick. reuse otherwise. + // except for tests, where Time.frameCount never increases. + // so during tests, we always rebuild. + // (otherwise [SyncVar] changes would never be serialized in tests) + // + // NOTE: != instead of < because int.max+1 overflows at some point. + if (lastSerialization.tick != tick +#if UNITY_EDITOR + || !Application.isPlaying +#endif + ) { - Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]"); - return; - } + // reset + lastSerialization.ResetWriters(); - // find the right component to invoke the function on - if (componentIndex >= NetworkBehaviours.Length) - { - Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]"); - return; - } + // serialize + SerializeServer(false, + lastSerialization.ownerWriter, + lastSerialization.observersWriter); - NetworkBehaviour invokeComponent = NetworkBehaviours[componentIndex]; - if (!RemoteProcedureCalls.Invoke(functionHash, remoteCallType, reader, invokeComponent, senderConnection)) - { - Debug.LogError($"Found no receiver for incoming {remoteCallType} [{functionHash}] on {gameObject.name}, the server and client should have the same NetworkBehaviour instances [netId={netId}]."); + // set tick + lastSerialization.tick = tick; + //Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}"); } + + // return it + return lastSerialization; } internal void AddObserver(NetworkConnectionToClient conn) { - if (observers == null) - { - Debug.LogError($"AddObserver for {gameObject} observer list is null"); - return; - } - if (observers.ContainsKey(conn.connectionId)) { // if we try to add a connectionId that was already added, then @@ -1139,23 +1203,19 @@ internal void AddObserver(NetworkConnectionToClient conn) conn.AddToObserving(this); } - // this is used when a connection is destroyed, since the "observers" property is read-only - internal void RemoveObserver(NetworkConnection conn) + // clear all component's dirty bits no matter what + internal void ClearAllComponentsDirtyBits() { - observers?.Remove(conn.connectionId); + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + comp.ClearAllDirtyBits(); + } } - // Called when NetworkIdentity is destroyed - internal void ClearObservers() + // this is used when a connection is destroyed, since the "observers" property is read-only + internal void RemoveObserver(NetworkConnectionToClient conn) { - if (observers != null) - { - foreach (NetworkConnectionToClient conn in observers.Values) - { - conn.RemoveFromObserving(this, true); - } - observers.Clear(); - } + observers.Remove(conn.connectionId); } /// Assign control of an object to a client via the client's NetworkConnection. @@ -1197,6 +1257,20 @@ public bool AssignClientAuthority(NetworkConnectionToClient conn) return true; } + // used when adding players + internal void SetClientOwner(NetworkConnectionToClient conn) + { + // do nothing if it already has an owner + if (connectionToClient != null && conn != connectionToClient) + { + Debug.LogError($"Object {this} netId={netId} already has an owner. Use RemoveClientAuthority() first", this); + return; + } + + // otherwise set the owner connection + connectionToClient = conn; + } + /// Removes ownership for an object. // Applies to objects that had authority set by AssignClientAuthority, // or NetworkServer.Spawn with a NetworkConnection parameter included. @@ -1232,16 +1306,17 @@ public void RemoveClientAuthority() // we can't destroy them (they are always in the scene). // instead we disable them and call Reset(). // + // Do not reset SyncObjects from Reset + // - Unspawned objects need to retain their list contents + // - They may be respawned, especially players, but others as well. + // // OLD COMMENT: // Marks the identity for future reset, this is because we cant reset // the identity during destroy as people might want to be able to read // the members inside OnDestroy(), and we have no way of invoking reset // after OnDestroy is called. - internal void Reset() + internal void ResetState() { - // make sure to call this before networkBehavioursCache is cleared below - ResetSyncObjects(); - hasSpawned = false; clientStarted = false; isClient = false; @@ -1249,7 +1324,7 @@ internal void Reset() //isLocalPlayer = false; <- cleared AFTER ClearLocalPlayer below! // remove authority flag. This object may be unspawned, not destroyed, on client. - hasAuthority = false; + isOwned = false; NotifyAuthority(); netId = 0; @@ -1273,45 +1348,64 @@ internal void Reset() isLocalPlayer = false; } - // clear all component's dirty bits no matter what - internal void ClearAllComponentsDirtyBits() + bool hadAuthority; + internal void NotifyAuthority() + { + if (!hadAuthority && isOwned) + OnStartAuthority(); + if (hadAuthority && !isOwned) + OnStopAuthority(); + hadAuthority = isOwned; + } + + internal void OnStartAuthority() { foreach (NetworkBehaviour comp in NetworkBehaviours) { - comp.ClearAllDirtyBits(); + // an exception in OnStartAuthority should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStartAuthority(); + } + catch (Exception e) + { + Debug.LogException(e, comp); + } } } - // Clear only dirty component's dirty bits. ignores components which - // may be dirty but not ready to be synced yet (because of syncInterval) - // - // NOTE: this used to be very important to avoid ever - // growing SyncList changes if they had no observers, - // but we've added SyncObject.isRecording since. - internal void ClearDirtyComponentsDirtyBits() + internal void OnStopAuthority() { foreach (NetworkBehaviour comp in NetworkBehaviours) { - if (comp.IsDirty()) + // an exception in OnStopAuthority should be caught, so that one + // component's exception doesn't stop all other components from + // being initialized + // => this is what Unity does for Start() etc. too. + // one exception doesn't stop all the other Start() calls! + try + { + comp.OnStopAuthority(); + } + catch (Exception e) { - comp.ClearAllDirtyBits(); + Debug.LogException(e, comp); } } } - void ResetSyncObjects() + // Called when NetworkIdentity is destroyed + internal void ClearObservers() { - // ResetSyncObjects is called by Reset, which is called by Unity. - // AddComponent() calls Reset(). - // AddComponent() is called before Awake(). - // so NetworkBehaviours may not be initialized yet. - if (NetworkBehaviours == null) - return; - - foreach (NetworkBehaviour comp in NetworkBehaviours) + foreach (NetworkConnectionToClient conn in observers.Values) { - comp.ResetSyncObjects(); + conn.RemoveFromObserving(this, true); } + observers.Clear(); } } } diff --git a/Assets/Mirror/Runtime/NetworkIdentity.cs.meta b/Assets/Mirror/Core/NetworkIdentity.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkIdentity.cs.meta rename to Assets/Mirror/Core/NetworkIdentity.cs.meta index 7b96521..2ca784b 100644 --- a/Assets/Mirror/Runtime/NetworkIdentity.cs.meta +++ b/Assets/Mirror/Core/NetworkIdentity.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkIdentity.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs b/Assets/Mirror/Core/NetworkLoop.cs similarity index 87% rename from Assets/Mirror/Runtime/NetworkLoop.cs rename to Assets/Mirror/Core/NetworkLoop.cs index 50d9e95..a9cd490 100644 --- a/Assets/Mirror/Runtime/NetworkLoop.cs +++ b/Assets/Mirror/Core/NetworkLoop.cs @@ -26,17 +26,8 @@ // to the beginning of PostLateUpdate doesn't actually work. using System; using UnityEngine; - -// PlayerLoop and LowLevel were in the Experimental namespace until 2019.3 -// https://docs.unity3d.com/2019.2/Documentation/ScriptReference/Experimental.LowLevel.PlayerLoop.html -// https://docs.unity3d.com/2019.3/Documentation/ScriptReference/LowLevel.PlayerLoop.html -#if UNITY_2019_3_OR_NEWER using UnityEngine.LowLevel; using UnityEngine.PlayerLoop; -#else -using UnityEngine.Experimental.LowLevel; -using UnityEngine.Experimental.PlayerLoop; -#endif namespace Mirror { @@ -45,7 +36,7 @@ public static class NetworkLoop // helper enum to add loop to begin/end of subSystemList internal enum AddMode { Beginning, End } - // callbacks in case someone needs to use early/lateupdate too. + // callbacks for others to hook into if they need Early/LateUpdate. public static Action OnEarlyUpdate; public static Action OnLateUpdate; @@ -69,7 +60,7 @@ internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction fun // recursively keep looking if (playerLoop.subSystemList != null) { - for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + for (int i = 0; i < playerLoop.subSystemList.Length; ++i) { int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType); if (index != -1) return index; @@ -108,6 +99,15 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T //foreach (PlayerLoopSystem sys in playerLoop.subSystemList) // Debug.Log($" ->{sys.type}"); + // make sure the function wasn't added yet. + // with domain reload disabled, it would otherwise be added twice: + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3392 + if (Array.FindIndex(playerLoop.subSystemList, (s => s.updateDelegate == function)) != -1) + { + // loop contains the function, so return true. + return true; + } + // resize & expand subSystemList to fit one more entry int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0; Array.Resize(ref playerLoop.subSystemList, oldListLength + 1); @@ -128,7 +128,6 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T // shift to the right, write into first array element Array.Copy(playerLoop.subSystemList, 0, playerLoop.subSystemList, 1, playerLoop.subSystemList.Length - 1); playerLoop.subSystemList[0] = system; - } // append our custom loop to the end else if (addMode == AddMode.End) @@ -148,7 +147,7 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T // recursively keep looking if (playerLoop.subSystemList != null) { - for(int i = 0; i < playerLoop.subSystemList.Length; ++i) + for (int i = 0; i < playerLoop.subSystemList.Length; ++i) { if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode)) return true; @@ -167,12 +166,7 @@ static void RuntimeInitializeOnLoad() // 2019 has GetCURRENTPlayerLoop which is safe to use without // breaking other custom system's custom loops. // see also: https://github.com/vis2k/Mirror/pull/2627/files - PlayerLoopSystem playerLoop = -#if UNITY_2019_3_OR_NEWER - PlayerLoop.GetCurrentPlayerLoop(); -#else - PlayerLoop.GetDefaultPlayerLoop(); -#endif + PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop(); // add NetworkEarlyUpdate to the end of EarlyUpdate so it runs after // any Unity initializations but before the first Update/FixedUpdate @@ -189,6 +183,11 @@ static void RuntimeInitializeOnLoad() static void NetworkEarlyUpdate() { + // loop functions run in edit mode and in play mode. + // however, we only want to call NetworkServer/Client in play mode. + if (!Application.isPlaying) return; + + NetworkTime.EarlyUpdate(); //Debug.Log($"NetworkEarlyUpdate {Time.time}"); NetworkServer.NetworkEarlyUpdate(); NetworkClient.NetworkEarlyUpdate(); @@ -198,6 +197,10 @@ static void NetworkEarlyUpdate() static void NetworkLateUpdate() { + // loop functions run in edit mode and in play mode. + // however, we only want to call NetworkServer/Client in play mode. + if (!Application.isPlaying) return; + //Debug.Log($"NetworkLateUpdate {Time.time}"); // invoke event before mirror does its final late updating. OnLateUpdate?.Invoke(); diff --git a/Assets/Mirror/Runtime/NetworkLoop.cs.meta b/Assets/Mirror/Core/NetworkLoop.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkLoop.cs.meta rename to Assets/Mirror/Core/NetworkLoop.cs.meta index 52b6e6a..522313b 100644 --- a/Assets/Mirror/Runtime/NetworkLoop.cs.meta +++ b/Assets/Mirror/Core/NetworkLoop.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkLoop.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs similarity index 79% rename from Assets/Mirror/Runtime/NetworkManager.cs rename to Assets/Mirror/Core/NetworkManager.cs index 37be9ae..ed0494d 100644 --- a/Assets/Mirror/Runtime/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using kcp2k; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Serialization; @@ -10,6 +9,7 @@ namespace Mirror { public enum PlayerSpawnMethod { Random, RoundRobin } public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host } + public enum HeadlessStartOptions { DoNothing, AutoStartServer, AutoStartClient } [DisallowMultipleComponent] [AddComponentMenu("Network/Network Manager")] @@ -29,13 +29,23 @@ public class NetworkManager : MonoBehaviour public bool runInBackground = true; /// Should the server auto-start when 'Server Build' is checked in build settings - [Tooltip("Should the server auto-start when 'Server Build' is checked in build settings")] - [FormerlySerializedAs("startOnHeadless")] - public bool autoStartServerBuild = true; + [Header("Auto-Start Options")] + + [Tooltip("Choose whether Server or Client should auto-start in headless builds")] + public HeadlessStartOptions headlessStartMode = HeadlessStartOptions.DoNothing; + + [Tooltip("Headless Start Mode in Editor\nwhen enabled, headless start mode will be used in editor as well.")] + public bool editorAutoStart; /// Server 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("Server 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.")] - public int serverTickRate = 30; + [Tooltip("Server / Client send rate per second.\nUse 60-100Hz for fast paced games like Counter-Strike to minimize latency.\nUse around 30Hz for games like WoW to minimize computations.\nUse around 1-10Hz for slow paced games like EVE.")] + [FormerlySerializedAs("serverTickRate")] + public int sendRate = 60; + + // 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.")] + // public int clientSendRate = 30; // 33 ms /// Automatically switch to this scene upon going offline (on start / on disconnect / on shutdown). [Header("Scene Management")] @@ -50,11 +60,13 @@ public class NetworkManager : MonoBehaviour [Tooltip("Scene that Mirror will switch to when the server is started. Clients will recieve a Scene Message to load the server's current scene when they connect.")] public string onlineScene = ""; + [Range(0, 60), Tooltip("Optional delay that can be used after disconnecting to show a 'Connection lost...' message or similar before loading the offline scene, which may take a long time in big projects.")] + public float offlineSceneLoadDelay = 0; + // transport layer [Header("Network Info")] [Tooltip("Transport component attached to this object that server and client will use to connect")] - [SerializeField] - protected Transport transport; + public Transport transport; /// Server's address for clients to connect to. [FormerlySerializedAs("m_NetworkAddress")] @@ -66,6 +78,15 @@ public class NetworkManager : MonoBehaviour [Tooltip("Maximum number of concurrent connections.")] public int maxConnections = 100; + // Mirror global disconnect inactive option, independent of Transport. + // not all Transports do this properly, and it's easiest to configure this just once. + // this is very useful for some projects, keep it. + [Tooltip("When enabled, the server automatically disconnects inactive connections after the configured timeout.")] + public bool disconnectInactiveConnections; + + [Tooltip("Timeout in seconds for server to automatically disconnect inactive connections if 'disconnectInactiveConnections' is enabled.")] + public float disconnectInactiveTimeout = 60f; + [Header("Authentication")] [Tooltip("Authentication component attached to this object")] public NetworkAuthenticator authenticator; @@ -96,6 +117,25 @@ public class NetworkManager : MonoBehaviour public static List startPositions = new List(); public static int startPositionIndex; + [Header("Security")] + [Tooltip("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 exploits.\nHowever, some games may want to allow exceptions in order to not interrupt the player's experience.")] + public bool exceptionsDisconnect = true; // security by default + + [Header("Snapshot Interpolation")] + public SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings(); + + [Header("Connection Quality")] + [Tooltip("Method to use for connection quality evaluation.\nSimple: based on rtt and jitter.\nPragmatic: based on snapshot interpolation adjustment.")] + public ConnectionQualityMethod evaluationMethod; + + [Tooltip("Interval in seconds to evaluate connection quality.\nSet to 0 to disable connection quality evaluation.")] + [Range(0, 60)] + [FormerlySerializedAs("connectionQualityInterval")] + public float evaluationInterval = 3; + + [Header("Interpolation UI - Requires Editor / Dev Build")] + public bool timeInterpolationGui = false; + /// The one and only NetworkManager public static NetworkManager singleton { get; internal set; } @@ -128,7 +168,7 @@ public virtual void OnValidate() // always >= 0 maxConnections = Mathf.Max(maxConnections, 0); - if (playerPrefab != null && playerPrefab.GetComponent() == null) + if (playerPrefab != null && !playerPrefab.TryGetComponent(out NetworkIdentity _)) { Debug.LogError("NetworkManager - Player Prefab must have a NetworkIdentity."); playerPrefab = null; @@ -137,7 +177,7 @@ public virtual void OnValidate() // This avoids the mysterious "Replacing existing prefab with assetId ... Old prefab 'Player', New prefab 'Player'" warning. if (playerPrefab != null && spawnPrefabs.Contains(playerPrefab)) { - Debug.LogWarning("NetworkManager - Player Prefab should not be added to Registered Spawnable Prefabs list...removed it."); + Debug.LogWarning("NetworkManager - Player Prefab doesn't need to be in Spawnable Prefabs list too. Removing it."); spawnPrefabs.Remove(playerPrefab); } } @@ -161,24 +201,6 @@ public virtual void Reset() return; } } - - // add transport if there is none yet. makes upgrading easier. - if (transport == null) - { -#if UNITY_EDITOR - // RecordObject needs to be called before we make the change - UnityEditor.Undo.RecordObject(gameObject, "Added default Transport"); -#endif - - transport = GetComponent(); - - // was a transport added yet? if not, add one - if (transport == null) - { - transport = gameObject.AddComponent(); - Debug.Log("NetworkManager: added default Transport because there was none yet."); - } - } } // virtual so that inheriting classes' Awake() can call base.Awake() too @@ -187,7 +209,8 @@ public virtual void Awake() // Don't allow collision-destroyed second instance to continue. if (!InitializeSingleton()) return; - Debug.Log("Mirror | mirror-networking.com | discord.gg/N9QVxbM"); + // Apply configuration in Awake once already + ApplyConfiguration(); // Set the networkSceneName to prevent a scene reload // if client connection to server fails. @@ -200,36 +223,60 @@ public virtual void Awake() // virtual so that inheriting classes' Start() can call base.Start() too public virtual void Start() { - // headless mode? then start the server - // can't do this in Awake because Awake is for initialization. - // some transports might not be ready until Start. + // Auto-start headless server or client. + // + // We can't do this in Awake because Awake is for initialization + // and some transports might not be ready until Start. // - // (tick rate is applied in StartServer!) -#if UNITY_SERVER - if (autoStartServerBuild) + // Auto-starting in Editor is useful for debugging, so that can + // be enabled with editorAutoStart. + if (Utils.IsHeadless()) { - StartServer(); + if (!Application.isEditor || editorAutoStart) + switch (headlessStartMode) + { + case HeadlessStartOptions.AutoStartServer: + StartServer(); + break; + case HeadlessStartOptions.AutoStartClient: + StartClient(); + break; + } } -#endif } - // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too - public virtual void LateUpdate() + // make sure to call base.Update() when overwriting + public virtual void Update() { - UpdateScene(); + ApplyConfiguration(); } - // keep the online scene change check in a separate function - bool IsServerOnlineSceneChangeNeeded() + // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too + public virtual void LateUpdate() { - // Only change scene if the requested online scene is not blank, and is not already loaded - return !string.IsNullOrWhiteSpace(onlineScene) && !IsSceneActive(onlineScene) && onlineScene != offlineScene; + UpdateScene(); } - public static bool IsSceneActive(string scene) + //////////////////////////////////////////////////////////////////////// + + // keep the online scene change check in a separate function. + // only change scene if the requested online scene is not blank, and is not already loaded. + bool IsServerOnlineSceneChangeNeeded() => + !string.IsNullOrWhiteSpace(onlineScene) && + !Utils.IsSceneActive(onlineScene) && + onlineScene != offlineScene; + + // NetworkManager exposes some NetworkServer/Client configuration. + // we apply it every Update() in order to avoid two sources of truth. + // fixes issues where NetworkServer.sendRate was never set because + // NetworkManager.StartServer was never called, etc. + // => all exposed settings should be applied at all times if NM exists. + void ApplyConfiguration() { - Scene activeScene = SceneManager.GetActiveScene(); - return activeScene.path == scene || activeScene.name == scene; + NetworkServer.tickRate = sendRate; + NetworkClient.snapshotSettings = snapshotSettings; + NetworkClient.connectionQualityInterval = evaluationInterval; + NetworkClient.connectionQualityMethod = evaluationMethod; } // full server setup code, without spawning objects yet @@ -238,6 +285,11 @@ void SetupServer() // Debug.Log("NetworkManager SetupServer"); InitializeSingleton(); + // apply settings before initializing anything + NetworkServer.disconnectInactiveConnections = disconnectInactiveConnections; + NetworkServer.disconnectInactiveTimeout = disconnectInactiveTimeout; + NetworkServer.exceptionsDisconnect = exceptionsDisconnect; + if (runInBackground) Application.runInBackground = true; @@ -252,18 +304,11 @@ void SetupServer() // start listening to network connections NetworkServer.Listen(maxConnections); - // call OnStartServer AFTER Listen, so that NetworkServer.active is - // true and we can call NetworkServer.Spawn in OnStartServer - // overrides. - // (useful for loading & spawning stuff from database etc.) - // - // note: there is no risk of someone connecting after Listen() and - // before OnStartServer() because this all runs in one thread - // and we don't start processing connects until Update. - OnStartServer(); - // this must be after Listen(), since that registers the default message handlers RegisterServerMessages(); + + // do not call OnStartServer here yet. + // this is up to the caller. different for server-only vs. host mode. } /// Starts the server, listening for incoming connections. @@ -295,6 +340,16 @@ public void StartServer() SetupServer(); + // call OnStartServer AFTER Listen, so that NetworkServer.active is + // true and we can call NetworkServer.Spawn in OnStartServer + // overrides. + // (useful for loading & spawning stuff from database etc.) + // + // note: there is no risk of someone connecting after Listen() and + // before OnStartServer() because this all runs in one thread + // and we don't start processing connects until Update. + OnStartServer(); + // scene change needed? then change scene and spawn afterwards. if (IsServerOnlineSceneChangeNeeded()) { @@ -307,19 +362,14 @@ public void StartServer() } } - /// Starts the client, connects it to the server with networkAddress. - public void StartClient() + void SetupClient() { - if (NetworkClient.active) - { - Debug.LogWarning("Client already started."); - return; - } - - mode = NetworkManagerMode.ClientOnly; - InitializeSingleton(); + // apply settings before initializing anything + NetworkClient.exceptionsDisconnect = exceptionsDisconnect; + // NetworkClient.sendRate = clientSendRate; + if (runInBackground) Application.runInBackground = true; @@ -329,17 +379,33 @@ public void StartClient() authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); } - // In case this is a headless client... - ConfigureHeadlessFrameRate(); + } - RegisterClientMessages(); + /// Starts the client, connects it to the server with networkAddress. + public void StartClient() + { + // Do checks and short circuits before setting anything up. + // If / when we retry, we won't have conflict issues. + if (NetworkClient.active) + { + Debug.LogWarning("Client already started."); + return; + } if (string.IsNullOrWhiteSpace(networkAddress)) { Debug.LogError("Must set the Network Address field in the manager"); return; } - // Debug.Log($"NetworkManager StartClient address:{networkAddress}"); + + mode = NetworkManagerMode.ClientOnly; + + SetupClient(); + + // In case this is a headless client... + ConfigureHeadlessFrameRate(); + + RegisterClientMessages(); NetworkClient.Connect(networkAddress); @@ -357,16 +423,7 @@ public void StartClient(Uri uri) mode = NetworkManagerMode.ClientOnly; - InitializeSingleton(); - - if (runInBackground) - Application.runInBackground = true; - - if (authenticator != null) - { - authenticator.OnStartClient(); - authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); - } + SetupClient(); RegisterClientMessages(); @@ -413,11 +470,6 @@ public void StartHost() // setup server first SetupServer(); - // call OnStartHost AFTER SetupServer. this way we can use - // NetworkServer.Spawn etc. in there too. just like OnStartServer - // is called after the server is actually properly started. - OnStartHost(); - // scene change needed? then change scene and spawn afterwards. // => BEFORE host client connects. if client auth succeeds then the // server tells it to load 'onlineScene'. we can't do that if @@ -474,6 +526,21 @@ void FinishStartHost() // TODO call this after spawnobjects and worry about the syncvar hook fix later? NetworkClient.ConnectHost(); + // invoke user callbacks AFTER ConnectHost has set .activeHost. + // this way initialization can properly handle host mode. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3302 + // where [SyncVar] hooks wouldn't be called for objects spawned in + // NetworkManager.OnStartServer, because .activeHost was still false. + // + // TODO is there a risk of someone connecting between Listen() and FinishStartHost()? + OnStartServer(); + + // call OnStartHost AFTER SetupServer. this way we can use + // NetworkServer.Spawn etc. in there too. just like OnStartServer + // is called after the server is actually properly started. + OnStartHost(); + // server scene was loaded. now spawn all the objects NetworkServer.SpawnObjects(); @@ -482,26 +549,12 @@ void FinishStartHost() // DO NOT do this earlier. it would cause race conditions where a // client will do things before the server is even fully started. //Debug.Log("StartHostClient called"); - StartHostClient(); - } - - void StartHostClient() - { - //Debug.Log("NetworkManager ConnectLocalClient"); - - if (authenticator != null) - { - authenticator.OnStartClient(); - authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); - } - - networkAddress = "localhost"; - NetworkServer.ActivateHostScene(); + SetupClient(); RegisterClientMessages(); - // ConnectLocalServer needs to be called AFTER RegisterClientMessages + // InvokeOnConnected needs to be called AFTER RegisterClientMessages // (https://github.com/vis2k/Mirror/pull/1249/) - NetworkClient.ConnectLocalServer(); + HostMode.InvokeOnConnected(); OnStartClient(); } @@ -510,14 +563,6 @@ void StartHostClient() public void StopHost() { OnStopHost(); - - // calling OnTransportDisconnected was needed to fix - // https://github.com/vis2k/Mirror/issues/1515 - // so that the host client receives a DisconnectMessage - // TODO reevaluate if this is still needed after all the disconnect - // fixes, and try to put this into LocalConnection.Disconnect! - NetworkServer.OnTransportDisconnected(NetworkConnection.LocalConnectionId); - StopClient(); StopServer(); } @@ -570,50 +615,29 @@ public void StopClient() if (mode == NetworkManagerMode.Offline) return; - if (authenticator != null) - { - authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); - authenticator.OnStopClient(); - } - - // Get Network Manager out of DDOL before going to offline scene - // to avoid collision and let a fresh Network Manager be created. - // IMPORTANT: .gameObject can be null if StopClient is called from - // OnApplicationQuit or from tests! - if (gameObject != null - && gameObject.scene.name == "DontDestroyOnLoad" - && !string.IsNullOrWhiteSpace(offlineScene) - && SceneManager.GetActiveScene().path != offlineScene) - SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); - - OnStopClient(); - - //Debug.Log("NetworkManager StopClient"); - - // set offline mode BEFORE changing scene so that FinishStartScene - // doesn't think we need initialize anything. - // set offline mode BEFORE NetworkClient.Disconnect so StopClient - // only runs once. - mode = NetworkManagerMode.Offline; + // For Host client, call OnServerDisconnect before NetworkClient.Disconnect + // because we need NetworkServer.localConnection to not be null + // NetworkClient.Disconnect will set it null. + if (mode == NetworkManagerMode.Host) + OnServerDisconnect(NetworkServer.localConnection); - // shutdown client + // ask client -> transport to disconnect. + // handle voluntary and involuntary disconnects in OnClientDisconnect. + // + // StopClient + // NetworkClient.Disconnect + // Transport.Disconnect + // ... + // Transport.OnClientDisconnect + // NetworkClient.OnTransportDisconnect + // NetworkManager.OnClientDisconnect NetworkClient.Disconnect(); - NetworkClient.Shutdown(); - - // If this is the host player, StopServer will already be changing scenes. - // Check loadingSceneAsync to ensure we don't double-invoke the scene change. - // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. - if (!string.IsNullOrWhiteSpace(offlineScene) && !IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) - { - ClientChangeScene(offlineScene, SceneOperation.Normal); - } - - networkSceneName = ""; } // called when quitting the application by closing the window / pressing // stop in the editor. virtual so that inheriting classes' // OnApplicationQuit() can call base.OnApplicationQuit() too + // (this can't be in OnDestroy: https://github.com/MirrorNetworking/Mirror/issues/3952) public virtual void OnApplicationQuit() { // stop client first @@ -641,10 +665,11 @@ public virtual void OnApplicationQuit() // useful for headless benchmark clients. public virtual void ConfigureHeadlessFrameRate() { -#if UNITY_SERVER - Application.targetFrameRate = serverTickRate; - // Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz."); -#endif + if (Utils.IsHeadless()) + { + Application.targetFrameRate = sendRate; + // Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz."); + } } bool InitializeSingleton() @@ -680,7 +705,21 @@ bool InitializeSingleton() // set active transport AFTER setting singleton. // so only if we didn't destroy ourselves. - Transport.activeTransport = transport; + + // This tries to avoid missing transport errors and more clearly tells user what to fix. + if (transport == null) + if (TryGetComponent(out Transport newTransport)) + { + Debug.LogWarning($"No Transport assigned to Network Manager - Using {newTransport} found on same object."); + transport = newTransport; + } + else + { + Debug.LogError("No Transport on Network Manager...add a transport and assign it."); + return false; + } + + Transport.active = transport; return true; } @@ -689,6 +728,7 @@ void RegisterServerMessages() NetworkServer.OnConnectedEvent = OnServerConnectInternal; NetworkServer.OnDisconnectedEvent = OnServerDisconnect; NetworkServer.OnErrorEvent = OnServerError; + NetworkServer.OnTransportExceptionEvent = OnServerTransportException; NetworkServer.RegisterHandler(OnServerAddPlayerInternal); // Network Server initially registers its own handler for this, so we replace it here. @@ -700,7 +740,10 @@ void RegisterClientMessages() NetworkClient.OnConnectedEvent = OnClientConnectInternal; NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal; NetworkClient.OnErrorEvent = OnClientError; - NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal); + NetworkClient.OnTransportExceptionEvent = OnClientTransportException; + + // Don't require authentication because server may send NotReadyMessage from ServerChangeScene + NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal, false); NetworkClient.RegisterHandler(OnClientSceneInternal, false); if (playerPrefab != null) @@ -763,6 +806,14 @@ public virtual void ServerChangeScene(string newSceneName) return; } + // Throw error if called from client + // Allow changing scene while stopping the server + if (!NetworkServer.active && newSceneName != offlineScene) + { + Debug.LogError("ServerChangeScene can only be called on an active server."); + return; + } + // Debug.Log($"ServerChangeScene {newSceneName}"); NetworkServer.SetAllClientsNotReady(); networkSceneName = newSceneName; @@ -781,7 +832,10 @@ public virtual void ServerChangeScene(string newSceneName) if (NetworkServer.active) { // notify all clients about the new scene - NetworkServer.SendToAll(new SceneMessage { sceneName = newSceneName }); + NetworkServer.SendToAll(new SceneMessage + { + sceneName = newSceneName + }); } startPositionIndex = 0; @@ -955,10 +1009,6 @@ void FinishLoadSceneHost() if (clientReadyConnection != null) { -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientConnect(clientReadyConnection); -#pragma warning restore 618 clientLoadedScene = true; clientReadyConnection = null; } @@ -992,13 +1042,7 @@ void FinishLoadSceneHost() OnServerSceneChanged(networkSceneName); if (NetworkClient.isConnected) - { - // let client know that we changed scene -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientSceneChanged(NetworkClient.connection); -#pragma warning restore 618 - } + OnClientSceneChanged(); } } @@ -1024,21 +1068,12 @@ void FinishLoadSceneClientOnly() if (clientReadyConnection != null) { -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientConnect(clientReadyConnection); -#pragma warning restore 618 clientLoadedScene = true; clientReadyConnection = null; } if (NetworkClient.isConnected) - { -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientSceneChanged(NetworkClient.connection); -#pragma warning restore 618 - } + OnClientSceneChanged(); } /// @@ -1070,7 +1105,7 @@ public static void UnRegisterStartPosition(Transform start) } /// Get the next NetworkStartPosition based on the selected PlayerSpawnMethod. - public Transform GetStartPosition() + public virtual Transform GetStartPosition() { // first remove any dead transforms startPositions.RemoveAll(t => t == null); @@ -1118,7 +1153,10 @@ void OnServerAuthenticated(NetworkConnectionToClient conn) // proceed with the login handshake by calling OnServerConnect if (networkSceneName != "" && networkSceneName != offlineScene) { - SceneMessage msg = new SceneMessage() { sceneName = networkSceneName }; + SceneMessage msg = new SceneMessage() + { + sceneName = networkSceneName + }; conn.Send(msg); } @@ -1141,7 +1179,7 @@ void OnServerAddPlayerInternal(NetworkConnectionToClient conn, AddPlayerMessage return; } - if (autoCreatePlayer && playerPrefab.GetComponent() == null) + if (autoCreatePlayer && !playerPrefab.TryGetComponent(out NetworkIdentity _)) { Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); return; @@ -1180,39 +1218,88 @@ void OnClientAuthenticated() // set connection to authenticated NetworkClient.connection.isAuthenticated = true; - // proceed with the login handshake by calling OnClientConnect - if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || IsSceneActive(onlineScene)) + // Set flag to wait for scene change? + if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || Utils.IsSceneActive(onlineScene)) { clientLoadedScene = false; -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientConnect(NetworkClient.connection); -#pragma warning restore 618 } else { - // will wait for scene id to come from the server. + // Scene message expected from server. clientLoadedScene = true; clientReadyConnection = NetworkClient.connection; } + + // Call virtual method regardless of whether a scene change is expected or not. + OnClientConnect(); } + // Transport callback, invoked after client fully disconnected. + // the call order should always be: + // Disconnect() -> ask Transport -> Transport.OnDisconnected -> Cleanup void OnClientDisconnectInternal() { //Debug.Log("NetworkManager.OnClientDisconnectInternal"); -#pragma warning disable 618 - // obsolete method calls new method because it's not empty - OnClientDisconnect(NetworkClient.connection); -#pragma warning restore 618 + + // Only let this run once. StopClient in Host mode changes to ServerOnly + if (mode == NetworkManagerMode.ServerOnly || mode == NetworkManagerMode.Offline) + return; + + // user callback + OnClientDisconnect(); + + if (authenticator != null) + { + authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); + authenticator.OnStopClient(); + } + + // set mode BEFORE changing scene so FinishStartScene doesn't re-initialize anything. + // set mode BEFORE NetworkClient.Disconnect so StopClient only runs once. + // set mode BEFORE OnStopClient so StopClient only runs once. + // If we got here from StopClient in Host mode, change to ServerOnly. + // - If StopHost was called, StopServer will put us in Offline mode. + if (mode == NetworkManagerMode.Host) + mode = NetworkManagerMode.ServerOnly; + else + mode = NetworkManagerMode.Offline; + + //Debug.Log("NetworkManager StopClient"); + OnStopClient(); + + // shutdown client + NetworkClient.Shutdown(); + + // Exit here if we're now in ServerOnly mode (StopClient called in Host mode). + if (mode == NetworkManagerMode.ServerOnly) return; + + // Get Network Manager out of DDOL before going to offline scene + // to avoid collision and let a fresh Network Manager be created. + // IMPORTANT: .gameObject can be null if StopClient is called from + // OnApplicationQuit or from tests! + if (gameObject != null + && gameObject.scene.name == "DontDestroyOnLoad" + && !string.IsNullOrWhiteSpace(offlineScene) + && SceneManager.GetActiveScene().path != offlineScene) + SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); + + // If StopHost called in Host mode, StopServer will change scenes after this. + // Check loadingSceneAsync to ensure we don't double-invoke the scene change. + // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. + if (!string.IsNullOrWhiteSpace(offlineScene) && !Utils.IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) + Invoke(nameof(ClientChangeOfflineScene), offlineSceneLoadDelay); + + networkSceneName = ""; } + // wrap ClientChangeScene call without parameters for use in Invoke. + void ClientChangeOfflineScene() => + ClientChangeScene(offlineScene, SceneOperation.Normal); + void OnClientNotReadyMessageInternal(NotReadyMessage msg) { //Debug.Log("NetworkManager.OnClientNotReadyMessageInternal"); NetworkClient.ready = false; -#pragma warning disable 618 - OnClientNotReady(NetworkClient.connection); -#pragma warning restore 618 OnClientNotReady(); // NOTE: clientReadyConnection is not set here! don't want OnClientConnect to be invoked again after scene changes. @@ -1224,13 +1311,11 @@ void OnClientSceneInternal(SceneMessage msg) // This needs to run for host client too. NetworkServer.active is checked there if (NetworkClient.isConnected) - { ClientChangeScene(msg.sceneName, msg.sceneOperation, msg.customHandling); - } } /// Called on the server when a new client connects. - public virtual void OnServerConnect(NetworkConnectionToClient conn) {} + public virtual void OnServerConnect(NetworkConnectionToClient conn) { } /// Called on the server when a client disconnects. // Called by NetworkServer.OnTransportDisconnect! @@ -1270,13 +1355,16 @@ public virtual void OnServerAddPlayer(NetworkConnectionToClient conn) } /// Called on server when transport raises an exception. NetworkConnection may be null. - public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) {} + public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) { } + + /// Called on server when transport raises an exception. NetworkConnection may be null. + public virtual void OnServerTransportException(NetworkConnectionToClient conn, Exception exception) { } /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed - public virtual void OnServerChangeScene(string newSceneName) {} + public virtual void OnServerChangeScene(string newSceneName) { } /// Called on server after a scene load with ServerChangeScene() is completed. - public virtual void OnServerSceneChanged(string sceneName) {} + public virtual void OnServerSceneChanged(string sceneName) { } /// Called on the client when connected to a server. By default it sets client as ready and adds a player. public virtual void OnClientConnect() @@ -1296,36 +1384,21 @@ public virtual void OnClientConnect() } } - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientConnect(NetworkConnection conn) => OnClientConnect(); - /// Called on clients when disconnected from a server. - public virtual void OnClientDisconnect() - { - if (mode == NetworkManagerMode.Offline) - return; - - StopClient(); - } + public virtual void OnClientDisconnect() { } - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientDisconnect(NetworkConnection conn) => OnClientDisconnect(); + /// Called on client when transport raises an exception. + public virtual void OnClientError(TransportError error, string reason) { } /// Called on client when transport raises an exception. - public virtual void OnClientError(Exception exception) {} + public virtual void OnClientTransportException(Exception exception) { } /// Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes. - public virtual void OnClientNotReady() {} - - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientNotReady(NetworkConnection conn) {} + public virtual void OnClientNotReady() { } /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed // customHandling: indicates if scene loading will be handled through overrides - public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) {} + public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) { } /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. // Scene changes can cause player objects to be destroyed. The default @@ -1334,41 +1407,46 @@ public virtual void OnClientChangeScene(string newSceneName, SceneOperation scen public virtual void OnClientSceneChanged() { // always become ready. - if (!NetworkClient.ready) NetworkClient.Ready(); + if (NetworkClient.connection.isAuthenticated && !NetworkClient.ready) NetworkClient.Ready(); // Only call AddPlayer for normal scene changes, not additive load/unload - if (clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) + if (NetworkClient.connection.isAuthenticated && clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) { // add player if existing one is null NetworkClient.AddPlayer(); } } - // Deprecated 2021-12-11 - [Obsolete("Remove the NetworkConnection parameter in your override and use NetworkClient.connection instead.")] - public virtual void OnClientSceneChanged(NetworkConnection conn) => OnClientSceneChanged(); - // Since there are multiple versions of StartServer, StartClient and // StartHost, to reliably customize their functionality, users would // need override all the versions. Instead these callbacks are invoked // from all versions, so users only need to implement this one case. /// This is invoked when a host is started. - public virtual void OnStartHost() {} + public virtual void OnStartHost() { } /// This is invoked when a server is started - including when a host is started. - public virtual void OnStartServer() {} + public virtual void OnStartServer() { } /// This is invoked when the client is started. - public virtual void OnStartClient() {} + public virtual void OnStartClient() { } /// This is called when a server is stopped - including when a host is stopped. - public virtual void OnStopServer() {} + public virtual void OnStopServer() { } /// This is called when a client is stopped. - public virtual void OnStopClient() {} + public virtual void OnStopClient() { } /// This is called when a host is stopped. - public virtual void OnStopHost() {} + public virtual void OnStopHost() { } + +#if DEBUG + // keep OnGUI even in builds. useful to debug snap interp. + void OnGUI() + { + if (!timeInterpolationGui) return; + NetworkClient.OnGUI(); + } +#endif } } diff --git a/Assets/Mirror/Runtime/NetworkManager.cs.meta b/Assets/Mirror/Core/NetworkManager.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkManager.cs.meta rename to Assets/Mirror/Core/NetworkManager.cs.meta index 0a7564a..6e81bbe 100644 --- a/Assets/Mirror/Runtime/NetworkManager.cs.meta +++ b/Assets/Mirror/Core/NetworkManager.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkManager.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs b/Assets/Mirror/Core/NetworkManagerHUD.cs similarity index 60% rename from Assets/Mirror/Runtime/NetworkManagerHUD.cs rename to Assets/Mirror/Core/NetworkManagerHUD.cs index cba968d..f78eede 100644 --- a/Assets/Mirror/Runtime/NetworkManagerHUD.cs +++ b/Assets/Mirror/Core/NetworkManagerHUD.cs @@ -1,5 +1,3 @@ -// vis2k: GUILayout instead of spacey += ...; removed Update hotkeys to avoid -// confusion if someone accidentally presses one. using UnityEngine; namespace Mirror @@ -23,26 +21,24 @@ void Awake() void OnGUI() { - GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 215, 9999)); + // If this width is changed, also change offsetX in GUIConsole::OnGUI + int width = 300; + + GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, width, 9999)); + if (!NetworkClient.isConnected && !NetworkServer.active) - { StartButtons(); - } else - { StatusLabels(); - } - // client ready if (NetworkClient.isConnected && !NetworkClient.ready) { if (GUILayout.Button("Client Ready")) { + // client ready NetworkClient.Ready(); if (NetworkClient.localPlayer == null) - { NetworkClient.AddPlayer(); - } } } @@ -55,44 +51,55 @@ void StartButtons() { if (!NetworkClient.active) { - // Server + Client - if (Application.platform != RuntimePlatform.WebGLPlayer) +#if UNITY_WEBGL + // cant be a server in webgl build + if (GUILayout.Button("Single Player")) { - if (GUILayout.Button("Host (Server + Client)")) - { - manager.StartHost(); - } + NetworkServer.listen = false; + manager.StartHost(); } +#else + // Server + Client + if (GUILayout.Button("Host (Server + Client)")) + manager.StartHost(); +#endif - // Client + IP + // Client + IP (+ PORT) GUILayout.BeginHorizontal(); + if (GUILayout.Button("Client")) - { manager.StartClient(); - } - // This updates networkAddress every frame from the TextField + manager.networkAddress = GUILayout.TextField(manager.networkAddress); + // only show a port field if we have a port transport + // we can't have "IP:PORT" in the address field since this only + // works for IPV4:PORT. + // for IPV6:PORT it would be misleading since IPV6 contains ":": + // 2001:0db8:0000:0000:0000:ff00:0042:8329 + if (Transport.active is PortTransport portTransport) + { + // use TryParse in case someone tries to enter non-numeric characters + if (ushort.TryParse(GUILayout.TextField(portTransport.Port.ToString()), out ushort port)) + portTransport.Port = port; + } + GUILayout.EndHorizontal(); // Server Only - if (Application.platform == RuntimePlatform.WebGLPlayer) - { - // cant be a server in webgl build - GUILayout.Box("( WebGL cannot be server )"); - } - else - { - if (GUILayout.Button("Server Only")) manager.StartServer(); - } +#if UNITY_WEBGL + // cant be a server in webgl build + GUILayout.Box("( WebGL cannot be server )"); +#else + if (GUILayout.Button("Server Only")) + manager.StartServer(); +#endif } else { // Connecting GUILayout.Label($"Connecting to {manager.networkAddress}.."); if (GUILayout.Button("Cancel Connection Attempt")) - { manager.StopClient(); - } } } @@ -104,45 +111,51 @@ void StatusLabels() // Client: ... if (NetworkServer.active && NetworkClient.active) { - GUILayout.Label($"Host: running via {Transport.activeTransport}"); + // host mode + GUILayout.Label($"Host: running via {Transport.active}"); } - // server only else if (NetworkServer.active) { - GUILayout.Label($"Server: running via {Transport.activeTransport}"); + // server only + GUILayout.Label($"Server: running via {Transport.active}"); } - // client only else if (NetworkClient.isConnected) { - GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.activeTransport}"); + // client only + GUILayout.Label($"Client: connected to {manager.networkAddress} via {Transport.active}"); } } void StopButtons() { - // stop host if host mode if (NetworkServer.active && NetworkClient.isConnected) { + GUILayout.BeginHorizontal(); +#if UNITY_WEBGL + if (GUILayout.Button("Stop Single Player")) + manager.StopHost(); +#else + // stop host if host mode if (GUILayout.Button("Stop Host")) - { manager.StopHost(); - } + + // stop client if host mode, leaving server up + if (GUILayout.Button("Stop Client")) + manager.StopClient(); +#endif + GUILayout.EndHorizontal(); } - // stop client if client-only else if (NetworkClient.isConnected) { + // stop client if client-only if (GUILayout.Button("Stop Client")) - { manager.StopClient(); - } } - // stop server if server-only else if (NetworkServer.active) { + // stop server if server-only if (GUILayout.Button("Stop Server")) - { manager.StopServer(); - } } } } diff --git a/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta b/Assets/Mirror/Core/NetworkManagerHUD.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta rename to Assets/Mirror/Core/NetworkManagerHUD.cs.meta index a720b9c..fb72f54 100644 --- a/Assets/Mirror/Runtime/NetworkManagerHUD.cs.meta +++ b/Assets/Mirror/Core/NetworkManagerHUD.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkManagerHUD.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkMessage.cs b/Assets/Mirror/Core/NetworkMessage.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkMessage.cs rename to Assets/Mirror/Core/NetworkMessage.cs diff --git a/Assets/Mirror/Runtime/NetworkMessage.cs.meta b/Assets/Mirror/Core/NetworkMessage.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkMessage.cs.meta rename to Assets/Mirror/Core/NetworkMessage.cs.meta index 73d3d8f..3afc348 100644 --- a/Assets/Mirror/Runtime/NetworkMessage.cs.meta +++ b/Assets/Mirror/Core/NetworkMessage.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkMessage.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkMessages.cs b/Assets/Mirror/Core/NetworkMessages.cs new file mode 100644 index 0000000..7a70d94 --- /dev/null +++ b/Assets/Mirror/Core/NetworkMessages.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using UnityEngine; + +namespace Mirror +{ + // for performance, we (ab)use c# generics to cache the message id in a static field + // this is significantly faster than doing the computation at runtime or looking up cached results via Dictionary + // generic classes have separate static fields per type specification + public static class NetworkMessageId where T : struct, NetworkMessage + { + // automated message id from type hash. + // platform independent via stable hashcode. + // => convenient so we don't need to track messageIds across projects + // => addons can work with each other without knowing their ids before + // => 2 bytes is enough to avoid collisions. + // registering a messageId twice will log a warning anyway. + public static readonly ushort Id = CalculateId(); + + // Gets the 32bit fnv1a hash + // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort + // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits + // This will create a more uniform 16bit hash, the method is described in: + // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding" + static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16(); + } + + // message packing all in one place, instead of constructing headers in all + // kinds of different places + // + // MsgType (2 bytes) + // Content (ContentSize bytes) + public static class NetworkMessages + { + // size of message id header in bytes + public const int IdSize = sizeof(ushort); + + // Id <> Type lookup for debugging, profiler, etc. + // important when debugging messageId errors! + public static readonly Dictionary Lookup = + new Dictionary(); + + // dump all types for debugging + public static void LogTypes() + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine("NetworkMessageIds:"); + foreach (KeyValuePair kvp in Lookup) + { + builder.AppendLine($" Id={kvp.Key} = {kvp.Value}"); + } + Debug.Log(builder.ToString()); + } + + // max message content size (without header) calculation for convenience + // -> Transport.GetMaxPacketSize is the raw maximum + // -> Every message gets serialized into <> + // -> Every serialized message get put into a batch with one timestamp per batch + // -> Every message in a batch has a varuint size header. + // use the worst case VarUInt size for the largest possible + // message size = int.max. + public static int MaxContentSize(int channelId) + { + // calculate the max possible size that can fit in a batch + int transportMax = Transport.active.GetMaxPacketSize(channelId); + return transportMax - IdSize - Batcher.MaxMessageOverhead(transportMax); + } + + // max message size which includes header + content. + public static int MaxMessageSize(int channelId) => + MaxContentSize(channelId) + IdSize; + + // automated message id from type hash. + // platform independent via stable hashcode. + // => convenient so we don't need to track messageIds across projects + // => addons can work with each other without knowing their ids before + // => 2 bytes is enough to avoid collisions. + // registering a messageId twice will log a warning anyway. + // keep this for convenience. easier to use than NetworkMessageId.Id. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort GetId() where T : struct, NetworkMessage => + NetworkMessageId.Id; + + // pack message before sending + // -> NetworkWriter passed as arg so that we can use .ToArraySegment + // and do an allocation free send before recycling it. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Pack(T message, NetworkWriter writer) + where T : struct, NetworkMessage + { + writer.WriteUShort(NetworkMessageId.Id); + writer.Write(message); + } + + // read only the message id. + // common function in case we ever change the header size. + public static bool UnpackId(NetworkReader reader, out ushort messageId) + { + // read message type + try + { + messageId = reader.ReadUShort(); + return true; + } + catch (System.IO.EndOfStreamException) + { + messageId = 0; + return false; + } + } + + // version for handlers with channelId + // inline! only exists for 20-30 messages and they call it all the time. + internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication, bool exceptionsDisconnect) + where T : struct, NetworkMessage + where C : NetworkConnection + => (conn, reader, channelId) => + { + // protect against DOS attacks if attackers try to send invalid + // data packets to crash the server/client. there are a thousand + // ways to cause an exception in data handling: + // - invalid headers + // - invalid message ids + // - invalid data causing exceptions + // - negative ReadBytesAndSize prefixes + // - invalid utf8 strings + // - etc. + // + // let's catch them all and then disconnect that connection to avoid + // further attacks. + T message = default; + // record start position for NetworkDiagnostics because reader might contain multiple messages if using batching + int startPos = reader.Position; + try + { + if (requireAuthentication && !conn.isAuthenticated) + { + // message requires authentication, but the connection was not authenticated + Debug.LogWarning($"Disconnecting connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet"); + conn.Disconnect(); + return; + } + + //Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}"); + + // if it is a value type, just use default(T) + // otherwise allocate a new instance + message = reader.Read(); + } + catch (Exception exception) + { + // should we disconnect on exceptions? + if (exceptionsDisconnect) + { + Debug.LogError($"Disconnecting connection: {conn} because reading a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); + conn.Disconnect(); + return; + } + // otherwise log it but allow the connection to keep playing + else + { + Debug.LogError($"Caught an Exception when reading a message from: {conn} of type {typeof(T)}. Reason: {exception}"); + return; + } + } + finally + { + int endPos = reader.Position; + // TODO: Figure out the correct channel + NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos); + } + + // user handler exception should not stop the whole server + try + { + // user implemented handler + handler((C)conn, message, channelId); + } + catch (Exception exception) + { + // should we disconnect on exceptions? + if (exceptionsDisconnect) + { + Debug.LogError($"Disconnecting connection: {conn} because handling a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); + conn.Disconnect(); + } + // otherwise log it but allow the connection to keep playing + else + { + Debug.LogError($"Caught an Exception when handling a message from: {conn} of type {typeof(T)}. Reason: {exception}"); + } + } + }; + + // version for handlers without channelId + // TODO obsolete this some day to always use the channelId version. + // all handlers in this version are wrapped with 1 extra action. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication, bool exceptionsDisconnect) + where T : struct, NetworkMessage + where C : NetworkConnection + { + // wrap action as channelId version, call original + void Wrapped(C conn, T msg, int _) => handler(conn, msg); + return WrapHandler((Action)Wrapped, requireAuthentication, exceptionsDisconnect); + } + } +} diff --git a/Assets/Mirror/Runtime/MessagePacking.cs.meta b/Assets/Mirror/Core/NetworkMessages.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/MessagePacking.cs.meta rename to Assets/Mirror/Core/NetworkMessages.cs.meta index 910b75c..d2600a3 100644 --- a/Assets/Mirror/Runtime/MessagePacking.cs.meta +++ b/Assets/Mirror/Core/NetworkMessages.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkMessages.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkReader.cs b/Assets/Mirror/Core/NetworkReader.cs similarity index 59% rename from Assets/Mirror/Runtime/NetworkReader.cs rename to Assets/Mirror/Core/NetworkReader.cs index 86eeef4..82fb7cd 100644 --- a/Assets/Mirror/Runtime/NetworkReader.cs +++ b/Assets/Mirror/Core/NetworkReader.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using System.Text; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; @@ -10,57 +11,82 @@ namespace Mirror // Note: This class is intended to be extremely pedantic, // and throw exceptions whenever stuff is going slightly wrong. // The exceptions will be handled in NetworkServer/NetworkClient. + // + // Note that NetworkWriter can be passed in constructor thanks to implicit + // ArraySegment conversion: + // NetworkReader reader = new NetworkReader(writer); public class NetworkReader { // internal buffer // byte[] pointer would work, but we use ArraySegment to also support // the ArraySegment constructor - ArraySegment buffer; + internal ArraySegment buffer; /// Next position to read from the buffer // 'int' is the best type for .Position. 'short' is too small if we send >32kb which would result in negative .Position // -> converting long to int is fine until 2GB of data (MAX_INT), so we don't have to worry about overflows here public int Position; - /// Total number of bytes to read from buffer - public int Length - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => buffer.Count; - } - /// Remaining bytes that can be read, for convenience. - public int Remaining - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Length - Position; - } + public int Remaining => buffer.Count - Position; - public NetworkReader(byte[] bytes) - { - buffer = new ArraySegment(bytes); - } + /// Total buffer capacity, independent of reader position. + public int Capacity => buffer.Count; + + // cache encoding for ReadString instead of creating it with each time + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + // member(!) to avoid static state. + // + // throwOnInvalidBytes is true. + // if false, it would silently ignore the invalid bytes but continue + // with the valid ones, creating strings like "a�������". + // instead, we want to catch it manually and return String.Empty. + // this is safer. see test: ReadString_InvalidUTF8(). + internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true); + + // while allocation free ReadArraySegment is encouraged, + // some functions can allocate a new byte[], List, Texture, etc. + // we should keep a reasonable allocation size limit: + // -> server won't accidentally allocate 2GB on a mobile device + // -> client won't allocate 2GB on server for ClientToServer [SyncVar]s + // -> unlike max string length of 64 KB, we need a larger limit here. + // large enough to not break existing projects, + // small enough to reasonably limit allocation attacks. + // -> we don't know the exact size of ReadList etc. because is + // managed. instead, this is considered a 'collection length' limit. + public const int AllocationLimit = 1024 * 1024 * 16; // 16 MB * sizeof(T) public NetworkReader(ArraySegment segment) { buffer = segment; } +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have the implicit byte[] to segment conversion yet + public NetworkReader(byte[] bytes) + { + buffer = new ArraySegment(bytes, 0, bytes.Length); + } +#endif + // sometimes it's useful to point a reader on another buffer instead of // allocating a new reader (e.g. NetworkReaderPool) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetBuffer(byte[] bytes) + public void SetBuffer(ArraySegment segment) { - buffer = new ArraySegment(bytes); + buffer = segment; Position = 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetBuffer(ArraySegment segment) +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have the implicit byte[] to segment conversion yet + public void SetBuffer(byte[] bytes) { - buffer = segment; + buffer = new ArraySegment(bytes, 0, bytes.Length); Position = 0; } +#endif // ReadBlittable from DOTSNET // this is extremely fast, but only works for blittable types. @@ -71,7 +97,26 @@ public void SetBuffer(ArraySegment segment) // Note: // ReadBlittable assumes same endianness for server & client. // All Unity 2018+ platforms are little endian. - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // + // This is not safe to expose to random structs. + // * StructLayout.Sequential is the default, which is safe. + // if the struct contains a reference type, it is converted to Auto. + // but since all structs here are unmanaged blittable, it's safe. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential + // * StructLayout.Pack depends on CPU word size. + // this may be different 4 or 8 on some ARM systems, etc. + // this is not safe, and would cause bytes/shorts etc. to be padded. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0 + // * If we force pack all to '1', they would have no padding which is + // great for bandwidth. but on some android systems, CPU can't read + // unaligned memory. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // * The only option would be to force explicit layout with multiples + // of word size. but this requires lots of weaver checking and is + // still questionable (IL2CPP etc.). + // + // Note: inlining ReadBlittable is enough. don't inline ReadInt etc. + // we don't want ReadBlittable to be copied in place everywhere. internal unsafe T ReadBlittable() where T : unmanaged { @@ -91,10 +136,10 @@ internal unsafe T ReadBlittable() // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices int size = sizeof(T); - // enough data to read? - if (Position + size > buffer.Count) + // ensure remaining + if (Remaining < size) { - throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> out of range: {ToString()}"); + throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> not enough data in buffer to read {size} bytes: {ToString()}"); } // read blittable @@ -132,23 +177,24 @@ internal unsafe T ReadBlittable() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal T? ReadBlittableNullable() where T : unmanaged => - ReadByte() != 0 ? ReadBlittable() : default(T?); + ReadByte() != 0 ? ReadBlittable() : default(T?); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte ReadByte() => ReadBlittable(); /// Read 'count' bytes into the bytes array // NOTE: returns byte[] because all reader functions return something. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte[] ReadBytes(byte[] bytes, int count) { + // user may call ReadBytes(ReadInt()). ensure positive count. + if (count < 0) throw new ArgumentOutOfRangeException("ReadBytes requires count >= 0"); + // check if passed byte array is big enough if (count > bytes.Length) { throw new EndOfStreamException($"ReadBytes can't read {count} + bytes because the passed byte[] only has length {bytes.Length}"); } - // check if within buffer limits - if (Position + count > buffer.Count) + // ensure remaining + if (Remaining < count) { throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}"); } @@ -159,11 +205,13 @@ public byte[] ReadBytes(byte[] bytes, int count) } /// Read 'count' bytes allocation-free as ArraySegment that points to the internal array. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySegment ReadBytesSegment(int count) { - // check if within buffer limits - if (Position + count > buffer.Count) + // user may call ReadBytes(ReadInt()). ensure positive count. + if (count < 0) throw new ArgumentOutOfRangeException("ReadBytesSegment requires count >= 0"); + + // ensure remaining + if (Remaining < count) { throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}"); } @@ -174,22 +222,21 @@ public ArraySegment ReadBytesSegment(int count) return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override string ToString() => - $"NetworkReader pos={Position} len={Length} buffer={BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count)}"; - /// Reads any data type that mirror supports. Uses weaver populated Reader(T).read - [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Read() { Func readerDelegate = Reader.read; if (readerDelegate == null) { - Debug.LogError($"No reader found for {typeof(T)}. Use a type supported by Mirror or define a custom reader"); + Debug.LogError($"No reader found for {typeof(T)}. Use a type supported by Mirror or define a custom reader extension for {typeof(T)}."); return default; } return readerDelegate(this); } + + // print the full buffer with position / capacity. + public override string ToString() => + $"[{buffer.ToHexString()} @ {Position}/{Capacity}]"; } /// Helper class that weaver populates with all reader types. diff --git a/Assets/Mirror/Runtime/NetworkReader.cs.meta b/Assets/Mirror/Core/NetworkReader.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkReader.cs.meta rename to Assets/Mirror/Core/NetworkReader.cs.meta index 65ad3f0..f1bd5bd 100644 --- a/Assets/Mirror/Runtime/NetworkReader.cs.meta +++ b/Assets/Mirror/Core/NetworkReader.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReader.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs b/Assets/Mirror/Core/NetworkReaderExtensions.cs similarity index 51% rename from Assets/Mirror/Runtime/NetworkReaderExtensions.cs rename to Assets/Mirror/Core/NetworkReaderExtensions.cs index 6137866..dd366f6 100644 --- a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs +++ b/Assets/Mirror/Core/NetworkReaderExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; -using System.Text; using UnityEngine; namespace Mirror @@ -11,85 +9,62 @@ namespace Mirror // but they do all need to be extensions. public static class NetworkReaderExtensions { - // cache encoding instead of creating it each time - // 1000 readers before: 1MB GC, 30ms - // 1000 readers after: 0.8MB GC, 18ms - static readonly UTF8Encoding encoding = new UTF8Encoding(false, true); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte ReadByte(this NetworkReader reader) => reader.ReadBlittable(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte? ReadByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static sbyte ReadSByte(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static sbyte? ReadSByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); // bool is not blittable. read as ushort. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char ReadChar(this NetworkReader reader) => (char)reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char? ReadCharNullable(this NetworkReader reader) => (char?)reader.ReadBlittableNullable(); // bool is not blittable. read as byte. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ReadBool(this NetworkReader reader) => reader.ReadBlittable() != 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool? ReadBoolNullable(this NetworkReader reader) { byte? value = reader.ReadBlittableNullable(); return value.HasValue ? (value.Value != 0) : default(bool?); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static short ReadShort(this NetworkReader reader) => (short)reader.ReadUShort(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static short? ReadShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort ReadUShort(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort? ReadUShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ReadInt(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int? ReadIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint ReadUInt(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint? ReadUIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long ReadLong(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long? ReadLongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // ReadInt/UInt/Long/ULong writes full bytes by default. + // define additional "VarInt" versions that Weaver will automatically prefer. + // 99% of the time [SyncVar] ints are small values, which makes this very much worth it. + [WeaverPriority] public static int ReadVarInt(this NetworkReader reader) => (int)Compression.DecompressVarInt(reader); + [WeaverPriority] public static uint ReadVarUInt(this NetworkReader reader) => (uint)Compression.DecompressVarUInt(reader); + [WeaverPriority] public static long ReadVarLong(this NetworkReader reader) => Compression.DecompressVarInt(reader); + [WeaverPriority] public static ulong ReadVarULong(this NetworkReader reader) => Compression.DecompressVarUInt(reader); + public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double ReadDouble(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double? ReadDoubleNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static decimal ReadDecimal(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static decimal? ReadDecimalNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + public static Half ReadHalf(this NetworkReader reader) => new Half(reader.ReadUShort()); + /// if an invalid utf8 string is sent - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReadString(this NetworkReader reader) { // read number of bytes @@ -99,116 +74,134 @@ public static string ReadString(this NetworkReader reader) if (size == 0) return null; - int realSize = size - 1; + ushort realSize = (ushort)(size - 1); // make sure it's within limits to avoid allocation attacks etc. - if (realSize >= NetworkWriter.MaxStringLength) - { - throw new EndOfStreamException($"ReadString too long: {realSize}. Limit is: {NetworkWriter.MaxStringLength}"); - } + if (realSize > NetworkWriter.MaxStringLength) + throw new EndOfStreamException($"NetworkReader.ReadString - Value too long: {realSize} bytes. Limit is: {NetworkWriter.MaxStringLength} bytes"); ArraySegment data = reader.ReadBytesSegment(realSize); // convert directly from buffer to string via encoding - return encoding.GetString(data.Array, data.Offset, data.Count); - } - - /// if count is invalid - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte[] ReadBytesAndSize(this NetworkReader reader) - { - // count = 0 means the array was null - // otherwise count -1 is the length of the array - uint count = reader.ReadUInt(); - // Use checked() to force it to throw OverflowException if data is invalid - return count == 0 ? null : reader.ReadBytes(checked((int)(count - 1u))); + // throws in case of invalid utf8. + // see test: ReadString_InvalidUTF8() + return reader.encoding.GetString(data.Array, data.Offset, data.Count); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] ReadBytes(this NetworkReader reader, int count) { + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + if (count > NetworkReader.AllocationLimit) + { + // throw EndOfStream for consistency with ReadBlittable when out of data + throw new EndOfStreamException($"NetworkReader attempted to allocate {count} bytes, which is larger than the allowed limit of {NetworkReader.AllocationLimit} bytes."); + } + byte[] bytes = new byte[count]; reader.ReadBytes(bytes, count); return bytes; } /// if count is invalid - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ArraySegment ReadBytesAndSizeSegment(this NetworkReader reader) + public static byte[] ReadBytesAndSize(this NetworkReader reader) { - // count = 0 means the array was null - // otherwise count - 1 is the length of the array - uint count = reader.ReadUInt(); + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint count = (uint)Compression.DecompressVarUInt(reader); + // uint count = reader.ReadUInt(); + // Use checked() to force it to throw OverflowException if data is invalid + return count == 0 ? null : reader.ReadBytes(checked((int)(count - 1u))); + } + // Reads ArraySegment and size header + /// if count is invalid + public static ArraySegment ReadArraySegmentAndSize(this NetworkReader reader) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint count = (uint)Compression.DecompressVarUInt(reader); + // uint count = reader.ReadUInt(); // Use checked() to force it to throw OverflowException if data is invalid return count == 0 ? default : reader.ReadBytesSegment(checked((int)(count - 1u))); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2 ReadVector2(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2? ReadVector2Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3 ReadVector3(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3? ReadVector3Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector4 ReadVector4(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector4? ReadVector4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2Int ReadVector2Int(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2Int? ReadVector2IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3Int ReadVector3Int(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3Int? ReadVector3IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color ReadColor(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color? ReadColorNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color32 ReadColor32(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Color32? ReadColor32Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Quaternion ReadQuaternion(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Quaternion? ReadQuaternionNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rect ReadRect(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + // Rect is a struct with properties instead of fields + public static Rect ReadRect(this NetworkReader reader) => new Rect(reader.ReadVector2(), reader.ReadVector2()); + public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRect(reader) : default(Rect?); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Plane ReadPlane(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + // Plane is a struct with properties instead of fields + public static Plane ReadPlane(this NetworkReader reader) => new Plane(reader.ReadVector3(), reader.ReadFloat()); + public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBool() ? ReadPlane(reader) : default(Plane?); + + // Ray is a struct with properties instead of fields + public static Ray ReadRay(this NetworkReader reader) => new Ray(reader.ReadVector3(), reader.ReadVector3()); + public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRay(reader) : default(Ray?); + + // LayerMask is a struct with properties instead of fields + public static LayerMask ReadLayerMask(this NetworkReader reader) + { + // LayerMask doesn't have a constructor that takes an initial value. + // 32 layers as a flags enum, max value of 496, we only need a UShort. + LayerMask layerMask = default; + layerMask.value = reader.ReadUShort(); + return layerMask; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Ray ReadRay(this NetworkReader reader) => reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBlittableNullable(); + public static LayerMask? ReadLayerMaskNullable(this NetworkReader reader) => reader.ReadBool() ? ReadLayerMask(reader) : default(LayerMask?); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader)=> reader.ReadBlittable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader) => reader.ReadBlittable(); public static Matrix4x4? ReadMatrix4x4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Guid ReadGuid(this NetworkReader reader) => new Guid(reader.ReadBytes(16)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid ReadGuid(this NetworkReader reader) + { +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have Span yet + return new Guid(reader.ReadBytes(16)); +#else + // ReadBlittable(Guid) isn't safe. see ReadBlittable comments. + // Guid is Sequential, but we can't guarantee packing. + if (reader.Remaining >= 16) + { + ReadOnlySpan span = new ReadOnlySpan(reader.buffer.Array, reader.buffer.Offset + reader.Position, 16); + reader.Position += 16; + return new Guid(span); + } + throw new EndOfStreamException($"ReadGuid out of range: {reader}"); +#endif + } public static Guid? ReadGuidNullable(this NetworkReader reader) => reader.ReadBool() ? ReadGuid(reader) : default(Guid?); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader) { uint netId = reader.ReadUInt(); @@ -222,7 +215,6 @@ public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader) return Utils.GetSpawnedInServerOrClient(netId); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader) { // read netId first. @@ -247,18 +239,16 @@ public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader) NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId); return identity != null - ? identity.NetworkBehaviours[componentIndex] - : null; + ? identity.NetworkBehaviours[componentIndex] + : null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T ReadNetworkBehaviour(this NetworkReader reader) where T : NetworkBehaviour { return reader.ReadNetworkBehaviour() as T; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static NetworkBehaviour.NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader) + public static NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader) { uint netId = reader.ReadUInt(); byte componentIndex = default; @@ -269,10 +259,9 @@ public static NetworkBehaviour.NetworkBehaviourSyncVar ReadNetworkBehaviourSyncV componentIndex = reader.ReadByte(); } - return new NetworkBehaviour.NetworkBehaviourSyncVar(netId, componentIndex); + return new NetworkBehaviourSyncVar(netId, componentIndex); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Transform ReadTransform(this NetworkReader reader) { // Don't use null propagation here as it could lead to MissingReferenceException @@ -280,7 +269,6 @@ public static Transform ReadTransform(this NetworkReader reader) return networkIdentity != null ? networkIdentity.transform : null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static GameObject ReadGameObject(this NetworkReader reader) { // Don't use null propagation here as it could lead to MissingReferenceException @@ -288,13 +276,32 @@ public static GameObject ReadGameObject(this NetworkReader reader) return networkIdentity != null ? networkIdentity.gameObject : null; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // while SyncList is recommended for NetworkBehaviours, + // structs may have .List members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Readers/GenerateReader() handles this manually. public static List ReadList(this NetworkReader reader) { - int length = reader.ReadInt(); - if (length < 0) - return null; - List result = new List(length); + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint length = (uint)Compression.DecompressVarUInt(reader); + // uint length = reader.ReadUInt(); + if (length == 0) return null; + length -= 1; + + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + if (length > NetworkReader.AllocationLimit) + { + // throw EndOfStream for consistency with ReadBlittable when out of data + throw new EndOfStreamException($"NetworkReader attempted to allocate a List<{typeof(T)}> {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}."); + } + + List result = new List((checked((int)length))); for (int i = 0; i < length; i++) { result.Add(reader.Read()); @@ -302,25 +309,55 @@ public static List ReadList(this NetworkReader reader) return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T[] ReadArray(this NetworkReader reader) + // while SyncSet is recommended for NetworkBehaviours, + // structs may have .Set members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Readers/GenerateReader() handles this manually. + public static HashSet ReadHashSet(this NetworkReader reader) { - int length = reader.ReadInt(); + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. - // we write -1 for null - if (length < 0) - return null; + // most sizes are small, read size as VarUInt! + uint length = (uint)Compression.DecompressVarUInt(reader); + //uint length = reader.ReadUInt(); + if (length == 0) return null; + length -= 1; - // todo throw an exception for other negative values (we never write them, likely to be attacker) + HashSet result = new HashSet(); + for (int i = 0; i < length; i++) + { + result.Add(reader.Read()); + } + return result; + } - // this assumes that a reader for T reads at least 1 bytes - // we can't know the exact size of T because it could have a user created reader - // NOTE: don't add to length as it could overflow if value is int.max - if (length > reader.Length - reader.Position) + public static T[] ReadArray(this NetworkReader reader) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + + // most sizes are small, read size as VarUInt! + uint length = (uint)Compression.DecompressVarUInt(reader); + //uint length = reader.ReadUInt(); + if (length == 0) return null; + length -= 1; + + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + if (length > NetworkReader.AllocationLimit) { - throw new EndOfStreamException($"Received array that is too large: {length}"); + // throw EndOfStream for consistency with ReadBlittable when out of data + throw new EndOfStreamException($"NetworkReader attempted to allocate an Array<{typeof(T)}> with {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}."); } + // we can't check if reader.Remaining < length, + // because we don't know sizeof(T) since it's a managed type. + // if (length > reader.Remaining) throw new EndOfStreamException($"Received array that is too large: {length}"); + T[] result = new T[length]; for (int i = 0; i < length; i++) { @@ -329,26 +366,55 @@ public static T[] ReadArray(this NetworkReader reader) return result; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Uri ReadUri(this NetworkReader reader) { string uriString = reader.ReadString(); return (string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Texture2D ReadTexture2D(this NetworkReader reader) { - Texture2D texture2D = new Texture2D(32, 32); - texture2D.SetPixels32(reader.Read()); + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + short width = reader.ReadShort(); + if (width == -1) return null; + + // read height + short height = reader.ReadShort(); + + // prevent allocation attacks with a reasonable limit. + // server shouldn't allocate too much on client devices. + // client shouldn't allocate too much on server in ClientToServer [SyncVar]s. + // log an error and return default. + // we don't want attackers to be able to trigger exceptions. + int totalSize = width * height; + if (totalSize > NetworkReader.AllocationLimit) + { + Debug.LogWarning($"NetworkReader attempted to allocate a Texture2D with total size (width * height) of {totalSize}, which is larger than the allowed limit of {NetworkReader.AllocationLimit}."); + return null; + } + + Texture2D texture2D = new Texture2D(width, height); + + // read pixel content + Color32[] pixels = reader.ReadArray(); + texture2D.SetPixels32(pixels); texture2D.Apply(); return texture2D; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Sprite ReadSprite(this NetworkReader reader) { - return Sprite.Create(reader.ReadTexture2D(), reader.ReadRect(), reader.ReadVector2()); + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + Texture2D texture = reader.ReadTexture2D(); + if (texture == null) return null; + + // otherwise create a valid sprite + return Sprite.Create(texture, reader.ReadRect(), reader.ReadVector2()); } + + public static DateTime ReadDateTime(this NetworkReader reader) => DateTime.FromOADate(reader.ReadDouble()); + public static DateTime? ReadDateTimeNullable(this NetworkReader reader) => reader.ReadBool() ? ReadDateTime(reader) : default(DateTime?); } } diff --git a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta b/Assets/Mirror/Core/NetworkReaderExtensions.cs.meta similarity index 61% rename from Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta rename to Assets/Mirror/Core/NetworkReaderExtensions.cs.meta index 66536c9..97f24e5 100644 --- a/Assets/Mirror/Runtime/NetworkReaderExtensions.cs.meta +++ b/Assets/Mirror/Core/NetworkReaderExtensions.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReaderExtensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkReaderPool.cs b/Assets/Mirror/Core/NetworkReaderPool.cs similarity index 71% rename from Assets/Mirror/Runtime/NetworkReaderPool.cs rename to Assets/Mirror/Core/NetworkReaderPool.cs index ebbfac5..f44adb8 100644 --- a/Assets/Mirror/Runtime/NetworkReaderPool.cs +++ b/Assets/Mirror/Core/NetworkReaderPool.cs @@ -17,12 +17,10 @@ public static class NetworkReaderPool 1000 ); - // DEPRECATED 2022-03-10 - [Obsolete("GetReader() was renamed to Get()")] - public static NetworkReaderPooled GetReader(byte[] bytes) => Get(bytes); + // expose count for testing + public static int Count => Pool.Count; /// Get the next reader in the pool. If pool is empty, creates a new Reader - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkReaderPooled Get(byte[] bytes) { // grab from pool & set buffer @@ -31,12 +29,7 @@ public static NetworkReaderPooled Get(byte[] bytes) return reader; } - // DEPRECATED 2022-03-10 - [Obsolete("GetReader() was renamed to Get()")] - public static NetworkReaderPooled GetReader(ArraySegment segment) => Get(segment); - /// Get the next reader in the pool. If pool is empty, creates a new Reader - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkReaderPooled Get(ArraySegment segment) { // grab from pool & set buffer @@ -45,10 +38,6 @@ public static NetworkReaderPooled Get(ArraySegment segment) return reader; } - // DEPRECATED 2022-03-10 - [Obsolete("Recycle() was renamed to Return()")] - public static void Recycle(NetworkReaderPooled reader) => Return(reader); - /// Returns a reader to the pool. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Return(NetworkReaderPooled reader) diff --git a/Assets/Mirror/Runtime/NetworkReaderPool.cs.meta b/Assets/Mirror/Core/NetworkReaderPool.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkReaderPool.cs.meta rename to Assets/Mirror/Core/NetworkReaderPool.cs.meta index 2c94768..60bf9f6 100644 --- a/Assets/Mirror/Runtime/NetworkReaderPool.cs.meta +++ b/Assets/Mirror/Core/NetworkReaderPool.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReaderPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkReaderPooled.cs b/Assets/Mirror/Core/NetworkReaderPooled.cs new file mode 100644 index 0000000..1508701 --- /dev/null +++ b/Assets/Mirror/Core/NetworkReaderPooled.cs @@ -0,0 +1,12 @@ +using System; + +namespace Mirror +{ + /// Pooled NetworkReader, automatically returned to pool when using 'using' + public sealed class NetworkReaderPooled : NetworkReader, IDisposable + { + internal NetworkReaderPooled(byte[] bytes) : base(bytes) {} + internal NetworkReaderPooled(ArraySegment segment) : base(segment) {} + public void Dispose() => NetworkReaderPool.Return(this); + } +} diff --git a/Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta b/Assets/Mirror/Core/NetworkReaderPooled.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta rename to Assets/Mirror/Core/NetworkReaderPooled.cs.meta index 4eb6e9d..0f7dee9 100644 --- a/Assets/Mirror/Runtime/NetworkReaderPooled.cs.meta +++ b/Assets/Mirror/Core/NetworkReaderPooled.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkReaderPooled.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs similarity index 58% rename from Assets/Mirror/Runtime/NetworkServer.cs rename to Assets/Mirror/Core/NetworkServer.cs index 45d00c4..5df6b9c 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -6,23 +6,62 @@ namespace Mirror { + public enum ReplacePlayerOptions + { + /// Player Object remains active on server and clients. Ownership is not removed + KeepAuthority, + /// Player Object remains active on server and clients. Only ownership is removed + KeepActive, + /// Player Object is unspawned on clients but remains on server + Unspawn, + /// Player Object is destroyed on server and clients + Destroy + } + + public enum RemovePlayerOptions + { + /// Player Object remains active on server and clients. Only ownership is removed + KeepActive, + /// Player Object is unspawned on clients but remains on server + Unspawn, + /// Player Object is destroyed on server and clients + Destroy + } + /// NetworkServer handles remote connections and has a local connection for a local client. - public static class NetworkServer + public static partial class NetworkServer { static bool initialized; public static int maxConnections; - /// Connection to host mode client (if any) - public static NetworkConnectionToClient localConnection { get; private set; } + /// Server 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. + // overwritten by NetworkManager (if any) + public static int tickRate = 60; + + // tick rate is in Hz. + // convert to interval in seconds for convenience where needed. + // + // send interval is 1 / sendRate. + // but for tests we need a way to set it to exactly 0. + // 1 / int.max would not be exactly 0, so handel that manually. + public static float tickInterval => tickRate < int.MaxValue ? 1f / tickRate : 0; // for 30 Hz, that's 33ms + + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + public static int sendRate => tickRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; - /// True is a local client is currently active on the server - public static bool localClientActive => localConnection != null; + /// Connection to host mode client (if any) + public static LocalConnectionToClient localConnection { get; private set; } /// Dictionary of all server connections, with connectionId as key public static Dictionary connections = new Dictionary(); - /// Message Handlers dictionary, with mesageId as key + /// Message Handlers dictionary, with messageId as key internal static Dictionary handlers = new Dictionary(); @@ -31,19 +70,45 @@ public static class NetworkServer public static readonly Dictionary spawned = new Dictionary(); - /// Single player mode can use dontListen to not accept incoming connections - // see also: https://github.com/vis2k/Mirror/pull/2595 - public static bool dontListen; + /// Single player mode can set listen=false to not accept incoming connections. + public static bool listen; + + // DEPRECATED 2024-10-14 + [Obsolete("NetworkServer.dontListen was replaced with NetworkServer.listen. The new value is the opposite, and avoids double negatives like 'dontListen=false'")] + public static bool dontListen + { + get => !listen; + set => listen = !value; + } - /// active checks if the server has been started + /// active checks if the server has been started either has standalone or as host server. public static bool active { get; internal set; } + /// active checks if the server has been started in host mode. + // naming consistent with NetworkClient.activeHost. + public static bool activeHost => localConnection != null; + // scene loading public static bool isLoadingScene; // interest management component (optional) // by default, everyone observes everyone - public static InterestManagement aoi; + public static InterestManagementBase aoi; + + // 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 + // exploits. + // + // However, some games may want to allow exceptions in order to not + // interrupt the player's experience. + public static bool exceptionsDisconnect = true; // security by default + + // Mirror global disconnect inactive option, independent of Transport. + // not all Transports do this properly, and it's easiest to configure this just once. + // this is very useful for some projects, keep it. + public static bool disconnectInactiveConnections; + public static float disconnectInactiveTimeout = 60; // OnConnected / OnDisconnected used to be NetworkMessages that were // invoked. this introduced a bug where external clients could send @@ -52,7 +117,57 @@ public static class NetworkServer // => public so that custom NetworkManagers can hook into it public static Action OnConnectedEvent; public static Action OnDisconnectedEvent; - public static Action OnErrorEvent; + public static Action OnErrorEvent; + public static Action OnTransportExceptionEvent; + + // keep track of actual achieved tick rate. + // might become lower under heavy load. + // very useful for profiling etc. + // measured over 1s each, same as frame rate. no EMA here. + public static int actualTickRate; + static double actualTickRateStart; // start time when counting + static int actualTickRateCounter; // current counter since start + + // profiling + // includes transport update time, because transport calls handlers etc. + // averaged over 1s by passing 'tickRate' to constructor. + public static TimeSample earlyUpdateDuration; + public static TimeSample lateUpdateDuration; + + // capture full Unity update time from before Early- to after LateUpdate + public static TimeSample fullUpdateDuration; + + /// Starts server and listens to incoming connections with max connections limit. + public static void Listen(int maxConns) + { + Initialize(); + maxConnections = maxConns; + + // only start server if we want to listen + if (listen) + { + Transport.active.ServerStart(); + + if (Transport.active is PortTransport portTransport) + { + if (Utils.IsHeadless()) + { +#if !UNITY_EDITOR + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Server listening on port {portTransport.Port}"); + Console.ResetColor(); +#else + Debug.Log($"Server listening on port {portTransport.Port}"); +#endif + } + } + else + Debug.Log("Server started listening"); + } + + active = true; + RegisterMessageHandlers(); + } // initialization / shutdown /////////////////////////////////////////// static void Initialize() @@ -60,6 +175,15 @@ static void Initialize() if (initialized) return; + // safety: ensure Weaving succeded. + // if it silently failed, we would get lots of 'writer not found' + // and other random errors at runtime instead. this is cleaner. + if (!WeaverFuse.Weaved()) + { + // if it failed, throw an exception to early exit all Listen calls. + throw new Exception("NetworkServer won't start because Weaving failed or didn't run."); + } + // Debug.Log($"NetworkServer Created version {Version.Current}"); //Make sure connections are cleared in case any old connections references exist from previous sessions @@ -67,102 +191,33 @@ static void Initialize() // reset Interest Management so that rebuild intervals // start at 0 when starting again. - if (aoi != null) aoi.Reset(); + if (aoi != null) aoi.ResetState(); // reset NetworkTime NetworkTime.ResetStatics(); - Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.activeTransport' first"); + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.active' first"); AddTransportHandlers(); initialized = true; + + // profiling + earlyUpdateDuration = new TimeSample(sendRate); + lateUpdateDuration = new TimeSample(sendRate); + fullUpdateDuration = new TimeSample(sendRate); } static void AddTransportHandlers() { // += so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnServerConnected += OnTransportConnected; - Transport.activeTransport.OnServerDataReceived += OnTransportData; - Transport.activeTransport.OnServerDisconnected += OnTransportDisconnected; - Transport.activeTransport.OnServerError += OnError; - } - - static void RemoveTransportHandlers() - { - // -= so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnServerConnected -= OnTransportConnected; - Transport.activeTransport.OnServerDataReceived -= OnTransportData; - Transport.activeTransport.OnServerDisconnected -= OnTransportDisconnected; - Transport.activeTransport.OnServerError -= OnError; - } - - // calls OnStartClient for all SERVER objects in host mode once. - // client doesn't get spawn messages for those, so need to call manually. - public static void ActivateHostScene() - { - foreach (NetworkIdentity identity in spawned.Values) - { - if (!identity.isClient) - { - // Debug.Log($"ActivateHostScene {identity.netId} {identity}"); - identity.OnStartClient(); - } - } - } - - internal static void RegisterMessageHandlers() - { - RegisterHandler(OnClientReadyMessage); - RegisterHandler(OnCommandMessage); - RegisterHandler(NetworkTime.OnServerPing, false); - } - - /// Starts server and listens to incoming connections with max connections limit. - public static void Listen(int maxConns) - { - Initialize(); - maxConnections = maxConns; - - // only start server if we want to listen - if (!dontListen) - { - Transport.activeTransport.ServerStart(); - //Debug.Log("Server started listening"); - } - - active = true; - RegisterMessageHandlers(); - } - - // Note: NetworkClient.DestroyAllClientObjects does the same on client. - static void CleanupSpawned() - { - // iterate a COPY of spawned. - // DestroyObject removes them from the original collection. - // removing while iterating is not allowed. - foreach (NetworkIdentity identity in spawned.Values.ToList()) - { - if (identity != null) - { - // scene object - if (identity.sceneId != 0) - { - // spawned scene objects are unspawned and reset. - // afterwards we disable them again. - // (they always stay in the scene, we don't destroy them) - DestroyObject(identity, DestroyMode.Reset); - identity.gameObject.SetActive(false); - } - // spawned prefabs - else - { - // spawned prefabs are unspawned and destroyed. - DestroyObject(identity, DestroyMode.Destroy); - } - } - } - - spawned.Clear(); +#pragma warning disable CS0618 // Type or member is obsolete + Transport.active.OnServerConnected += OnTransportConnected; +#pragma warning restore CS0618 // Type or member is obsolete + Transport.active.OnServerConnectedWithAddress += OnTransportConnectedWithAddress; + Transport.active.OnServerDataReceived += OnTransportData; + Transport.active.OnServerDisconnected += OnTransportDisconnected; + Transport.active.OnServerError += OnTransportError; + Transport.active.OnServerTransportException += OnTransportException; } /// Shuts down the server and disconnects all clients @@ -183,7 +238,7 @@ public static void Shutdown() // someone might enabled dontListen at runtime. // but we still need to stop the server. // fixes https://github.com/vis2k/Mirror/issues/2536 - Transport.activeTransport.ServerStop(); + Transport.active.ServerStop(); // transport handlers are hooked into when initializing. // so only remove them when shutting down. @@ -193,19 +248,24 @@ public static void Shutdown() } // Reset all statics here.... - dontListen = false; - active = false; + listen = true; isLoadingScene = false; + lastSendTime = 0; + actualTickRate = 0; localConnection = null; connections.Clear(); connectionsCopy.Clear(); handlers.Clear(); - newObservers.Clear(); - // this calls spawned.Clear() + // destroy all spawned objects, _then_ set inactive. + // make sure .active is still true before calling this. + // otherwise modifying SyncLists in OnStopServer would throw + // because .IsWritable() check checks if NetworkServer.active. + // https://github.com/MirrorNetworking/Mirror/issues/3344 CleanupSpawned(); + active = false; // sets nextNetworkId to 1 // sets clientAuthorityCallback to null @@ -217,8 +277,185 @@ public static void Shutdown() OnConnectedEvent = null; OnDisconnectedEvent = null; OnErrorEvent = null; + OnTransportExceptionEvent = null; + + if (aoi != null) aoi.ResetState(); + } + + static void RemoveTransportHandlers() + { + // -= so that other systems can also hook into it (i.e. statistics) +#pragma warning disable CS0618 // Type or member is obsolete + Transport.active.OnServerConnected -= OnTransportConnected; +#pragma warning restore CS0618 // Type or member is obsolete + Transport.active.OnServerConnectedWithAddress -= OnTransportConnectedWithAddress; + Transport.active.OnServerDataReceived -= OnTransportData; + Transport.active.OnServerDisconnected -= OnTransportDisconnected; + Transport.active.OnServerError -= OnTransportError; + } + + // Note: NetworkClient.DestroyAllClientObjects does the same on client. + static void CleanupSpawned() + { + // iterate a COPY of spawned. + // DestroyObject removes them from the original collection. + // removing while iterating is not allowed. + foreach (NetworkIdentity identity in spawned.Values.ToList()) + { + if (identity != null) + { + // NetworkServer.Destroy resets if scene object, destroys if prefab. + Destroy(identity.gameObject); + } + } + + spawned.Clear(); + } + + internal static void RegisterMessageHandlers() + { + RegisterHandler(OnClientReadyMessage); + RegisterHandler(OnCommandMessage); + RegisterHandler(NetworkTime.OnServerPing, false); + RegisterHandler(NetworkTime.OnServerPong, false); + RegisterHandler(OnEntityStateMessage, true); + RegisterHandler(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through + } + + // remote calls //////////////////////////////////////////////////////// + // Handle command from specific player, this could be one of multiple + // players on a single client + // default ready handler. + static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) + { + // Debug.Log($"Default handler for ready message from {conn}"); + SetClientReady(conn); + } + + static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId) + { + if (!conn.isReady) + { + // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning. + // Ignore commands that may have been in flight before client received NotReadyMessage message. + // Unreliable messages may be out of order, so don't spam warnings for those. + if (channelId == Channels.Reliable) + { + // Attempt to identify the target object, component, and method to narrow down the cause of the error. + if (spawned.TryGetValue(msg.netId, out NetworkIdentity netIdentity)) + if (msg.componentIndex < netIdentity.NetworkBehaviours.Length && netIdentity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component) + if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName)) + { + Debug.LogWarning($"Command {methodName} received for {netIdentity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] when client not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } + + if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string method)) + { + Debug.LogWarning($"Command {method} received from {conn} when client was not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } + + Debug.LogWarning($"Command received from {conn} while client is not ready.\nThis may be ignored if client intentionally set NotReady."); + } + return; + } + + if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + // over reliable channel, commands should always come after spawn. + // over unreliable, they might come in before the object was spawned. + // for example, NetworkTransform. + // let's not spam the console for unreliable out of order messages. + if (channelId == Channels.Reliable) + Debug.LogWarning($"Spawned object not found when handling Command message netId={msg.netId}"); + return; + } + + // Commands can be for player objects, OR other objects with client-authority + // -> so if this connection's controller has a different netId then + // only allow the command if clientAuthorityOwner + bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash); + if (requiresAuthority && identity.connectionToClient != conn) + { + // Attempt to identify the component and method to narrow down the cause of the error. + if (msg.componentIndex < identity.NetworkBehaviours.Length && identity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component) + if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName)) + { + Debug.LogWarning($"Command {methodName} received for {identity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] without authority"); + return; + } + + Debug.LogWarning($"Command received for {identity.name} [netId={msg.netId}] without authority"); + return; + } + + // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); + + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) + identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn); + } + + // client to server broadcast ////////////////////////////////////////// + // for client's owned ClientToServer components. + static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message) + { + // 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) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + if (!identity.DeserializeServer(reader)) + { + if (exceptionsDisconnect) + { + Debug.LogError($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"Server failed to deserialize client 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. don't spam server logs. + // 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. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. + + // TODO validation? + // maybe we shouldn't allow timeline to deviate more than a certain %. + // for now, this is only used for client authority movement. - if (aoi != null) aoi.Reset(); + // Unity 2019 doesn't have Time.timeAsDouble yet + // + // NetworkTime uses unscaled time and ignores Time.timeScale. + // fixes Time.timeScale getting server & client time out of sync: + // https://github.com/MirrorNetworking/Mirror/issues/3409 + connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); } // connections ///////////////////////////////////////////////////////// @@ -264,11 +501,6 @@ internal static void RemoveLocalConnection() RemoveConnection(0); } - /// True if we have no external connections (host is allowed) - // DEPRECATED 2022-02-05 - [Obsolete("Use !HasExternalConnections() instead of NoExternalConnections() to avoid double negatives.")] - public static bool NoExternalConnections() => !HasExternalConnections(); - /// True if we have external connections (that are not host) public static bool HasExternalConnections() { @@ -300,9 +532,21 @@ public static void SendToAll(T message, int channelId = Channels.Reliable, bo using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkServer.SendToAll: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + // filter and then send to all internet connections at once // -> makes code more complicated, but is HIGHLY worth it to // avoid allocations, allow for multicast, etc. @@ -340,16 +584,28 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI where T : struct, NetworkMessage { // Debug.Log($"Server.SendToObservers {typeof(T)}"); - if (identity == null || identity.observers == null || identity.observers.Count == 0) + if (identity == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message into byte[] once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); - foreach (NetworkConnection conn in identity.observers.Values) + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkServer.SendToObservers: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + + foreach (NetworkConnectionToClient conn in identity.observers.Values) { conn.Send(segment, channelId); } @@ -359,22 +615,34 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI } /// Send a message to only clients which are ready with option to include the owner of the object identity - // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + // TODO obsolete this later. it's not used anymore public static void SendToReadyObservers(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) where T : struct, NetworkMessage { // Debug.Log($"Server.SendToReady {typeof(T)}"); - if (identity == null || identity.observers == null || identity.observers.Count == 0) + if (identity == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); + // validate packet size immediately. + // we know how much can fit into one batch at max. + // if it's larger, log an error immediately with the type . + // previously we only logged in Update() when processing batches, + // but there we don't have type information anymore. + int max = NetworkMessages.MaxMessageSize(channelId); + if (writer.Position > max) + { + Debug.LogError($"NetworkServer.SendToReadyObservers: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller."); + return; + } + int count = 0; - foreach (NetworkConnection conn in identity.observers.Values) + foreach (NetworkConnectionToClient conn in identity.observers.Values) { bool isOwner = conn == identity.connectionToClient; if ((!isOwner || includeOwner) && conn.isReady) @@ -388,48 +656,57 @@ public static void SendToReadyObservers(NetworkIdentity identity, T message, } } - // Deprecated 2021-09-19 - [Obsolete("SendToReady(identity, message, ...) was renamed to SendToReadyObservers because that's what it does.")] - public static void SendToReady(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) - where T : struct, NetworkMessage => - SendToReadyObservers(identity, message, includeOwner, channelId); - /// Send a message to only clients which are ready including the owner of the NetworkIdentity - // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + // TODO obsolete this later. it's not used anymore public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) where T : struct, NetworkMessage { SendToReadyObservers(identity, message, true, channelId); } - // Deprecated 2021-09-19 - [Obsolete("SendToReady(identity, message, ...) was renamed to SendToReadyObservers because that's what it does.")] - public static void SendToReady(NetworkIdentity identity, T message, int channelId) - where T : struct, NetworkMessage => - SendToReadyObservers(identity, message, channelId); - // transport events //////////////////////////////////////////////////// // called by transport static void OnTransportConnected(int connectionId) + => OnTransportConnectedWithAddress(connectionId, Transport.active.ServerGetClientAddress(connectionId)); + + static void OnTransportConnectedWithAddress(int connectionId, string clientAddress) + { + if (IsConnectionAllowed(connectionId, clientAddress)) + { + // create a connection + NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId, clientAddress); + OnConnected(conn); + } + else + { + // kick the client immediately + Transport.active.ServerDisconnect(connectionId); + } + } + + static bool IsConnectionAllowed(int connectionId, string address) { - // Debug.Log($"Server accepted client:{connectionId}"); + // only accept connections while listening + if (!listen) + { + Debug.Log($"Server not listening, rejecting connectionId={connectionId} with address={address}"); + return false; + } // connectionId needs to be != 0 because 0 is reserved for local player // note that some transports like kcp generate connectionId by // hashing which can be < 0 as well, so we need to allow < 0! if (connectionId == 0) { - Debug.LogError($"Server.HandleConnect: invalid connectionId: {connectionId} . Needs to be != 0, because 0 is reserved for local player."); - Transport.activeTransport.ServerDisconnect(connectionId); - return; + Debug.LogError($"Server.HandleConnect: invalid connectionId={connectionId}. Needs to be != 0, because 0 is reserved for local player."); + return false; } // connectionId not in use yet? if (connections.ContainsKey(connectionId)) { - Transport.activeTransport.ServerDisconnect(connectionId); - // Debug.Log($"Server connectionId {connectionId} already in use...kicked client"); - return; + Debug.LogError($"Server connectionId={connectionId} already in use. Client with address={address} will be kicked"); + return false; } // are more connections allowed? if not, kick @@ -437,18 +714,13 @@ static void OnTransportConnected(int connectionId) // less code and third party transport might not do that anyway) // (this way we could also send a custom 'tooFull' message later, // Transport can't do that) - if (connections.Count < maxConnections) - { - // add connection - NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId); - OnConnected(conn); - } - else + if (connections.Count >= maxConnections) { - // kick - Transport.activeTransport.ServerDisconnect(connectionId); - // Debug.Log($"Server full, kicked client {connectionId}"); + Debug.LogError($"Server full, client connectionId={connectionId} with address={address} will be kicked"); + return false; } + + return true; } internal static void OnConnected(NetworkConnectionToClient conn) @@ -462,7 +734,7 @@ internal static void OnConnected(NetworkConnectionToClient conn) static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId) { - if (MessagePacking.Unpack(reader, out ushort msgType)) + if (NetworkMessages.UnpackId(reader, out ushort msgType)) { // try to invoke the handler for that message if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) @@ -505,8 +777,14 @@ internal static void OnTransportData(int connectionId, ArraySegment data, // always process all messages in the batch. if (!connection.unbatcher.AddBatch(data)) { - Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id)"); - connection.Disconnect(); + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id)."); + return; } @@ -521,38 +799,52 @@ internal static void OnTransportData(int connectionId, ArraySegment data, // the next time. // => consider moving processing to NetworkEarlyUpdate. while (!isLoadingScene && - connection.unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) + connection.unbatcher.GetNextMessage(out ArraySegment message, out double remoteTimestamp)) { - // enough to read at least header size? - if (reader.Remaining >= MessagePacking.HeaderSize) + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message)) { - // make remoteTimeStamp available to the user - connection.remoteTimeStamp = remoteTimestamp; - - // handle message - if (!UnpackAndInvoke(connection, reader, channelId)) + // enough to read at least header size? + if (reader.Remaining >= NetworkMessages.IdSize) { - // warn, disconnect and return if failed - // -> warning because attackers might send random data - // -> messages in a batch aren't length prefixed. - // failing to read one would cause undefined - // behaviour for every message afterwards. - // so we need to disconnect. - // -> return to avoid the below unbatches.count error. - // we already disconnected and handled it. - Debug.LogWarning($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); - connection.Disconnect(); + // make remoteTimeStamp available to the user + connection.remoteTimeStamp = remoteTimestamp; + + // handle message + if (!UnpackAndInvoke(connection, reader, channelId)) + { + // warn, disconnect and return if failed + // -> warning because attackers might send random data + // -> messages in a batch aren't length prefixed. + // failing to read one would cause undefined + // behaviour for every message afterwards. + // so we need to disconnect. + // -> return to avoid the below unbatches.count error. + // we already disconnected and handled it. + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: failed to unpack and invoke message from connectionId:{connectionId}."); + + return; + } + } + // otherwise disconnect + else + { + if (exceptionsDisconnect) + { + Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting."); + connection.Disconnect(); + } + else + Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id)."); + return; } } - // otherwise disconnect - else - { - // WARNING, not error. can happen if attacker sends random data. - Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id). Disconnecting {connectionId}"); - connection.Disconnect(); - return; - } } // if we weren't interrupted by a scene change, @@ -589,6 +881,7 @@ internal static void OnTransportDisconnected(int connectionId) // Debug.Log($"Server disconnect client:{connectionId}"); if (connections.TryGetValue(connectionId, out NetworkConnectionToClient conn)) { + conn.Cleanup(); RemoveConnection(connectionId); // Debug.Log($"Server lost client:{connectionId}"); @@ -607,12 +900,43 @@ internal static void OnTransportDisconnected(int connectionId) } } - static void OnError(int connectionId, Exception exception) + // transport errors are forwarded to high level + static void OnTransportError(int connectionId, TransportError error, string reason) + { + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Server Transport Error for connId={connectionId}: {error}: {reason}. This is fine."); + // try get connection. passes null otherwise. + connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); + OnErrorEvent?.Invoke(conn, error, reason); + } + + // transport errors are forwarded to high level + static void OnTransportException(int connectionId, Exception exception) { - Debug.LogException(exception); + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Server Transport Exception for connId={connectionId}: {exception}"); // try get connection. passes null otherwise. connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); - OnErrorEvent?.Invoke(conn, exception); + OnTransportExceptionEvent?.Invoke(conn, exception); + } + + /// Destroys all of the connection's owned objects on the server. + // This is used when a client disconnects, to remove the players for + // that client. This also destroys non-player objects that have client + // authority set for this connection. + public static void DestroyPlayerForConnection(NetworkConnectionToClient conn) + { + // destroy all objects owned by this connection, including the player object + conn.DestroyOwnedObjects(); + // remove connection from all of its observing entities observers + // fixes https://github.com/vis2k/Mirror/issues/2737 + // -> cleaning those up in NetworkConnection.Disconnect is NOT enough + // because voluntary disconnects from the other end don't call + // NetworkConnection.Disconnect() + conn.RemoveFromObservingsObservers(); + conn.identity = null; } // message handlers //////////////////////////////////////////////////// @@ -622,12 +946,16 @@ static void OnError(int connectionId, Exception exception) public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessageId.Id; if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } /// Register a handler for message type T. Most should require authentication. @@ -635,34 +963,54 @@ public static void RegisterHandler(Action handl public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessageId.Id; if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); + } + + /// Replace a handler for message type T. Most should require authentication. + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ReplaceHandler((_, value) => { handler(value); }, requireAuthentication); } /// Replace a handler for message type T. Most should require authentication. public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } /// Replace a handler for message type T. Most should require authentication. - public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ReplaceHandler((_, value) => { handler(value); }, requireAuthentication); + ushort msgType = NetworkMessageId.Id; + + // register Id <> Type in lookup for debugging. + NetworkMessages.Lookup[msgType] = typeof(T); + + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect); } /// Unregister a handler for a message type T. public static void UnregisterHandler() where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessageId.Id; handlers.Remove(msgType); } @@ -671,8 +1019,7 @@ public static void UnregisterHandler() internal static bool GetNetworkIdentity(GameObject go, out NetworkIdentity identity) { - identity = go.GetComponent(); - if (identity == null) + if (!go.TryGetComponent(out identity)) { Debug.LogError($"GameObject {go.name} doesn't have NetworkIdentity."); return false; @@ -693,7 +1040,7 @@ public static void DisconnectAll() // we are iterating here. // see also: https://github.com/vis2k/Mirror/issues/2357 // this whole process should be simplified some day. - // until then, let's copy .Values to avoid InvalidOperatinException. + // until then, let's copy .Values to avoid InvalidOperationException. // note that this is only called when stopping the server, so the // copy is no performance problem. foreach (NetworkConnectionToClient conn in connections.Values.ToList()) @@ -716,10 +1063,30 @@ public static void DisconnectAll() // cleanup connections.Clear(); localConnection = null; - active = false; + // this used to set active=false. + // however, then Shutdown can't properly destroy objects: + // https://github.com/MirrorNetworking/Mirror/issues/3344 + // "DisconnectAll" should only disconnect all, not set inactive. + // active = false; } // add/remove/replace player /////////////////////////////////////////// + /// Called by server after AddPlayer message to add the player for the connection. + // When a player is added for a connection, the client for that + // connection is made ready automatically. The player object is + // automatically spawned, so you do not need to call NetworkServer.Spawn + // for that object. This function is used for "adding" a player, not for + // "replacing" the player on a connection. If there is already a player + // on this playerControllerId for this connection, this will fail. + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return AddPlayerForConnection(conn, player); + } + /// Called by server after AddPlayer message to add the player for the connection. // When a player is added for a connection, the client for that // connection is made ready automatically. The player object is @@ -729,10 +1096,9 @@ public static void DisconnectAll() // on this playerControllerId for this connection, this will fail. public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player) { - NetworkIdentity identity = player.GetComponent(); - if (identity == null) + if (!player.TryGetComponent(out NetworkIdentity identity)) { - Debug.LogWarning($"AddPlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); + Debug.LogWarning($"AddPlayer: player GameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); return false; } @@ -753,7 +1119,7 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.hasAuthority = true; + identity.isOwned = true; NetworkClient.InternalAddPlayer(identity); } @@ -766,29 +1132,38 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb return true; } - /// Called by server after AddPlayer message to add the player for the connection. - // When a player is added for a connection, the client for that - // connection is made ready automatically. The player object is - // automatically spawned, so you do not need to call NetworkServer.Spawn - // for that object. This function is used for "adding" a player, not for - // "replacing" the player on a connection. If there is already a player - // on this playerControllerId for this connection, this will fail. - public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId) + // Deprecated 2024-008-09 + [Obsolete("Use ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, ReplacePlayerOptions replacePlayerOptions) instead")] + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, bool keepAuthority = false) { if (GetNetworkIdentity(player, out NetworkIdentity identity)) - { identity.assetId = assetId; - } - return AddPlayerForConnection(conn, player); + + return ReplacePlayerForConnection(conn, player, keepAuthority ? ReplacePlayerOptions.KeepAuthority : ReplacePlayerOptions.KeepActive); } - /// Replaces connection's player object. The old object is not destroyed. - // This does NOT change the ready state of the connection, so it can - // safely be used while changing scenes. + // Deprecated 2024-008-09 + [Obsolete("Use ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, ReplacePlayerOptions replacePlayerOptions) instead")] public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, bool keepAuthority = false) { - NetworkIdentity identity = player.GetComponent(); - if (identity == null) + return ReplacePlayerForConnection(conn, player, keepAuthority ? ReplacePlayerOptions.KeepAuthority : ReplacePlayerOptions.KeepActive); + } + + /// Replaces connection's player object. The old object is not destroyed. + // This does NOT change the ready state of the connection, so it can safely be used while changing scenes. + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, ReplacePlayerOptions replacePlayerOptions) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + identity.assetId = assetId; + + return ReplacePlayerForConnection(conn, player, replacePlayerOptions); + } + + /// Replaces connection's player object. The old object is not destroyed. + // This does NOT change the ready state of the connection, so it can safely be used while changing scenes. + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, ReplacePlayerOptions replacePlayerOptions) + { + if (!player.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError($"ReplacePlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); return false; @@ -813,7 +1188,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.hasAuthority = true; + identity.isOwned = true; NetworkClient.InternalAddPlayer(identity); } @@ -828,31 +1203,61 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga Respawn(identity); - if (keepAuthority) - { - // This needs to be sent to clear isLocalPlayer on - // client while keeping hasAuthority true - SendChangeOwnerMessage(previousPlayer, conn); - } - else - { - // This clears both isLocalPlayer and hasAuthority on client - previousPlayer.RemoveClientAuthority(); + switch (replacePlayerOptions) + { + case ReplacePlayerOptions.KeepAuthority: + // This needs to be sent to clear isLocalPlayer on + // client while keeping hasAuthority true + SendChangeOwnerMessage(previousPlayer, conn); + break; + case ReplacePlayerOptions.KeepActive: + // This clears both isLocalPlayer and hasAuthority on client + previousPlayer.RemoveClientAuthority(); + break; + case ReplacePlayerOptions.Unspawn: + UnSpawn(previousPlayer.gameObject); + break; + case ReplacePlayerOptions.Destroy: + Destroy(previousPlayer.gameObject); + break; } return true; } - /// Replaces connection's player object. The old object is not destroyed. - // This does NOT change the ready state of the connection, so it can - // safely be used while changing scenes. - public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId, bool keepAuthority = false) + /// Removes the player object from the connection + // destroyServerObject: Indicates whether the server object should be destroyed + // Deprecated 2024-06-06 + [Obsolete("Use RemovePlayerForConnection(NetworkConnectionToClient conn, RemovePlayerOptions removeOptions) instead")] + public static void RemovePlayerForConnection(NetworkConnectionToClient conn, bool destroyServerObject) { - if (GetNetworkIdentity(player, out NetworkIdentity identity)) + if (destroyServerObject) + RemovePlayerForConnection(conn, RemovePlayerOptions.Destroy); + else + RemovePlayerForConnection(conn, RemovePlayerOptions.Unspawn); + } + + /// Removes player object for the connection. Options to keep the object in play, unspawn it, or destroy it. + public static void RemovePlayerForConnection(NetworkConnectionToClient conn, RemovePlayerOptions removeOptions = RemovePlayerOptions.KeepActive) + { + if (conn.identity == null) return; + + switch (removeOptions) { - identity.assetId = assetId; + case RemovePlayerOptions.KeepActive: + conn.identity.connectionToClient = null; + conn.owned.Remove(conn.identity); + SendChangeOwnerMessage(conn.identity, conn); + break; + case RemovePlayerOptions.Unspawn: + UnSpawn(conn.identity.gameObject); + break; + case RemovePlayerOptions.Destroy: + Destroy(conn.identity.gameObject); + break; } - return ReplacePlayerForConnection(conn, player, keepAuthority); + + conn.identity = null; } // ready /////////////////////////////////////////////////////////////// @@ -875,6 +1280,73 @@ public static void SetClientReady(NetworkConnectionToClient conn) SpawnObserversForConnection(conn); } + static void SpawnObserversForConnection(NetworkConnectionToClient conn) + { + //Debug.Log($"Spawning {spawned.Count} objects for conn {conn}"); + + if (!conn.isReady) + { + // client needs to finish initializing before we can spawn objects + // otherwise it would not find them. + return; + } + + // let connection know that we are about to start spawning... + conn.Send(new ObjectSpawnStartedMessage()); + + // add connection to each nearby NetworkIdentity's observers, which + // internally sends a spawn message for each one to the connection. + foreach (NetworkIdentity identity in spawned.Values) + { + // try with far away ones in ummorpg! + if (identity.gameObject.activeSelf) //TODO this is different + { + //Debug.Log($"Sending spawn message for current server objects name:{identity.name} netId:{identity.netId} sceneId:{identity.sceneId:X}"); + + // we need to support three cases: + // - legacy system (identity has .visibility) + // - new system (networkserver has .aoi) + // - default case: no .visibility and no .aoi means add all + // connections by default) + // + // ForceHidden/ForceShown overwrite all systems so check it + // first! + + // ForceShown: add no matter what + if (identity.visibility == Visibility.ForceShown) + { + identity.AddObserver(conn); + } + // ForceHidden: don't show no matter what + else if (identity.visibility == Visibility.ForceHidden) + { + // do nothing + } + // default: legacy system / new system / no system support + else if (identity.visibility == Visibility.Default) + { + // aoi system + if (aoi != null) + { + // call OnCheckObserver + if (aoi.OnCheckObserver(identity, conn)) + identity.AddObserver(conn); + } + // no system: add all observers by default + else + { + identity.AddObserver(conn); + } + } + } + } + + // let connection know that we finished spawning, so it can call + // OnStartClient on each one (only after all were spawned, which + // is how Unity's Start() function works too) + conn.Send(new ObjectSpawnFinishedMessage()); + } + /// Marks the client of the connection to be not-ready. // Clients that are not ready do not receive spawned objects or state // synchronization updates. They client can be made ready again by @@ -898,21 +1370,14 @@ public static void SetAllClientsNotReady() } } - // default ready handler. - static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) - { - // Debug.Log($"Default handler for ready message from {conn}"); - SetClientReady(conn); - } - // 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 { @@ -921,68 +1386,38 @@ internal static void HideForConnection(NetworkIdentity identity, NetworkConnecti conn.Send(msg); } - /// Removes the player object from the connection - // destroyServerObject: Indicates whether the server object should be destroyed - public static void RemovePlayerForConnection(NetworkConnection conn, bool destroyServerObject) - { - if (conn.identity != null) - { - if (destroyServerObject) - Destroy(conn.identity.gameObject); - else - UnSpawn(conn.identity.gameObject); - - conn.identity = null; - } - //else Debug.Log($"Connection {conn} has no identity"); - } - - // remote calls //////////////////////////////////////////////////////// - // Handle command from specific player, this could be one of multiple - // players on a single client - static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId) + // spawning //////////////////////////////////////////////////////////// + internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnectionToClient conn) { - if (!conn.isReady) - { - // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning. - // Ignore commands that may have been in flight before client received NotReadyMessage message. - // Unreliable messages may be out of order, so don't spam warnings for those. - if (channelId == Channels.Reliable) - Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady."); - return; - } + if (identity.serverOnly) return; - if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) - { - // over reliable channel, commands should always come after spawn. - // over unreliable, they might come in before the object was spawned. - // for example, NetworkTransform. - // let's not spam the console for unreliable out of order messages. - if (channelId == Channels.Reliable) - Debug.LogWarning($"Spawned object not found when handling Command message [netId={msg.netId}]"); - return; - } + //Debug.Log($"Server SendSpawnMessage: name:{identity.name} sceneId:{identity.sceneId:X} netid:{identity.netId}"); - // Commands can be for player objects, OR other objects with client-authority - // -> so if this connection's controller has a different netId then - // only allow the command if clientAuthorityOwner - bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash); - if (requiresAuthority && identity.connectionToClient != conn) + // one writer for owner, one for observers + using (NetworkWriterPooled ownerWriter = NetworkWriterPool.Get(), observersWriter = NetworkWriterPool.Get()) { - Debug.LogWarning($"Command for object without authority [netId={msg.netId}]"); - return; + bool isOwner = identity.connectionToClient == conn; + ArraySegment payload = CreateSpawnMessagePayload(isOwner, identity, ownerWriter, observersWriter); + SpawnMessage message = new SpawnMessage + { + netId = identity.netId, + isLocalPlayer = conn.identity == identity, + isOwner = isOwner, + sceneId = identity.sceneId, + assetId = identity.assetId, + // use local values for VR support + position = identity.transform.localPosition, + rotation = identity.transform.localRotation, + scale = identity.transform.localScale, + payload = payload + }; + conn.Send(message); } - - // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); - - using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) - identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn as NetworkConnectionToClient); } - // spawning //////////////////////////////////////////////////////////// static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) { - // Only call OnSerializeAllSafely if there are NetworkBehaviours + // Only call SerializeAll if there are NetworkBehaviours if (identity.NetworkBehaviours.Length == 0) { return default; @@ -990,7 +1425,7 @@ static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentit // serialize all components with initialState = true // (can be null if has none) - identity.OnSerializeAllSafely(true, ownerWriter, observersWriter); + identity.SerializeServer(true, ownerWriter, observersWriter); // convert to ArraySegment to avoid reader allocations // if nothing was written, .ToArraySegment returns an empty segment. @@ -1004,124 +1439,94 @@ static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentit return payload; } - internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn) + internal static void SendChangeOwnerMessage(NetworkIdentity identity, NetworkConnectionToClient conn) { - if (identity.serverOnly) return; + // Don't send if identity isn't spawned or only exists on server + if (identity.netId == 0 || identity.serverOnly) return; - //Debug.Log($"Server SendSpawnMessage: name:{identity.name} sceneId:{identity.sceneId:X} netid:{identity.netId}"); + // Don't send if conn doesn't have the identity spawned yet + // May be excluded from the client by interest management + if (!conn.observing.Contains(identity)) return; - // one writer for owner, one for observers - using (NetworkWriterPooled ownerWriter = NetworkWriterPool.Get(), observersWriter = NetworkWriterPool.Get()) - { - bool isOwner = identity.connectionToClient == conn; - ArraySegment payload = CreateSpawnMessagePayload(isOwner, identity, ownerWriter, observersWriter); - SpawnMessage message = new SpawnMessage - { - netId = identity.netId, - isLocalPlayer = conn.identity == identity, - isOwner = isOwner, - sceneId = identity.sceneId, - assetId = identity.assetId, - // use local values for VR support - position = identity.transform.localPosition, - rotation = identity.transform.localRotation, - scale = identity.transform.localScale, - payload = payload - }; - conn.Send(message); - } - } - - internal static void SendChangeOwnerMessage(NetworkIdentity identity, NetworkConnectionToClient conn) - { - // Don't send if identity isn't spawned or only exists on server - if (identity.netId == 0 || identity.serverOnly) return; - - // Don't send if conn doesn't have the identity spawned yet - // May be excluded from the client by interest management - if (!conn.observing.Contains(identity)) return; - - //Debug.Log($"Server SendChangeOwnerMessage: name={identity.name} netid={identity.netId}"); - - conn.Send(new ChangeOwnerMessage + //Debug.Log($"Server SendChangeOwnerMessage: name={identity.name} netid={identity.netId}"); + + conn.Send(new ChangeOwnerMessage { netId = identity.netId, isOwner = identity.connectionToClient == conn, - isLocalPlayer = conn.identity == identity + isLocalPlayer = (conn.identity == identity && identity.connectionToClient == conn) }); } - static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) - { - // verify if we can spawn this - if (Utils.IsPrefab(obj)) - { - Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first."); - return; - } + // check NetworkIdentity parent before spawning it. + // - without parent, they are spawned + // - with parent, only if the parent is active in hierarchy + // + // note that active parents may have inactive parents of their own. + // we need to check .activeInHierarchy. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + static bool ValidParent(NetworkIdentity identity) => + identity.transform.parent == null || + identity.transform.parent.gameObject.activeInHierarchy; + /// Spawns NetworkIdentities in the scene on the server. + // NetworkIdentity objects in a scene are disabled by default. Calling + // SpawnObjects() causes these scene objects to be enabled and spawned. + // It is like calling NetworkServer.Spawn() for each of them. + public static bool SpawnObjects() + { + // only if server active if (!active) - { - Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server."); - return; - } + return false; - NetworkIdentity identity = obj.GetComponent(); - if (identity == null) - { - Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}"); - return; - } + // find all NetworkIdentities in the scene. + // all of them are disabled because of NetworkScenePostProcess. + NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); - if (identity.SpawnedFromInstantiate) + // first pass: activate all scene objects + foreach (NetworkIdentity identity in identities) { - // Using Instantiate on SceneObject is not allowed, so stop spawning here - // NetworkIdentity.Awake already logs error, no need to log a second error here - return; + // only spawn scene objects which haven't been spawned yet. + // SpawnObjects may be called multiple times for additive scenes. + // https://github.com/MirrorNetworking/Mirror/issues/3318 + // + // note that we even activate objects under inactive parents. + // while they are not spawned, they do need to be activated + // in order to be spawned later. so here, we don't check parents. + // https://github.com/MirrorNetworking/Mirror/issues/3330 + if (Utils.IsSceneObject(identity) && identity.netId == 0) + { + // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); + identity.gameObject.SetActive(true); + } } - identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; - - // special case to make sure hasAuthority is set - // on start server in host mode - if (ownerConnection is LocalConnectionToClient) - identity.hasAuthority = true; - - identity.OnStartServer(); - - // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); - - if (aoi) + // second pass: spawn all scene objects + foreach (NetworkIdentity identity in identities) { - // This calls user code which might throw exceptions - // We don't want this to leave us in bad state - try + // scene objects may be children of inactive parents. + // users would put them under disabled parents to 'deactivate' them. + // those should not be used by Mirror at all. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + if (Utils.IsSceneObject(identity) && identity.netId == 0 && ValidParent(identity)) { - aoi.OnSpawned(identity); - } - catch (Exception e) - { - Debug.LogException(e); + // pass connection so that authority is not lost when server loads a scene + // https://github.com/vis2k/Mirror/pull/2987 + Spawn(identity.gameObject, identity.connectionToClient); } } - RebuildObservers(identity, true); - } - - /// 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) - { - SpawnObject(obj, ownerConnection); + return true; } /// 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, GameObject ownerPlayer) { - NetworkIdentity identity = ownerPlayer.GetComponent(); - if (identity == null) + if (!ownerPlayer.TryGetComponent(out NetworkIdentity identity)) { Debug.LogError("Player object has no NetworkIdentity"); return; @@ -1136,9 +1541,31 @@ public static void Spawn(GameObject obj, GameObject ownerPlayer) Spawn(obj, identity.connectionToClient); } + static void Respawn(NetworkIdentity identity) + { + if (identity.netId == 0) + { + // If the object has not been spawned, then do a full spawn and update observers + Spawn(identity.gameObject, identity.connectionToClient); + } + else + { + // otherwise just replace his data + SendSpawnMessage(identity, identity.connectionToClient); + } + } + + /// 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, 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, Guid assetId, NetworkConnection ownerConnection = null) + public static void Spawn(GameObject obj, uint assetId, NetworkConnectionToClient ownerConnection = null) { if (GetNetworkIdentity(obj, out NetworkIdentity identity)) { @@ -1147,181 +1574,119 @@ public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerCo SpawnObject(obj, ownerConnection); } - internal static bool ValidateSceneObject(NetworkIdentity identity) + static void SpawnObject(GameObject obj, NetworkConnectionToClient ownerConnection) { - if (identity.gameObject.hideFlags == HideFlags.NotEditable || - identity.gameObject.hideFlags == HideFlags.HideAndDontSave) - return false; - -#if UNITY_EDITOR - if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) - return false; -#endif - - // If not a scene object - return identity.sceneId != 0; - } + // verify if we can spawn this + if (Utils.IsPrefab(obj)) + { + Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first.", obj); + return; + } - /// Spawns NetworkIdentities in the scene on the server. - // NetworkIdentity objects in a scene are disabled by default. Calling - // SpawnObjects() causes these scene objects to be enabled and spawned. - // It is like calling NetworkServer.Spawn() for each of them. - public static bool SpawnObjects() - { - // only if server active if (!active) - return false; - - NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); - - // first pass: activate all scene objects - foreach (NetworkIdentity identity in identities) { - if (ValidateSceneObject(identity)) - { - // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); - identity.gameObject.SetActive(true); - - // fix https://github.com/vis2k/Mirror/issues/2778: - // -> SetActive(true) does NOT call Awake() if the parent - // is inactive - // -> we need Awake() to initialize NetworkBehaviours[] etc. - // because our second pass below spawns and works with it - // => detect this situation and manually call Awake for - // proper initialization - if (!identity.gameObject.activeInHierarchy) - identity.Awake(); - } + Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server.", obj); + return; } - // second pass: spawn all scene objects - foreach (NetworkIdentity identity in identities) + if (!obj.TryGetComponent(out NetworkIdentity identity)) { - if (ValidateSceneObject(identity)) - // pass connection so that authority is not lost when server loads a scene - // https://github.com/vis2k/Mirror/pull/2987 - Spawn(identity.gameObject, identity.connectionToClient); + Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}", obj); + return; } - return true; - } - - static void Respawn(NetworkIdentity identity) - { - if (identity.netId == 0) + if (identity.SpawnedFromInstantiate) { - // If the object has not been spawned, then do a full spawn and update observers - Spawn(identity.gameObject, identity.connectionToClient); + // Using Instantiate on SceneObject is not allowed, so stop spawning here + // NetworkIdentity.Awake already logs error, no need to log a second error here + return; } - else + + // Spawn should only be called once per netId. + // calling it twice would lead to undefined behaviour. + // https://github.com/MirrorNetworking/Mirror/pull/3205 + if (spawned.ContainsKey(identity.netId)) { - // otherwise just replace his data - SendSpawnMessage(identity, identity.connectionToClient); + Debug.LogWarning($"{identity.name} [netId={identity.netId}] was already spawned.", identity.gameObject); + return; } - } - static void SpawnObserversForConnection(NetworkConnectionToClient conn) - { - //Debug.Log($"Spawning {spawned.Count} objects for conn {conn}"); + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; - if (!conn.isReady) + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.isOwned = true; + + // NetworkServer.Unspawn sets object as inactive. + // NetworkServer.Spawn needs to set them active again in case they were previously unspawned / inactive. + identity.gameObject.SetActive(true); + + // only call OnStartServer if not spawned yet. + // check used to be in NetworkIdentity. may not be necessary anymore. + if (!identity.isServer && identity.netId == 0) { - // client needs to finish initializing before we can spawn objects - // otherwise it would not find them. - return; + // configure NetworkIdentity + // this may be called in host mode, so we need to initialize + // isLocalPlayer/isClient flags too. + identity.isLocalPlayer = NetworkClient.localPlayer == identity; + identity.isClient = NetworkClient.active; + identity.isServer = true; + identity.netId = NetworkIdentity.GetNextNetworkId(); + + // add to spawned (after assigning netId) + spawned[identity.netId] = identity; + + // callback after all fields were set + identity.OnStartServer(); } - // let connection know that we are about to start spawning... - conn.Send(new ObjectSpawnStartedMessage()); + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); - // add connection to each nearby NetworkIdentity's observers, which - // internally sends a spawn message for each one to the connection. - foreach (NetworkIdentity identity in spawned.Values) + if (aoi) { - // try with far away ones in ummorpg! - if (identity.gameObject.activeSelf) //TODO this is different + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try { - //Debug.Log($"Sending spawn message for current server objects name:{identity.name} netId:{identity.netId} sceneId:{identity.sceneId:X}"); - - // we need to support three cases: - // - legacy system (identity has .visibility) - // - new system (networkserver has .aoi) - // - default case: no .visibility and no .aoi means add all - // connections by default) - // - // ForceHidden/ForceShown overwrite all systems so check it - // first! - - // ForceShown: add no matter what - if (identity.visible == Visibility.ForceShown) - { - identity.AddObserver(conn); - } - // ForceHidden: don't show no matter what - else if (identity.visible == Visibility.ForceHidden) - { - // do nothing - } - // default: legacy system / new system / no system support - else if (identity.visible == Visibility.Default) - { - // aoi system - if (aoi != null) - { - // call OnCheckObserver - if (aoi.OnCheckObserver(identity, conn)) - identity.AddObserver(conn); - } - // no system: add all observers by default - else - { - identity.AddObserver(conn); - } - } + aoi.OnSpawned(identity); + } + catch (Exception e) + { + Debug.LogException(e); } } - // let connection know that we finished spawning, so it can call - // OnStartClient on each one (only after all were spawned, which - // is how Unity's Start() function works too) - conn.Send(new ObjectSpawnFinishedMessage()); + RebuildObservers(identity, true); } - /// This takes an object that has been spawned and un-spawns it. - // The object will be removed from clients that it was spawned on, or - // the custom spawn handler function on the client will be called for - // the object. - // Unlike when calling NetworkServer.Destroy(), on the server the object - // will NOT be destroyed. This allows the server to re-use the object, - // even spawn it again later. - public static void UnSpawn(GameObject obj) => DestroyObject(obj, DestroyMode.Reset); - - // destroy ///////////////////////////////////////////////////////////// - /// Destroys all of the connection's owned objects on the server. - // This is used when a client disconnects, to remove the players for - // that client. This also destroys non-player objects that have client - // authority set for this connection. - public static void DestroyPlayerForConnection(NetworkConnectionToClient conn) + // internal Unspawn function which has the 'resetState' parameter. + // resetState calls .ResetState() on the object after unspawning. + // this is necessary for scene objects, but not for prefabs since we + // don't want to reset their isServer flags etc. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3832 + static void UnSpawnInternal(GameObject obj, bool resetState) { - // destroy all objects owned by this connection, including the player object - conn.DestroyOwnedObjects(); - // remove connection from all of its observing entities observers - // fixes https://github.com/vis2k/Mirror/issues/2737 - // -> cleaning those up in NetworkConnection.Disconnect is NOT enough - // because voluntary disconnects from the other end don't call - // NetworkConnectionn.Disconnect() - conn.RemoveFromObservingsObservers(); - conn.identity = null; - } + // Debug.Log($"DestroyObject instance:{identity.netId}"); - // sometimes we want to GameObject.Destroy it. - // sometimes we want to just unspawn on clients and .Reset() it on server. - // => 'bool destroy' isn't obvious enough. it's really destroy OR reset! - enum DestroyMode { Destroy, Reset } + // NetworkServer.Unspawn should only be called on server or host. + // on client, show a warning to explain what it does. + if (!active) + { + Debug.LogWarning("NetworkServer.Unspawn() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients."); + return; + } - static void DestroyObject(NetworkIdentity identity, DestroyMode mode) - { - // Debug.Log($"DestroyObject instance:{identity.netId}"); + if (obj == null) + { + Debug.Log("NetworkServer.Unspawn(): object is null"); + return; + } + + if (!GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + return; + } // only call OnRebuildObservers while active, // not while shutting down @@ -1346,12 +1711,18 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) identity.connectionToClient?.RemoveOwnedObject(identity); // send object destroy message to all observers, clear observers - SendToObservers(identity, new ObjectDestroyMessage{netId = identity.netId}); + SendToObservers(identity, new ObjectDestroyMessage + { + netId = identity.netId + }); identity.ClearObservers(); // in host mode, call OnStopClient/OnStopLocalPlayer manually - if (NetworkClient.active && localClientActive) + if (NetworkClient.active && activeHost) { + // fix: #3962 custom unspawn handler for this prefab (for prefab pools etc.) + NetworkClient.InvokeUnSpawnHandler(identity.assetId, identity.gameObject); + if (identity.isLocalPlayer) identity.OnStopLocalPlayer(); @@ -1359,84 +1730,99 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) // The object may have been spawned with host client ownership, // e.g. a pet so we need to clear hasAuthority and call // NotifyAuthority which invokes OnStopAuthority if hasAuthority. - identity.hasAuthority = false; + identity.isOwned = false; identity.NotifyAuthority(); // remove from NetworkClient dictionary + NetworkClient.connection.owned.Remove(identity); NetworkClient.spawned.Remove(identity.netId); } // we are on the server. call OnStopServer. identity.OnStopServer(); - // are we supposed to GameObject.Destroy() it completely? - if (mode == DestroyMode.Destroy) + // finally reset the state and deactivate it + if (resetState) { - identity.destroyCalled = true; - - // Destroy if application is running - if (Application.isPlaying) - { - UnityEngine.Object.Destroy(identity.gameObject); - } - // Destroy can't be used in Editor during tests. use DestroyImmediate. - else - { - GameObject.DestroyImmediate(identity.gameObject); - } - } - // otherwise simply .Reset() and set inactive again - else if (mode == DestroyMode.Reset) - { - identity.Reset(); + identity.ResetState(); + identity.gameObject.SetActive(false); } } - static void DestroyObject(GameObject obj, DestroyMode mode) + /// This takes an object that has been spawned and un-spawns it. + // The object will be removed from clients that it was spawned on, or + // the custom spawn handler function on the client will be called for + // the object. + // Unlike when calling NetworkServer.Destroy(), on the server the object + // will NOT be destroyed. This allows the server to re-use the object, + // even spawn it again later. + public static void UnSpawn(GameObject obj) => UnSpawnInternal(obj, resetState: true); + + // destroy ///////////////////////////////////////////////////////////// + /// Destroys this object and corresponding objects on all clients. + // In some cases it is useful to remove an object but not delete it on + // the server. For that, use NetworkServer.UnSpawn() instead of + // NetworkServer.Destroy(). + public static void Destroy(GameObject obj) { - if (obj == null) + // NetworkServer.Destroy should only be called on server or host. + // on client, show a warning to explain what it does. + if (!active) { - Debug.Log("NetworkServer DestroyObject is null"); + Debug.LogWarning("NetworkServer.Destroy() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients."); return; } - if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + if (obj == null) { - DestroyObject(identity, mode); + Debug.Log("NetworkServer.Destroy(): object is null"); + return; } - } - /// Destroys this object and corresponding objects on all clients. - // In some cases it is useful to remove an object but not delete it on - // the server. For that, use NetworkServer.UnSpawn() instead of - // NetworkServer.Destroy(). - public static void Destroy(GameObject obj) => DestroyObject(obj, DestroyMode.Destroy); - - // interest management ///////////////////////////////////////////////// - // Helper function to add all server connections as observers. - // This is used if none of the components provides their own - // OnRebuildObservers function. - internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity) - { - // add all server connections - foreach (NetworkConnectionToClient conn in connections.Values) + // get the NetworkIdentity component first + if (!GetNetworkIdentity(obj, out NetworkIdentity identity)) { - // only if authenticated (don't send to people during logins) - if (conn.isReady) - identity.AddObserver(conn); + Debug.LogWarning($"NetworkServer.Destroy() called on {obj.name} which doesn't have a NetworkIdentity component."); + return; } - // add local host connection (if any) - if (localConnection != null && localConnection.isReady) + // is this a scene object? + // then we simply unspawn & reset it so it can still be spawned again. + // we never destroy scene objects on server or on client, since once + // they are gone, they are gone forever and can't be instantiate again. + // for example, server may Destroy() a scene object and once a match + // restarts, the scene objects would be gone from the new match. + if (identity.sceneId != 0) { - identity.AddObserver(localConnection); + UnSpawnInternal(obj, resetState: true); } - } + // is this a prefab? + // then we destroy it completely. + else + { + // unspawn without calling ResetState. + // otherwise isServer/isClient flags might be reset in OnDestroy. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3832 + UnSpawnInternal(obj, resetState: false); + identity.destroyCalled = true; - // allocate newObservers helper HashSet only once - // internal for tests - internal static readonly HashSet newObservers = new HashSet(); + // Destroy if application is running + if (Application.isPlaying) + { + UnityEngine.Object.Destroy(obj); + } + // Destroy can't be used in Editor during tests. use DestroyImmediate. + else + { + GameObject.DestroyImmediate(obj); + } + } + } + // interest management ///////////////////////////////////////////////// + // Helper function to add all server connections as observers. + // This is used if none of the components provides their own + // OnRebuildObservers function. // rebuild observers default method (no AOI) - adds all connections static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) { @@ -1445,107 +1831,32 @@ static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) if (initialize) { // not force hidden? - if (identity.visible != Visibility.ForceHidden) + if (identity.visibility != Visibility.ForceHidden) { AddAllReadyServerConnectionsToObservers(identity); } - } - } - - // rebuild observers via interest management system - static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) - { - // clear newObservers hashset before using it - newObservers.Clear(); - - // not force hidden? - if (identity.visible != Visibility.ForceHidden) - { - aoi.OnRebuildObservers(identity, newObservers); - } - - // IMPORTANT: AFTER rebuilding add own player connection in any case - // to ensure player always sees himself no matter what. - // -> OnRebuildObservers might clear observers, so we need to add - // the player's own connection AFTER. 100% fail safe. - // -> fixes https://github.com/vis2k/Mirror/issues/692 where a - // player might teleport out of the ProximityChecker's cast, - // losing the own connection as observer. - if (identity.connectionToClient != null) - { - newObservers.Add(identity.connectionToClient); - } - - bool changed = false; - - // add all newObservers that aren't in .observers yet - foreach (NetworkConnectionToClient conn in newObservers) - { - // only add ready connections. - // otherwise the player might not be in the world yet or anymore - if (conn != null && conn.isReady) - { - if (initialize || !identity.observers.ContainsKey(conn.connectionId)) - { - // new observer - conn.AddToObserving(identity); - // Debug.Log($"New Observer for {gameObject} {conn}"); - changed = true; - } - } - } - - // remove all old .observers that aren't in newObservers anymore - foreach (NetworkConnectionToClient conn in identity.observers.Values) - { - if (!newObservers.Contains(conn)) + else if (identity.connectionToClient != null) { - // removed observer - conn.RemoveFromObserving(identity, false); - // Debug.Log($"Removed Observer for {gameObjec} {conn}"); - changed = true; + // force hidden, but add owner connection + identity.AddObserver(identity.connectionToClient); } } + } - // copy new observers to observers - if (changed) + internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity) + { + // add all server connections + foreach (NetworkConnectionToClient conn in connections.Values) { - identity.observers.Clear(); - foreach (NetworkConnectionToClient conn in newObservers) - { - if (conn != null && conn.isReady) - identity.observers.Add(conn.connectionId, conn); - } + // only if authenticated (don't send to people during logins) + if (conn.isReady) + identity.AddObserver(conn); } - // special case for host mode: we use SetHostVisibility to hide - // NetworkIdentities that aren't in observer range from host. - // this is what games like Dota/Counter-Strike do too, where a host - // does NOT see all players by default. they are in memory, but - // hidden to the host player. - // - // this code is from UNET, it's a bit strange but it works: - // * it hides newly connected identities in host mode - // => that part was the intended behaviour - // * it hides ALL NetworkIdentities in host mode when the host - // connects but hasn't selected a character yet - // => this only works because we have no .localConnection != null - // check. at this stage, localConnection is null because - // StartHost starts the server first, then calls this code, - // then starts the client and sets .localConnection. so we can - // NOT add a null check without breaking host visibility here. - // * it hides ALL NetworkIdentities in server-only mode because - // observers never contain the 'null' .localConnection - // => that was not intended, but let's keep it as it is so we - // don't break anything in host mode. it's way easier than - // iterating all identities in a special function in StartHost. - if (initialize) + // add local host connection (if any) + if (localConnection != null && localConnection.isReady) { - if (!newObservers.Contains(localConnection)) - { - if (aoi != null) - aoi.SetHostVisibility(identity, false); - } + identity.AddObserver(localConnection); } } @@ -1566,30 +1877,27 @@ static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) // both worlds without any worrying now! public static void RebuildObservers(NetworkIdentity identity, bool initialize) { - // observers are null until OnStartServer creates them - if (identity.observers == null) - return; - // if there is no interest management system, // or if 'force shown' then add all connections - if (aoi == null || identity.visible == Visibility.ForceShown) + if (aoi == null || identity.visibility == Visibility.ForceShown) { RebuildObserversDefault(identity, initialize); } // otherwise let interest management system rebuild else { - RebuildObserversCustom(identity, initialize); + aoi.Rebuild(identity, initialize); } } + // broadcasting //////////////////////////////////////////////////////// // helper function to get the right serialization for a connection - static NetworkWriter GetEntitySerializationForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) { // get serialization for this entity (cached) // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - NetworkIdentitySerialization serialization = identity.GetSerializationAtTick(Time.frameCount); + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount); // is this entity owned by this connection? bool owned = identity.connectionToClient == connection; @@ -1618,6 +1926,7 @@ static NetworkWriter GetEntitySerializationForConnection(NetworkIdentity identit static void BroadcastToConnection(NetworkConnectionToClient connection) { // for each entity that this connection is seeing + bool hasNull = false; foreach (NetworkIdentity identity in connection.observing) { // make sure it's not null or destroyed. @@ -1628,7 +1937,7 @@ static void BroadcastToConnection(NetworkConnectionToClient connection) { // get serialization for this entity viewed by this connection // (if anything was serialized this time) - NetworkWriter serialization = GetEntitySerializationForConnection(identity, connection); + NetworkWriter serialization = SerializeForConnection(identity, connection); if (serialization != null) { EntityStateMessage message = new EntityStateMessage @@ -1643,8 +1952,31 @@ static void BroadcastToConnection(NetworkConnectionToClient connection) // always call Remove in OnObjectDestroy everywhere. // if it does have null then someone used // GameObject.Destroy instead of NetworkServer.Destroy. - else 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."); + 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."); + } } + + // recover from null entries. + // otherwise every broadcast will spam the warning and slow down performance until restart. + if (hasNull) connection.observing.RemoveWhere(identity => identity == null); + } + + // helper function to check a connection for inactivity and disconnect if necessary + // returns true if disconnected + static bool DisconnectIfInactive(NetworkConnectionToClient connection) + { + // check for inactivity + if (disconnectInactiveConnections && + !connection.IsAlive(disconnectInactiveTimeout)) + { + Debug.LogWarning($"Disconnecting {connection} for inactivity!"); + connection.Disconnect(); + return true; + } + return false; } // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate @@ -1669,11 +2001,26 @@ static void Broadcast() // go through all connections foreach (NetworkConnectionToClient connection in connectionsCopy) { + // check for inactivity. disconnects if necessary. + if (DisconnectIfInactive(connection)) + continue; + // has this connection joined the world yet? // for each READY connection: // pull in UpdateVarsMessage for each entity it observes if (connection.isReady) { + // send time for snapshot interpolation every sendInterval. + // BroadcastToConnection() may not send if nothing is new. + // + // sent over unreliable. + // NetworkTime / Transform both use unreliable. + // + // make sure Broadcast() is only called every sendInterval, + // even if targetFrameRate isn't set in host mode (!) + // (done via AccurateInterval) + connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); + // broadcast world state to this connection BroadcastToConnection(connection); } @@ -1681,30 +2028,6 @@ static void Broadcast() // update connection to flush out batched messages connection.Update(); } - - // TODO this is way too slow because we iterate ALL spawned :/ - // TODO this is way too complicated :/ - // to understand what this tries to prevent, consider this example: - // monster has health=100 - // we change health=200, dirty bit is set - // player comes in range, gets full serialization spawn packet. - // next Broadcast(), player gets the health=200 change because dirty bit was set. - // - // this code clears all dirty bits if no players are around to prevent it. - // BUT there are two issues: - // 1. what if a playerB was around the whole time? - // 2. why don't we handle broadcast and spawn packets both HERE? - // handling spawn separately is why we need this complex magic - // - // see test: DirtyBitsAreClearedForSpawnedWithoutObservers() - // see test: SyncObjectChanges_DontGrowWithoutObservers() - // - // PAUL: we also do this to avoid ever growing SyncList .changes - //ClearSpawnedDirtyBits(); - // - // this was moved to NetworkIdentity.AddObserver! - // same result, but no more O(N) loop in here! - // TODO remove this comment after moving spawning into Broadcast()! } // update ////////////////////////////////////////////////////////////// @@ -1712,21 +2035,77 @@ static void Broadcast() // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkEarlyUpdate() { + // measure update time for profiling. + if (active) + { + earlyUpdateDuration.Begin(); + fullUpdateDuration.Begin(); + } + // process all incoming messages first before updating the world - if (Transport.activeTransport != null) - Transport.activeTransport.ServerEarlyUpdate(); + if (Transport.active != null) + Transport.active.ServerEarlyUpdate(); + + // step each connection's local time interpolation in early update. + foreach (NetworkConnectionToClient connection in connections.Values) + connection.UpdateTimeInterpolation(); + + if (active) earlyUpdateDuration.End(); } internal static void NetworkLateUpdate() { - // only broadcast world if active if (active) - Broadcast(); + { + // measure update time for profiling. + lateUpdateDuration.Begin(); + + // only broadcast world if active + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + // Unity 2019 doesn't have Time.timeAsDouble yet + bool sendIntervalElapsed = AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime); + if (!Application.isPlaying || sendIntervalElapsed) + Broadcast(); + } // process all outgoing messages after updating the world // (even if not active. still want to process disconnects etc.) - if (Transport.activeTransport != null) - Transport.activeTransport.ServerLateUpdate(); + if (Transport.active != null) + Transport.active.ServerLateUpdate(); + + // measure actual tick rate every second. + if (active) + { + ++actualTickRateCounter; + + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= actualTickRateStart + 1) + { + // calculate avg by exact elapsed time. + // assuming 1s wouldn't be accurate, usually a few more ms passed. + float elapsed = (float)(NetworkTime.localTime - actualTickRateStart); + actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed); + actualTickRateStart = NetworkTime.localTime; + actualTickRateCounter = 0; + } + + // measure total update time. including transport. + // because in early update, transport update calls handlers. + lateUpdateDuration.End(); + fullUpdateDuration.End(); + } } } } diff --git a/Assets/Mirror/Runtime/NetworkServer.cs.meta b/Assets/Mirror/Core/NetworkServer.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkServer.cs.meta rename to Assets/Mirror/Core/NetworkServer.cs.meta index 9861342..84d41e8 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs.meta +++ b/Assets/Mirror/Core/NetworkServer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkStartPosition.cs b/Assets/Mirror/Core/NetworkStartPosition.cs similarity index 100% rename from Assets/Mirror/Runtime/NetworkStartPosition.cs rename to Assets/Mirror/Core/NetworkStartPosition.cs diff --git a/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta b/Assets/Mirror/Core/NetworkStartPosition.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkStartPosition.cs.meta rename to Assets/Mirror/Core/NetworkStartPosition.cs.meta index ae9ab89..64a7fe4 100644 --- a/Assets/Mirror/Runtime/NetworkStartPosition.cs.meta +++ b/Assets/Mirror/Core/NetworkStartPosition.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkStartPosition.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkTime.cs b/Assets/Mirror/Core/NetworkTime.cs new file mode 100644 index 0000000..6319970 --- /dev/null +++ b/Assets/Mirror/Core/NetworkTime.cs @@ -0,0 +1,243 @@ +// NetworkTime now uses NetworkClient's snapshot interpolated timeline. +// this gives ideal results & ensures everything is on the same timeline. +// previously, NetworkTransforms were on separate timelines. +// +// however, some of the old NetworkTime code remains for ping time (rtt). +// some users may still be using that. +using System; +using System.Runtime.CompilerServices; +using UnityEngine; +#if !UNITY_2020_3_OR_NEWER +using Stopwatch = System.Diagnostics.Stopwatch; +#endif + +namespace Mirror +{ + /// Synchronizes server time to clients. + public static class NetworkTime + { + /// Ping message interval, used to calculate latency / RTT and predicted time. + // 2s was enough to get a good average RTT. + // for prediction, we want to react to latency changes more rapidly. + const float DefaultPingInterval = 0.1f; // for resets + public static float PingInterval = DefaultPingInterval; + + /// Average out the last few results from Ping + // const because it's used immediately in _rtt constructor. + public const int PingWindowSize = 50; // average over 50 * 100ms = 5s + + static double lastPingTime; + + static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(PingWindowSize); + + /// Returns double precision clock time _in this system_, unaffected by the network. +#if UNITY_2020_3_OR_NEWER + public static double localTime + { + // NetworkTime uses unscaled time and ignores Time.timeScale. + // fixes Time.timeScale getting server & client time out of sync: + // https://github.com/MirrorNetworking/Mirror/issues/3409 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Time.unscaledTimeAsDouble; + } +#else + // need stopwatch for older Unity versions, but it's quite slow. + // CAREFUL: unlike Time.time, the stopwatch time is not a FRAME time. + // it changes during the frame, so we have an extra step to "cache" it in EarlyUpdate. + static readonly Stopwatch stopwatch = new Stopwatch(); + static NetworkTime() => stopwatch.Start(); + static double localFrameTime; + public static double localTime => localFrameTime; +#endif + + /// The time in seconds since the server started. + // via global NetworkClient snapshot interpolated timeline (if client). + // on server, this is simply Time.timeAsDouble. + // + // I measured the accuracy of float and I got this: + // for the same day, accuracy is better than 1 ms + // after 1 day, accuracy goes down to 7 ms + // after 10 days, accuracy is 61 ms + // after 30 days , accuracy is 238 ms + // after 60 days, accuracy is 454 ms + // in other words, if the server is running for 2 months, + // and you cast down to float, then the time will jump in 0.4s intervals. + public static double time + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => NetworkServer.active + ? localTime + : NetworkClient.localTimeline; + } + + // prediction ////////////////////////////////////////////////////////// + // NetworkTime.time is server time, behind by bufferTime. + // for prediction, we want server time, ahead by latency. + // so that client inputs at predictedTime=2 arrive on server at time=2. + // the more accurate this is, the more closesly will corrections be + // be applied and the less jitter we will see. + // + // we'll use a two step process to calculate predicted time: + // 1. move snapshot interpolated time to server time, without being behind by bufferTime + // 2. constantly send this time to server (included in ping message) + // server replies with how far off it was. + // client averages that offset and applies it to predictedTime to get ever closer. + // + // this is also very easy to test & verify: + // - add LatencySimulation with 50ms latency + // - log predictionError on server in OnServerPing, see if it gets closer to 0 + // + // credits: FakeByte, imer, NinjaKickja, mischa + // const because it's used immediately in _predictionError constructor. + + static int PredictionErrorWindowSize = 20; // average over 20 * 100ms = 2s + static ExponentialMovingAverage _predictionErrorUnadjusted = new ExponentialMovingAverage(PredictionErrorWindowSize); + public static double predictionErrorUnadjusted => _predictionErrorUnadjusted.Value; + public static double predictionErrorAdjusted { get; private set; } // for debugging + + /// Predicted timeline in order for client inputs to be timestamped with the exact time when they will most likely arrive on the server. This is the basis for all prediction like PredictedRigidbody. + // on client, this is based on localTime (aka Time.time) instead of the snapshot interpolated timeline. + // this gives much better and immediately accurate results. + // -> snapshot interpolation timeline tries to emulate a server timeline without hard offset corrections. + // -> predictedTime does have hard offset corrections, so might as well use Time.time directly for this. + // + // note that predictedTime over unreliable is enough! + // even with reliable components, it gives better results than if we were + // to implemented predictedTime over reliable channel. + public static double predictedTime + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => NetworkServer.active + ? localTime // server always uses it's own timeline + : localTime + predictionErrorUnadjusted; // add the offset that the server told us we are off by + } + //////////////////////////////////////////////////////////////////////// + + /// Clock difference in seconds between the client and the server. Always 0 on server. + // original implementation used 'client - server' time. keep it this way. + // TODO obsolete later. people shouldn't worry about this. + public static double offset => localTime - time; + + /// Round trip time (in seconds) that it takes a message to go client->server->client. + public static double rtt => _rtt.Value; + + /// Round trip time variance aka jitter, in seconds. + // "rttVariance" instead of "rttVar" for consistency with older versions. + public static double rttVariance => _rtt.Variance; + + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod] + public static void ResetStatics() + { + PingInterval = DefaultPingInterval; + lastPingTime = 0; + _rtt = new ExponentialMovingAverage(PingWindowSize); +#if !UNITY_2020_3_OR_NEWER + stopwatch.Restart(); +#endif + } + + internal static void UpdateClient() + { + // localTime (double) instead of Time.time for accuracy over days + if (localTime >= lastPingTime + PingInterval) + SendPing(); + } + + // Separate method so we can call it from NetworkClient directly. + internal static void SendPing() + { + // send raw predicted time without the offset applied yet. + // we then apply the offset to it after. + NetworkPingMessage pingMessage = new NetworkPingMessage + ( + localTime, + predictedTime + ); + NetworkClient.Send(pingMessage, Channels.Unreliable); + lastPingTime = localTime; + } + + // client rtt calculation ////////////////////////////////////////////// + // executed at the server when we receive a ping message + // reply with a pong containing the time from the client + // and time from the server + internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMessage message) + { + // calculate the prediction offset that the client needs to apply to unadjusted time to reach server time. + // this will be sent back to client for corrections. + double unadjustedError = localTime - message.localTime; + + // to see how well the client's final prediction worked, compare with adjusted time. + // this is purely for debugging. + // >0 means: server is ... seconds ahead of client's prediction (good if small) + // <0 means: server is ... seconds behind client's prediction. + // in other words, client is predicting too far ahead (not good) + double adjustedError = localTime - message.predictedTimeAdjusted; + // Debug.Log($"[Server] unadjustedError:{(unadjustedError*1000):F1}ms adjustedError:{(adjustedError*1000):F1}ms"); + + // Debug.Log($"OnServerPing conn:{conn}"); + NetworkPongMessage pongMessage = new NetworkPongMessage + ( + message.localTime, + unadjustedError, + adjustedError + ); + conn.Send(pongMessage, Channels.Unreliable); + } + + // Executed at the client when we receive a Pong message + // find out how long it took since we sent the Ping + // and update time offset & prediction offset. + internal static void OnClientPong(NetworkPongMessage message) + { + // prevent attackers from sending timestamps which are in the future + if (message.localTime > localTime) return; + + // how long did this message take to come back + double newRtt = localTime - message.localTime; + _rtt.Add(newRtt); + + // feed unadjusted prediction error into our exponential moving average + // store adjusted prediction error for debug / GUI purposes + _predictionErrorUnadjusted.Add(message.predictionErrorUnadjusted); + predictionErrorAdjusted = message.predictionErrorAdjusted; + // Debug.Log($"[Client] predictionError avg={(_predictionErrorUnadjusted.Value*1000):F1} ms"); + } + + // server rtt calculation ////////////////////////////////////////////// + // Executed at the client when we receive a ping message from the server. + // in other words, this is for server sided ping + rtt calculation. + // reply with a pong containing the time from the server + internal static void OnClientPing(NetworkPingMessage message) + { + // Debug.Log($"OnClientPing conn:{conn}"); + NetworkPongMessage pongMessage = new NetworkPongMessage + ( + message.localTime, + 0, 0 // server doesn't predict + ); + NetworkClient.Send(pongMessage, Channels.Unreliable); + } + + // Executed at the server when we receive a Pong message back. + // find out how long it took since we sent the Ping + // and update time offset + internal static void OnServerPong(NetworkConnectionToClient conn, NetworkPongMessage message) + { + // prevent attackers from sending timestamps which are in the future + if (message.localTime > localTime) return; + + // how long did this message take to come back + double newRtt = localTime - message.localTime; + conn._rtt.Add(newRtt); + } + + internal static void EarlyUpdate() + { +#if !UNITY_2020_3_OR_NEWER + localFrameTime = stopwatch.Elapsed.TotalSeconds; +#endif + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkTime.cs.meta b/Assets/Mirror/Core/NetworkTime.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkTime.cs.meta rename to Assets/Mirror/Core/NetworkTime.cs.meta index 1dc9e0a..0049ede 100644 --- a/Assets/Mirror/Runtime/NetworkTime.cs.meta +++ b/Assets/Mirror/Core/NetworkTime.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkTime.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkWriter.cs b/Assets/Mirror/Core/NetworkWriter.cs similarity index 64% rename from Assets/Mirror/Runtime/NetworkWriter.cs rename to Assets/Mirror/Core/NetworkWriter.cs index 442075f..7ecf126 100644 --- a/Assets/Mirror/Runtime/NetworkWriter.cs +++ b/Assets/Mirror/Core/NetworkWriter.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.CompilerServices; +using System.Text; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; @@ -8,16 +9,32 @@ namespace Mirror /// Network Writer for most simple types like floats, ints, buffers, structs, etc. Use NetworkWriterPool.GetReader() to avoid allocations. public class NetworkWriter { - public const int MaxStringLength = 1024 * 32; + // the limit of ushort is so we can write string size prefix as only 2 bytes. + // -1 so we can still encode 'null' into it too. + public const ushort MaxStringLength = ushort.MaxValue - 1; // create writer immediately with it's own buffer so no one can mess with it and so that we can resize it. // note: BinaryWriter allocates too much, so we only use a MemoryStream // => 1500 bytes by default because on average, most packets will be <= MTU - byte[] buffer = new byte[1500]; + public const int DefaultCapacity = 1500; + internal byte[] buffer = new byte[DefaultCapacity]; /// Next position to write to the buffer public int Position; + /// Current capacity. Automatically resized if necessary. + public int Capacity => buffer.Length; + + // cache encoding for WriteString instead of creating it each time. + // 1000 readers before: 1MB GC, 30ms + // 1000 readers after: 0.8MB GC, 18ms + // not(!) static for thread safety. + // + // throwOnInvalidBytes is true. + // writer should throw and user should fix if this ever happens. + // unlike reader, which needs to expect it to happen from attackers. + internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true); + /// Reset both the position and length of the stream // Leaves the capacity the same so that we can reuse this writer without // extra allocations @@ -31,7 +48,7 @@ public void Reset() // 1. 'has space' checks are necessary even for fixed sized writers. // 2. all writers will eventually be large enough to stop resizing. [MethodImpl(MethodImplOptions.AggressiveInlining)] - void EnsureCapacity(int value) + internal void EnsureCapacity(int value) { if (buffer.Length < value) { @@ -41,7 +58,7 @@ void EnsureCapacity(int value) } /// Copies buffer until 'Position' to a new array. - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // Try to use ToArraySegment instead to avoid allocations! public byte[] ToArray() { byte[] data = new byte[Position]; @@ -51,10 +68,13 @@ public byte[] ToArray() /// Returns allocation-free ArraySegment until 'Position'. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArraySegment ToArraySegment() - { - return new ArraySegment(buffer, 0, Position); - } + public ArraySegment ToArraySegment() => + new ArraySegment(buffer, 0, Position); + + // implicit conversion for convenience + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ArraySegment(NetworkWriter w) => + w.ToArraySegment(); // WriteBlittable from DOTSNET. // this is extremely fast, but only works for blittable types. @@ -82,7 +102,26 @@ public ArraySegment ToArraySegment() // WriteBlittable assumes same endianness for server & client. // All Unity 2018+ platforms are little endian. // => run NetworkWriterTests.BlittableOnThisPlatform() to verify! - [MethodImpl(MethodImplOptions.AggressiveInlining)] + // + // This is not safe to expose to random structs. + // * StructLayout.Sequential is the default, which is safe. + // if the struct contains a reference type, it is converted to Auto. + // but since all structs here are unmanaged blittable, it's safe. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential + // * StructLayout.Pack depends on CPU word size. + // this may be different 4 or 8 on some ARM systems, etc. + // this is not safe, and would cause bytes/shorts etc. to be padded. + // see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0 + // * If we force pack all to '1', they would have no padding which is + // great for bandwidth. but on some android systems, CPU can't read + // unaligned memory. + // see also: https://github.com/vis2k/Mirror/issues/3044 + // * The only option would be to force explicit layout with multiples + // of word size. but this requires lots of weaver checking and is + // still questionable (IL2CPP etc.). + // + // Note: inlining WriteBlittable is enough. don't inline WriteInt etc. + // we don't want WriteBlittable to be copied in place everywhere. internal unsafe void WriteBlittable(T value) where T : unmanaged { @@ -148,21 +187,38 @@ internal void WriteBlittableNullable(T? value) WriteBlittable(value.Value); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteByte(byte value) => WriteBlittable(value); // for byte arrays with consistent size, where the reader knows how many to read // (like a packet opcode that's always the same) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteBytes(byte[] buffer, int offset, int count) + public void WriteBytes(byte[] array, int offset, int count) { EnsureCapacity(Position + count); - Array.ConstrainedCopy(buffer, offset, this.buffer, Position, count); + Array.ConstrainedCopy(array, offset, this.buffer, Position, count); Position += count; } + // write an unsafe byte* array. + // useful for bit tree compression, etc. + public unsafe bool WriteBytes(byte* ptr, int offset, int size) + { + EnsureCapacity(Position + size); + + fixed (byte* destination = &buffer[Position]) + { + // write 'size' bytes at position + // 10 mio writes: 868ms + // Array.Copy(value.Array, value.Offset, buffer, Position, value.Count); + // 10 mio writes: 775ms + // Buffer.BlockCopy(value.Array, value.Offset, buffer, Position, value.Count); + // 10 mio writes: 637ms + UnsafeUtility.MemCpy(destination, ptr + offset, size); + } + + Position += size; + return true; + } /// Writes any type that mirror supports. Uses weaver populated Writer(T).write. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(T value) { Action writeDelegate = Writer.write; @@ -175,6 +231,12 @@ public void Write(T value) writeDelegate(this, value); } } + + // print with buffer content for easier debugging. + // [content, position / capacity]. + // showing "position / space" would be too confusing. + public override string ToString() => + $"[{ToArraySegment().ToHexString()} @ {Position}/{Capacity}]"; } /// Helper class that weaver populates with all writer types. diff --git a/Assets/Mirror/Runtime/NetworkWriter.cs.meta b/Assets/Mirror/Core/NetworkWriter.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/NetworkWriter.cs.meta rename to Assets/Mirror/Core/NetworkWriter.cs.meta index c938496..17cba61 100644 --- a/Assets/Mirror/Runtime/NetworkWriter.cs.meta +++ b/Assets/Mirror/Core/NetworkWriter.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriter.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkWriterExtensions.cs b/Assets/Mirror/Core/NetworkWriterExtensions.cs new file mode 100644 index 0000000..1f6588f --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriterExtensions.cs @@ -0,0 +1,471 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // Mirror's Weaver automatically detects all NetworkWriter function types, + // but they do all need to be extensions. + public static class NetworkWriterExtensions + { + public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteBlittable(value); + public static void WriteByteNullable(this NetworkWriter writer, byte? value) => writer.WriteBlittableNullable(value); + + public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteBlittable(value); + public static void WriteSByteNullable(this NetworkWriter writer, sbyte? value) => writer.WriteBlittableNullable(value); + + // char is not blittable. convert to ushort. + public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteBlittable((ushort)value); + public static void WriteCharNullable(this NetworkWriter writer, char? value) => writer.WriteBlittableNullable((ushort?)value); + + // bool is not blittable. convert to byte. + public static void WriteBool(this NetworkWriter writer, bool value) => writer.WriteBlittable((byte)(value ? 1 : 0)); + public static void WriteBoolNullable(this NetworkWriter writer, bool? value) => writer.WriteBlittableNullable(value.HasValue ? ((byte)(value.Value ? 1 : 0)) : new byte?()); + + public static void WriteShort(this NetworkWriter writer, short value) => writer.WriteBlittable(value); + public static void WriteShortNullable(this NetworkWriter writer, short? value) => writer.WriteBlittableNullable(value); + + public static void WriteUShort(this NetworkWriter writer, ushort value) => writer.WriteBlittable(value); + public static void WriteUShortNullable(this NetworkWriter writer, ushort? value) => writer.WriteBlittableNullable(value); + + public static void WriteInt(this NetworkWriter writer, int value) => writer.WriteBlittable(value); + public static void WriteIntNullable(this NetworkWriter writer, int? value) => writer.WriteBlittableNullable(value); + + public static void WriteUInt(this NetworkWriter writer, uint value) => writer.WriteBlittable(value); + public static void WriteUIntNullable(this NetworkWriter writer, uint? value) => writer.WriteBlittableNullable(value); + + public static void WriteLong(this NetworkWriter writer, long value) => writer.WriteBlittable(value); + public static void WriteLongNullable(this NetworkWriter writer, long? value) => writer.WriteBlittableNullable(value); + + public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value); + public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value); + + // WriteInt/UInt/Long/ULong writes full bytes by default. + // define additional "VarInt" versions that Weaver will automatically prefer. + // 99% of the time [SyncVar] ints are small values, which makes this very much worth it. + [WeaverPriority] public static void WriteVarInt(this NetworkWriter writer, int value) => Compression.CompressVarInt(writer, value); + [WeaverPriority] public static void WriteVarUInt(this NetworkWriter writer, uint value) => Compression.CompressVarUInt(writer, value); + [WeaverPriority] public static void WriteVarLong(this NetworkWriter writer, long value) => Compression.CompressVarInt(writer, value); + [WeaverPriority] public static void WriteVarULong(this NetworkWriter writer, ulong value) => Compression.CompressVarUInt(writer, value); + + public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value); + public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value); + + public static void WriteDouble(this NetworkWriter writer, double value) => writer.WriteBlittable(value); + public static void WriteDoubleNullable(this NetworkWriter writer, double? value) => writer.WriteBlittableNullable(value); + + public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value); + public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value); + + public static void WriteHalf(this NetworkWriter writer, Half value) => writer.WriteUShort(value._value); + + public static void WriteString(this NetworkWriter writer, string value) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (value == null) + { + writer.WriteUShort(0); + return; + } + + // WriteString copies into the buffer manually. + // need to ensure capacity here first, manually. + int maxSize = writer.encoding.GetMaxByteCount(value.Length); + writer.EnsureCapacity(writer.Position + 2 + maxSize); // 2 bytes position + N bytes encoding + + // encode it into the buffer first. + // reserve 2 bytes for header after we know how much was written. + int written = writer.encoding.GetBytes(value, 0, value.Length, writer.buffer, writer.Position + 2); + + // check if within max size, otherwise Reader can't read it. + if (written > NetworkWriter.MaxStringLength) + throw new IndexOutOfRangeException($"NetworkWriter.WriteString - Value too long: {written} bytes. Limit: {NetworkWriter.MaxStringLength} bytes"); + + // .Position is unchanged, so fill in the size header now. + // we already ensured that max size fits into ushort.max-1. + writer.WriteUShort(checked((ushort)(written + 1))); // Position += 2 + + // now update position by what was written above + writer.Position += written; + } + + // Weaver needs a write function with just one byte[] parameter + // (we don't name it .Write(byte[]) because it's really a WriteBytesAndSize since we write size / null info too) + public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer) + { + // buffer might be null, so we can't use .Length in that case + writer.WriteBytesAndSize(buffer, 0, buffer != null ? buffer.Length : 0); + } + + // for byte arrays with dynamic size, where the reader doesn't know how many will come + // (like an inventory with different items etc.) + public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer, int offset, int count) + { + // null is supported because [SyncVar]s might be structs with null byte[] arrays. + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (buffer == null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + // writer.WriteUInt(0u); + return; + } + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)count) + 1u); + // writer.WriteUInt(checked((uint)count) + 1u); + writer.WriteBytes(buffer, offset, count); + } + + // writes ArraySegment of byte (most common type) and size header + public static void WriteArraySegmentAndSize(this NetworkWriter writer, ArraySegment segment) + { + writer.WriteBytesAndSize(segment.Array, segment.Offset, segment.Count); + } + + // writes ArraySegment of any type, and size header + public static void WriteArraySegment(this NetworkWriter writer, ArraySegment segment) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + // + // ArraySegment technically can't be null, but users may call: + // - WriteArraySegment + // - ReadArray + // in which case ReadArray needs null support. both need to be compatible. + int count = segment.Count; + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)count) + 1u); + // writer.WriteUInt(checked((uint)count) + 1u); + for (int i = 0; i < count; i++) + { + writer.Write(segment.Array[segment.Offset + i]); + } + } + + public static void WriteVector2(this NetworkWriter writer, Vector2 value) => writer.WriteBlittable(value); + public static void WriteVector2Nullable(this NetworkWriter writer, Vector2? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector3(this NetworkWriter writer, Vector3 value) => writer.WriteBlittable(value); + public static void WriteVector3Nullable(this NetworkWriter writer, Vector3? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector4(this NetworkWriter writer, Vector4 value) => writer.WriteBlittable(value); + public static void WriteVector4Nullable(this NetworkWriter writer, Vector4? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) => writer.WriteBlittable(value); + public static void WriteVector2IntNullable(this NetworkWriter writer, Vector2Int? value) => writer.WriteBlittableNullable(value); + + public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) => writer.WriteBlittable(value); + public static void WriteVector3IntNullable(this NetworkWriter writer, Vector3Int? value) => writer.WriteBlittableNullable(value); + + public static void WriteColor(this NetworkWriter writer, Color value) => writer.WriteBlittable(value); + public static void WriteColorNullable(this NetworkWriter writer, Color? value) => writer.WriteBlittableNullable(value); + + public static void WriteColor32(this NetworkWriter writer, Color32 value) => writer.WriteBlittable(value); + public static void WriteColor32Nullable(this NetworkWriter writer, Color32? value) => writer.WriteBlittableNullable(value); + + public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value); + public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value); + + // Rect is a struct with properties instead of fields + public static void WriteRect(this NetworkWriter writer, Rect value) + { + writer.WriteVector2(value.position); + writer.WriteVector2(value.size); + } + public static void WriteRectNullable(this NetworkWriter writer, Rect? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WriteRect(value.Value); + } + + // Plane is a struct with properties instead of fields + public static void WritePlane(this NetworkWriter writer, Plane value) + { + writer.WriteVector3(value.normal); + writer.WriteFloat(value.distance); + } + public static void WritePlaneNullable(this NetworkWriter writer, Plane? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WritePlane(value.Value); + } + + // Ray is a struct with properties instead of fields + public static void WriteRay(this NetworkWriter writer, Ray value) + { + writer.WriteVector3(value.origin); + writer.WriteVector3(value.direction); + } + public static void WriteRayNullable(this NetworkWriter writer, Ray? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WriteRay(value.Value); + } + + // LayerMask is a struct with properties instead of fields + public static void WriteLayerMask(this NetworkWriter writer, LayerMask layerMask) + { + // 32 layers as a flags enum, max value of 496, we only need a UShort. + writer.WriteUShort((ushort)layerMask.value); + } + public static void WriteLayerMaskNullable(this NetworkWriter writer, LayerMask? layerMask) + { + writer.WriteBool(layerMask.HasValue); + if (layerMask.HasValue) + writer.WriteLayerMask(layerMask.Value); + } + + public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value); + public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value); + + public static void WriteGuid(this NetworkWriter writer, Guid value) + { +#if !UNITY_2021_3_OR_NEWER + // Unity 2019 doesn't have Span yet + byte[] data = value.ToByteArray(); + writer.WriteBytes(data, 0, data.Length); +#else + // WriteBlittable(Guid) isn't safe. see WriteBlittable comments. + // Guid is Sequential, but we can't guarantee packing. + // TryWriteBytes is safe and allocation free. + writer.EnsureCapacity(writer.Position + 16); + value.TryWriteBytes(new Span(writer.buffer, writer.Position, 16)); + writer.Position += 16; +#endif + } + public static void WriteGuidNullable(this NetworkWriter writer, Guid? value) + { + writer.WriteBool(value.HasValue); + if (value.HasValue) + writer.WriteGuid(value.Value); + } + + public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + + // users might try to use unspawned / prefab GameObjects in + // rpcs/cmds/syncvars/messages. they would be null on the other + // end, and it might not be obvious why. let's make it obvious. + // https://github.com/vis2k/Mirror/issues/2060 + // + // => warning (instead of exception) because we also use a warning + // if a GameObject doesn't have a NetworkIdentity component etc. + if (value.netId == 0) + Debug.LogWarning($"Attempted to serialize unspawned GameObject: {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc."); + + writer.WriteUInt(value.netId); + } + + public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + + // users might try to use unspawned / prefab NetworkBehaviours in + // rpcs/cmds/syncvars/messages. they would be null on the other + // end, and it might not be obvious why. let's make it obvious. + // https://github.com/vis2k/Mirror/issues/2060 + // and more recently https://github.com/MirrorNetworking/Mirror/issues/3399 + // + // => warning (instead of exception) because we also use a warning + // when writing an unspawned NetworkIdentity + if (value.netId == 0) + { + Debug.LogWarning($"Attempted to serialize unspawned NetworkBehaviour: of type {value.GetType()} on GameObject {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc."); + writer.WriteUInt(0); + return; + } + + writer.WriteUInt(value.netId); + writer.WriteByte(value.ComponentIndex); + } + + public static void WriteTransform(this NetworkWriter writer, Transform value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + if (value.TryGetComponent(out NetworkIdentity identity)) + { + writer.WriteUInt(identity.netId); + } + else + { + // if users attempt to pass a transform without NetworkIdentity + // to a [Command] or [SyncVar], it should show an obvious warning. + Debug.LogWarning($"Attempted to sync a Transform ({value}) which isn't networked. Transforms without a NetworkIdentity component can't be synced."); + writer.WriteUInt(0); + } + } + + public static void WriteGameObject(this NetworkWriter writer, GameObject value) + { + if (value == null) + { + writer.WriteUInt(0); + return; + } + + // warn if the GameObject doesn't have a NetworkIdentity, + if (!value.TryGetComponent(out NetworkIdentity identity)) + Debug.LogWarning($"Attempted to sync a GameObject ({value}) which isn't networked. GameObject without a NetworkIdentity component can't be synced."); + + // serialize the correct amount of data in any case to make sure + // that the other end can read the expected amount of data too. + writer.WriteNetworkIdentity(identity); + } + + // while SyncList is recommended for NetworkBehaviours, + // structs may have .List members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Writers/GenerateWriter() handles this manually. + public static void WriteList(this NetworkWriter writer, List list) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (list is null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + // writer.WriteUInt(0); + return; + } + + // check if within max size, otherwise Reader can't read it. + if (list.Count > NetworkReader.AllocationLimit) + throw new IndexOutOfRangeException($"NetworkWriter.WriteList - List<{typeof(T)}> too big: {list.Count} elements. Limit: {NetworkReader.AllocationLimit}"); + + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)list.Count) + 1u); + // writer.WriteUInt(checked((uint)list.Count) + 1u); + for (int i = 0; i < list.Count; i++) + writer.Write(list[i]); + } + + // while SyncSet is recommended for NetworkBehaviours, + // structs may have .Set members which weaver needs to be able to + // fully serialize for NetworkMessages etc. + // note that Weaver/Writers/GenerateWriter() handles this manually. + public static void WriteHashSet(this NetworkWriter writer, HashSet hashSet) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (hashSet is null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + //writer.WriteUInt(0); + return; + } + + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)hashSet.Count) + 1u); + //writer.WriteUInt(checked((uint)hashSet.Count) + 1u); + foreach (T item in hashSet) + writer.Write(item); + } + + public static void WriteArray(this NetworkWriter writer, T[] array) + { + // we offset count by '1' to easily support null without writing another byte. + // encoding null as '0' instead of '-1' also allows for better compression + // (ushort vs. short / varuint vs. varint) etc. + if (array is null) + { + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, 0u); + // writer.WriteUInt(0); + return; + } + + // check if within max size, otherwise Reader can't read it. + if (array.Length > NetworkReader.AllocationLimit) + throw new IndexOutOfRangeException($"NetworkWriter.WriteArray - Array<{typeof(T)}> too big: {array.Length} elements. Limit: {NetworkReader.AllocationLimit}"); + + // most sizes are small, write size as VarUInt! + Compression.CompressVarUInt(writer, checked((uint)array.Length) + 1u); + // writer.WriteUInt(checked((uint)array.Length) + 1u); + for (int i = 0; i < array.Length; i++) + writer.Write(array[i]); + } + + public static void WriteUri(this NetworkWriter writer, Uri uri) + { + writer.WriteString(uri?.ToString()); + } + + public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D) + { + // TODO allocation protection when sending textures to server. + // currently can allocate 32k x 32k x 4 byte = 3.8 GB + + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + // simply send -1 for width. + if (texture2D == null) + { + writer.WriteShort(-1); + return; + } + + // check if within max size, otherwise Reader can't read it. + int totalSize = texture2D.width * texture2D.height; + if (totalSize > NetworkReader.AllocationLimit) + throw new IndexOutOfRangeException($"NetworkWriter.WriteTexture2D - Texture2D total size (width*height) too big: {totalSize}. Limit: {NetworkReader.AllocationLimit}"); + + // write dimensions first so reader can create the texture with size + // 32k x 32k short is more than enough + writer.WriteShort((short)texture2D.width); + writer.WriteShort((short)texture2D.height); + writer.WriteArray(texture2D.GetPixels32()); + } + + public static void WriteSprite(this NetworkWriter writer, Sprite sprite) + { + // support 'null' textures for [SyncVar]s etc. + // https://github.com/vis2k/Mirror/issues/3144 + // simply send a 'null' for texture content. + if (sprite == null) + { + writer.WriteTexture2D(null); + return; + } + + writer.WriteTexture2D(sprite.texture); + writer.WriteRect(sprite.rect); + writer.WriteVector2(sprite.pivot); + } + + public static void WriteDateTime(this NetworkWriter writer, DateTime dateTime) + { + writer.WriteDouble(dateTime.ToOADate()); + } + + public static void WriteDateTimeNullable(this NetworkWriter writer, DateTime? dateTime) + { + writer.WriteBool(dateTime.HasValue); + if (dateTime.HasValue) + writer.WriteDouble(dateTime.Value.ToOADate()); + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta b/Assets/Mirror/Core/NetworkWriterExtensions.cs.meta similarity index 61% rename from Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta rename to Assets/Mirror/Core/NetworkWriterExtensions.cs.meta index 9bbdaf0..f0c4e31 100644 --- a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs.meta +++ b/Assets/Mirror/Core/NetworkWriterExtensions.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriterExtensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/NetworkWriterPool.cs b/Assets/Mirror/Core/NetworkWriterPool.cs similarity index 77% rename from Assets/Mirror/Runtime/NetworkWriterPool.cs rename to Assets/Mirror/Core/NetworkWriterPool.cs index c63323d..23f6026 100644 --- a/Assets/Mirror/Runtime/NetworkWriterPool.cs +++ b/Assets/Mirror/Core/NetworkWriterPool.cs @@ -1,5 +1,4 @@ // API consistent with Microsoft's ObjectPool. -using System; using System.Runtime.CompilerServices; namespace Mirror @@ -19,12 +18,10 @@ public static class NetworkWriterPool 1000 ); - // DEPRECATED 2022-03-10 - [Obsolete("GetWriter() was renamed to Get()")] - public static NetworkWriterPooled GetWriter() => Get(); + // expose count for testing + public static int Count => Pool.Count; /// Get a writer from the pool. Creates new one if pool is empty. - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static NetworkWriterPooled Get() { // grab from pool & reset position @@ -33,10 +30,6 @@ public static NetworkWriterPooled Get() return writer; } - // DEPRECATED 2022-03-10 - [Obsolete("Recycle() was renamed to Return()")] - public static void Recycle(NetworkWriterPooled writer) => Return(writer); - /// Return a writer to the pool. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Return(NetworkWriterPooled writer) diff --git a/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta b/Assets/Mirror/Core/NetworkWriterPool.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkWriterPool.cs.meta rename to Assets/Mirror/Core/NetworkWriterPool.cs.meta index 19d2bb7..530777c 100644 --- a/Assets/Mirror/Runtime/NetworkWriterPool.cs.meta +++ b/Assets/Mirror/Core/NetworkWriterPool.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriterPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/NetworkWriterPooled.cs b/Assets/Mirror/Core/NetworkWriterPooled.cs new file mode 100644 index 0000000..963ce33 --- /dev/null +++ b/Assets/Mirror/Core/NetworkWriterPooled.cs @@ -0,0 +1,10 @@ +using System; + +namespace Mirror +{ + /// Pooled NetworkWriter, automatically returned to pool when using 'using' + public sealed class NetworkWriterPooled : NetworkWriter, IDisposable + { + public void Dispose() => NetworkWriterPool.Return(this); + } +} diff --git a/Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta b/Assets/Mirror/Core/NetworkWriterPooled.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta rename to Assets/Mirror/Core/NetworkWriterPooled.cs.meta index 5571d6f..cd170b4 100644 --- a/Assets/Mirror/Runtime/NetworkWriterPooled.cs.meta +++ b/Assets/Mirror/Core/NetworkWriterPooled.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/NetworkWriterPooled.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/PortTransport.cs b/Assets/Mirror/Core/PortTransport.cs new file mode 100644 index 0000000..19a7dfd --- /dev/null +++ b/Assets/Mirror/Core/PortTransport.cs @@ -0,0 +1,13 @@ +// convenience interface for transports which use a port. +// useful for cases where someone wants to 'just set the port' independent of +// which transport it is. +// +// note that not all transports have ports, but most do. + +namespace Mirror +{ + public interface PortTransport + { + ushort Port { get; set; } + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs.meta b/Assets/Mirror/Core/PortTransport.cs.meta similarity index 54% rename from Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs.meta rename to Assets/Mirror/Core/PortTransport.cs.meta index 35ef1fe..682143b 100644 --- a/Assets/Mirror/Components/Experimental/NetworkLerpRigidbody.cs.meta +++ b/Assets/Mirror/Core/PortTransport.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 7f032128052c95a46afb0ddd97d994cc +guid: f7c7c2820d7974cb28c7bfe9aae890a0 MonoImporter: externalObjects: {} serializedVersion: 2 @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/PortTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Prediction.meta b/Assets/Mirror/Core/Prediction.meta new file mode 100644 index 0000000..0483f5a --- /dev/null +++ b/Assets/Mirror/Core/Prediction.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7e8e801f9c7f4b858d9a6c162e64ca84 +timeCreated: 1694005962 diff --git a/Assets/Mirror/Core/Prediction/Prediction.cs b/Assets/Mirror/Core/Prediction/Prediction.cs new file mode 100644 index 0000000..d669945 --- /dev/null +++ b/Assets/Mirror/Core/Prediction/Prediction.cs @@ -0,0 +1,195 @@ +// standalone, easy to test algorithms for prediction +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + // prediction may capture Rigidbody3D/2D/etc. state + // have a common interface. + public interface PredictedState + { + double timestamp { get; } + + // use Vector3 for both Rigidbody3D and Rigidbody2D, that's fine + Vector3 position { get; set; } + Vector3 positionDelta { get; set; } + + Quaternion rotation { get; set; } + Quaternion rotationDelta { get; set; } + + Vector3 velocity { get; set; } + Vector3 velocityDelta { get; set; } + + Vector3 angularVelocity { get; set; } + Vector3 angularVelocityDelta { get; set; } + } + + public static class Prediction + { + // get the two states closest to a given timestamp. + // those can be used to interpolate the exact state at that time. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! + public static bool Sample( + SortedList history, + double timestamp, // current server time + out T before, + out T after, + out int afterIndex, + out double t) // interpolation factor + { + before = default; + after = default; + t = 0; + afterIndex = -1; + + // can't sample an empty history + // interpolation needs at least two entries. + // can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0. + if (history.Count < 2) { + return false; + } + + // older than oldest + if (timestamp < history.Keys[0]) { + return false; + } + + // iterate through the history + // TODO this needs to be faster than O(N) + // search around that area. + // should be O(1) most of the time, unless sampling was off. + int index = 0; // manually count when iterating. easier than for-int loop. + KeyValuePair prev = new KeyValuePair(); + + // SortedList foreach iteration allocates a LOT. use for-int instead. + // foreach (KeyValuePair entry in history) { + for (int i = 0; i < history.Count; ++i) + { + double key = history.Keys[i]; + T value = history.Values[i]; + + // exact match? + if (timestamp == key) + { + before = value; + after = value; + afterIndex = index; + t = Mathd.InverseLerp(key, key, timestamp); + return true; + } + + // did we check beyond timestamp? then return the previous two. + if (key > timestamp) + { + before = prev.Value; + after = value; + afterIndex = index; + t = Mathd.InverseLerp(prev.Key, key, timestamp); + return true; + } + + // remember the last + prev = new KeyValuePair(key, value); + index += 1; + } + + return false; + } + + // inserts a server state into the client's history. + // readjust the deltas of the states after the inserted one. + // returns the corrected final position. + // => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower! + public static T CorrectHistory( + SortedList history, + int stateHistoryLimit, + T corrected, // corrected state with timestamp + T before, // state in history before the correction + T after, // state in history after the correction + int afterIndex) // index of the 'after' value so we don't need to find it again here + where T: PredictedState + { + // respect the limit + // TODO unit test to check if it respects max size + if (history.Count >= stateHistoryLimit) + { + history.RemoveAt(0); + afterIndex -= 1; // we removed the first value so all indices are off by one now + } + + // PERFORMANCE OPTIMIZATION: avoid O(N) insertion, only readjust all values after. + // the end result is the same since after.delta and after.position are both recalculated. + // it's technically not correct if we were to reconstruct final position from 0..after..end but + // we never do, we only ever iterate from after..end! + // + // insert the corrected state into the history, or overwrite if already exists + // SortedList insertions are O(N)! + // history[corrected.timestamp] = corrected; + // afterIndex += 1; // we inserted the corrected value before the previous index + + // the entry behind the inserted one still has the delta from (before, after). + // we need to correct it to (corrected, after). + // + // for example: + // before: (t=1.0, delta=10, position=10) + // after: (t=3.0, delta=20, position=30) + // + // then we insert: + // corrected: (t=2.5, delta=__, position=25) + // + // previous delta was from t=1.0 to t=3.0 => 2.0 + // inserted delta is from t=2.5 to t=3.0 => 0.5 + // multiplier is 0.5 / 2.0 = 0.25 + // multiply 'after.delta(20)' by 0.25 to get the new 'after.delta(5) + // + // so the new history is: + // before: (t=1.0, delta=10, position=10) + // corrected: (t=2.5, delta=__, position=25) + // after: (t=3.0, delta= 5, position=__) + // + // so when we apply the correction, the new after.position would be: + // corrected.position(25) + after.delta(5) = 30 + // + double previousDeltaTime = after.timestamp - before.timestamp; // 3.0 - 1.0 = 2.0 + double correctedDeltaTime = after.timestamp - corrected.timestamp; // 3.0 - 2.5 = 0.5 + + // fix multiplier becoming NaN if previousDeltaTime is 0: + // double multiplier = correctedDeltaTime / previousDeltaTime; + double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25 + + // recalculate 'after.delta' with the multiplier + after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier); + after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier); + after.angularVelocityDelta = Vector3.Lerp(Vector3.zero, after.angularVelocityDelta, (float)multiplier); + // Quaternions always need to be normalized in order to be a valid rotation after operations + after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier).normalized; + + // changes aren't saved until we overwrite them in the history + history[after.timestamp] = after; + + // second step: readjust all absolute values by rewinding client's delta moves on top of it. + T last = corrected; + for (int i = afterIndex; i < history.Count; ++i) + { + double key = history.Keys[i]; + T value = history.Values[i]; + + // correct absolute position based on last + delta. + value.position = last.position + value.positionDelta; + value.velocity = last.velocity + value.velocityDelta; + value.angularVelocity = last.angularVelocity + value.angularVelocityDelta; + // Quaternions always need to be normalized in order to be a valid rotation after operations + value.rotation = (value.rotationDelta * last.rotation).normalized; // quaternions add delta by multiplying in this order + + // save the corrected entry into history. + history[key] = value; + + // save last + last = value; + } + + // third step: return the final recomputed state. + return last; + } + } +} diff --git a/Assets/Mirror/Core/Prediction/Prediction.cs.meta b/Assets/Mirror/Core/Prediction/Prediction.cs.meta new file mode 100644 index 0000000..37e078f --- /dev/null +++ b/Assets/Mirror/Core/Prediction/Prediction.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 216d494d910445ea8a7acc7c889212d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Prediction/Prediction.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/RemoteCalls.cs b/Assets/Mirror/Core/RemoteCalls.cs similarity index 75% rename from Assets/Mirror/Runtime/RemoteCalls.cs rename to Assets/Mirror/Core/RemoteCalls.cs index 127e241..5dbe52f 100644 --- a/Assets/Mirror/Runtime/RemoteCalls.cs +++ b/Assets/Mirror/Core/RemoteCalls.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using UnityEngine; namespace Mirror.RemoteCalls @@ -31,6 +30,8 @@ public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCa /// Used to help manage remote calls for NetworkBehaviours public static class RemoteProcedureCalls { + public const string InvokeRpcPrefix = "InvokeUserCode_"; + // one lookup for all remote calls. // allows us to easily add more remote call types without duplicating code. // note: do not clear those with [RuntimeInitializeOnLoad] @@ -38,13 +39,14 @@ public static class RemoteProcedureCalls // IMPORTANT: cmd/rpc functions are identified via **HASHES**. // an index would requires half the bandwidth, but introduces issues // where static constructors are lazily called, so index order isn't - // guaranteed: + // guaranteed. keep hashes to avoid: // https://github.com/vis2k/Mirror/pull/3135 // https://github.com/vis2k/Mirror/issues/3138 - // keep the 4 byte hash for stability! - static readonly Dictionary remoteCallDelegates = new Dictionary(); + // BUT: 2 byte hash is enough if we check for collisions. that's what we + // do for NetworkMessage as well. + static readonly Dictionary remoteCallDelegates = new Dictionary(); - static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, int functionHash) + static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, ushort functionHash) { if (remoteCallDelegates.ContainsKey(functionHash)) { @@ -58,17 +60,17 @@ static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallT // otherwise notify user. there is a rare chance of string // hash collisions. - Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them"); + Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them. To save bandwidth, we only use 2 bytes for the hash, which has a small chance of collisions."); } return false; } // pass full function name to avoid ClassA.Func & ClassB.Func collisions - internal static int RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) + internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) { // type+func so Inventory.RpcUse != Equipment.RpcUse - int hash = functionFullName.GetStableHashCode(); + ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF); if (CheckIfDelegateExists(componentType, remoteCallType, func, hash)) return hash; @@ -96,19 +98,30 @@ public static void RegisterRpc(Type componentType, string functionFullName, Remo RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func); // to clean up tests - internal static void RemoveDelegate(int hash) => + internal static void RemoveDelegate(ushort hash) => remoteCallDelegates.Remove(hash); + internal static bool GetFunctionMethodName(ushort functionHash, out string methodName) + { + if (remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker)) + { + methodName = invoker.function.GetMethodName().Replace(InvokeRpcPrefix, ""); + return true; + } + methodName = ""; + return false; + } + // note: no need to throw an error if not found. // an attacker might just try to call a cmd with an rpc's hash etc. // returning false is enough. - static bool GetInvokerForHash(int functionHash, RemoteCallType remoteCallType, out Invoker invoker) => + static bool GetInvokerForHash(ushort functionHash, RemoteCallType remoteCallType, out Invoker invoker) => remoteCallDelegates.TryGetValue(functionHash, out invoker) && invoker != null && invoker.callType == remoteCallType; // InvokeCmd/Rpc Delegate can all use the same function here - internal static bool Invoke(int functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null) + internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null) { // IMPORTANT: we check if the message's componentIndex component is // actually of the right type. prevents attackers trying @@ -124,12 +137,12 @@ internal static bool Invoke(int functionHash, RemoteCallType remoteCallType, Net } // check if the command 'requiresAuthority' which is set in the attribute - internal static bool CommandRequiresAuthority(int cmdHash) => + internal static bool CommandRequiresAuthority(ushort cmdHash) => GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) && invoker.cmdRequiresAuthority; /// Gets the handler function by hash. Useful for profilers and debuggers. - public static RemoteCallDelegate GetDelegate(int functionHash) => + public static RemoteCallDelegate GetDelegate(ushort functionHash) => remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker) ? invoker.function : null; diff --git a/Assets/Mirror/Runtime/RemoteCalls.cs.meta b/Assets/Mirror/Core/RemoteCalls.cs.meta similarity index 55% rename from Assets/Mirror/Runtime/RemoteCalls.cs.meta rename to Assets/Mirror/Core/RemoteCalls.cs.meta index 7bbc087..f3f822d 100644 --- a/Assets/Mirror/Runtime/RemoteCalls.cs.meta +++ b/Assets/Mirror/Core/RemoteCalls.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: f50cefa9e65db5f4f85c893b9661c6f0 +guid: d2cdbcbd1e377d6408a91acbec31ba16 MonoImporter: externalObjects: {} serializedVersion: 2 @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/RemoteCalls.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation.meta b/Assets/Mirror/Core/SnapshotInterpolation.meta similarity index 100% rename from Assets/Mirror/Runtime/SnapshotInterpolation.meta rename to Assets/Mirror/Core/SnapshotInterpolation.meta diff --git a/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs new file mode 100644 index 0000000..6b8ba8a --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs @@ -0,0 +1,17 @@ +// Snapshot interface so we can reuse it for all kinds of systems. +// for example, NetworkTransform, NetworkRigidbody, CharacterController etc. +// NOTE: we use '' and 'where T : Snapshot' to avoid boxing. +// List would cause allocations through boxing. +namespace Mirror +{ + public interface Snapshot + { + // the remote timestamp (when it was sent by the remote) + double remoteTime { get; set; } + + // the local timestamp (when it was received on our end) + // technically not needed for basic snapshot interpolation. + // only for dynamic buffer time adjustment. + double localTime { get; set; } + } +} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta similarity index 60% rename from Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta rename to Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta index 24eedd7..de82446 100644 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs.meta +++ b/Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs new file mode 100644 index 0000000..50339fb --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs @@ -0,0 +1,390 @@ +// snapshot interpolation V2 by mischa +// +// Unity independent to be engine agnostic & easy to test. +// boxing: in C#, uses does not box! passing the interface would box! +// +// credits: +// glenn fiedler: https://gafferongames.com/post/snapshot_interpolation/ +// fholm: netcode streams +// fakebyte: standard deviation for dynamic adjustment +// ninjakicka: math & debugging +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class SortedListExtensions + { + // removes the first 'amount' elements from the sorted list + public static void RemoveRange(this SortedList list, int amount) + { + // remove the first element 'amount' times. + // handles -1 and > count safely. + for (int i = 0; i < amount && i < list.Count; ++i) + list.RemoveAt(0); + } + } + + public static class SnapshotInterpolation + { + // calculate timescale for catch-up / slow-down + // note that negative threshold should be <0. + // caller should verify (i.e. Unity OnValidate). + // improves branch prediction. + public static double Timescale( + double drift, // how far we are off from bufferTime + double catchupSpeed, // in % [0,1] + double slowdownSpeed, // in % [0,1] + double absoluteCatchupNegativeThreshold, // in seconds (careful, we may run out of snapshots) + double absoluteCatchupPositiveThreshold) // in seconds + { + // if the drift time is too large, it means we are behind more time. + // so we need to speed up the timescale. + // note the threshold should be sendInterval * catchupThreshold. + if (drift > absoluteCatchupPositiveThreshold) + { + // localTimeline += 0.001; // too simple, this would ping pong + return 1 + catchupSpeed; // n% faster + } + + // if the drift time is too small, it means we are ahead of time. + // so we need to slow down the timescale. + // note the threshold should be sendInterval * catchupThreshold. + if (drift < absoluteCatchupNegativeThreshold) + { + // localTimeline -= 0.001; // too simple, this would ping pong + return 1 - slowdownSpeed; // n% slower + } + + // keep constant timescale while within threshold. + // this way we have perfectly smooth speed most of the time. + return 1; + } + + // calculate dynamic buffer time adjustment + public static double DynamicAdjustment( + double sendInterval, + double jitterStandardDeviation, + double dynamicAdjustmentTolerance) + { + // jitter is equal to delivery time standard variation. + // delivery time is made up of 'sendInterval+jitter'. + // .Average would be dampened by the constant sendInterval + // .StandardDeviation is the changes in 'jitter' that we want + // so add it to send interval again. + double intervalWithJitter = sendInterval + jitterStandardDeviation; + + // how many multiples of sendInterval is that? + // we want to convert to bufferTimeMultiplier later. + double multiples = intervalWithJitter / sendInterval; + + // add the tolerance + double safezone = multiples + dynamicAdjustmentTolerance; + // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}"); + return safezone; + } + + // helper function to insert a snapshot if it doesn't exist yet. + // extra function so we can use it for both cases: + // NetworkClient global timeline insertions & adjustments via Insert. + // NetworkBehaviour local insertion without any time adjustments. + public static bool InsertIfNotExists( + SortedList buffer, // snapshot buffer + int bufferLimit, // don't grow infinitely + T snapshot) // the newly received snapshot + where T : Snapshot + { + // slow clients may not be able to process incoming snapshots fast enough. + // infinitely growing snapshots would make it even worse. + // for example, run NetworkRigidbodyBenchmark while deep profiling client. + // the client just grows and reallocates the buffer forever. + if (buffer.Count >= bufferLimit) return false; + + // SortedList does not allow duplicates. + // we don't need to check ContainsKey (which is expensive). + // simply add and compare count before/after for the return value. + + //if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive + // buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists + + int before = buffer.Count; + buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists + return buffer.Count > before; + } + + // clamp timeline for cases where it gets too far behind. + // for example, a client app may go into the background and get updated + // with 1hz for a while. by the time it's back it's at least 30 frames + // behind, possibly more if the transport also queues up. In this + // scenario, at 1% catch up it took around 20+ seconds to finally catch + // up. For these kinds of scenarios it will be better to snap / clamp. + // + // to reproduce, try snapshot interpolation demo and press the button to + // simulate the client timeline at multiple seconds behind. it'll take + // a long time to catch up if the timeline is a long time behind. + public static double TimelineClamp( + double localTimeline, + double bufferTime, + double latestRemoteTime) + { + // we want local timeline to always be 'bufferTime' behind remote. + double targetTime = latestRemoteTime - bufferTime; + + // we define a boundary of 'bufferTime' around the target time. + // this is where catchup / slowdown will happen. + // outside of the area, we clamp. + double lowerBound = targetTime - bufferTime; // how far behind we can get + double upperBound = targetTime + bufferTime; // how far ahead we can get + return Mathd.Clamp(localTimeline, lowerBound, upperBound); + } + + // call this for every received snapshot. + // adds / inserts it to the list & initializes local time if needed. + public static void InsertAndAdjust( + SortedList buffer, // snapshot buffer + int bufferLimit, // don't grow infinitely + T snapshot, // the newly received snapshot + ref double localTimeline, // local interpolation time based on server time + ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time + float sendInterval, // for debugging + double bufferTime, // offset for buffering + double catchupSpeed, // in % [0,1] + double slowdownSpeed, // in % [0,1] + ref ExponentialMovingAverage driftEma, // for catchup / slowdown + float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) + float catchupPositiveThreshold, // in % of sendInterval + ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment + where T : Snapshot + { + // first snapshot? + // initialize local timeline. + // we want it to be behind by 'offset'. + // + // note that the first snapshot may be a lagging packet. + // so we would always be behind by that lag. + // this requires catchup later. + if (buffer.Count == 0) + localTimeline = snapshot.remoteTime - bufferTime; + + // insert into the buffer. + // + // note that we might insert it between our current interpolation + // which is fine, it adds another data point for accuracy. + // + // note that insert may be called twice for the same key. + // by default, this would throw. + // need to handle it silently. + if (InsertIfNotExists(buffer, bufferLimit, snapshot)) + { + // dynamic buffer adjustment needs delivery interval jitter + if (buffer.Count >= 2) + { + // note that this is not entirely accurate for scrambled inserts. + // + // we always use the last two, not what we just inserted + // even if we were to use the diff for what we just inserted, + // a scrambled insert would still not be 100% accurate: + // => assume a buffer of AC, with delivery time C-A + // => we then insert B, with delivery time B-A + // => but then technically the first C-A wasn't correct, + // as it would have to be C-B + // + // in practice, scramble is rare and won't make much difference + double previousLocalTime = buffer.Values[buffer.Count - 2].localTime; + double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime; + + // this is the delivery time since last snapshot + double localDeliveryTime = lastestLocalTime - previousLocalTime; + + // feed the local delivery time to the EMA. + // this is what the original stream did too. + // our final dynamic buffer adjustment is different though. + // we use standard deviation instead of average. + deliveryTimeEma.Add(localDeliveryTime); + } + + // adjust timescale to catch up / slow down after each insertion + // because that is when we add new values to our EMA. + + // we want localTimeline to be about 'bufferTime' behind. + // for that, we need the delivery time EMA. + // snapshots may arrive out of order, we can not use last-timeline. + // we need to use the inserted snapshot's time - timeline. + double latestRemoteTime = snapshot.remoteTime; + + // ensure timeline stays within a reasonable bound behind/ahead. + localTimeline = TimelineClamp(localTimeline, bufferTime, latestRemoteTime); + + // calculate timediff after localTimeline override changes + double timeDiff = latestRemoteTime - localTimeline; + + // next, calculate average of a few seconds worth of timediffs. + // this gives smoother results. + // + // to calculate the average, we could simply loop through the + // last 'n' seconds worth of timediffs, but: + // - our buffer may only store a few snapshots (bufferTime) + // - looping through seconds worth of snapshots every time is + // expensive + // + // to solve this, we use an exponential moving average. + // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + // which is basically fancy math to do the same but faster. + // additionally, it allows us to look at more timeDiff values + // than we sould have access to in our buffer :) + driftEma.Add(timeDiff); + + // timescale depends on driftEma. + // driftEma only changes when inserting. + // therefore timescale only needs to be calculated when inserting. + // saves CPU cycles in Update. + + // next up, calculate how far we are currently away from bufferTime + double drift = driftEma.Value - bufferTime; + + // convert relative thresholds to absolute values based on sendInterval + double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold; + double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold; + + // next, set localTimescale to catchup consistently in Update(). + // we quantize between default/catchup/slowdown, + // this way we have 'default' speed most of the time(!). + // and only catch up / slow down for a little bit occasionally. + // a consistent multiplier would never be exactly 1.0. + localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold); + + // debug logging + // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}"); + } + } + + // sample snapshot buffer to find the pair around the given time. + // returns indices so we can use it with RemoveRange to clear old snaps. + // make sure to use use buffer.Values[from/to], not buffer[from/to]. + // make sure to only call this is we have > 0 snapshots. + public static void Sample( + SortedList buffer, // snapshot buffer + double localTimeline, // local interpolation time based on server time. this is basically remoteTime-bufferTime. + out int from, // the snapshot <= time + out int to, // the snapshot >= time + out double t) // interpolation factor + where T : Snapshot + { + from = -1; + to = -1; + t = 0; + + // sample from [0,count-1] so we always have two at 'i' and 'i+1'. + for (int i = 0; i < buffer.Count - 1; ++i) + { + // is local time between these two? + T first = buffer.Values[i]; + T second = buffer.Values[i + 1]; + if (localTimeline >= first.remoteTime && + localTimeline <= second.remoteTime) + { + // use these two snapshots + from = i; + to = i + 1; + t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline); + return; + } + } + + // didn't find two snapshots around local time. + // so pick either the first or last, depending on which is closer. + + // oldest snapshot ahead of local time? + if (buffer.Values[0].remoteTime > localTimeline) + { + from = to = 0; + t = 0; + } + // otherwise initialize both to the last one + else + { + from = to = buffer.Count - 1; + t = 0; + } + } + + // progress local timeline every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // decoupled from Step for easier testing and so we can progress + // time only once in NetworkClient, while stepping for each component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StepTime( + double deltaTime, // engine delta time (unscaled) + ref double localTimeline, // local interpolation time based on server time + double localTimescale) // catchup / slowdown is applied to time every update) + { + // move local forward in time, scaled with catchup / slowdown applied + localTimeline += deltaTime * localTimescale; + } + + // sample, clear old. + // call this every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // returns true if there is anything to apply (requires at least 1 snap) + // from/to/t are out parameters instead of an interpolated 'computed'. + // this allows us to store from/to/t globally (i.e. in NetworkClient) + // and have each component apply the interpolation manually. + // besides, passing "Func Interpolate" would allocate anyway. + public static void StepInterpolation( + SortedList buffer, // snapshot buffer + double localTimeline, // local interpolation time based on server time. this is basically remoteTime-bufferTime. + out T fromSnapshot, // we interpolate 'from' this snapshot + out T toSnapshot, // 'to' this snapshot + out double t) // at ratio 't' [0,1] + where T : Snapshot + { + // check this in caller: + // nothing to do if there are no snapshots at all yet + // if (buffer.Count == 0) return false; + + // sample snapshot buffer at local interpolation time + Sample(buffer, localTimeline, out int from, out int to, out t); + + // save from/to + fromSnapshot = buffer.Values[from]; + toSnapshot = buffer.Values[to]; + + // remove older snapshots that we definitely don't need anymore. + // after(!) using the indices. + // + // if we have 3 snapshots and we are between 2nd and 3rd: + // from = 1, to = 2 + // then we need to remove the first one, which is exactly 'from'. + // because 'from-1' = 0 would remove none. + buffer.RemoveRange(from); + } + + // update time, sample, clear old. + // call this every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // returns true if there is anything to apply (requires at least 1 snap) + // from/to/t are out parameters instead of an interpolated 'computed'. + // this allows us to store from/to/t globally (i.e. in NetworkClient) + // and have each component apply the interpolation manually. + // besides, passing "Func Interpolate" would allocate anyway. + public static void Step( + SortedList buffer, // snapshot buffer + double deltaTime, // engine delta time (unscaled) + ref double localTimeline, // local interpolation time based on server time + double localTimescale, // catchup / slowdown is applied to time every update + out T fromSnapshot, // we interpolate 'from' this snapshot + out T toSnapshot, // 'to' this snapshot + out double t) // at ratio 't' [0,1] + where T : Snapshot + { + StepTime(deltaTime, ref localTimeline, localTimescale); + StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t); + } + } +} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta similarity index 59% rename from Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta rename to Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta index 244c5fb..6cb24d5 100644 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs.meta +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs new file mode 100644 index 0000000..74feae4 --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs @@ -0,0 +1,70 @@ +// snapshot interpolation settings struct. +// can easily be exposed in Unity inspectors. +using System; +using UnityEngine; + +namespace Mirror +{ + // class so we can define defaults easily + [Serializable] + public class SnapshotInterpolationSettings + { + // decrease bufferTime at runtime to see the catchup effect. + // increase to see slowdown. + // 'double' so we can have very precise dynamic adjustment without rounding + [Header("Buffering")] + [Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")] + public double bufferTimeMultiplier = 2; + + [Tooltip("If a client can't process snapshots fast enough, don't store too many.")] + public int bufferLimit = 32; + + // catchup ///////////////////////////////////////////////////////////// + // catchup thresholds in 'frames'. + // half a frame might be too aggressive. + [Header("Catchup / Slowdown")] + [Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")] + public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots + + [Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")] + public float catchupPositiveThreshold = 1; + + [Tooltip("Local timeline acceleration in % while catching up.")] + [Range(0, 1)] + public double catchupSpeed = 0.02f; // see snap interp demo. 1% is too slow. + + [Tooltip("Local timeline slowdown in % while slowing down.")] + [Range(0, 1)] + public double slowdownSpeed = 0.04f; // slow down a little faster so we don't encounter empty buffer (= jitter) + + [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")] + public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway + + // dynamic buffer time adjustment ////////////////////////////////////// + // dynamically adjusts bufferTimeMultiplier for smooth results. + // to understand how this works, try this manually: + // + // - disable dynamic adjustment + // - set jitter = 0.2 (20% is a lot!) + // - notice some stuttering + // - disable interpolation to see just how much jitter this really is(!) + // - enable interpolation again + // - manually increase bufferTimeMultiplier to 3-4 + // ... the cube slows down (blue) until it's smooth + // - with dynamic adjustment enabled, it will set 4 automatically + // ... the cube slows down (blue) until it's smooth as well + // + // note that 20% jitter is extreme. + // for this to be perfectly smooth, set the safety tolerance to '2'. + // but realistically this is not necessary, and '1' is enough. + [Header("Dynamic Adjustment")] + [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")] + public bool dynamicAdjustment = true; + + [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")] + public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments) + + [Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")] + public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time + } +} diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs.meta new file mode 100644 index 0000000..f90b1da --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: f955b76b7956417088c03992b3622dc9 +timeCreated: 1678507210 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolationSettings.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs new file mode 100644 index 0000000..5bfdd3a --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs @@ -0,0 +1,15 @@ +namespace Mirror +{ + // empty snapshot that is only used to progress client's local timeline. + public struct TimeSnapshot : Snapshot + { + public double remoteTime { get; set; } + public double localTime { get; set; } + + public TimeSnapshot(double remoteTime, double localTime) + { + this.remoteTime = remoteTime; + this.localTime = localTime; + } + } +} diff --git a/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta new file mode 100644 index 0000000..2215879 --- /dev/null +++ b/Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: afe2b5ed49634971a2aec720ad74e5cd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SnapshotInterpolation/TimeSnapshot.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs b/Assets/Mirror/Core/SyncDictionary.cs similarity index 61% rename from Assets/Mirror/Runtime/SyncDictionary.cs rename to Assets/Mirror/Core/SyncDictionary.cs index c63077c..29bc931 100644 --- a/Assets/Mirror/Runtime/SyncDictionary.cs +++ b/Assets/Mirror/Core/SyncDictionary.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; @@ -5,22 +6,44 @@ namespace Mirror { public class SyncIDictionary : SyncObject, IDictionary, IReadOnlyDictionary { - public delegate void SyncDictionaryChanged(Operation op, TKey key, TValue item); + /// This is called after the item is added with TKey + public Action OnAdd; - protected readonly IDictionary objects; + /// This is called after the item is changed with TKey. TValue is the OLD item + public Action OnSet; - public int Count => objects.Count; - public bool IsReadOnly { get; private set; } - public event SyncDictionaryChanged Callback; + /// This is called after the item is removed with TKey. TValue is the OLD item + public Action OnRemove; + + /// This is called before the data is cleared + public Action OnClear; public enum Operation : byte { OP_ADD, - OP_CLEAR, + OP_SET, OP_REMOVE, - OP_SET + OP_CLEAR } + /// + /// This is called for all changes to the Dictionary. + /// For OP_ADD, TValue is the NEW value of the entry. + /// For OP_SET and OP_REMOVE, TValue is the OLD value of the entry. + /// For OP_CLEAR, both TKey and TValue are default. + /// + public Action OnChange; + + protected readonly IDictionary objects; + + public SyncIDictionary(IDictionary objects) + { + this.objects = objects; + } + + public int Count => objects.Count; + public bool IsReadOnly => !IsWritable(); + struct Change { internal Operation operation; @@ -30,7 +53,7 @@ struct Change // list of changes. // -> insert/delete/clear is only ONE change - // -> changing the same slot 10x caues 10 changes. + // -> changing the same slot 10x causes 10 changes. // -> note that this grows until next sync(!) // TODO Dictionary to avoid ever growing changes / redundant changes! readonly List changes = new List(); @@ -41,14 +64,6 @@ struct Change // so we need to skip them int changesAhead; - public override void Reset() - { - IsReadOnly = false; - changes.Clear(); - changesAhead = 0; - objects.Clear(); - } - public ICollection Keys => objects.Keys; public ICollection Values => objects.Values; @@ -57,38 +72,6 @@ public override void Reset() IEnumerable IReadOnlyDictionary.Values => objects.Values; - // throw away all the changes - // this should be called after a successful sync - public override void ClearChanges() => changes.Clear(); - - public SyncIDictionary(IDictionary objects) - { - this.objects = objects; - } - - void AddOperation(Operation op, TKey key, TValue item) - { - if (IsReadOnly) - { - throw new System.InvalidOperationException("SyncDictionaries can only be modified by the server"); - } - - Change change = new Change - { - operation = op, - key = key, - item = item - }; - - if (IsRecording()) - { - changes.Add(change); - OnDirty?.Invoke(); - } - - Callback?.Invoke(op, key, item); - } - public override void OnSerializeAll(NetworkWriter writer) { // if init, write the full list content @@ -120,11 +103,13 @@ public override void OnSerializeDelta(NetworkWriter writer) switch (change.operation) { case Operation.OP_ADD: - case Operation.OP_REMOVE: case Operation.OP_SET: writer.Write(change.key); writer.Write(change.item); break; + case Operation.OP_REMOVE: + writer.Write(change.key); + break; case Operation.OP_CLEAR: break; } @@ -133,9 +118,6 @@ public override void OnSerializeDelta(NetworkWriter writer) public override void OnDeserializeAll(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - // if init, write the full list content int count = (int)reader.ReadUInt(); @@ -157,9 +139,6 @@ public override void OnDeserializeAll(NetworkReader reader) public override void OnDeserializeDelta(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - int changesCount = (int)reader.ReadUInt(); for (int i = 0; i < changesCount; i++) @@ -180,55 +159,71 @@ public override void OnDeserializeDelta(NetworkReader reader) item = reader.Read(); if (apply) { - objects[key] = item; + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + if (objects.TryGetValue(key, out TValue oldItem)) + { + objects[key] = item; // assign after TryGetValue + AddOperation(Operation.OP_SET, key, item, oldItem, false); + } + else + { + objects[key] = item; // assign after TryGetValue + AddOperation(Operation.OP_ADD, key, item, default, false); + } } break; case Operation.OP_CLEAR: if (apply) { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, default, default, default, false); + // clear after invoking the callback so users can iterate the dictionary + // and take appropriate action on the items before they are wiped. objects.Clear(); } break; case Operation.OP_REMOVE: key = reader.Read(); - item = reader.Read(); if (apply) { - objects.Remove(key); + if (objects.TryGetValue(key, out TValue oldItem)) + { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + objects.Remove(key); + AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false); + } } break; } - if (apply) - { - Callback?.Invoke(operation, key, item); - } - // we just skipped this change - else + if (!apply) { + // we just skipped this change changesAhead--; } } } - public void Clear() - { - objects.Clear(); - AddOperation(Operation.OP_CLEAR, default, default); - } - - public bool ContainsKey(TKey key) => objects.ContainsKey(key); + // throw away all the changes + // this should be called after a successful sync + public override void ClearChanges() => changes.Clear(); - public bool Remove(TKey key) + public override void Reset() { - if (objects.TryGetValue(key, out TValue item) && objects.Remove(key)) - { - AddOperation(Operation.OP_REMOVE, key, item); - return true; - } - return false; + changes.Clear(); + changesAhead = 0; + objects.Clear(); } public TValue this[TKey i] @@ -238,42 +233,31 @@ public TValue this[TKey i] { if (ContainsKey(i)) { + TValue oldItem = objects[i]; objects[i] = value; - AddOperation(Operation.OP_SET, i, value); + AddOperation(Operation.OP_SET, i, value, oldItem, true); } else { objects[i] = value; - AddOperation(Operation.OP_ADD, i, value); + AddOperation(Operation.OP_ADD, i, value, default, true); } } } public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value); - public void Add(TKey key, TValue value) - { - objects.Add(key, value); - AddOperation(Operation.OP_ADD, key, value); - } - - public void Add(KeyValuePair item) => Add(item.Key, item.Value); + public bool ContainsKey(TKey key) => objects.ContainsKey(key); - public bool Contains(KeyValuePair item) - { - return TryGetValue(item.Key, out TValue val) && EqualityComparer.Default.Equals(val, item.Value); - } + public bool Contains(KeyValuePair item) => TryGetValue(item.Key, out TValue val) && EqualityComparer.Default.Equals(val, item.Value); public void CopyTo(KeyValuePair[] array, int arrayIndex) { if (arrayIndex < 0 || arrayIndex > array.Length) - { throw new System.ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range"); - } + if (array.Length - arrayIndex < Count) - { throw new System.ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array"); - } int i = arrayIndex; foreach (KeyValuePair item in objects) @@ -283,14 +267,78 @@ public void CopyTo(KeyValuePair[] array, int arrayIndex) } } + public void Add(KeyValuePair item) => Add(item.Key, item.Value); + + public void Add(TKey key, TValue value) + { + objects.Add(key, value); + AddOperation(Operation.OP_ADD, key, value, default, true); + } + + public bool Remove(TKey key) + { + if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key)) + { + AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true); + return true; + } + return false; + } + public bool Remove(KeyValuePair item) { bool result = objects.Remove(item.Key); if (result) + AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true); + + return result; + } + + public void Clear() + { + AddOperation(Operation.OP_CLEAR, default, default, default, true); + // clear after invoking the callback so users can iterate the dictionary + // and take appropriate action on the items before they are wiped. + objects.Clear(); + } + + void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess) + { + if (checkAccess && IsReadOnly) + throw new InvalidOperationException("SyncDictionaries can only be modified by the owner."); + + Change change = new Change + { + operation = op, + key = key, + item = item + }; + + if (IsRecording()) + { + changes.Add(change); + OnDirty?.Invoke(); + } + + switch (op) { - AddOperation(Operation.OP_REMOVE, item.Key, item.Value); + case Operation.OP_ADD: + OnAdd?.Invoke(key); + OnChange?.Invoke(op, key, item); + break; + case Operation.OP_SET: + OnSet?.Invoke(key, oldItem); + OnChange?.Invoke(op, key, oldItem); + break; + case Operation.OP_REMOVE: + OnRemove?.Invoke(key, oldItem); + OnChange?.Invoke(op, key, oldItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + OnChange?.Invoke(op, default, default); + break; } - return result; } public IEnumerator> GetEnumerator() => objects.GetEnumerator(); @@ -300,9 +348,9 @@ public bool Remove(KeyValuePair item) public class SyncDictionary : SyncIDictionary { - public SyncDictionary() : base(new Dictionary()) {} - public SyncDictionary(IEqualityComparer eq) : base(new Dictionary(eq)) {} - public SyncDictionary(IDictionary d) : base(new Dictionary(d)) {} + public SyncDictionary() : base(new Dictionary()) { } + public SyncDictionary(IEqualityComparer eq) : base(new Dictionary(eq)) { } + public SyncDictionary(IDictionary d) : base(new Dictionary(d)) { } public new Dictionary.ValueCollection Values => ((Dictionary)objects).Values; public new Dictionary.KeyCollection Keys => ((Dictionary)objects).Keys; public new Dictionary.Enumerator GetEnumerator() => ((Dictionary)objects).GetEnumerator(); diff --git a/Assets/Mirror/Runtime/SyncDictionary.cs.meta b/Assets/Mirror/Core/SyncDictionary.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/SyncDictionary.cs.meta rename to Assets/Mirror/Core/SyncDictionary.cs.meta index 1c20b57..d9362fe 100644 --- a/Assets/Mirror/Runtime/SyncDictionary.cs.meta +++ b/Assets/Mirror/Core/SyncDictionary.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncDictionary.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/SyncList.cs b/Assets/Mirror/Core/SyncList.cs similarity index 66% rename from Assets/Mirror/Runtime/SyncList.cs rename to Assets/Mirror/Core/SyncList.cs index 9eb0a59..3986725 100644 --- a/Assets/Mirror/Runtime/SyncList.cs +++ b/Assets/Mirror/Core/SyncList.cs @@ -6,24 +6,53 @@ namespace Mirror { public class SyncList : SyncObject, IList, IReadOnlyList { - public delegate void SyncListChanged(Operation op, int itemIndex, T oldItem, T newItem); + /// This is called after the item is added with index + public Action OnAdd; - readonly IList objects; - readonly IEqualityComparer comparer; + /// This is called after the item is inserted with index + public Action OnInsert; - public int Count => objects.Count; - public bool IsReadOnly { get; private set; } - public event SyncListChanged Callback; + /// This is called after the item is set with index and OLD Value + public Action OnSet; + + /// This is called after the item is removed with index and OLD Value + public Action OnRemove; + + /// This is called before the list is cleared so the list can be iterated + public Action OnClear; public enum Operation : byte { OP_ADD, - OP_CLEAR, + OP_SET, OP_INSERT, OP_REMOVEAT, - OP_SET + OP_CLEAR } + /// + /// This is called for all changes to the List. + /// For OP_ADD and OP_INSERT, T is the NEW value of the entry. + /// For OP_SET and OP_REMOVE, T is the OLD value of the entry. + /// For OP_CLEAR, T is default. + /// + // TODO deprecate in favor of explicit Callback, later rename Callback to OnChange for consistency with other SyncCollections. + public Action OnChange; + + /// + /// This is called for all changes to the List. + /// Parameters: Operation, index, oldItem, newItem. + /// Sometimes we need both oldItem and newItem. + /// Keep for compatibility since 10 years of projects use this. + /// + public Action Callback; + + readonly IList objects; + readonly IEqualityComparer comparer; + + public int Count => objects.Count; + public bool IsReadOnly => !IsWritable(); + struct Change { internal Operation operation; @@ -43,7 +72,7 @@ struct Change // so we need to skip them int changesAhead; - public SyncList() : this(EqualityComparer.Default) {} + public SyncList() : this(EqualityComparer.Default) { } public SyncList(IEqualityComparer comparer) { @@ -63,18 +92,15 @@ public SyncList(IList objects, IEqualityComparer comparer = null) public override void Reset() { - IsReadOnly = false; changes.Clear(); changesAhead = 0; objects.Clear(); } - void AddOperation(Operation op, int itemIndex, T oldItem, T newItem) + void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkAccess) { - if (IsReadOnly) - { - throw new InvalidOperationException("Synclists can only be modified at the server"); - } + if (checkAccess && IsReadOnly) + throw new InvalidOperationException("Synclists can only be modified by the owner."); Change change = new Change { @@ -89,7 +115,34 @@ void AddOperation(Operation op, int itemIndex, T oldItem, T newItem) OnDirty?.Invoke(); } - Callback?.Invoke(op, itemIndex, oldItem, newItem); + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(itemIndex); + OnChange?.Invoke(op, itemIndex, newItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_INSERT: + OnInsert?.Invoke(itemIndex); + OnChange?.Invoke(op, itemIndex, newItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_SET: + OnSet?.Invoke(itemIndex, oldItem); + OnChange?.Invoke(op, itemIndex, oldItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_REMOVEAT: + OnRemove?.Invoke(itemIndex, oldItem); + OnChange?.Invoke(op, itemIndex, oldItem); + Callback?.Invoke(op, itemIndex, oldItem, newItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + OnChange?.Invoke(op, itemIndex, default); + Callback?.Invoke(op, itemIndex, default, default); + break; + } } public override void OnSerializeAll(NetworkWriter writer) @@ -144,9 +197,6 @@ public override void OnSerializeDelta(NetworkWriter writer) public override void OnDeserializeAll(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - // if init, write the full list content int count = (int)reader.ReadUInt(); @@ -167,9 +217,6 @@ public override void OnDeserializeAll(NetworkReader reader) public override void OnDeserializeDelta(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - int changesCount = (int)reader.ReadUInt(); for (int i = 0; i < changesCount; i++) @@ -191,12 +238,24 @@ public override void OnDeserializeDelta(NetworkReader reader) { index = objects.Count; objects.Add(newItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_ADD, objects.Count - 1, default, newItem, false); } break; case Operation.OP_CLEAR: if (apply) { + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, 0, default, default, false); + // clear after invoking the callback so users can iterate the list + // and take appropriate action on the items before they are wiped. objects.Clear(); } break; @@ -207,6 +266,11 @@ public override void OnDeserializeDelta(NetworkReader reader) if (apply) { objects.Insert(index, newItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_INSERT, index, default, newItem, false); } break; @@ -216,6 +280,11 @@ public override void OnDeserializeDelta(NetworkReader reader) { oldItem = objects[index]; objects.RemoveAt(index); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_REMOVEAT, index, oldItem, default, false); } break; @@ -226,17 +295,18 @@ public override void OnDeserializeDelta(NetworkReader reader) { oldItem = objects[index]; objects[index] = newItem; + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_SET, index, oldItem, newItem, false); } break; } - if (apply) - { - Callback?.Invoke(operation, index, oldItem, newItem); - } - // we just skipped this change - else + if (!apply) { + // we just skipped this change changesAhead--; } } @@ -245,21 +315,21 @@ public override void OnDeserializeDelta(NetworkReader reader) public void Add(T item) { objects.Add(item); - AddOperation(Operation.OP_ADD, objects.Count - 1, default, item); + AddOperation(Operation.OP_ADD, objects.Count - 1, default, item, true); } public void AddRange(IEnumerable range) { foreach (T entry in range) - { Add(entry); - } } public void Clear() { + AddOperation(Operation.OP_CLEAR, 0, default, default, true); + // clear after invoking the callback so users can iterate the list + // and take appropriate action on the items before they are wiped. objects.Clear(); - AddOperation(Operation.OP_CLEAR, 0, default, default); } public bool Contains(T item) => IndexOf(item) >= 0; @@ -300,7 +370,7 @@ public List FindAll(Predicate match) public void Insert(int index, T item) { objects.Insert(index, item); - AddOperation(Operation.OP_INSERT, index, default, item); + AddOperation(Operation.OP_INSERT, index, default, item, true); } public void InsertRange(int index, IEnumerable range) @@ -317,9 +387,8 @@ public bool Remove(T item) int index = IndexOf(item); bool result = index >= 0; if (result) - { RemoveAt(index); - } + return result; } @@ -327,7 +396,7 @@ public void RemoveAt(int index) { T oldItem = objects[index]; objects.RemoveAt(index); - AddOperation(Operation.OP_REMOVEAT, index, oldItem, default); + AddOperation(Operation.OP_REMOVEAT, index, oldItem, default, true); } public int RemoveAll(Predicate match) @@ -338,9 +407,7 @@ public int RemoveAll(Predicate match) toRemove.Add(objects[i]); foreach (T entry in toRemove) - { Remove(entry); - } return toRemove.Count; } @@ -354,7 +421,7 @@ public T this[int i] { T oldItem = objects[i]; objects[i] = value; - AddOperation(Operation.OP_SET, i, oldItem, value); + AddOperation(Operation.OP_SET, i, oldItem, value, true); } } } @@ -379,6 +446,7 @@ public struct Enumerator : IEnumerator { readonly SyncList list; int index; + public T Current { get; private set; } public Enumerator(SyncList list) @@ -391,16 +459,15 @@ public Enumerator(SyncList list) public bool MoveNext() { if (++index >= list.Count) - { return false; - } + Current = list[index]; return true; } public void Reset() => index = -1; object IEnumerator.Current => Current; - public void Dispose() {} + public void Dispose() { } } } } diff --git a/Assets/Mirror/Runtime/SyncList.cs.meta b/Assets/Mirror/Core/SyncList.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/SyncList.cs.meta rename to Assets/Mirror/Core/SyncList.cs.meta index 088ef1e..2a1d886 100644 --- a/Assets/Mirror/Runtime/SyncList.cs.meta +++ b/Assets/Mirror/Core/SyncList.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncList.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/SyncObject.cs b/Assets/Mirror/Core/SyncObject.cs similarity index 83% rename from Assets/Mirror/Runtime/SyncObject.cs rename to Assets/Mirror/Core/SyncObject.cs index 7df3b67..405e240 100644 --- a/Assets/Mirror/Runtime/SyncObject.cs +++ b/Assets/Mirror/Core/SyncObject.cs @@ -17,22 +17,24 @@ public abstract class SyncObject /// Used internally to check if we are currently tracking changes. // prevents ever growing .changes lists: - // if a monster has no observers but we keep modifing a SyncObject, + // if a monster has no observers but we keep modifying a SyncObject, // then the changes would never be flushed and keep growing, // because OnSerialize isn't called without observers. // => Func so we can set it to () => observers.Count > 0 // without depending on NetworkComponent/NetworkIdentity here. - // => virtual so it sipmly always records by default + // => virtual so it simply always records by default public Func IsRecording = () => true; + // SyncList/Set/etc. shouldn't be modifiable if not owned. + // otherwise they would silently get out of sync. + // need a lambda because InitSyncObject is called in ctor, when + // 'isClient' etc. aren't initialized yet. + public Func IsWritable = () => true; + /// Discard all the queued changes // Consider the object fully synchronized with clients public abstract void ClearChanges(); - // Deprecated 2021-09-17 - [Obsolete("Deprecated: Use ClearChanges instead.")] - public void Flush() => ClearChanges(); - /// Write a full copy of the object public abstract void OnSerializeAll(NetworkWriter writer); diff --git a/Assets/Mirror/Runtime/SyncObject.cs.meta b/Assets/Mirror/Core/SyncObject.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/SyncObject.cs.meta rename to Assets/Mirror/Core/SyncObject.cs.meta index 736c651..1b78301 100644 --- a/Assets/Mirror/Runtime/SyncObject.cs.meta +++ b/Assets/Mirror/Core/SyncObject.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncObject.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/SyncSet.cs b/Assets/Mirror/Core/SyncSet.cs similarity index 64% rename from Assets/Mirror/Runtime/SyncSet.cs rename to Assets/Mirror/Core/SyncSet.cs index 94e353f..7e9fee7 100644 --- a/Assets/Mirror/Runtime/SyncSet.cs +++ b/Assets/Mirror/Core/SyncSet.cs @@ -6,21 +6,35 @@ namespace Mirror { public class SyncSet : SyncObject, ISet { - public delegate void SyncSetChanged(Operation op, T item); + /// This is called after the item is added. T is the new item. + public Action OnAdd; - protected readonly ISet objects; + /// This is called after the item is removed. T is the OLD item + public Action OnRemove; - public int Count => objects.Count; - public bool IsReadOnly { get; private set; } - public event SyncSetChanged Callback; + /// This is called BEFORE the data is cleared + public Action OnClear; public enum Operation : byte { OP_ADD, - OP_CLEAR, - OP_REMOVE + OP_REMOVE, + OP_CLEAR } + /// + /// This is called for all changes to the Set. + /// For OP_ADD, T is the NEW value of the entry. + /// For OP_REMOVE, T is the OLD value of the entry. + /// For OP_CLEAR, T is default. + /// + public Action OnChange; + + protected readonly ISet objects; + + public int Count => objects.Count; + public bool IsReadOnly => !IsWritable(); + struct Change { internal Operation operation; @@ -47,7 +61,6 @@ public SyncSet(ISet objects) public override void Reset() { - IsReadOnly = false; changes.Clear(); changesAhead = 0; objects.Clear(); @@ -57,18 +70,36 @@ public override void Reset() // this should be called after a successful sync public override void ClearChanges() => changes.Clear(); - void AddOperation(Operation op, T item) + void AddOperation(Operation op, T oldItem, T newItem, bool checkAccess) { - if (IsReadOnly) - { - throw new InvalidOperationException("SyncSets can only be modified at the server"); - } + if (checkAccess && IsReadOnly) + throw new InvalidOperationException("SyncSets can only be modified by the owner."); - Change change = new Change + Change change = default; + switch (op) { - operation = op, - item = item - }; + case Operation.OP_ADD: + change = new Change + { + operation = op, + item = newItem + }; + break; + case Operation.OP_REMOVE: + change = new Change + { + operation = op, + item = oldItem + }; + break; + case Operation.OP_CLEAR: + change = new Change + { + operation = op, + item = default + }; + break; + } if (IsRecording()) { @@ -76,10 +107,24 @@ void AddOperation(Operation op, T item) OnDirty?.Invoke(); } - Callback?.Invoke(op, item); + switch (op) + { + case Operation.OP_ADD: + OnAdd?.Invoke(newItem); + OnChange?.Invoke(op, newItem); + break; + case Operation.OP_REMOVE: + OnRemove?.Invoke(oldItem); + OnChange?.Invoke(op, oldItem); + break; + case Operation.OP_CLEAR: + OnClear?.Invoke(); + OnChange?.Invoke(op, default); + break; + } } - void AddOperation(Operation op) => AddOperation(op, default); + void AddOperation(Operation op, bool checkAccess) => AddOperation(op, default, default, checkAccess); public override void OnSerializeAll(NetworkWriter writer) { @@ -87,9 +132,7 @@ public override void OnSerializeAll(NetworkWriter writer) writer.WriteUInt((uint)objects.Count); foreach (T obj in objects) - { writer.Write(obj); - } // all changes have been applied already // thus the client will need to skip all the pending changes @@ -113,22 +156,17 @@ public override void OnSerializeDelta(NetworkWriter writer) case Operation.OP_ADD: writer.Write(change.item); break; - - case Operation.OP_CLEAR: - break; - case Operation.OP_REMOVE: writer.Write(change.item); break; + case Operation.OP_CLEAR: + break; } } } public override void OnDeserializeAll(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - // if init, write the full list content int count = (int)reader.ReadUInt(); @@ -149,9 +187,6 @@ public override void OnDeserializeAll(NetworkReader reader) public override void OnDeserializeDelta(NetworkReader reader) { - // This list can now only be modified by synchronization - IsReadOnly = true; - int changesCount = (int)reader.ReadUInt(); for (int i = 0; i < changesCount; i++) @@ -161,41 +196,55 @@ public override void OnDeserializeDelta(NetworkReader reader) // apply the operation only if it is a new change // that we have not applied yet bool apply = changesAhead == 0; - T item = default; + T oldItem = default; + T newItem = default; switch (operation) { case Operation.OP_ADD: - item = reader.Read(); + newItem = reader.Read(); if (apply) { - objects.Add(item); + objects.Add(newItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_ADD, default, newItem, false); } break; - case Operation.OP_CLEAR: + case Operation.OP_REMOVE: + oldItem = reader.Read(); if (apply) { - objects.Clear(); + objects.Remove(oldItem); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_REMOVE, oldItem, default, false); } break; - case Operation.OP_REMOVE: - item = reader.Read(); + case Operation.OP_CLEAR: if (apply) { - objects.Remove(item); + // add dirty + changes. + // ClientToServer needs to set dirty in server OnDeserialize. + // no access check: server OnDeserialize can always + // write, even for ClientToServer (for broadcasting). + AddOperation(Operation.OP_CLEAR, false); + // clear after invoking the callback so users can iterate the set + // and take appropriate action on the items before they are wiped. + objects.Clear(); } break; } - if (apply) - { - Callback?.Invoke(operation, item); - } - // we just skipped this change - else + if (!apply) { + // we just skipped this change changesAhead--; } } @@ -205,7 +254,7 @@ public bool Add(T item) { if (objects.Add(item)) { - AddOperation(Operation.OP_ADD, item); + AddOperation(Operation.OP_ADD, default, item, true); return true; } return false; @@ -214,15 +263,15 @@ public bool Add(T item) void ICollection.Add(T item) { if (objects.Add(item)) - { - AddOperation(Operation.OP_ADD, item); - } + AddOperation(Operation.OP_ADD, default, item, true); } public void Clear() { + AddOperation(Operation.OP_CLEAR, true); + // clear after invoking the callback so users can iterate the set + // and take appropriate action on the items before they are wiped. objects.Clear(); - AddOperation(Operation.OP_CLEAR); } public bool Contains(T item) => objects.Contains(item); @@ -233,7 +282,7 @@ public bool Remove(T item) { if (objects.Remove(item)) { - AddOperation(Operation.OP_REMOVE, item); + AddOperation(Operation.OP_REMOVE, item, default, true); return true; } return false; @@ -253,17 +302,13 @@ public void ExceptWith(IEnumerable other) // remove every element in other from this foreach (T element in other) - { Remove(element); - } } public void IntersectWith(IEnumerable other) { if (other is ISet otherSet) - { IntersectWithSet(otherSet); - } else { HashSet otherAsSet = new HashSet(other); @@ -276,12 +321,8 @@ void IntersectWithSet(ISet otherSet) List elements = new List(objects); foreach (T element in elements) - { if (!otherSet.Contains(element)) - { Remove(element); - } - } } public bool IsProperSubsetOf(IEnumerable other) => objects.IsProperSubsetOf(other); @@ -300,38 +341,26 @@ void IntersectWithSet(ISet otherSet) public void SymmetricExceptWith(IEnumerable other) { if (other == this) - { Clear(); - } else - { foreach (T element in other) - { if (!Remove(element)) - { Add(element); - } - } - } } // custom implementation so we can do our own Clear/Add/Remove for delta public void UnionWith(IEnumerable other) { if (other != this) - { foreach (T element in other) - { Add(element); - } - } } } public class SyncHashSet : SyncSet { - public SyncHashSet() : this(EqualityComparer.Default) {} - public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer ?? EqualityComparer.Default)) {} + public SyncHashSet() : this(EqualityComparer.Default) { } + public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer ?? EqualityComparer.Default)) { } // allocation free enumerator public new HashSet.Enumerator GetEnumerator() => ((HashSet)objects).GetEnumerator(); @@ -339,8 +368,8 @@ public SyncHashSet(IEqualityComparer comparer) : base(new HashSet(comparer public class SyncSortedSet : SyncSet { - public SyncSortedSet() : this(Comparer.Default) {} - public SyncSortedSet(IComparer comparer) : base(new SortedSet(comparer ?? Comparer.Default)) {} + public SyncSortedSet() : this(Comparer.Default) { } + public SyncSortedSet(IComparer comparer) : base(new SortedSet(comparer ?? Comparer.Default)) { } // allocation free enumerator public new SortedSet.Enumerator GetEnumerator() => ((SortedSet)objects).GetEnumerator(); diff --git a/Assets/Mirror/Runtime/SyncSet.cs.meta b/Assets/Mirror/Core/SyncSet.cs.meta similarity index 64% rename from Assets/Mirror/Runtime/SyncSet.cs.meta rename to Assets/Mirror/Core/SyncSet.cs.meta index 6eeef1c..8eb0efe 100644 --- a/Assets/Mirror/Runtime/SyncSet.cs.meta +++ b/Assets/Mirror/Core/SyncSet.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/SyncSet.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Empty/Logging.meta b/Assets/Mirror/Core/Threading.meta similarity index 77% rename from Assets/Mirror/Editor/Empty/Logging.meta rename to Assets/Mirror/Core/Threading.meta index 257467f..037993e 100644 --- a/Assets/Mirror/Editor/Empty/Logging.meta +++ b/Assets/Mirror/Core/Threading.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 4d97731cd74ac8b4b8aad808548ef9cd +guid: 752fcafbee1ec45c9a43c0cf65da39de folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs new file mode 100644 index 0000000..c0bc926 --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs @@ -0,0 +1,45 @@ +// API consistent with Microsoft's ObjectPool. +// thread safe. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class ConcurrentNetworkWriterPool + { + // initial capacity to avoid allocations in the first few frames + // 1000 * 1200 bytes = around 1 MB. + public const int InitialCapacity = 1000; + + + // reuse ConcurrentPool + // we still wrap it in NetworkWriterPool.Get/Recycle so we can reset the + // position before reusing. + // this is also more consistent with NetworkReaderPool where we need to + // assign the internal buffer before reusing. + static readonly ConcurrentPool pool = + new ConcurrentPool( + // new object function + () => new ConcurrentNetworkWriterPooled(), + // initial capacity to avoid allocations in the first few frames + // 1000 * 1200 bytes = around 1 MB. + InitialCapacity + ); + + // pool size access for debugging & tests + public static int Count => pool.Count; + + public static ConcurrentNetworkWriterPooled Get() + { + // grab from pool & reset position + ConcurrentNetworkWriterPooled writer = pool.Get(); + writer.Position = 0; + return writer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Return(ConcurrentNetworkWriterPooled writer) + { + pool.Return(writer); + } + } +} diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs.meta b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs.meta new file mode 100644 index 0000000..8ff2757 --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fdf46e334f52400c854c9732f6fcf005 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs new file mode 100644 index 0000000..4baa7df --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs @@ -0,0 +1,10 @@ +using System; + +namespace Mirror +{ + /// Pooled (not threadsafe) NetworkWriter used from Concurrent pool (thread safe). Automatically returned to concurrent pool when using 'using' + public sealed class ConcurrentNetworkWriterPooled : NetworkWriter, IDisposable + { + public void Dispose() => ConcurrentNetworkWriterPool.Return(this); + } +} diff --git a/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs.meta b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs.meta new file mode 100644 index 0000000..8a1e745 --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9163d963b36b4e389318f312bfd8e488 +timeCreated: 1691485295 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ConcurrentNetworkWriterPooled.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/ConcurrentPool.cs b/Assets/Mirror/Core/Threading/ConcurrentPool.cs new file mode 100644 index 0000000..eeac0cc --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentPool.cs @@ -0,0 +1,44 @@ +// Pool to avoid allocations (from libuv2k) +// API consistent with Microsoft's ObjectPool. +// concurrent for thread safe access. +// +// currently not in use. keep it in case we need it again. +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public class ConcurrentPool + { + // Mirror is single threaded, no need for concurrent collections + // concurrent bag is for items who's order doesn't matter. + // just about right for our use case here. + readonly ConcurrentBag objects = new ConcurrentBag(); + + // some types might need additional parameters in their constructor, so + // we use a Func generator + readonly Func objectGenerator; + + public ConcurrentPool(Func objectGenerator, int initialCapacity) + { + this.objectGenerator = objectGenerator; + + // allocate an initial pool so we have fewer (if any) + // allocations in the first few frames (or seconds). + for (int i = 0; i < initialCapacity; ++i) + objects.Add(objectGenerator()); + } + + // take an element from the pool, or create a new one if empty + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Get() => objects.TryTake(out T obj) ? obj : objectGenerator(); + + // return an element to the pool + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Return(T item) => objects.Add(item); + + // count to see how many objects are in the pool. useful for tests. + public int Count => objects.Count; + } +} diff --git a/Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta b/Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta new file mode 100644 index 0000000..cb36abf --- /dev/null +++ b/Assets/Mirror/Core/Threading/ConcurrentPool.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ed304bd790ff478ca37233f66d04d1c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ConcurrentPool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/ThreadLog.cs b/Assets/Mirror/Core/Threading/ThreadLog.cs new file mode 100644 index 0000000..36dca5f --- /dev/null +++ b/Assets/Mirror/Core/Threading/ThreadLog.cs @@ -0,0 +1,112 @@ +// threaded Debug.Log support (mischa 2022) +// +// Editor shows Debug.Logs from different threads. +// Builds don't show Debug.Logs from different threads. +// +// need to hook into logMessageReceivedThreaded to receive them in builds too. +using System.Collections.Concurrent; +using System.Threading; +using UnityEngine; + +namespace Mirror +{ + public static class ThreadLog + { + // queue log messages from threads + struct LogEntry + { + public int threadId; + public LogType type; + public string message; + public string stackTrace; + + public LogEntry(int threadId, LogType type, string message, string stackTrace) + { + this.threadId = threadId; + this.type = type; + this.message = message; + this.stackTrace = stackTrace; + } + } + + // ConcurrentQueue allocations are fine here. + // logs allocate anywway. + static readonly ConcurrentQueue logs = + new ConcurrentQueue(); + + // main thread id + static int mainThreadId; + +#if !UNITY_EDITOR + // Editor as of Unity 2021 does log threaded messages. + // only builds don't. + // do nothing in editor, otherwise we would log twice. + // before scene load ensures thread logs are all caught. + // otherwise some component's Awake may be called before we hooked it up. + // for example, ThreadedTransport's early logs wouldn't be caught. + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void Initialize() + { + + // set main thread id + mainThreadId = Thread.CurrentThread.ManagedThreadId; + + // receive threaded log calls + Application.logMessageReceivedThreaded -= OnLog; // remove old first. TODO unnecessary? + Application.logMessageReceivedThreaded += OnLog; + + // process logs on main thread Update + NetworkLoop.OnLateUpdate -= OnLateUpdate; // remove old first. TODO unnecessary? + NetworkLoop.OnLateUpdate += OnLateUpdate; + + // log for debugging + Debug.Log("ThreadLog initialized."); + } +#endif + + static bool IsMainThread() => + Thread.CurrentThread.ManagedThreadId == mainThreadId; + + // callback runs on the same thread where the Debug.Log is called. + // we can use this to buffer messages for main thread here. + static void OnLog(string message, string stackTrace, LogType type) + { + // only enqueue messages from other threads. + // otherwise OnLateUpdate main thread logging would be enqueued + // as well, causing deadlock. + if (IsMainThread()) return; + + // queue for logging from main thread later + logs.Enqueue(new LogEntry(Thread.CurrentThread.ManagedThreadId, type, message, stackTrace)); + } + + static void OnLateUpdate() + { + // process queued logs on main thread + while (logs.TryDequeue(out LogEntry entry)) + { + switch (entry.type) + { + // add [Thread#] prefix to make it super obvious where this log message comes from. + // some projects may see unexpected messages that were previously hidden, + // since Unity wouldn't log them without ThreadLog.cs. + case LogType.Log: + Debug.Log($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Warning: + Debug.LogWarning($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Error: + Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Exception: + Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + case LogType.Assert: + Debug.LogAssertion($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}"); + break; + } + } + } + } +} diff --git a/Assets/Mirror/Core/Threading/ThreadLog.cs.meta b/Assets/Mirror/Core/Threading/ThreadLog.cs.meta new file mode 100644 index 0000000..432955d --- /dev/null +++ b/Assets/Mirror/Core/Threading/ThreadLog.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 22360406b3844808b0a305486758a703 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/ThreadLog.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Threading/WorkerThread.cs b/Assets/Mirror/Core/Threading/WorkerThread.cs new file mode 100644 index 0000000..f570dcc --- /dev/null +++ b/Assets/Mirror/Core/Threading/WorkerThread.cs @@ -0,0 +1,169 @@ +// worker thread for Unity (mischa 2022) +// thread with proper exception handling, profling, init, cleanup, etc. for Unity. +// use this from main thread. +using System; +using System.Diagnostics; +using System.Threading; +using UnityEngine.Profiling; +using Debug = UnityEngine.Debug; + +namespace Mirror +{ + public class WorkerThread + { + readonly Thread thread; + + protected volatile bool active; + + // stopwatch so we don't need to use Unity's Time (engine independent) + readonly Stopwatch watch = new Stopwatch(); + + // callbacks need to be set after constructor. + // inheriting classes can't pass their member funcs to base ctor. + // don't set them while the thread is running! + // -> Tick() returns a bool so it can easily stop the thread + // without needing to throw InterruptExceptions or similar. + public Action Init; + public Func Tick; + public Action Cleanup; + + public WorkerThread(string identifier) + { + // start the thread wrapped in safety guard + // if main application terminates, this thread needs to terminate too + thread = new Thread( + () => Guard(identifier) + ); + thread.IsBackground = true; + } + + public void Start() + { + // only if thread isn't already running + if (thread.IsAlive) + { + Debug.LogWarning("WorkerThread is still active, can't start it again."); + return; + } + + active = true; + thread.Start(); + } + + // signal the thread to stop gracefully. + // returns immediately, but the thread may take a while to stop. + // may be overwritten to clear more flags like 'computing' etc. + public virtual void SignalStop() => active = false; + + // wait for the thread to fully stop + public bool StopBlocking(float timeout) + { + // only if alive + if (!thread.IsAlive) return true; + + // double precision for long running servers. + watch.Restart(); + + // signal to stop + SignalStop(); + + // wait while thread is still alive + while (IsAlive) + { + // simply wait.. + Thread.Sleep(0); + + // deadlock detection + if (watch.Elapsed.TotalSeconds >= timeout) + { + // force kill all threads as last resort to stop them. + // return false to indicate deadlock. + Interrupt(); + return false; + } + } + return true; + } + + public bool IsAlive => thread.IsAlive; + + // signal an interrupt in the thread. + // this function is very safe to use. + // https://stackoverflow.com/questions/5950994/thread-abort-vs-thread-interrupt + // + // note this does not always kill the thread: + // "If this thread is not currently blocked in a wait, sleep, or join + // state, it will be interrupted when it next begins to block." + // https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.interrupt?view=net-6.0 + // + // in other words, "while (true) {}" wouldn't throw an interrupt exception. + // and that's _okay_. using interrupt is safe & best practice. + // => Unity still aborts deadlocked threads on script reload. + // => and we catch + warn on AbortException. + public void Interrupt() => thread.Interrupt(); + + // thread constructor needs callbacks. + // always define them, and make them call actions. + // those can be set at any time. + void OnInit() => Init?.Invoke(); + bool OnTick() => Tick?.Invoke() ?? false; + void OnCleanup() => Cleanup?.Invoke(); + + // guarded wrapper for thread code. + // catches exceptions which would otherwise be silent. + // shows in Unity profiler. + // etc. + public void Guard(string identifier) + { + try + { + // log when work begins = thread starts. + // very important for debugging threads. + Debug.Log($"{identifier}: started."); + + // show this thread in Unity profiler + Profiler.BeginThreadProfiling("Mirror Worker Threads", $"{identifier}"); + + // run init once + OnInit(); + + // run thread func while active + while (active) + { + // Tick() returns a bool so it can easily stop the thread + // without needing to throw InterruptExceptions or similar. + if (!OnTick()) break; + } + } + // Thread.Interrupt() will gracefully raise a InterruptedException. + catch (ThreadInterruptedException) + { + Debug.Log($"{identifier}: interrupted. That's okay."); + } + // Unity domain reload will cause a ThreadAbortException. + // for example, when saving a changed script while in play mode. + catch (ThreadAbortException) + { + Debug.LogWarning($"{identifier}: aborted. This may happen after domain reload. That's okay."); + } + catch (Exception e) + { + Debug.LogException(e); + } + finally + { + // run cleanup (if any) + active = false; + OnCleanup(); + + // remove this thread from Unity profiler + Profiler.EndThreadProfiling(); + + // log when work ends = thread terminates. + // very important for debugging threads. + // 'finally' to log no matter what (even if exceptions) + Debug.Log($"{identifier}: ended."); + } + } + } +} diff --git a/Assets/Mirror/Core/Threading/WorkerThread.cs.meta b/Assets/Mirror/Core/Threading/WorkerThread.cs.meta new file mode 100644 index 0000000..694601d --- /dev/null +++ b/Assets/Mirror/Core/Threading/WorkerThread.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 605fa1d7e32f40a08e5549bb43fc5c07 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Threading/WorkerThread.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools.meta b/Assets/Mirror/Core/Tools.meta new file mode 100644 index 0000000..b8128f6 --- /dev/null +++ b/Assets/Mirror/Core/Tools.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d9c73cc2749d43268600b80df0b55c4d +timeCreated: 1667486812 diff --git a/Assets/Mirror/Core/Tools/AccurateInterval.cs b/Assets/Mirror/Core/Tools/AccurateInterval.cs new file mode 100644 index 0000000..9d2ce0e --- /dev/null +++ b/Assets/Mirror/Core/Tools/AccurateInterval.cs @@ -0,0 +1,86 @@ +// accurate interval from Mirror II. +// for sync / send intervals where it matters. +// does not(!) do catch-up. +// +// first, let's understand the problem. +// say we need an interval of 10 Hz, so every 100ms in Update we do: +// if (Time.time >= lastTime + interval) +// { +// lastTime = Time.time; +// ... +// } +// +// this seems fine, but actually Time.time will always be a few ms beyond +// the interval. but since lastTime is reset to Time.time, the remainder +// is always ignored away. +// with fixed tickRate servers (say 30 Hz), the remainder is significant! +// +// in practice if we have a 30 Hz tickRate server with a 30 Hz sendRate, +// the above way to measure the interval would result in a 18-19 Hz sendRate! +// => this is not just a little off. this is _way_ off, by almost half. +// => displaying actual + target tick/send rate will show this very easily. +// +// we need an accurate way to measure intervals for where it matters. +// and it needs to be testable to guarantee results. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class AccurateInterval + { + // static func instead of storing interval + lastTime struct. + // + don't need to initialize struct ctor with interval in Awake + // + allows for interval changes at runtime too + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Elapsed(double time, double interval, ref double lastTime) + { + // enough time elapsed? + if (time < lastTime + interval) + return false; + + // naive implementation: + //lastTime = time; + + // accurate but doesn't handle heavy load situations: + //lastTime += interval; + + // heavy load edge case: + // * interval is 100ms + // * server is under heavy load, Updates slow down to 1/s + // * Elapsed(1.000) returns true. + // technically 10 intervals have elapsed. + // * server recovers to normal, Updates are every 10ms again + // * Elapsed(1.010) should return false again until 1.100. + // + // increasing lastTime by interval would require 10 more calls + // to ever catch up again: + // lastTime += interval + // + // as result, the next 10 calls to Elapsed would return true. + // Elapsed(1.001) => true + // Elapsed(1.002) => true + // Elapsed(1.003) => true + // ... + // even though technically the delta was not >= interval. + // + // this would keep the server under heavy load, and it may never + // catch-up. this is not ideal for large virtual worlds. + // + // instead, we want to skip multiples of 'interval' and only + // keep the remainder. + // + // see also: AccurateIntervalTests.Slowdown() + + // easy to understand: + //double elapsed = time - lastTime; + //double remainder = elapsed % interval; + //lastTime = time - remainder; + + // easier: set to rounded multiples of interval (fholm). + // long to match double time. + long multiplier = (long)(time / interval); + lastTime = multiplier * interval; + return true; + } + } +} diff --git a/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta b/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta new file mode 100644 index 0000000..4c1f473 --- /dev/null +++ b/Assets/Mirror/Core/Tools/AccurateInterval.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c1b18064e25046f28b88db65a4012ec1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/AccurateInterval.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Compression.cs b/Assets/Mirror/Core/Tools/Compression.cs similarity index 59% rename from Assets/Mirror/Runtime/Compression.cs rename to Assets/Mirror/Core/Tools/Compression.cs index 3e4b0f6..f6bd06f 100644 --- a/Assets/Mirror/Runtime/Compression.cs +++ b/Assets/Mirror/Core/Tools/Compression.cs @@ -8,6 +8,168 @@ namespace Mirror /// Functions to Compress Quaternions and Floats public static class Compression { + // divide by precision (functions backported from Mirror II) + // for example, 0.1 cm precision converts '5.0f' float to '50' long. + // + // 'long' instead of 'int' to allow for large enough worlds. + // value / precision exceeds int.max range too easily. + // Convert.ToInt32/64 would throw. + // https://github.com/vis2k/DOTSNET/issues/59 + // + // 'long' and 'int' will result in the same bandwidth though. + // for example, ScaleToLong(10.5, 0.1) = 105. + // int: 0x00000069 + // long: 0x0000000000000069 + // delta compression will reduce both to 1 byte. + // + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + public static bool ScaleToLong(float value, float precision, out long result) + { + // user might try to pass precision = 0 to disable rounding. + // this is not supported. + // throw to make the user fix this immediately. + // otherwise we would have to reinterpret-cast if ==0 etc. + // this function should be kept simple. + // if rounding isn't wanted, this function shouldn't be called. + if (precision == 0) throw new DivideByZeroException($"ScaleToLong: precision=0 would cause null division. If rounding isn't wanted, don't call this function."); + + // catch OverflowException if value/precision > long.max. + // attackers should never be able to throw exceptions. + try + { + result = Convert.ToInt64(value / precision); + return true; + } + // clamp to .max/.min. + // returning '0' would make far away entities reset to origin. + // returning 'max' would keep them stuck at the end of the world. + // the latter is much easier to debug. + catch (OverflowException) + { + result = value > 0 ? long.MaxValue : long.MinValue; + return false; + } + } + + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Vector3 value, float precision, out long x, out long y, out long z) + { + // attempt to convert every component. + // do not return early if one conversion returned 'false'. + // the return value is optional. always attempt to convert all. + bool result = true; + result &= ScaleToLong(value.x, precision, out x); + result &= ScaleToLong(value.y, precision, out y); + result &= ScaleToLong(value.z, precision, out z); + return result; + } + + // returns + // 'true' if scaling was possible within 'long' bounds. + // 'false' if clamping was necessary. + // never throws. checking result is optional. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Quaternion value, float precision, out long x, out long y, out long z, out long w) + { + // attempt to convert every component. + // do not return early if one conversion returned 'false'. + // the return value is optional. always attempt to convert all. + bool result = true; + result &= ScaleToLong(value.x, precision, out x); + result &= ScaleToLong(value.y, precision, out y); + result &= ScaleToLong(value.z, precision, out z); + result &= ScaleToLong(value.w, precision, out w); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Vector3 value, float precision, out Vector3Long quantized) + { + quantized = Vector3Long.zero; + return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ScaleToLong(Quaternion value, float precision, out Vector4Long quantized) + { + quantized = Vector4Long.zero; + return ScaleToLong(value, precision, out quantized.x, out quantized.y, out quantized.z, out quantized.w); + } + + // multiple by precision. + // for example, 0.1 cm precision converts '50' long to '5.0f' float. + public static float ScaleToFloat(long value, float precision) + { + // user might try to pass precision = 0 to disable rounding. + // this is not supported. + // throw to make the user fix this immediately. + // otherwise we would have to reinterpret-cast if ==0 etc. + // this function should be kept simple. + // if rounding isn't wanted, this function shouldn't be called. + if (precision == 0) throw new DivideByZeroException($"ScaleToLong: precision=0 would cause null division. If rounding isn't wanted, don't call this function."); + + return value * precision; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ScaleToFloat(long x, long y, long z, float precision) + { + Vector3 v; + v.x = ScaleToFloat(x, precision); + v.y = ScaleToFloat(y, precision); + v.z = ScaleToFloat(z, precision); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion ScaleToFloat(long x, long y, long z, long w, float precision) + { + Quaternion v; + v.x = ScaleToFloat(x, precision); + v.y = ScaleToFloat(y, precision); + v.z = ScaleToFloat(z, precision); + v.w = ScaleToFloat(w, precision); + return v; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ScaleToFloat(Vector3Long value, float precision) => + ScaleToFloat(value.x, value.y, value.z, precision); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion ScaleToFloat(Vector4Long value, float precision) => + ScaleToFloat(value.x, value.y, value.z, value.w, precision); + + // scale a float within min/max range to an ushort between min/max range + // note: can also use this for byte range from byte.MinValue to byte.MaxValue + public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget) + { + // note: C# ushort - ushort => int, hence so many casts + // max ushort - min ushort only fits into something bigger + int targetRange = maxTarget - minTarget; + float valueRange = maxValue - minValue; + float valueRelative = value - minValue; + return (ushort)(minTarget + (ushort)(valueRelative / valueRange * targetRange)); + } + + // scale an ushort within min/max range to a float between min/max range + // note: can also use this for byte range from byte.MinValue to byte.MaxValue + public static float ScaleUShortToFloat(ushort value, ushort minValue, ushort maxValue, float minTarget, float maxTarget) + { + // note: C# ushort - ushort => int, hence so many casts + float targetRange = maxTarget - minTarget; + ushort valueRange = (ushort)(maxValue - minValue); + ushort valueRelative = (ushort)(value - minValue); + return minTarget + (valueRelative / (float)valueRange * targetRange); + } + // quaternion compression ////////////////////////////////////////////// // smallest three: https://gafferongames.com/post/snapshot_compression/ // compresses 16 bytes quaternion into 4 bytes @@ -50,46 +212,9 @@ public static int LargestAbsoluteComponentIndex(Vector4 value, out float largest return largestIndex; } - // scale a float within min/max range to an ushort between min/max range - // note: can also use this for byte range from byte.MinValue to byte.MaxValue - public static ushort ScaleFloatToUShort(float value, float minValue, float maxValue, ushort minTarget, ushort maxTarget) - { - // note: C# ushort - ushort => int, hence so many casts - // max ushort - min ushort only fits into something bigger - int targetRange = maxTarget - minTarget; - float valueRange = maxValue - minValue; - float valueRelative = value - minValue; - return (ushort)(minTarget + (ushort)(valueRelative / valueRange * targetRange)); - } - - // scale an ushort within min/max range to a float between min/max range - // note: can also use this for byte range from byte.MinValue to byte.MaxValue - public static float ScaleUShortToFloat(ushort value, ushort minValue, ushort maxValue, float minTarget, float maxTarget) - { - // note: C# ushort - ushort => int, hence so many casts - float targetRange = maxTarget - minTarget; - ushort valueRange = (ushort)(maxValue - minValue); - ushort valueRelative = (ushort)(value - minValue); - return minTarget + (valueRelative / (float)valueRange * targetRange); - } - const float QuaternionMinRange = -0.707107f; const float QuaternionMaxRange = 0.707107f; - const ushort TenBitsMax = 0x3FF; - - // helper function to access 'nth' component of quaternion - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static float QuaternionElement(Quaternion q, int element) - { - switch (element) - { - case 0: return q.x; - case 1: return q.y; - case 2: return q.z; - case 3: return q.w; - default: return 0; - } - } + const ushort TenBitsMax = 0b11_1111_1111; // note: assumes normalized quaternions public static uint CompressQuaternion(Quaternion q) @@ -107,7 +232,7 @@ public static uint CompressQuaternion(Quaternion q) // [largest] always positive by negating the entire quaternion if // [largest] is negative. in quaternion space (x,y,z,w) and // (-x,-y,-z,-w) represent the same rotation." - if (QuaternionElement(q, largestIndex) < 0) + if (q[largestIndex] < 0) withoutLargest = -withoutLargest; // put index & three floats into one integer. @@ -194,11 +319,46 @@ public static Quaternion DecompressQuaternion(uint data) } // varint compression ////////////////////////////////////////////////// + // helper function to predict varint size for a given number. + // useful when checking if a message + size header will fit, etc. + public static int VarUIntSize(ulong value) + { + if (value <= 240) + return 1; + if (value <= 2287) + return 2; + if (value <= 67823) + return 3; + if (value <= 16777215) + return 4; + if (value <= 4294967295) + return 5; + if (value <= 1099511627775) + return 6; + if (value <= 281474976710655) + return 7; + if (value <= 72057594037927935) + return 8; + return 9; + } + + // helper function to predict varint size for a given number. + // useful when checking if a message + size header will fit, etc. + public static int VarIntSize(long value) + { + // CompressVarInt zigzags it first + ulong zigzagged = (ulong)((value >> 63) ^ (value << 1)); + return VarUIntSize(zigzagged); + } + // compress ulong varint. - // same result for int, short and byte. only need one function. + // same result for ulong, uint, ushort and byte. only need one function. // NOT an extension. otherwise weaver might accidentally use it. public static void CompressVarUInt(NetworkWriter writer, ulong value) { + // straight forward implementation: + // keep this for understanding & debugging. + /* if (value <= 240) { writer.WriteByte((byte)value); @@ -280,6 +440,81 @@ public static void CompressVarUInt(NetworkWriter writer, ulong value) writer.WriteByte((byte)((value >> 48) & 0xFF)); writer.WriteByte((byte)((value >> 56) & 0xFF)); } + */ + + // faster implementation writes multiple bytes at once. + // avoids extra Space, WriteBlittable overhead. + // VarInt is in hot path, performance matters here. + if (value <= 240) + { + byte a = (byte)value; + writer.WriteByte(a); + return; + } + if (value <= 2287) + { + byte a = (byte)(((value - 240) >> 8) + 241); + byte b = (byte)((value - 240) & 0xFF); + writer.WriteUShort((ushort)(b << 8 | a)); + return; + } + if (value <= 67823) + { + byte a = (byte)249; + byte b = (byte)((value - 2288) >> 8); + byte c = (byte)((value - 2288) & 0xFF); + writer.WriteByte(a); + writer.WriteUShort((ushort)(c << 8 | b)); + return; + } + if (value <= 16777215) + { + byte a = (byte)250; + uint b = (uint)(value << 8); + writer.WriteUInt(b | a); + return; + } + if (value <= 4294967295) + { + byte a = (byte)251; + uint b = (uint)value; + writer.WriteByte(a); + writer.WriteUInt(b); + return; + } + if (value <= 1099511627775) + { + byte a = (byte)252; + byte b = (byte)(value & 0xFF); + uint c = (uint)(value >> 8); + writer.WriteUShort((ushort)(b << 8 | a)); + writer.WriteUInt(c); + return; + } + if (value <= 281474976710655) + { + byte a = (byte)253; + byte b = (byte)(value & 0xFF); + byte c = (byte)((value >> 8) & 0xFF); + uint d = (uint)(value >> 16); + writer.WriteByte(a); + writer.WriteUShort((ushort)(c << 8 | b)); + writer.WriteUInt(d); + return; + } + if (value <= 72057594037927935) + { + byte a = 254; + ulong b = value << 8; + writer.WriteULong(b | a); + return; + } + + // all others + { + writer.WriteByte(255); + writer.WriteULong(value); + } } // zigzag encoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba diff --git a/Assets/Mirror/Runtime/Compression.cs.meta b/Assets/Mirror/Core/Tools/Compression.cs.meta similarity index 62% rename from Assets/Mirror/Runtime/Compression.cs.meta rename to Assets/Mirror/Core/Tools/Compression.cs.meta index e35474b..e6308ca 100644 --- a/Assets/Mirror/Runtime/Compression.cs.meta +++ b/Assets/Mirror/Core/Tools/Compression.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Compression.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/DeltaCompression.cs b/Assets/Mirror/Core/Tools/DeltaCompression.cs new file mode 100644 index 0000000..50b9b2f --- /dev/null +++ b/Assets/Mirror/Core/Tools/DeltaCompression.cs @@ -0,0 +1,58 @@ +// manual delta compression for some types. +// varint(b-a) +// Mirror can't use Mirror II's bit-tree delta compression. +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public static class DeltaCompression + { + // delta (usually small), then zigzag varint to support +- changes + // parameter order: (last, current) makes most sense (Q3 does this too). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, long last, long current) => + Compression.CompressVarInt(writer, current - last); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Decompress(NetworkReader reader, long last) => + last + Compression.DecompressVarInt(reader); + + // delta (usually small), then zigzag varint to support +- changes + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, Vector3Long last, Vector3Long current) + { + Compress(writer, last.x, current.x); + Compress(writer, last.y, current.y); + Compress(writer, last.z, current.z); + } + + // delta (usually small), then zigzag varint to support +- changes + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Compress(NetworkWriter writer, Vector4Long last, Vector4Long current) + { + Compress(writer, last.x, current.x); + Compress(writer, last.y, current.y); + Compress(writer, last.z, current.z); + Compress(writer, last.w, current.w); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long Decompress(NetworkReader reader, Vector3Long last) + { + long x = Decompress(reader, last.x); + long y = Decompress(reader, last.y); + long z = Decompress(reader, last.z); + return new Vector3Long(x, y, z); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long Decompress(NetworkReader reader, Vector4Long last) + { + long x = Decompress(reader, last.x); + long y = Decompress(reader, last.y); + long z = Decompress(reader, last.z); + long w = Decompress(reader, last.w); + return new Vector4Long(x, y, z, w); + } + } +} diff --git a/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta b/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta new file mode 100644 index 0000000..83a4d41 --- /dev/null +++ b/Assets/Mirror/Core/Tools/DeltaCompression.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6b8f3fffcb4754c15bc5ed4c33e2497b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/DeltaCompression.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs new file mode 100644 index 0000000..674abb8 --- /dev/null +++ b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs @@ -0,0 +1,53 @@ +// N-day EMA implementation from Mirror with a few changes (struct etc.) +// it calculates an exponential moving average roughly equivalent to the last n observations +// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average +using System; + +namespace Mirror +{ + public struct ExponentialMovingAverage + { + readonly double alpha; + bool initialized; + + public double Value; + public double Variance; + public double StandardDeviation; // absolute value, see test + + public ExponentialMovingAverage(int n) + { + // standard N-day EMA alpha calculation + alpha = 2.0 / (n + 1); + initialized = false; + Value = 0; + Variance = 0; + StandardDeviation = 0; + } + + public void Add(double newValue) + { + // simple algorithm for EMA described here: + // https://en.wikipedia.org/wiki/Moving_average#Exponentially_weighted_moving_variance_and_standard_deviation + if (initialized) + { + double delta = newValue - Value; + Value += alpha * delta; + Variance = (1 - alpha) * (Variance + alpha * delta * delta); + StandardDeviation = Math.Sqrt(Variance); + } + else + { + Value = newValue; + initialized = true; + } + } + + public void Reset() + { + initialized = false; + Value = 0; + Variance = 0; + StandardDeviation = 0; + } + } +} diff --git a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta similarity index 60% rename from Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta rename to Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta index d0d8210..533ab12 100644 --- a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs.meta +++ b/Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/ExponentialMovingAverage.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Extensions.cs b/Assets/Mirror/Core/Tools/Extensions.cs new file mode 100644 index 0000000..e9399b3 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Extensions.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Mirror +{ + public static class Extensions + { + public static string ToHexString(this ArraySegment segment) => + BitConverter.ToString(segment.Array, segment.Offset, segment.Count); + + // string.GetHashCode is not guaranteed to be the same on all + // machines, but we need one that is the same on all machines. + // Uses fnv1a as hash function for more uniform distribution http://www.isthe.com/chongo/tech/comp/fnv/ + // Tests: https://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed + // NOTE: Do not call this from hot path because it's slow O(N) for long method names. + // - As of 2012-02-16 There are 2 design-time callers (weaver) and 1 runtime caller that caches. + public static int GetStableHashCode(this string text) + { + unchecked + { + uint hash = 0x811c9dc5; + uint prime = 0x1000193; + + for (int i = 0; i < text.Length; ++i) + { + byte value = (byte)text[i]; + hash = hash ^ value; + hash *= prime; + } + + //UnityEngine.Debug.Log($"Created stable hash {(ushort)hash} for {text}"); + return (int)hash; + } + } + + // smaller version of our GetStableHashCode. + // careful, this significantly increases chance of collisions. + public static ushort GetStableHashCode16(this string text) + { + // deterministic hash + int hash = GetStableHashCode(text); + + // Gets the 32bit fnv1a hash + // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort + // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits + // This will create a more uniform 16bit hash, the method is described in: + // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding" + return (ushort)((hash >> 16) ^ hash); + } + + // previously in DotnetCompatibility.cs + // leftover from the UNET days. supposedly for windows store? + public static string GetMethodName(this Delegate func) + { +#if NETFX_CORE + return func.GetMethodInfo().Name; +#else + return func.Method.Name; +#endif + } + + // helper function to copy to List + // C# only provides CopyTo(T[]) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CopyTo(this IEnumerable source, List destination) + { + // foreach allocates. use AddRange. + destination.AddRange(source); + } + +#if !UNITY_2021_OR_NEWER + // Unity 2020 and earlier don't have Queue.TryDequeue which we need for batching. + public static bool TryDequeue(this Queue source, out T element) + { + if (source.Count > 0) + { + element = source.Dequeue(); + return true; + } + + element = default; + return false; + } +#endif + +#if !UNITY_2021_OR_NEWER + // Unity 2020 and earlier don't have ConcurrentQueue.Clear which we need for ThreadedTransport. + public static void Clear(this ConcurrentQueue source) + { + // while count > 0 risks deadlock if other thread write at the same time. + // our safest solution is a best-effort approach to clear 'Count' once. + int count = source.Count; // get it only once + for (int i = 0; i < count; ++i) + { + source.TryDequeue(out _); + } + } +#endif + +#if !UNITY_2021_3_OR_NEWER + // Some patch versions of Unity 2021.3 and earlier don't have transform.GetPositionAndRotation which we use for performance in some places + public static void GetPositionAndRotation(this Transform transform, out Vector3 position, out Quaternion rotation) + { + position = transform.position; + rotation = transform.rotation; + } + + public static void SetPositionAndRotation(this Transform transform, Vector3 position, Quaternion rotation) + { + transform.position = position; + transform.rotation = rotation; + } + + public static void GetLocalPositionAndRotation(this Transform transform, out Vector3 position, out Quaternion rotation) + { + position = transform.localPosition; + rotation = transform.localRotation; + } + + public static void SetLocalPositionAndRotation(this Transform transform, Vector3 position, Quaternion rotation) + { + transform.localPosition = position; + transform.localRotation = rotation; + } +#endif + + // IPEndPoint address only to pretty string. + // useful for to get a connection's address for IP bans etc. + public static string PrettyAddress(this IPEndPoint endPoint) + { + if (endPoint == null) return ""; + + // Map to IPv4 if "IsIPv4MappedToIPv6" for readability + // "::ffff:127.0.0.1" -> "127.0.0.1" + return + endPoint.Address.IsIPv4MappedToIPv6 + ? endPoint.Address.MapToIPv4().ToString() + : endPoint.Address.ToString(); + } + } +} diff --git a/Assets/Mirror/Runtime/Extensions.cs.meta b/Assets/Mirror/Core/Tools/Extensions.cs.meta similarity index 58% rename from Assets/Mirror/Runtime/Extensions.cs.meta rename to Assets/Mirror/Core/Tools/Extensions.cs.meta index c2a18b7..13bd75f 100644 --- a/Assets/Mirror/Runtime/Extensions.cs.meta +++ b/Assets/Mirror/Core/Tools/Extensions.cs.meta @@ -1,4 +1,4 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: decf32fd053744d18f35712b7a6f5116 MonoImporter: externalObjects: {} @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Extensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Half.cs b/Assets/Mirror/Core/Tools/Half.cs new file mode 100644 index 0000000..33eb08e --- /dev/null +++ b/Assets/Mirror/Core/Tools/Half.cs @@ -0,0 +1,773 @@ +// half float from .NET 5: +// https://devblogs.microsoft.com/dotnet/introducing-the-half-type/ +// +// drop in from dotnet/runtime source: +// https://github.com/dotnet/runtime/blob/e188d6ac90fe56320cca51c53709ef1c72f063d5/src/libraries/System.Private.CoreLib/src/System/Half.cs#L17 +// removing all the stuff that's not in Unity though. + + + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace System +{ + // Portions of the code implemented below are based on the 'Berkeley SoftFloat Release 3e' algorithms. + + /// + /// Represents a half-precision floating-point number. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Half + : IComparable, + IComparable, + IEquatable + { + private const NumberStyles DefaultParseStyle = NumberStyles.Float | NumberStyles.AllowThousands; + + // Constants for manipulating the private bit-representation + + internal const ushort SignMask = 0x8000; + internal const int SignShift = 15; + internal const byte ShiftedSignMask = SignMask >> SignShift; + + internal const ushort BiasedExponentMask = 0x7C00; + internal const int BiasedExponentShift = 10; + internal const int BiasedExponentLength = 5; + internal const byte ShiftedBiasedExponentMask = BiasedExponentMask >> BiasedExponentShift; + + internal const ushort TrailingSignificandMask = 0x03FF; + + internal const byte MinSign = 0; + internal const byte MaxSign = 1; + + internal const byte MinBiasedExponent = 0x00; + internal const byte MaxBiasedExponent = 0x1F; + + internal const byte ExponentBias = 15; + + internal const sbyte MinExponent = -14; + internal const sbyte MaxExponent = +15; + + internal const ushort MinTrailingSignificand = 0x0000; + internal const ushort MaxTrailingSignificand = 0x03FF; + + internal const int TrailingSignificandLength = 10; + internal const int SignificandLength = TrailingSignificandLength + 1; + + // Constants representing the private bit-representation for various default values + + private const ushort PositiveZeroBits = 0x0000; + private const ushort NegativeZeroBits = 0x8000; + + private const ushort EpsilonBits = 0x0001; + + private const ushort PositiveInfinityBits = 0x7C00; + private const ushort NegativeInfinityBits = 0xFC00; + + private const ushort PositiveQNaNBits = 0x7E00; + private const ushort NegativeQNaNBits = 0xFE00; + + private const ushort MinValueBits = 0xFBFF; + private const ushort MaxValueBits = 0x7BFF; + + private const ushort PositiveOneBits = 0x3C00; + private const ushort NegativeOneBits = 0xBC00; + + private const ushort SmallestNormalBits = 0x0400; + + private const ushort EBits = 0x4170; + private const ushort PiBits = 0x4248; + private const ushort TauBits = 0x4648; + + // Well-defined and commonly used values + + public static Half Epsilon => new Half(EpsilonBits); // 5.9604645E-08 + + public static Half PositiveInfinity => new Half(PositiveInfinityBits); // 1.0 / 0.0; + + public static Half NegativeInfinity => new Half(NegativeInfinityBits); // -1.0 / 0.0 + + public static Half NaN => new Half(NegativeQNaNBits); // 0.0 / 0.0 + + public static Half MinValue => new Half(MinValueBits); // -65504 + + public static Half MaxValue => new Half(MaxValueBits); // 65504 + + internal readonly ushort _value; // internal representation + + internal Half(ushort value) + { + _value = value; + } + + private Half(bool sign, ushort exp, ushort sig) => _value = (ushort)(((sign ? 1 : 0) << SignShift) + (exp << BiasedExponentShift) + sig); + + internal byte BiasedExponent + { + get + { + ushort bits = _value; + return ExtractBiasedExponentFromBits(bits); + } + } + + internal sbyte Exponent + { + get + { + return (sbyte)(BiasedExponent - ExponentBias); + } + } + + internal ushort Significand + { + get + { + return (ushort)(TrailingSignificand | ((BiasedExponent != 0) ? (1U << BiasedExponentShift) : 0U)); + } + } + + internal ushort TrailingSignificand + { + get + { + ushort bits = _value; + return ExtractTrailingSignificandFromBits(bits); + } + } + + internal static byte ExtractBiasedExponentFromBits(ushort bits) + { + return (byte)((bits >> BiasedExponentShift) & ShiftedBiasedExponentMask); + } + + internal static ushort ExtractTrailingSignificandFromBits(ushort bits) + { + return (ushort)(bits & TrailingSignificandMask); + } + + public static bool operator <(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative && !AreZero(left, right); + } + + return (left._value != right._value) && ((left._value < right._value) ^ leftIsNegative); + } + + public static bool operator >(Half left, Half right) + { + return right < left; + } + + public static bool operator <=(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative || AreZero(left, right); + } + + return (left._value == right._value) || ((left._value < right._value) ^ leftIsNegative); + } + + public static bool operator >=(Half left, Half right) + { + return right <= left; + } + + public static bool operator ==(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is not equal to anything, including itself. + return false; + } + + // IEEE defines that positive and negative zero are equivalent. + return (left._value == right._value) || AreZero(left, right); + } + + public static bool operator !=(Half left, Half right) + { + return !(left == right); + } + + /// Determines whether the specified value is finite (zero, subnormal, or normal). + /// This effectively checks the value is not NaN and not infinite. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFinite(Half value) + { + uint bits = value._value; + return (~bits & PositiveInfinityBits) != 0; + } + + /// Determines whether the specified value is infinite. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInfinity(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) == PositiveInfinityBits; + } + + /// Determines whether the specified value is NaN. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) > PositiveInfinityBits; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsNaNOrZero(Half value) + { + uint bits = value._value; + return ((bits - 1) & ~SignMask) >= PositiveInfinityBits; + } + + /// Determines whether the specified value is negative. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNegative(Half value) + { + return (short)(value._value) < 0; + } + + /// Determines whether the specified value is negative infinity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNegativeInfinity(Half value) + { + return value._value == NegativeInfinityBits; + } + + /// Determines whether the specified value is normal (finite, but not zero or subnormal). + /// This effectively checks the value is not NaN, not infinite, not subnormal, and not zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNormal(Half value) + { + uint bits = value._value; + return (ushort)((bits & ~SignMask) - SmallestNormalBits) < (PositiveInfinityBits - SmallestNormalBits); + } + + /// Determines whether the specified value is positive infinity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPositiveInfinity(Half value) + { + return value._value == PositiveInfinityBits; + } + + /// Determines whether the specified value is subnormal (finite, but not zero or normal). + /// This effectively checks the value is not NaN, not infinite, not normal, and not zero. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSubnormal(Half value) + { + uint bits = value._value; + return (ushort)((bits & ~SignMask) - 1) < MaxTrailingSignificand; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsZero(Half value) + { + uint bits = value._value; + return (bits & ~SignMask) == 0; + } + + private static bool AreZero(Half left, Half right) + { + // IEEE defines that positive and negative zero are equal, this gives us a quick equality check + // for two values by or'ing the private bits together and stripping the sign. They are both zero, + // and therefore equivalent, if the resulting value is still zero. + return ((left._value | right._value) & ~SignMask) == 0; + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + public int CompareTo(object obj) + { + if (obj is Half other) + { + return CompareTo(other); + } + return (obj is null) ? 1 : throw new ArgumentException("SR.Arg_MustBeHalf"); + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + public int CompareTo(Half other) + { + if (this < other) + { + return -1; + } + + if (this > other) + { + return 1; + } + + if (this == other) + { + return 0; + } + + if (IsNaN(this)) + { + return IsNaN(other) ? 0 : -1; + } + + return 1; + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified . + /// + public override bool Equals(object obj) + { + return (obj is Half other) && Equals(other); + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified value. + /// + public bool Equals(Half other) + { + return _value == other._value + || AreZero(this, other) + || (IsNaN(this) && IsNaN(other)); + } + + /// + /// Serves as the default hash function. + /// + public override int GetHashCode() + { + uint bits = _value; + + if (IsNaNOrZero(this)) + { + // Ensure that all NaNs and both zeros have the same hash code + bits &= PositiveInfinityBits; + } + + return (int)bits; + } + + /// + /// Returns a string representation of the current value. + /// + public override string ToString() + { + return ((float)this).ToString(); + } + + // + // Explicit Convert To Half + // + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + public static explicit operator Half(char value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + public static explicit operator Half(decimal value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(short value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(int value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(long value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(float value) + { + // Unity implement this! + return new Half(Mathf.FloatToHalf(value)); + } + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(ushort value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(uint value) => (Half)(float)value; + + /// Explicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static explicit operator Half(ulong value) => (Half)(float)value; + + // + // Explicit Convert From Half + // + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator byte(Half value) => (byte)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator char(Half value) => (char)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator decimal(Half value) => (decimal)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator short(Half value) => (short)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator int(Half value) => (int)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator long(Half value) => (long)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator sbyte(Half value) => (sbyte)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator ushort(Half value) => (ushort)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator uint(Half value) => (uint)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator ulong(Half value) => (ulong)(float)value; + + // + // Implicit Convert To Half + // + + /// Implicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static implicit operator Half(byte value) => (Half)(float)value; + + /// Implicitly converts a value to its nearest representable half-precision floating-point value. + /// converted to its nearest representable half-precision floating-point value. + public static implicit operator Half(sbyte value) => (Half)(float)value; + + /// Explicitly converts a half-precision floating-point value to its nearest representable value. + /// converted to its nearest representable value. + public static explicit operator float(Half value) + { + return Mathf.HalfToFloat(value._value); + } + + // IEEE 754 specifies NaNs to be propagated + internal static Half Negate(Half value) + { + return IsNaN(value) ? value : new Half((ushort)(value._value ^ SignMask)); + } + + public static Half operator +(Half left, Half right) => (Half)((float)left + (float)right); + + // + // IDecrementOperators + // + + public static Half operator --(Half value) + { + var tmp = (float)value; + --tmp; + return (Half)tmp; + } + + // + // IDivisionOperators + // + + public static Half operator /(Half left, Half right) => (Half)((float)left / (float)right); + + // + // IExponentialFunctions + // + + public static Half Exp(Half x) => (Half)Math.Exp((float)x); + + // + // IFloatingPoint + // + + public static Half Ceiling(Half x) => (Half)Math.Ceiling((float)x); + + public static Half Floor(Half x) => (Half)Math.Floor((float)x); + + public static Half Round(Half x) => (Half)Math.Round((float)x); + + public static Half Round(Half x, int digits) => (Half)Math.Round((float)x, digits); + + public static Half Round(Half x, MidpointRounding mode) => (Half)Math.Round((float)x, mode); + + public static Half Round(Half x, int digits, MidpointRounding mode) => (Half)Math.Round((float)x, digits, mode); + + public static Half Truncate(Half x) => (Half)Math.Truncate((float)x); + + // + // IFloatingPointConstants + // + + public static Half E => new Half(EBits); + + public static Half Pi => new Half(PiBits); + + public static Half Tau => new Half(TauBits); + + // + // IFloatingPointIeee754 + // + + public static Half NegativeZero => new Half(NegativeZeroBits); + + public static Half Atan2(Half y, Half x) => (Half)Math.Atan2((float)y, (float)x); + + public static Half Lerp(Half value1, Half value2, Half amount) => (Half)Mathf.Lerp((float)value1, (float)value2, (float)amount); + + // + // IHyperbolicFunctions + // + + public static Half Cosh(Half x) => (Half)Math.Cosh((float)x); + + public static Half Sinh(Half x) => (Half)Math.Sinh((float)x); + + public static Half Tanh(Half x) => (Half)Math.Tanh((float)x); + + // + // IIncrementOperators + // + + public static Half operator ++(Half value) + { + var tmp = (float)value; + ++tmp; + return (Half)tmp; + } + + // + // ILogarithmicFunctions + // + + public static Half Log(Half x) => (Half)Math.Log((float)x); + + public static Half Log(Half x, Half newBase) => (Half)Math.Log((float)x, (float)newBase); + + // + // IModulusOperators + // + + public static Half operator %(Half left, Half right) => (Half)((float)left % (float)right); + + // + // IMultiplicativeIdentity + // + + public static Half MultiplicativeIdentity => new Half(PositiveOneBits); + + // + // IMultiplyOperators + // + + public static Half operator *(Half left, Half right) => (Half)((float)left * (float)right); + + // + // INumber + // + + public static Half Clamp(Half value, Half min, Half max) => (Half)Mathf.Clamp((float)value, (float)min, (float)max); + + public static Half CopySign(Half value, Half sign) + { + // This method is required to work for all inputs, + // including NaN, so we operate on the raw bits. + uint xbits = value._value; + uint ybits = sign._value; + + // Remove the sign from x, and remove everything but the sign from y + // Then, simply OR them to get the correct sign + return new Half((ushort)((xbits & ~SignMask) | (ybits & SignMask))); + } + + public static Half Max(Half x, Half y) => (Half)Math.Max((float)x, (float)y); + + public static Half MaxNumber(Half x, Half y) + { + // This matches the IEEE 754:2019 `maximumNumber` function + // + // It does not propagate NaN inputs back to the caller and + // otherwise returns the larger of the inputs. It + // treats +0 as larger than -0 as per the specification. + + if (x != y) + { + if (!IsNaN(y)) + { + return y < x ? x : y; + } + + return x; + } + + return IsNegative(y) ? x : y; + } + + public static Half Min(Half x, Half y) => (Half)Math.Min((float)x, (float)y); + + public static Half MinNumber(Half x, Half y) + { + // This matches the IEEE 754:2019 `minimumNumber` function + // + // It does not propagate NaN inputs back to the caller and + // otherwise returns the larger of the inputs. It + // treats +0 as larger than -0 as per the specification. + + if (x != y) + { + if (!IsNaN(y)) + { + return x < y ? x : y; + } + + return x; + } + + return IsNegative(x) ? x : y; + } + + public static int Sign(Half value) + { + if (IsNaN(value)) + { + throw new ArithmeticException("SR.Arithmetic_NaN"); + } + + if (IsZero(value)) + { + return 0; + } + else if (IsNegative(value)) + { + return -1; + } + + return +1; + } + + // + // INumberBase + // + + public static Half One => new Half(PositiveOneBits); + + public static Half Zero => new Half(PositiveZeroBits); + + public static Half Abs(Half value) => new Half((ushort)(value._value & ~SignMask)); + + public static bool IsPositive(Half value) => (short)(value._value) >= 0; + + public static bool IsRealNumber(Half value) + { + // A NaN will never equal itself so this is an + // easy and efficient way to check for a real number. + +#pragma warning disable CS1718 + return value == value; +#pragma warning restore CS1718 + } + + // + // IPowerFunctions + // + + public static Half Pow(Half x, Half y) => (Half)Math.Pow((float)x, (float)y); + + // + // IRootFunctions + // + + public static Half Sqrt(Half x) => (Half)Math.Sqrt((float)x); + + // + // ISignedNumber + // + + public static Half NegativeOne => new Half(NegativeOneBits); + + // + // ISubtractionOperators + // + + public static Half operator -(Half left, Half right) => (Half)((float)left - (float)right); + + // + // ITrigonometricFunctions + // + + public static Half Acos(Half x) => (Half)Math.Acos((float)x); + + public static Half Asin(Half x) => (Half)Math.Asin((float)x); + + public static Half Atan(Half x) => (Half)Math.Atan((float)x); + + public static Half Cos(Half x) => (Half)Math.Cos((float)x); + + public static Half Sin(Half x) => (Half)Math.Sin((float)x); + + public static Half Tan(Half x) => (Half)Math.Tan((float)x); + + // + // IUnaryNegationOperators + // + + public static Half operator -(Half value) => (Half)(-(float)value); + + // + // IUnaryPlusOperators + // + + public static Half operator +(Half value) => value; + } +} diff --git a/Assets/Mirror/Core/Tools/Half.cs.meta b/Assets/Mirror/Core/Tools/Half.cs.meta new file mode 100644 index 0000000..030a09a --- /dev/null +++ b/Assets/Mirror/Core/Tools/Half.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 70b5947c49174f3c90121edd21ea6b15 +timeCreated: 1729783714 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Half.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Mathd.cs b/Assets/Mirror/Core/Tools/Mathd.cs similarity index 67% rename from Assets/Mirror/Runtime/Mathd.cs rename to Assets/Mirror/Core/Tools/Mathd.cs index 2dfa2f9..374471a 100644 --- a/Assets/Mirror/Runtime/Mathd.cs +++ b/Assets/Mirror/Core/Tools/Mathd.cs @@ -1,28 +1,32 @@ // 'double' precision variants for some of Unity's Mathf functions. - using System.Runtime.CompilerServices; namespace Mirror { public static class Mathd { - /// Linearly interpolates between a and b by t with no limit to t. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double LerpUnclamped(double a, double b, double t) => - a + (b - a) * t; - + // Unity 2020 doesn't have Math.Clamp yet. /// Clamps value between 0 and 1 and returns value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Clamp01(double value) + public static double Clamp(double value, double min, double max) { - if (value < 0.0) - return 0; - return value > 1 ? 1 : value; + if (value < min) return min; + if (value > max) return max; + return value; } + /// Clamps value between 0 and 1 and returns value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Clamp01(double value) => Clamp(value, 0, 1); + /// Calculates the linear parameter t that produces the interpolant value within the range [a, b]. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double InverseLerp(double a, double b, double value) => a != b ? Clamp01((value - a) / (b - a)) : 0; + + /// Linearly interpolates between a and b by t with no limit to t. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double LerpUnclamped(double a, double b, double t) => + a + (b - a) * t; } } diff --git a/Assets/Mirror/Runtime/Mathd.cs.meta b/Assets/Mirror/Core/Tools/Mathd.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/Mathd.cs.meta rename to Assets/Mirror/Core/Tools/Mathd.cs.meta index 927c55a..cfc4b51 100644 --- a/Assets/Mirror/Runtime/Mathd.cs.meta +++ b/Assets/Mirror/Core/Tools/Mathd.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Mathd.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Pool.cs b/Assets/Mirror/Core/Tools/Pool.cs similarity index 75% rename from Assets/Mirror/Runtime/Pool.cs rename to Assets/Mirror/Core/Tools/Pool.cs index e526139..f26d49b 100644 --- a/Assets/Mirror/Runtime/Pool.cs +++ b/Assets/Mirror/Core/Tools/Pool.cs @@ -8,7 +8,8 @@ namespace Mirror { public class Pool { - // Mirror is single threaded, no need for concurrent collections + // Mirror is single threaded, no need for concurrent collections. + // stack increases the chance that a reused writer remains in cache. readonly Stack objects = new Stack(); // some types might need additional parameters in their constructor, so @@ -25,17 +26,21 @@ public Pool(Func objectGenerator, int initialCapacity) objects.Push(objectGenerator()); } - // DEPRECATED 2022-03-10 - [Obsolete("Take() was renamed to Get()")] - public T Take() => Get(); - // take an element from the pool, or create a new one if empty [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Get() => objects.Count > 0 ? objects.Pop() : objectGenerator(); // return an element to the pool [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Return(T item) => objects.Push(item); + public void Return(T item) + { + // make sure we can't accidentally insert null values into the pool. + // debugging this would be hard since it would only show on get(). + if (item == null) + throw new ArgumentNullException(nameof(item)); + + objects.Push(item); + } // count to see how many objects are in the pool. useful for tests. public int Count => objects.Count; diff --git a/Assets/Mirror/Runtime/Pool.cs.meta b/Assets/Mirror/Core/Tools/Pool.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/Pool.cs.meta rename to Assets/Mirror/Core/Tools/Pool.cs.meta index 7d12a20..1434250 100644 --- a/Assets/Mirror/Runtime/Pool.cs.meta +++ b/Assets/Mirror/Core/Tools/Pool.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Pool.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Readme.txt b/Assets/Mirror/Core/Tools/Readme.txt new file mode 100644 index 0000000..09bd920 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Readme.txt @@ -0,0 +1 @@ +Standalone algorithms & structs to help build Mirror. \ No newline at end of file diff --git a/Assets/Mirror/Core/Tools/Readme.txt.meta b/Assets/Mirror/Core/Tools/Readme.txt.meta new file mode 100644 index 0000000..b8d44b0 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Readme.txt.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: da033671de7d49e0838223a997c56bf1 +timeCreated: 1667486850 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Readme.txt + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/TimeSample.cs b/Assets/Mirror/Core/Tools/TimeSample.cs new file mode 100644 index 0000000..111e971 --- /dev/null +++ b/Assets/Mirror/Core/Tools/TimeSample.cs @@ -0,0 +1,61 @@ +// TimeSample from Mirror II. +// simple profiling sample, averaged for display in statistics. +// usable in builds without unitiy profiler overhead etc. +// +// .average may safely be called from main thread while Begin/End is in another. +// i.e. worker threads, transport, etc. +using System.Diagnostics; +using System.Threading; + +namespace Mirror +{ + public struct TimeSample + { + // UnityEngine.Time isn't thread safe. use stopwatch instead. + readonly Stopwatch watch; + + // remember when Begin was called + double beginTime; + + // keep accumulating times over the given interval. + // (not readonly. we modify its contents.) + ExponentialMovingAverage ema; + + // average in seconds. + // code often runs in sub-millisecond time. float is more precise. + // + // set with Interlocked for thread safety. + // can be read from main thread while sampling happens in other thread. + public double average; // THREAD SAFE + + // average over N begin/end captures + public TimeSample(int n) + { + watch = new Stopwatch(); + watch.Start(); + ema = new ExponentialMovingAverage(n); + beginTime = 0; + average = 0; + } + + // begin is called before the code to be sampled + public void Begin() + { + // remember when Begin was called. + // keep StopWatch running so we can average over the given interval. + beginTime = watch.Elapsed.TotalSeconds; + // Debug.Log($"Begin @ {beginTime:F4}"); + } + + // end is called after the code to be sampled + public void End() + { + // add duration in seconds to accumulated durations + double elapsed = watch.Elapsed.TotalSeconds - beginTime; + ema.Add(elapsed); + + // expose new average thread safely + Interlocked.Exchange(ref average, ema.Value); + } + } +} diff --git a/Assets/Mirror/Core/Tools/TimeSample.cs.meta b/Assets/Mirror/Core/Tools/TimeSample.cs.meta new file mode 100644 index 0000000..1aad767 --- /dev/null +++ b/Assets/Mirror/Core/Tools/TimeSample.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 26c32f6429554546a88d800c846c74ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/TimeSample.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Utils.cs b/Assets/Mirror/Core/Tools/Utils.cs new file mode 100644 index 0000000..406484f --- /dev/null +++ b/Assets/Mirror/Core/Tools/Utils.cs @@ -0,0 +1,222 @@ +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.SceneManagement; + +namespace Mirror +{ + // Handles network messages on client and server + public delegate void NetworkMessageDelegate(NetworkConnection conn, NetworkReader reader, int channelId); + + // Handles requests to spawn objects on the client + public delegate GameObject SpawnDelegate(Vector3 position, uint assetId); + + public delegate GameObject SpawnHandlerDelegate(SpawnMessage msg); + + // Handles requests to unspawn objects on the client + public delegate void UnSpawnDelegate(GameObject spawned); + + // channels are const ints instead of an enum so people can add their own + // channels (can't extend an enum otherwise). + // + // note that Mirror is slowly moving towards quake style networking which + // will only require reliable for handshake, and unreliable for the rest. + // so eventually we can change this to an Enum and transports shouldn't + // add custom channels anymore. + public static class Channels + { + public const int Reliable = 0; // ordered + public const int Unreliable = 1; // unordered + } + + public static class Utils + { + // detect headless / dedicated server mode + // SystemInfo.graphicsDeviceType is never null in the editor. + // UNITY_SERVER works in builds for all Unity versions 2019 LTS and later. + // For Unity 2019 / 2020, there is no way to detect Server Build checkbox + // state in Build Settings, so they never auto-start headless server / client. + // UNITY_SERVER works in the editor in Unity 2021 LTS and later + // because that's when Dedicated Server platform was added. + // It is intentional for editor play mode to auto-start headless server / client + // when Dedicated Server platform is selected in the editor so that editor + // acts like a headless build to every extent possible for testing / debugging. + public static bool IsHeadless() => +#if UNITY_SERVER + true; +#else + SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null; +#endif + + // detect WebGL mode + public const bool IsWebGL = +#if UNITY_WEBGL + true; +#else + false; +#endif + + // detect Debug mode + public const bool IsDebug = +#if DEBUG + true; +#else + false; +#endif + + public static uint GetTrueRandomUInt() + { + // use Crypto RNG to avoid having time based duplicates + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + byte[] bytes = new byte[4]; + rng.GetBytes(bytes); + return BitConverter.ToUInt32(bytes, 0); + } + } + + public static bool IsPrefab(GameObject obj) + { +#if UNITY_EDITOR + return UnityEditor.PrefabUtility.IsPartOfPrefabAsset(obj); +#else + return false; +#endif + } + + // simplified IsSceneObject check from Mirror II + public static bool IsSceneObject(NetworkIdentity identity) + { + // original UNET / Mirror still had the IsPersistent check. + // it never fires though. even for Prefabs dragged to the Scene. + // (see Scene Objects example scene.) + // #if UNITY_EDITOR + // if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) + // return false; + // #endif + + return identity.gameObject.hideFlags != HideFlags.NotEditable && + identity.gameObject.hideFlags != HideFlags.HideAndDontSave && + identity.sceneId != 0; + } + + public static bool IsSceneObjectWithPrefabParent(GameObject gameObject, out GameObject prefab) + { + prefab = null; + +#if UNITY_EDITOR + if (!UnityEditor.PrefabUtility.IsPartOfPrefabInstance(gameObject)) + { + return false; + } + prefab = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(gameObject); +#endif + + if (prefab == null) + { + Debug.LogError($"Failed to find prefab parent for scene object [name:{gameObject.name}]"); + return false; + } + return true; + } + + // is a 2D point in screen? (from ummorpg) + // (if width = 1024, then indices from 0..1023 are valid (=1024 indices) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPointInScreen(Vector2 point) => + 0 <= point.x && point.x < Screen.width && + 0 <= point.y && point.y < Screen.height; + + // pretty print bytes as KB/MB/GB/etc. from DOTSNET + // long to support > 2GB + // divides by floats to return "2.5MB" etc. + public static string PrettyBytes(long bytes) + { + // bytes + if (bytes < 1024) + return $"{bytes} B"; + // kilobytes + else if (bytes < 1024L * 1024L) + return $"{(bytes / 1024f):F2} KB"; + // megabytes + else if (bytes < 1024 * 1024L * 1024L) + return $"{(bytes / (1024f * 1024f)):F2} MB"; + // gigabytes + return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB"; + } + + // pretty print seconds as hours:minutes:seconds(.milliseconds/100)s. + // double for long running servers. + public static string PrettySeconds(double seconds) + { + TimeSpan t = TimeSpan.FromSeconds(seconds); + string res = ""; + if (t.Days > 0) res += $"{t.Days}d"; + if (t.Hours > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Hours}h"; + if (t.Minutes > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Minutes}m"; + // 0.5s, 1.5s etc. if any milliseconds. 1s, 2s etc. if any seconds + if (t.Milliseconds > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Seconds}.{(t.Milliseconds / 100)}s"; + else if (t.Seconds > 0) res += $"{(res.Length > 0 ? " " : "")}{t.Seconds}s"; + // if the string is still empty because the value was '0', then at least + // return the seconds instead of returning an empty string + return res != "" ? res : "0s"; + } + + // universal .spawned function + public static NetworkIdentity GetSpawnedInServerOrClient(uint netId) + { + // server / host mode: use the one from server. + // host mode has access to all spawned. + if (NetworkServer.active) + { + NetworkServer.spawned.TryGetValue(netId, out NetworkIdentity entry); + return entry; + } + + // client + if (NetworkClient.active) + { + NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity entry); + return entry; + } + + return null; + } + + // keep a GUI window in screen. + // for example. if it's at x=1000 and screen is resized to w=500, + // it won't get lost in the invisible area etc. + public static Rect KeepInScreen(Rect rect) + { + // ensure min + rect.x = Math.Max(rect.x, 0); + rect.y = Math.Max(rect.y, 0); + + // ensure max + rect.x = Math.Min(rect.x, Screen.width - rect.width); + rect.y = Math.Min(rect.y, Screen.width - rect.height); + + return rect; + } + + // create local connections pair and connect them + public static void CreateLocalConnections( + out LocalConnectionToClient connectionToClient, + out LocalConnectionToServer connectionToServer) + { + connectionToServer = new LocalConnectionToServer(); + connectionToClient = new LocalConnectionToClient(); + connectionToServer.connectionToClient = connectionToClient; + connectionToClient.connectionToServer = connectionToServer; + } + + public static bool IsSceneActive(string scene) + { + Scene activeScene = SceneManager.GetActiveScene(); + return activeScene.path == scene || + activeScene.name == scene; + } + } +} diff --git a/Assets/Mirror/Runtime/Utils.cs.meta b/Assets/Mirror/Core/Tools/Utils.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/Utils.cs.meta rename to Assets/Mirror/Core/Tools/Utils.cs.meta index 7cf1557..aa65c05 100644 --- a/Assets/Mirror/Runtime/Utils.cs.meta +++ b/Assets/Mirror/Core/Tools/Utils.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Utils.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Vector3Long.cs b/Assets/Mirror/Core/Tools/Vector3Long.cs new file mode 100644 index 0000000..d54a46c --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector3Long.cs @@ -0,0 +1,125 @@ +#pragma warning disable CS0659 // 'Vector3Long' overrides Object.Equals(object o) but does not override Object.GetHashCode() +#pragma warning disable CS0661 // 'Vector3Long' defines operator == or operator != but does not override Object.GetHashCode() + +// Vector3Long by mischa (based on game engine project) +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public struct Vector3Long + { + public long x; + public long y; + public long z; + + public static readonly Vector3Long zero = new Vector3Long(0, 0, 0); + public static readonly Vector3Long one = new Vector3Long(1, 1, 1); + public static readonly Vector3Long forward = new Vector3Long(0, 0, 1); + public static readonly Vector3Long back = new Vector3Long(0, 0, -1); + public static readonly Vector3Long left = new Vector3Long(-1, 0, 0); + public static readonly Vector3Long right = new Vector3Long(1, 0, 0); + public static readonly Vector3Long up = new Vector3Long(0, 1, 0); + public static readonly Vector3Long down = new Vector3Long(0, -1, 0); + + // constructor ///////////////////////////////////////////////////////// + public Vector3Long(long x, long y, long z) + { + this.x = x; + this.y = y; + this.z = z; + } + + // operators /////////////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator +(Vector3Long a, Vector3Long b) => + new Vector3Long(a.x + b.x, a.y + b.y, a.z + b.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator -(Vector3Long a, Vector3Long b) => + new Vector3Long(a.x - b.x, a.y - b.y, a.z - b.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator -(Vector3Long v) => + new Vector3Long(-v.x, -v.y, -v.z); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator *(Vector3Long a, long n) => + new Vector3Long(a.x * n, a.y * n, a.z * n); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3Long operator *(long n, Vector3Long a) => + new Vector3Long(a.x * n, a.y * n, a.z * n); + + // == returns true if approximately equal (with epsilon). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector3Long a, Vector3Long b) => + a.x == b.x && + a.y == b.y && + a.z == b.z; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector3Long a, Vector3Long b) => !(a == b); + + // NO IMPLICIT System.Numerics.Vector3Long conversion because double<->float + // would silently lose precision in large worlds. + + // [i] component index. useful for iterating all components etc. + public long this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + default: throw new IndexOutOfRangeException($"Vector3Long[{index}] out of range."); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + default: throw new IndexOutOfRangeException($"Vector3Long[{index}] out of range."); + } + } + } + + // instance functions ////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"({x} {y} {z})"; + + // equality //////////////////////////////////////////////////////////// + // implement Equals & HashCode explicitly for performance. + // calling .Equals (instead of "==") checks for exact equality. + // (API compatibility) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Vector3Long other) => + x == other.x && y == other.y && z == other.z; + + // Equals(object) can reuse Equals(Vector4) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object other) => + other is Vector3Long vector4 && Equals(vector4); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(x, y, z); +#endif + } +} diff --git a/Assets/Mirror/Core/Tools/Vector3Long.cs.meta b/Assets/Mirror/Core/Tools/Vector3Long.cs.meta new file mode 100644 index 0000000..68e89d5 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector3Long.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 18efa4e349254185ad257401dd24628b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Vector3Long.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/Tools/Vector4Long.cs b/Assets/Mirror/Core/Tools/Vector4Long.cs new file mode 100644 index 0000000..0eba99a --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector4Long.cs @@ -0,0 +1,126 @@ +#pragma warning disable CS0659 // 'Vector4Long' overrides Object.Equals(object o) but does not override Object.GetHashCode() +#pragma warning disable CS0661 // 'Vector4Long' defines operator == or operator != but does not override Object.GetHashCode() + +// Vector4Long by mischa (based on game engine project) +using System; +using System.Runtime.CompilerServices; + +namespace Mirror +{ + public struct Vector4Long + { + public long x; + public long y; + public long z; + public long w; + + public static readonly Vector4Long zero = new Vector4Long(0, 0, 0, 0); + public static readonly Vector4Long one = new Vector4Long(1, 1, 1, 1); + + // constructor ///////////////////////////////////////////////////////// + public Vector4Long(long x, long y, long z, long w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + // operators /////////////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator +(Vector4Long a, Vector4Long b) => + new Vector4Long(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator -(Vector4Long a, Vector4Long b) => + new Vector4Long(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator -(Vector4Long v) => + new Vector4Long(-v.x, -v.y, -v.z, -v.w); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator *(Vector4Long a, long n) => + new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector4Long operator *(long n, Vector4Long a) => + new Vector4Long(a.x * n, a.y * n, a.z * n, a.w * n); + + // == returns true if approximately equal (with epsilon). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector4Long a, Vector4Long b) => + a.x == b.x && + a.y == b.y && + a.z == b.z && + a.w == b.w; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector4Long a, Vector4Long b) => !(a == b); + + // NO IMPLICIT System.Numerics.Vector4Long conversion because double<->float + // would silently lose precision in large worlds. + + // [i] component index. useful for iterating all components etc. + public long this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + switch (index) + { + case 0: return x; + case 1: return y; + case 2: return z; + case 3: return w; + default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range."); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + switch (index) + { + case 0: + x = value; + break; + case 1: + y = value; + break; + case 2: + z = value; + break; + case 3: + w = value; + break; + default: throw new IndexOutOfRangeException($"Vector4Long[{index}] out of range."); + } + } + } + + // instance functions ////////////////////////////////////////////////// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"({x} {y} {z} {w})"; + + // equality //////////////////////////////////////////////////////////// + // implement Equals & HashCode explicitly for performance. + // calling .Equals (instead of "==") checks for exact equality. + // (API compatibility) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Vector4Long other) => + x == other.x && y == other.y && z == other.z && w == other.w; + + // Equals(object) can reuse Equals(Vector4) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object other) => + other is Vector4Long vector4 && Equals(vector4); + +#if UNITY_2021_3_OR_NEWER + // Unity 2019/2020 don't have HashCode.Combine yet. + // this is only to avoid reflection. without defining, it works too. + // default generated by rider + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => HashCode.Combine(x, y, z, w); +#endif + } +} diff --git a/Assets/Mirror/Core/Tools/Vector4Long.cs.meta b/Assets/Mirror/Core/Tools/Vector4Long.cs.meta new file mode 100644 index 0000000..b6ed4d8 --- /dev/null +++ b/Assets/Mirror/Core/Tools/Vector4Long.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9f69bf6b6d73476ab3f31dd635bb6497 +timeCreated: 1726777293 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Tools/Vector4Long.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transport.cs b/Assets/Mirror/Core/Transport.cs similarity index 88% rename from Assets/Mirror/Runtime/Transport.cs rename to Assets/Mirror/Core/Transport.cs index f831bf1..926a5c0 100644 --- a/Assets/Mirror/Runtime/Transport.cs +++ b/Assets/Mirror/Core/Transport.cs @@ -30,13 +30,18 @@ namespace Mirror /// Abstract transport layer component public abstract class Transport : MonoBehaviour { - // common ////////////////////////////////////////////////////////////// /// The current transport used by Mirror. - public static Transport activeTransport; + public static Transport active; /// Is this transport available in the current platform? public abstract bool Available(); + /// Is this transport encrypted for secure communication? + public virtual bool IsEncrypted => false; + + /// If encrypted, which cipher is used? + public virtual string EncryptionCipher => ""; + // client ////////////////////////////////////////////////////////////// /// Called by Transport when the client connected to the server. public Action OnClientConnected; @@ -52,15 +57,23 @@ public abstract class Transport : MonoBehaviour public Action, int> OnClientDataSent; /// Called by Transport when the client encountered an error. - public Action OnClientError; + public Action OnClientError; + + /// Called by Transport when the client encountered an error. + public Action OnClientTransportException; /// Called by Transport when the client disconnected from the server. public Action OnClientDisconnected; // server ////////////////////////////////////////////////////////////// - /// Called by Transport when a new client connected to the server. + + // Deprecated 2024-07-20 + [Obsolete("Use OnServerConnectedWithAddress and pass the remote client address instead")] public Action OnServerConnected; + /// Called by Transport when a new client connected to the server. + public Action OnServerConnectedWithAddress; + /// Called by Transport when the server received a message from a client. public Action, int> OnServerDataReceived; @@ -73,7 +86,11 @@ public abstract class Transport : MonoBehaviour /// Called by Transport when a server's connection encountered a problem. /// If a Disconnect will also be raised, raise the Error first. - public Action OnServerError; + public Action OnServerError; + + /// Called by Transport when a server's connection encountered a problem. + /// If a Disconnect will also be raised, raise the Error first. + public Action OnServerTransportException; /// Called by Transport when a client disconnected from the server. public Action OnServerDisconnected; @@ -180,6 +197,7 @@ public virtual void ServerLateUpdate() {} public abstract void Shutdown(); /// Called by Unity when quitting. Inheriting Transports should call base for proper Shutdown. + // (this can't be in OnDestroy: https://github.com/MirrorNetworking/Mirror/issues/3952) public virtual void OnApplicationQuit() { // stop transport (e.g. to shut down threads) diff --git a/Assets/Mirror/Runtime/Transport.cs.meta b/Assets/Mirror/Core/Transport.cs.meta similarity index 63% rename from Assets/Mirror/Runtime/Transport.cs.meta rename to Assets/Mirror/Core/Transport.cs.meta index 55072e1..4931067 100644 --- a/Assets/Mirror/Runtime/Transport.cs.meta +++ b/Assets/Mirror/Core/Transport.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/Transport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/TransportError.cs b/Assets/Mirror/Core/TransportError.cs new file mode 100644 index 0000000..b452015 --- /dev/null +++ b/Assets/Mirror/Core/TransportError.cs @@ -0,0 +1,17 @@ +// Mirror transport error code enum. +// most transport implementations should use a subset of this, +// and then translate the transport error codes to mirror error codes. +namespace Mirror +{ + public enum TransportError : byte + { + DnsResolve, // failed to resolve a host name + Refused, // connection refused by other end. server full etc. + Timeout, // ping timeout or dead link + Congestion, // more messages than transport / network can process + InvalidReceive, // recv invalid packet (possibly intentional attack) + InvalidSend, // user tried to send invalid data + ConnectionClosed, // connection closed voluntarily or lost involuntarily + Unexpected // unexpected error / exception, requires fix. + } +} diff --git a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta b/Assets/Mirror/Core/TransportError.cs.meta similarity index 54% rename from Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta rename to Assets/Mirror/Core/TransportError.cs.meta index a569990..1b56a9b 100644 --- a/Assets/Mirror/Components/NetworkTransform2k/NetworkTransform.cs.meta +++ b/Assets/Mirror/Core/TransportError.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2f74aedd71d9a4f55b3ce499326d45fb +guid: ce162bdedd704db9b8c35d163f0c1d54 MonoImporter: externalObjects: {} serializedVersion: 2 @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/TransportError.cs + uploadId: 736421 diff --git a/Assets/Mirror/Core/WeaverFuse.cs b/Assets/Mirror/Core/WeaverFuse.cs new file mode 100644 index 0000000..f850323 --- /dev/null +++ b/Assets/Mirror/Core/WeaverFuse.cs @@ -0,0 +1,22 @@ +// safety fuse for weaver to flip. +// runtime can check this to ensure weaving succeded. +// otherwise running server/client would give lots of random 'writer not found' etc. errors. +// this is much cleaner. +// +// note that ILPostProcessor errors already block entering playmode. +// however, issues could still stop the weaving from running at all. +// WeaverFuse can check if it actually ran. +namespace Mirror +{ + public static class WeaverFuse + { + // this trick only works for ILPostProcessor. + // CompilationFinishedHook can't weaver Mirror.dll. + public static bool Weaved() => +#if UNITY_2020_3_OR_NEWER + false; +#else + true; +#endif + } +} diff --git a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs.meta b/Assets/Mirror/Core/WeaverFuse.cs.meta similarity index 55% rename from Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs.meta rename to Assets/Mirror/Core/WeaverFuse.cs.meta index df466bd..b4572a5 100644 --- a/Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs.meta +++ b/Assets/Mirror/Core/WeaverFuse.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ab2cbc52526ea384ba280d13cd1a57b9 +guid: 4de3dfbcbd2e41fcac947c04bcac52c9 MonoImporter: externalObjects: {} serializedVersion: 2 @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Core/WeaverFuse.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/AndroidManifestHelper.cs b/Assets/Mirror/Editor/AndroidManifestHelper.cs index 78f408d..bae0082 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 manfiest 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/AndroidManifestHelper.cs.meta b/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta index 1281aea..996e3e9 100644 --- a/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta +++ b/Assets/Mirror/Editor/AndroidManifestHelper.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/AndroidManifestHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/EditorHelper.cs b/Assets/Mirror/Editor/EditorHelper.cs index b10c7b0..c3551ab 100644 --- a/Assets/Mirror/Editor/EditorHelper.cs +++ b/Assets/Mirror/Editor/EditorHelper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; @@ -27,5 +28,14 @@ public static string FindPath() return string.Empty; } } + + + public static IEnumerable IterateOverProject(string filter) + { + foreach (string guid in AssetDatabase.FindAssets(filter)) + { + yield return AssetDatabase.GUIDToAssetPath(guid); + } + } } } diff --git a/Assets/Mirror/Editor/EditorHelper.cs.meta b/Assets/Mirror/Editor/EditorHelper.cs.meta index a1cd814..c2a4577 100644 --- a/Assets/Mirror/Editor/EditorHelper.cs.meta +++ b/Assets/Mirror/Editor/EditorHelper.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/EditorHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs b/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs deleted file mode 100644 index 18ab111..0000000 --- a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-12-12 diff --git a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta b/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta deleted file mode 100644 index 79a200d..0000000 --- a/Assets/Mirror/Editor/Empty/EnterPlayModeSettingsCheck.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b15a0d2ca0909400eb53dd6fe894cddd -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs b/Assets/Mirror/Editor/Empty/LogLevelWindow.cs deleted file mode 100644 index 82e5275..0000000 --- a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs +++ /dev/null @@ -1 +0,0 @@ -// File moved to Mirror/Editor/Logging/LogLevelWindow.cs \ No newline at end of file diff --git a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta b/Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta deleted file mode 100644 index b8cbaeb..0000000 --- a/Assets/Mirror/Editor/Empty/LogLevelWindow.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f28def2148ed5194abe70af012a4e3e0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs b/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta deleted file mode 100644 index 832876f..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelWindow.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c3dbf48190d77d243b87962a82c3b164 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs b/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta deleted file mode 100644 index 3214b08..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogLevelsGUI.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9d6ce9d62a2d2ec4d8cef8a0d22b8dd2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs b/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta b/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta deleted file mode 100644 index 2c1fac4..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/LogSettingsEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 8f4ecb3d81ce9ff44b91f311ee46d4ea -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs b/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta b/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta deleted file mode 100644 index b4c277d..0000000 --- a/Assets/Mirror/Editor/Empty/Logging/NetworkLogSettingsEditor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 37fb96d5bbf965d47acfc5c8589a1b71 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs b/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta b/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta deleted file mode 100644 index a1a0af3..0000000 --- a/Assets/Mirror/Editor/Empty/ScriptableObjectUtility.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4d54a29ddd5b52b4eaa07ed39c0e3e83 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Icon/MirrorIcon.png b/Assets/Mirror/Editor/Icon/MirrorIcon.png new file mode 100644 index 0000000..a77ca23 --- /dev/null +++ b/Assets/Mirror/Editor/Icon/MirrorIcon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f1ee2247319c7a4e5d1ab13ad4981c21c5a408299ae4158050a63ac7f1eba47 +size 138247 diff --git a/Assets/Mirror/Editor/Icon/MirrorIcon.png.meta b/Assets/Mirror/Editor/Icon/MirrorIcon.png.meta new file mode 100644 index 0000000..18e01bd --- /dev/null +++ b/Assets/Mirror/Editor/Icon/MirrorIcon.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 7453abfe9e8b2c04a8a47eb536fe21eb +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 9 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: -1 + mipBias: -100 + wrapU: -1 + wrapV: -1 + wrapW: -1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - serializedVersion: 2 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - serializedVersion: 2 + buildTarget: WebGL + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + vertices: [] + indices: + edges: [] + weights: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Icon/MirrorIcon.png + uploadId: 736421 diff --git a/Assets/Mirror/Editor/InspectorHelper.cs.meta b/Assets/Mirror/Editor/InspectorHelper.cs.meta index 852ff71..91205ff 100644 --- a/Assets/Mirror/Editor/InspectorHelper.cs.meta +++ b/Assets/Mirror/Editor/InspectorHelper.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/InspectorHelper.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/LagCompensatorInspector.cs b/Assets/Mirror/Editor/LagCompensatorInspector.cs new file mode 100644 index 0000000..f706384 --- /dev/null +++ b/Assets/Mirror/Editor/LagCompensatorInspector.cs @@ -0,0 +1,14 @@ +using UnityEditor; + +namespace Mirror +{ + [CustomEditor(typeof(LagCompensator))] + public class LagCompensatorInspector : Editor + { + public override void OnInspectorGUI() + { + EditorGUILayout.HelpBox("Preview Component - Feedback appreciated on GitHub or Discord!", MessageType.Warning); + DrawDefaultInspector(); + } + } +} diff --git a/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta b/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta new file mode 100644 index 0000000..0dba2ed --- /dev/null +++ b/Assets/Mirror/Editor/LagCompensatorInspector.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 703e39b5385ae2e479987ff4ec0707a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/LagCompensatorInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef b/Assets/Mirror/Editor/Mirror.Editor.asmdef index 0d59f9f..800e67b 100644 --- a/Assets/Mirror/Editor/Mirror.Editor.asmdef +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef @@ -2,8 +2,9 @@ "name": "Mirror.Editor", "rootNamespace": "", "references": [ - "Mirror", - "Unity.Mirror.CodeGen" + "GUID:30817c1a0e6d646d99c048fc403f5979", + "GUID:72872094b21c16e48b631b2224833d49", + "GUID:1d0b9d21c3ff546a4aa32399dfd33474" ], "includePlatforms": [ "Editor" diff --git a/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta b/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta index e2e6f2a..2d148c7 100644 --- a/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta +++ b/Assets/Mirror/Editor/Mirror.Editor.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Mirror.Editor.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs index 54b3ae7..52c56d6 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs @@ -43,7 +43,8 @@ bool SyncsAnything(Type scriptClass) void OnEnable() { - if (target == null) { Debug.LogWarning("NetworkBehaviourInspector had no target object"); return; } + // sometimes target is null. just return early. + if (target == null) return; // If target's base class is changed from NetworkBehaviour to MonoBehaviour // then Unity temporarily keep using this Inspector causing things to break @@ -85,7 +86,15 @@ protected void DrawDefaultSyncSettings() EditorGUILayout.Space(); EditorGUILayout.LabelField("Sync Settings", EditorStyles.boldLabel); - EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + // sync direction + SerializedProperty syncDirection = serializedObject.FindProperty("syncDirection"); + EditorGUILayout.PropertyField(syncDirection); + + // sync mdoe: only show for ServerToClient components + if (syncDirection.enumValueIndex == (int)SyncDirection.ServerToClient) + EditorGUILayout.PropertyField(serializedObject.FindProperty("syncMode")); + + // sync interval EditorGUILayout.PropertyField(serializedObject.FindProperty("syncInterval")); // apply diff --git a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta index 78d9fa8..428f07d 100644 --- a/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta +++ b/Assets/Mirror/Editor/NetworkBehaviourInspector.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkBehaviourInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs b/Assets/Mirror/Editor/NetworkInformationPreview.cs index 2c8874b..b4dbe5e 100644 --- a/Assets/Mirror/Editor/NetworkInformationPreview.cs +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs @@ -126,7 +126,9 @@ float DrawNetworkIdentityInfo(NetworkIdentity identity, float initialX, float Y) Vector2 maxValueLabelSize = GetMaxNameLabelSize(infos); Rect labelRect = new Rect(initialX, Y, maxNameLabelSize.x, maxNameLabelSize.y); - Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y); + + // height needs a +1 to line up nicely + Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y + 1); foreach (NetworkIdentityInfo info in infos) { @@ -171,7 +173,7 @@ float DrawNetworkBehaviors(NetworkIdentity identity, float initialX, float Y) float DrawObservers(NetworkIdentity identity, float initialX, float Y) { - if (identity.observers != null && identity.observers.Count > 0) + if (identity.observers.Count > 0) { Rect observerRect = new Rect(initialX, Y + 10, 200, 20); @@ -252,7 +254,7 @@ IEnumerable GetNetworkIdentityInfo(NetworkIdentity identity infos.Add(GetString("Network ID", identity.netId.ToString())); infos.Add(GetBoolean("Is Client", identity.isClient)); infos.Add(GetBoolean("Is Server", identity.isServer)); - infos.Add(GetBoolean("Has Authority", identity.hasAuthority)); + infos.Add(GetBoolean("Is Owned", identity.isOwned)); infos.Add(GetBoolean("Is Local Player", identity.isLocalPlayer)); } return infos; diff --git a/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta b/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta index 9bf2de4..e67d4ea 100644 --- a/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta +++ b/Assets/Mirror/Editor/NetworkInformationPreview.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkInformationPreview.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkManagerEditor.cs b/Assets/Mirror/Editor/NetworkManagerEditor.cs index 94b0844..a7aa9bc 100644 --- a/Assets/Mirror/Editor/NetworkManagerEditor.cs +++ b/Assets/Mirror/Editor/NetworkManagerEditor.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEditorInternal; using UnityEngine; @@ -42,6 +45,98 @@ public override void OnInspectorGUI() { serializedObject.ApplyModifiedProperties(); } + + if (GUILayout.Button("Populate Spawnable Prefabs")) + { + ScanForNetworkIdentities(); + } + + // clicking the Populate button in a large project can add hundreds of entries. + // have a clear button in case that wasn't intended. + GUI.enabled = networkManager.spawnPrefabs.Count > 0; + if (GUILayout.Button("Clear Spawnable Prefabs")) + { + ClearNetworkIdentities(); + } + GUI.enabled = true; + } + + void ScanForNetworkIdentities() + { + List identities = new List(); + bool cancelled = false; + try + { + string[] paths = EditorHelper.IterateOverProject("t:prefab").ToArray(); + int count = 0; + foreach (string path in paths) + { + // ignore test & example prefabs. + // users sometimes keep the folders in their projects. + if (path.Contains("Mirror/Tests/") || + path.Contains("Mirror/Examples/")) + { + continue; + } + + if (EditorUtility.DisplayCancelableProgressBar("Searching for NetworkIdentities..", + $"Scanned {count}/{paths.Length} prefabs. Found {identities.Count} new ones", + count / (float)paths.Length)) + { + cancelled = true; + break; + } + + count++; + + NetworkIdentity ni = AssetDatabase.LoadAssetAtPath(path); + if (!ni) + { + continue; + } + + if (!networkManager.spawnPrefabs.Contains(ni.gameObject)) + { + identities.Add(ni.gameObject); + } + + } + } + finally + { + + EditorUtility.ClearProgressBar(); + if (!cancelled) + { + // RecordObject is needed for "*" to show up in Scene. + // however, this only saves List.Count without the entries. + Undo.RecordObject(networkManager, "NetworkManager: populated prefabs"); + + // add the entries + networkManager.spawnPrefabs.AddRange(identities); + + // sort alphabetically for better UX + networkManager.spawnPrefabs = networkManager.spawnPrefabs.OrderBy(go => go.name).ToList(); + + // SetDirty is required to save the individual entries properly. + EditorUtility.SetDirty(target); + } + // Loading assets might use a lot of memory, so try to unload them after + Resources.UnloadUnusedAssets(); + } + } + + void ClearNetworkIdentities() + { + // RecordObject is needed for "*" to show up in Scene. + // however, this only saves List.Count without the entries. + Undo.RecordObject(networkManager, "NetworkManager: cleared prefabs"); + + // add the entries + networkManager.spawnPrefabs.Clear(); + + // SetDirty is required to save the individual entries properly. + EditorUtility.SetDirty(target); } static void DrawHeader(Rect headerRect) diff --git a/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta b/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta index 7fe8dbc..78cc6d8 100644 --- a/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta +++ b/Assets/Mirror/Editor/NetworkManagerEditor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkManagerEditor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/NetworkScenePostProcess.cs b/Assets/Mirror/Editor/NetworkScenePostProcess.cs index c60493d..9baad1b 100644 --- a/Assets/Mirror/Editor/NetworkScenePostProcess.cs +++ b/Assets/Mirror/Editor/NetworkScenePostProcess.cs @@ -33,9 +33,7 @@ public static void OnPostProcessScene() // if we had a [ConflictComponent] attribute that would be better than this check. // also there is no context about which scene this is in. if (identity.GetComponent() != null) - { Debug.LogError("NetworkManager has a NetworkIdentity component. This will cause the NetworkManager object to be disabled, so it is not recommended."); - } // not spawned before? // OnPostProcessScene is called after additive scene loads too, @@ -57,19 +55,28 @@ public static void OnPostProcessScene() else { // there are two cases where sceneId == 0: - // * if we have a prefab open in the prefab scene - // * if an unopened scene needs resaving - // show a proper error message in both cases so the user - // knows what to do. + // if we have a prefab open in the prefab scene string path = identity.gameObject.scene.path; if (string.IsNullOrWhiteSpace(path)) + { + // pressing play while in prefab edit mode used to freeze/crash Unity 2019. + // this seems fine now so we don't need to stop the editor anymore. +#if UNITY_2020_3_OR_NEWER + Debug.LogWarning($"{identity.name} was open in Prefab Edit Mode while launching with Mirror. If this causes issues, please let us know."); +#else Debug.LogError($"{identity.name} is currently open in Prefab Edit Mode. Please open the actual scene before launching Mirror."); + EditorApplication.isPlaying = false; +#endif + } + // if an unopened scene needs resaving else - Debug.LogError($"Scene {path} needs to be opened and resaved, because the scene object {identity.name} has no valid sceneId yet."); + { - // either way we shouldn't continue. nothing good will - // happen when trying to launch with invalid sceneIds. - EditorApplication.isPlaying = false; + // nothing good will happen when trying to launch with invalid sceneIds. + // show an error and stop playing immediately. + Debug.LogError($"Scene {path} needs to be opened and resaved, because the scene object {identity.name} has no valid sceneId yet."); + EditorApplication.isPlaying = false; + } } } } @@ -80,28 +87,20 @@ static void PrepareSceneObject(NetworkIdentity identity) // set scene hash identity.SetSceneIdSceneHashPartInternal(); - // disable it + // spawnable scene objects are force disabled on scene load to + // ensure Start/Update/etc. aren't called until actually spawned. + // // note: NetworkIdentity.OnDisable adds itself to the // spawnableObjects dictionary (only if sceneId != 0) identity.gameObject.SetActive(false); // safety check for prefabs with more than one NetworkIdentity -#if UNITY_2018_2_OR_NEWER GameObject prefabGO = PrefabUtility.GetCorrespondingObjectFromSource(identity.gameObject); -#else - GameObject prefabGO = PrefabUtility.GetPrefabParent(identity.gameObject); -#endif if (prefabGO) { -#if UNITY_2018_3_OR_NEWER GameObject prefabRootGO = prefabGO.transform.root.gameObject; -#else - GameObject prefabRootGO = PrefabUtility.FindPrefabRoot(prefabGO); -#endif if (prefabRootGO != null && prefabRootGO.GetComponentsInChildren().Length > 1) - { Debug.LogWarning($"Prefab {prefabRootGO.name} has several NetworkIdentity components attached to itself or its children, this is not supported."); - } } } } diff --git a/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta b/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta index b567cc9..2ea2e63 100644 --- a/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta +++ b/Assets/Mirror/Editor/NetworkScenePostProcess.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/NetworkScenePostProcess.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/ReadOnlyDrawer.cs b/Assets/Mirror/Editor/ReadOnlyDrawer.cs new file mode 100644 index 0000000..4a09707 --- /dev/null +++ b/Assets/Mirror/Editor/ReadOnlyDrawer.cs @@ -0,0 +1,19 @@ +using UnityEngine; +using UnityEditor; + +namespace Mirror +{ + [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] + public class ReadOnlyDrawer : PropertyDrawer + { + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + // Cache the current GUI enabled state + bool prevGuiEnabledState = GUI.enabled; + + GUI.enabled = false; + EditorGUI.PropertyField(position, property, label, true); + GUI.enabled = prevGuiEnabledState; + } + } +} diff --git a/Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta b/Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta new file mode 100644 index 0000000..81d626f --- /dev/null +++ b/Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 22f17bdd21f104c41bc175937fefbdec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/ReadOnlyDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SceneDrawer.cs.meta b/Assets/Mirror/Editor/SceneDrawer.cs.meta index 6a996dc..04d6cdc 100644 --- a/Assets/Mirror/Editor/SceneDrawer.cs.meta +++ b/Assets/Mirror/Editor/SceneDrawer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/SceneDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs index 2c95bcf..735b018 100644 --- a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs +++ b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Reflection; +using System.Text.RegularExpressions; using UnityEditor; namespace Mirror @@ -18,7 +19,12 @@ public SyncObjectCollectionField(FieldInfo field) { this.field = field; visible = false; - label = $"{field.Name} [{field.FieldType.Name}]"; + + // field.FieldType.Name has a backtick and number at the end, e.g. SyncList`1 + // so we split it and only take the first part so it looks nicer. + // e.g. SyncList`1 -> SyncList + // Better to do it one time here than in hot path in OnInspectorGUI + label = $"{field.Name} [{Regex.Replace(field.FieldType.Name, @"`\d+", "")}]"; } } diff --git a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta index 44ba75d..b830f9a 100644 --- a/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta +++ b/Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 6f90afab12e04f0e945d83e9d38308a3 -timeCreated: 1632556645 \ No newline at end of file +timeCreated: 1632556645 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/SyncObjectCollectionsDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta index 6311f1d..3a9216d 100644 --- a/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta +++ b/Assets/Mirror/Editor/SyncVarAttributeDrawer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/SyncVarAttributeDrawer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/SyncVarDrawer.cs b/Assets/Mirror/Editor/SyncVarDrawer.cs deleted file mode 100644 index b0532ae..0000000 --- a/Assets/Mirror/Editor/SyncVarDrawer.cs +++ /dev/null @@ -1,35 +0,0 @@ -// SyncVar looks like this in the Inspector: -// Health -// Value: 42 -// instead, let's draw ._Value directly so it looks like this: -// Health: 42 -// -// BUG: Unity also doesn't show custom drawer for readonly fields (#1368395) -using UnityEditor; -using UnityEngine; - -namespace Mirror -{ - [CustomPropertyDrawer(typeof(SyncVar<>))] - public class SyncVarDrawer : PropertyDrawer - { - static readonly GUIContent syncVarIndicatorContent = new GUIContent("SyncVar", "This variable is a SyncVar."); - - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - Vector2 syncVarIndicatorRect = EditorStyles.miniLabel.CalcSize(syncVarIndicatorContent); - float valueWidth = position.width - syncVarIndicatorRect.x; - - Rect valueRect = new Rect(position.x, position.y, valueWidth, position.height); - Rect labelRect = new Rect(position.x + valueWidth, position.y, syncVarIndicatorRect.x, position.height); - - EditorGUI.PropertyField(valueRect, property.FindPropertyRelative("_Value"), label, true); - GUI.Label(labelRect, syncVarIndicatorContent, EditorStyles.miniLabel); - } - - public override float GetPropertyHeight(SerializedProperty property, GUIContent label) - { - return EditorGUI.GetPropertyHeight(property.FindPropertyRelative("_Value")); - } - } -} diff --git a/Assets/Mirror/Editor/SyncVarDrawer.cs.meta b/Assets/Mirror/Editor/SyncVarDrawer.cs.meta deleted file mode 100644 index 0ee91aa..0000000 --- a/Assets/Mirror/Editor/SyncVarDrawer.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 874812594431423b84f763b987ff9681 -timeCreated: 1632553007 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta index d356af8..a58871b 100644 --- a/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta +++ b/Assets/Mirror/Editor/Weaver/AssemblyInfo.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/AssemblyInfo.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Empty.meta b/Assets/Mirror/Editor/Weaver/Empty.meta deleted file mode 100644 index 6e29ee7..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 30fc290f2ff9c29498f54f63de12ca6f -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs b/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs deleted file mode 100644 index a88144a..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs +++ /dev/null @@ -1 +0,0 @@ -// Removed Oct 1 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta deleted file mode 100644 index 685f914..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/GenericArgumentResolver.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: fd67b3f7c2d66074a9bc7a23787e2ffb -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs deleted file mode 100644 index b38f171..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// removed Oct 5 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta deleted file mode 100644 index cbea4d6..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/MessageClassProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e25c00c88fc134f6ea7ab00ae4db8083 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/Program.cs b/Assets/Mirror/Editor/Weaver/Empty/Program.cs deleted file mode 100644 index a214b81..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/Program.cs +++ /dev/null @@ -1 +0,0 @@ -// Removed 05/09/20 diff --git a/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta deleted file mode 100644 index 0a14018..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/Program.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 0152994c9591626408fcfec96fcc7933 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs deleted file mode 100644 index a88144a..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// Removed Oct 1 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta deleted file mode 100644 index 0a7c2aa..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncDictionaryProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 29e4a45f69822462ab0b15adda962a29 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs deleted file mode 100644 index 2fdbc52..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2020-09 diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta deleted file mode 100644 index 81b9576..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncEventProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a5d8b25543a624384944b599e5a832a8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs b/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs deleted file mode 100644 index a88144a..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs +++ /dev/null @@ -1 +0,0 @@ -// Removed Oct 1 2020 \ No newline at end of file diff --git a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta deleted file mode 100644 index b73b047..0000000 --- a/Assets/Mirror/Editor/Weaver/Empty/SyncListProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4f3445268e45d437fac325837aff3246 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint.meta b/Assets/Mirror/Editor/Weaver/EntryPoint.meta index 81827c5..54b718a 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 251338e67afb4cefa38da924f8c50a6e -timeCreated: 1628851818 \ No newline at end of file +timeCreated: 1628851818 diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs index 9016949..b5db851 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs @@ -57,11 +57,7 @@ public static void WeaveExistingAssemblies() } } -#if UNITY_2019_3_OR_NEWER EditorUtility.RequestScriptReload(); -#else - UnityEditorInternal.InternalEditorUtility.RequestScriptReload(); -#endif } static Assembly FindCompilationPipelineAssembly(string assemblyName) => @@ -79,13 +75,16 @@ public static void OnCompilationFinished(string assemblyPath, CompilerMessage[] return; } - // Should not run on the editor only assemblies - if (assemblyPath.Contains("-Editor") || assemblyPath.Contains(".Editor")) + // Should not run on the editor only assemblies (test ones still need to be weaved) + if (assemblyPath.Contains("-Editor") || + (assemblyPath.Contains(".Editor") && !assemblyPath.Contains(".Tests"))) { return; } - // don't weave mirror files + // skip Mirror.dll because CompilationFinishedHook can't weave itself. + // this would cause a sharing violation. + // skip Mirror.Weaver.dll too. string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); if (assemblyName == MirrorRuntimeAssemblyName || assemblyName == MirrorWeaverAssemblyName) { diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta index ed537ab..33e73f7 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedHook.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta index f8c7139..3d07ffc 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 47026732f0fa475c94bd1dd41f1de559 -timeCreated: 1629379868 \ No newline at end of file +timeCreated: 1629379868 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPoint/CompilationFinishedLogger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta index eca31e3..af6dddc 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: b73d0f106ba84aa983baa5142b08a0a9 -timeCreated: 1628851346 \ No newline at end of file +timeCreated: 1628851346 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPoint/EnterPlayModeHook.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta index 6ef7bf3..d1fa101 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 09082db63d1d48d9ab91320165c1b684 -timeCreated: 1628859005 \ No newline at end of file +timeCreated: 1628859005 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta index 1e5091e..c442f3b 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 9009d1db4ed44f6694a92bf8ad7738e9 -timeCreated: 1630129423 \ No newline at end of file +timeCreated: 1630129423 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/CompiledAssemblyFromFile.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs index cbc8e41..0823234 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs @@ -14,7 +14,7 @@ // we need a custom resolver for ILPostProcessor. #if UNITY_2020_3_OR_NEWER using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Threading; @@ -26,12 +26,23 @@ namespace Mirror.Weaver class ILPostProcessorAssemblyResolver : IAssemblyResolver { readonly string[] assemblyReferences; - readonly Dictionary assemblyCache = - new Dictionary(); + + // originally we used Dictionary + lock. + // Resolve() is called thousands of times for large projects. + // ILPostProcessor is multithreaded, so best to use ConcurrentDictionary without the lock here. + readonly ConcurrentDictionary assemblyCache = + new ConcurrentDictionary(); + + // Resolve() calls FindFile() every time. + // thousands of times for String => mscorlib alone in large projects. + // cache the results! ILPostProcessor is multithreaded, so use a ConcurrentDictionary here. + readonly ConcurrentDictionary fileNameCache = + new ConcurrentDictionary(); + readonly ICompiledAssembly compiledAssembly; AssemblyDefinition selfAssembly; - Logger Log; + readonly Logger Log; public ILPostProcessorAssemblyResolver(ICompiledAssembly compiledAssembly, Logger Log) { @@ -54,56 +65,82 @@ protected virtual void Dispose(bool disposing) public AssemblyDefinition Resolve(AssemblyNameReference name) => Resolve(name, new ReaderParameters(ReadingMode.Deferred)); + // here is an example on when this is called: + // Player : NetworkBehaviour has a [SyncVar] of type String. + // Weaver's SyncObjectInitializer checks if ImplementsSyncObject() + // which needs to resolve the type 'String' from mscorlib. + // Resolve() lives in CecilX.MetadataResolver.Resolve() + // which calls assembly_resolver.Resolve(). + // which uses our ILPostProcessorAssemblyResolver here. + // + // for large projects, this is called thousands of times for mscorlib alone. + // initially ILPostProcessorAssemblyResolver took 30x longer than with CompilationFinishedHook. + // we need to cache and speed up everything we can here! public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) { - lock (assemblyCache) + if (name.Name == compiledAssembly.Name) + return selfAssembly; + + // cache FindFile. + // in large projects, this is called thousands(!) of times for String=>mscorlib alone. + // reduces a single String=>mscorlib resolve from 0.771ms to 0.015ms. + // => 50x improvement in TypeReference.Resolve() speed! + // => 22x improvement in Weaver speed! + if (!fileNameCache.TryGetValue(name.Name, out string fileName)) { - if (name.Name == compiledAssembly.Name) - return selfAssembly; + fileName = FindFile(name.Name); + fileNameCache.TryAdd(name.Name, fileName); + } - string fileName = FindFile(name); - if (fileName == null) + if (fileName == null) + { + // returning null will throw exceptions in our weaver where. + // let's make it obvious why we returned null for easier debugging. + // NOTE: if this fails for "System.Private.CoreLib": + // ILPostProcessorReflectionImporter fixes it! + + // the fix for #2503 started showing this warning for Bee.BeeDriver on mac, + // which is for compilation. we can ignore that one. + if (!name.Name.StartsWith("Bee.BeeDriver")) { - // returning null will throw exceptions in our weaver where. - // let's make it obvious why we returned null for easier debugging. - // NOTE: if this fails for "System.Private.CoreLib": - // ILPostProcessorReflectionImporter fixes it! Log.Warning($"ILPostProcessorAssemblyResolver.Resolve: Failed to find file for {name}"); - return null; } + return null; + } - DateTime lastWriteTime = File.GetLastWriteTime(fileName); - - string cacheKey = fileName + lastWriteTime; - - if (assemblyCache.TryGetValue(cacheKey, out AssemblyDefinition result)) - return result; - - parameters.AssemblyResolver = this; + // try to get cached assembly by filename + writetime + DateTime lastWriteTime = File.GetLastWriteTime(fileName); + string cacheKey = fileName + lastWriteTime; + if (assemblyCache.TryGetValue(cacheKey, out AssemblyDefinition result)) + return result; - MemoryStream ms = MemoryStreamFor(fileName); + // otherwise resolve and cache a new assembly + parameters.AssemblyResolver = this; + MemoryStream ms = MemoryStreamFor(fileName); - string pdb = fileName + ".pdb"; - if (File.Exists(pdb)) - parameters.SymbolStream = MemoryStreamFor(pdb); + string pdb = fileName + ".pdb"; + if (File.Exists(pdb)) + parameters.SymbolStream = MemoryStreamFor(pdb); - AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters); - assemblyCache.Add(cacheKey, assemblyDefinition); - return assemblyDefinition; - } + AssemblyDefinition assemblyDefinition = AssemblyDefinition.ReadAssembly(ms, parameters); + assemblyCache.TryAdd(cacheKey, assemblyDefinition); + return assemblyDefinition; } // find assemblyname in assembly's references - string FindFile(AssemblyNameReference name) + string FindFile(string name) { - string fileName = assemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == name.Name + ".dll"); - if (fileName != null) - return fileName; + // perhaps the type comes from a .dll or .exe + // check both in one call without Linq instead of iterating twice like originally + foreach (string r in assemblyReferences) + { + if (Path.GetFileNameWithoutExtension(r) == name) + return r; + } - // perhaps the type comes from an exe instead - fileName = assemblyReferences.FirstOrDefault(r => Path.GetFileName(r) == name.Name + ".exe"); - if (fileName != null) - return fileName; + // this is called thousands(!) of times. + // constructing strings only once saves ~0.1ms per call for mscorlib. + string dllName = name + ".dll"; // Unfortunately the current ICompiledAssembly API only provides direct references. // It is very much possible that a postprocessor ends up investigating a type in a directly @@ -114,7 +151,7 @@ string FindFile(AssemblyNameReference name) // got passed, and if we find the file in there, we resolve to it. foreach (string parentDir in assemblyReferences.Select(Path.GetDirectoryName).Distinct()) { - string candidate = Path.Combine(parentDir, name.Name + ".dll"); + string candidate = Path.Combine(parentDir, dllName); if (File.Exists(candidate)) return candidate; } @@ -122,8 +159,9 @@ string FindFile(AssemblyNameReference name) return null; } - // open file as MemoryStream - // attempts multiple times, not sure why.. + // open file as MemoryStream. + // ILPostProcessor is multithreaded. + // retry a few times in case another thread is still accessing the file. static MemoryStream MemoryStreamFor(string fileName) { return Retry(10, TimeSpan.FromSeconds(1), () => diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta index 07289dd..1ece15f 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 0b3e94696e22440ead0b3a42411bbe14 -timeCreated: 1629693784 \ No newline at end of file +timeCreated: 1629693784 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorAssemblyResolver.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta index e06dfa7..b7dabcc 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 2a4b115486b74d27a9540f3c39ae2d46 -timeCreated: 1630152191 \ No newline at end of file +timeCreated: 1630152191 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorFromFile.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta index 9d7e0a2..7b16277 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 5f113eb695b348b5b28cd85358c8959a -timeCreated: 1628859074 \ No newline at end of file +timeCreated: 1628859074 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorHook.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs index 2c070cc..e8595fd 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs @@ -24,6 +24,7 @@ void Add(string message, DiagnosticType logType) public void LogDiagnostics(string message, DiagnosticType logType = DiagnosticType.Warning) { + // TODO IN-44868 FIX IS IN 2021.3.32f1, 2022.3.11f1, 2023.2.0b13 and 2023.3.0a8 // DiagnosticMessage can't display \n for some reason. // it just cuts it off and we don't see any stack trace. // so let's replace all line breaks so we get the stack trace. diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta index 8bb72e0..dee8c87 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: e7b56e7826664e34a415e4b70d958f2a -timeCreated: 1629533154 \ No newline at end of file +timeCreated: 1629533154 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorLogger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta index d361e21..40788da 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 6403a7e3b3ae4e009ae282f111d266e0 -timeCreated: 1629709256 \ No newline at end of file +timeCreated: 1629709256 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporter.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta index d9b6f6b..ff5c4ca 100644 --- a/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta +++ b/Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: a1003b568bad4e69b961c4c81d5afd96 -timeCreated: 1629709223 \ No newline at end of file +timeCreated: 1629709223 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/EntryPointILPostProcessor/ILPostProcessorReflectionImporterProvider.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Extensions.cs b/Assets/Mirror/Editor/Weaver/Extensions.cs index e5ddb1f..566a51a 100644 --- a/Assets/Mirror/Editor/Weaver/Extensions.cs +++ b/Assets/Mirror/Editor/Weaver/Extensions.cs @@ -12,8 +12,18 @@ public static bool Is(this TypeReference td, Type type) => ? td.GetElementType().FullName == type.FullName : td.FullName == type.FullName; + // check if 'td' is exactly of type T. + // it does not check if any base type is of , only the specific type. + // for example: + // NetworkConnection Is NetworkConnection: true + // NetworkConnectionToClient Is NetworkConnection: false public static bool Is(this TypeReference td) => Is(td, typeof(T)); + // check if 'tr' is derived from T. + // it does not check if 'tr' is exactly T. + // for example: + // NetworkConnection IsDerivedFrom: false + // NetworkConnectionToClient IsDerivedFrom: true public static bool IsDerivedFrom(this TypeReference tr) => IsDerivedFrom(tr, typeof(T)); public static bool IsDerivedFrom(this TypeReference tr, Type baseClass) @@ -79,7 +89,10 @@ public static bool IsMultidimensionalArray(this TypeReference tr) => public static bool IsNetworkIdentityField(this TypeReference tr) => tr.Is() || tr.Is() || - tr.IsDerivedFrom(); + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + tr.IsDerivedFrom() || + tr.Is(); public static bool CanBeResolved(this TypeReference parent) { @@ -229,7 +242,16 @@ public static IEnumerable FindAllPublicFields(this TypeDefiniti { foreach (FieldDefinition field in typeDefinition.Fields) { - if (field.IsStatic || field.IsPrivate) + // ignore static, private, protected fields + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3485 + // credit: James Frowen + if (field.IsStatic || field.IsPrivate || field.IsFamily) + continue; + + // also ignore internal fields + // we dont want to create different writers for this type if they are in current dll or another dll + // so we have to ignore internal in all cases + if (field.IsAssembly) continue; if (field.IsNotSerialized) @@ -266,7 +288,7 @@ public static AssemblyNameReference FindReference(this ModuleDefinition module, // Takes generic arguments from child class and applies them to parent reference, if possible // eg makes `Base` in Child : Base have `int` instead of `T` - // Originally by James-Frowen under MIT + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 public static TypeReference ApplyGenericParameters(this TypeReference parentReference, TypeReference childReference) @@ -306,7 +328,7 @@ public static TypeReference ApplyGenericParameters(this TypeReference parentRefe } // Finds the type reference for a generic parameter with the provided name in the child reference - // Originally by James-Frowen under MIT + // Originally by James-Frowen under MIT // https://github.com/MirageNet/Mirage/commit/cf91e1d54796866d2cf87f8e919bb5c681977e45 static TypeReference FindMatchingGenericArgument(TypeReference childReference, string paramName) { diff --git a/Assets/Mirror/Editor/Weaver/Extensions.cs.meta b/Assets/Mirror/Editor/Weaver/Extensions.cs.meta index 78660f9..d337f22 100644 --- a/Assets/Mirror/Editor/Weaver/Extensions.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Extensions.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Extensions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Helpers.cs.meta b/Assets/Mirror/Editor/Weaver/Helpers.cs.meta index 231f539..c8bf3d1 100644 --- a/Assets/Mirror/Editor/Weaver/Helpers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Helpers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Helpers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Logger.cs.meta b/Assets/Mirror/Editor/Weaver/Logger.cs.meta index 3f62978..9aebf80 100644 --- a/Assets/Mirror/Editor/Weaver/Logger.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Logger.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Logger.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs index 55893f7..db2d5af 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs @@ -10,10 +10,11 @@ public static class CommandProcessor // generates code like: public void CmdThrust(float thrusting, int spin) { - NetworkWriter networkWriter = new NetworkWriter(); - networkWriter.Write(thrusting); - networkWriter.WritePackedUInt32((uint)spin); - base.SendCommandInternal(cmdName, networkWriter, channel); + NetworkWriterPooled writer = NetworkWriterPool.Get(); + writer.Write(thrusting); + writer.WritePackedUInt32((uint)spin); + base.SendCommandInternal(cmdName, cmdHash, writer, channel); + NetworkWriterPool.Return(writer); } public void CallCmdThrust(float thrusting, int spin) @@ -52,6 +53,11 @@ public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Write worker.Emit(OpCodes.Ldarg_0); // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, channel); @@ -78,7 +84,7 @@ protected static void InvokeCmdCmdThrust(NetworkBehaviour obj, NetworkReader rea */ public static MethodDefinition ProcessCommandInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition method, MethodDefinition cmdCallFunc, ref bool WeavingFailed) { - string cmdName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, method); + string cmdName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, method); MethodDefinition cmd = new MethodDefinition(cmdName, MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, diff --git a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta index 20c3e15..488c9c6 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/CommandProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta index 3c81894..9cbd357 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/MethodProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta index ef3f5f4..154c73e 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs index ac00f65..47f1b94 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -26,6 +26,9 @@ class NetworkBehaviourProcessor List syncObjects = new List(); // Dictionary syncVarNetIds = new Dictionary(); + // - Every syncvar with a hook has a new field created to store the Action delegate so we don't allocate on every hook invocation + // This dictionary maps each syncvar field to the field that will store the hook method delegate instance, and the method from which the delegate instance is constructed from + Dictionary syncVarHookDelegates = new Dictionary(); readonly List commands = new List(); readonly List clientRpcs = new List(); readonly List targetRpcs = new List(); @@ -71,7 +74,7 @@ public bool Process(ref bool WeavingFailed) MarkAsProcessed(netBehaviourSubclass); // deconstruct tuple and set fields - (syncVars, syncVarNetIds) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed); + (syncVars, syncVarNetIds, syncVarHookDelegates) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed); syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed); @@ -205,20 +208,29 @@ public static bool WriteArguments(ILProcessor worker, Writers writers, Logger Lo } #region mark / check type as processed - public const string ProcessedFunctionName = "MirrorProcessed"; + public const string ProcessedFunctionName = "Weaved"; - // by adding an empty MirrorProcessed() function + // check if the type has a "Weaved" function already public static bool WasProcessed(TypeDefinition td) { return td.GetMethod(ProcessedFunctionName) != null; } + // add the Weaved() function which returns true. + // can be called at runtime and from tests to check if weaving succeeded. public void MarkAsProcessed(TypeDefinition td) { if (!WasProcessed(td)) { - MethodDefinition versionMethod = new MethodDefinition(ProcessedFunctionName, MethodAttributes.Private, weaverTypes.Import(typeof(void))); + // add a function: + // public override bool MirrorProcessed() { return true; } + // ReuseSlot means 'override'. + MethodDefinition versionMethod = new MethodDefinition( + ProcessedFunctionName, + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.ReuseSlot, + weaverTypes.Import(typeof(bool))); ILProcessor worker = versionMethod.Body.GetILProcessor(); + worker.Emit(OpCodes.Ldc_I4_1); worker.Emit(OpCodes.Ret); td.Methods.Add(versionMethod); } @@ -312,7 +324,7 @@ void InjectIntoStaticConstructor(ref bool WeavingFailed) // we need to inject several initializations into NetworkBehaviour ctor void InjectIntoInstanceConstructor(ref bool WeavingFailed) { - if (syncObjects.Count == 0) + if ((syncObjects.Count == 0) && (syncVarHookDelegates.Count == 0)) return; // find instance constructor @@ -340,6 +352,14 @@ void InjectIntoInstanceConstructor(ref bool WeavingFailed) SyncObjectInitializer.GenerateSyncObjectInitializer(ctorWorker, weaverTypes, fd); } + // initialize all delegate fields in ctor + foreach(KeyValuePair entry in syncVarHookDelegates) + { + FieldDefinition syncVarField = entry.Key; + (FieldDefinition hookDelegate, MethodDefinition hookMethod) = entry.Value; + syncVarAttributeProcessor.GenerateSyncVarHookDelegateInitializer(ctorWorker, syncVarField, hookDelegate, hookMethod); + } + // add final 'Ret' instruction to ctor ctorWorker.Append(ctorWorker.Create(OpCodes.Ret)); } @@ -397,7 +417,7 @@ void GenerateSerialization(ref bool WeavingFailed) MethodDefinition serialize = new MethodDefinition(SerializeMethodName, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - weaverTypes.Import()); + weaverTypes.Import(typeof(void))); serialize.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, weaverTypes.Import())); serialize.Parameters.Add(new ParameterDefinition("forceAll", ParameterAttributes.None, weaverTypes.Import())); @@ -405,10 +425,7 @@ void GenerateSerialization(ref bool WeavingFailed) serialize.Body.InitLocals = true; - // loc_0, this local variable is to determine if any variable was dirty - VariableDefinition dirtyLocal = new VariableDefinition(weaverTypes.Import()); - serialize.Body.Variables.Add(dirtyLocal); - + // base.SerializeSyncVars(writer, forceAll); MethodReference baseSerialize = Resolvers.TryResolveMethodInParents(netBehaviourSubclass.BaseType, assembly, SerializeMethodName); if (baseSerialize != null) { @@ -419,16 +436,20 @@ void GenerateSerialization(ref bool WeavingFailed) // forceAll worker.Emit(OpCodes.Ldarg_2); worker.Emit(OpCodes.Call, baseSerialize); - // set dirtyLocal to result of base.OnSerialize() - worker.Emit(OpCodes.Stloc_0); } - // Generates: if (forceAll); + // Generates: + // if (forceAll) + // { + // writer.WriteInt(health); + // ... + // } Instruction initialStateLabel = worker.Create(OpCodes.Nop); // forceAll - worker.Emit(OpCodes.Ldarg_2); - worker.Emit(OpCodes.Brfalse, initialStateLabel); + worker.Emit(OpCodes.Ldarg_2); // load 'forceAll' flag + worker.Emit(OpCodes.Brfalse, initialStateLabel); // start the 'if forceAll' branch + // generates write.Write(syncVar) for each SyncVar in forceAll case foreach (FieldDefinition syncVarDef in syncVars) { FieldReference syncVar = syncVarDef; @@ -442,7 +463,21 @@ void GenerateSerialization(ref bool WeavingFailed) // this worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, syncVar); - MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + MethodReference writeFunc; + // For NBs we always need to use the default NetworkBehaviour write func + // since the reader counter part uses that exact layout which is not easy to change + // without introducing more edge cases + // effectively this disallows custom NB-type writers/readers on SyncVars + // see: https://github.com/MirrorNetworking/Mirror/issues/2680 + if (syncVar.FieldType.IsDerivedFrom()) + { + writeFunc = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + } + else + { + writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + } + if (writeFunc != null) { worker.Emit(OpCodes.Call, writeFunc); @@ -455,22 +490,21 @@ void GenerateSerialization(ref bool WeavingFailed) } } - // always return true if forceAll - - // Generates: return true - worker.Emit(OpCodes.Ldc_I4_1); + // if (forceAll) then always return at the end of the 'if' case worker.Emit(OpCodes.Ret); - // Generates: end if (forceAll); + // end the 'if' case for "if (forceAll)" worker.Append(initialStateLabel); + //////////////////////////////////////////////////////////////////// + // write dirty bits before the data fields // Generates: writer.WritePackedUInt64 (base.get_syncVarDirtyBits ()); // writer worker.Emit(OpCodes.Ldarg_1); // base worker.Emit(OpCodes.Ldarg_0); - worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference); + worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference); MethodReference writeUint64Func = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); worker.Emit(OpCodes.Call, writeUint64Func); @@ -480,7 +514,6 @@ void GenerateSerialization(ref bool WeavingFailed) int dirtyBit = syncVarAccessLists.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); foreach (FieldDefinition syncVarDef in syncVars) { - FieldReference syncVar = syncVarDef; if (netBehaviourSubclass.HasGenericParameters) { @@ -491,7 +524,7 @@ void GenerateSerialization(ref bool WeavingFailed) // Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL) // base worker.Emit(OpCodes.Ldarg_0); - worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference); + worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference); // 8 bytes = long worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit); worker.Emit(OpCodes.And); @@ -504,7 +537,21 @@ void GenerateSerialization(ref bool WeavingFailed) worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, syncVar); - MethodReference writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + MethodReference writeFunc; + // For NBs we always need to use the default NetworkBehaviour write func + // since the reader counter part uses that exact layout which is not easy to change + // without introducing more edge cases + // effectively this disallows custom NB-type writers/readers on SyncVars + // see: https://github.com/MirrorNetworking/Mirror/issues/2680 + if (syncVar.FieldType.IsDerivedFrom()) + { + writeFunc = writers.GetWriteFunc(weaverTypes.Import(), ref WeavingFailed); + } + else + { + writeFunc = writers.GetWriteFunc(syncVar.FieldType, ref WeavingFailed); + } + if (writeFunc != null) { worker.Emit(OpCodes.Call, writeFunc); @@ -516,11 +563,6 @@ void GenerateSerialization(ref bool WeavingFailed) return; } - // something was dirty - worker.Emit(OpCodes.Ldc_I4_1); - // set dirtyLocal to true - worker.Emit(OpCodes.Stloc_0); - worker.Append(varLabel); dirtyBit += 1; } @@ -529,8 +571,7 @@ void GenerateSerialization(ref bool WeavingFailed) //worker.Emit(OpCodes.Ldstr, $"Injected Serialize {netBehaviourSubclass.Name}"); //worker.Emit(OpCodes.Call, WeaverTypes.logErrorReference); - // generate: return dirtyLocal - worker.Emit(OpCodes.Ldloc_0); + // generate: return worker.Emit(OpCodes.Ret); netBehaviourSubclass.Methods.Add(serialize); } @@ -552,15 +593,18 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldflda, syncVar); } - // hook? then push 'new Action(Hook)' onto stack - MethodDefinition hookMethod = syncVarAttributeProcessor.GetHookMethod(netBehaviourSubclass, syncVar, ref WeavingFailed); - if (hookMethod != null) + // If a hook exists, then we need to load the hook delegate on the stack + // The hook delegate is created once in the constructor and stored in an instance field + // We load the delegate from this instance field to avoid instantiating a new delegate instance every time (drastically reduces allocations) + if(syncVarHookDelegates.TryGetValue(syncVar, out (FieldDefinition hookDelegateField, MethodDefinition) value)) { - syncVarAttributeProcessor.GenerateNewActionFromHookMethod(syncVar, worker, hookMethod); + // A hook exists. Push this.hookDelegateField onto the stack + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, value.hookDelegateField); } - // otherwise push 'null' as hook else { + // No hook exists. Push 'null' as hook worker.Emit(OpCodes.Ldnull); } @@ -589,10 +633,9 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldflda, netIdField); worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity); } - // TODO this only uses the persistent netId for types DERIVED FROM NB. - // not if the type is just 'NetworkBehaviour'. - // this is what original implementation did too. fix it after. - else if (syncVar.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (syncVar.FieldType.IsDerivedFrom() || syncVar.FieldType.Is()) { // reader worker.Emit(OpCodes.Ldarg_1); diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta index 67c27dc..a48253f 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs index 280240c..2a919d2 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs @@ -1,4 +1,5 @@ // finds all readers and writers and register them +using System.Collections.Generic; using System.Linq; using Mono.CecilX; using Mono.CecilX.Cil; @@ -17,10 +18,53 @@ public static bool Process(AssemblyDefinition CurrentAssembly, IAssemblyResolver // otherwise Unity crashes when running tests ProcessMirrorAssemblyClasses(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed); - // find readers/writers in the assembly we are in right now. + // process dependencies first, this way weaver can process types of other assemblies properly. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2503 + // + // find NetworkReader/Writer extensions in referenced assemblies + IEnumerable assemblyReferences = FindProcessTargetAssemblies(CurrentAssembly, resolver) + .Where(assembly => assembly != null && assembly != CurrentAssembly); + + foreach (AssemblyDefinition referencedAssembly in assemblyReferences) + ProcessAssemblyClasses(CurrentAssembly, referencedAssembly, writers, readers, ref WeavingFailed); + return ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly, writers, readers, ref WeavingFailed); } + // look for assembly instead of relying on CurrentAssembly.MainModule. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3816 + static List FindProcessTargetAssemblies(AssemblyDefinition assembly, IAssemblyResolver resolver) + { + HashSet processedAssemblies = new HashSet(); + List assemblies = new List(); + ProcessAssembly(assembly); + return assemblies; + + void ProcessAssembly(AssemblyDefinition current) + { + // If the assembly has already been processed, we skip it + if (current.FullName == Weaver.MirrorAssemblyName || !processedAssemblies.Add(current.FullName)) + return; + + IEnumerable references = current.MainModule.AssemblyReferences; + + // If there is no Mirror reference, there will be no ReaderWriter or NetworkMessage, so skip + if (references.All(reference => reference.Name != Weaver.MirrorAssemblyName)) + return; + + // Add the assembly to the processed set and list + assemblies.Add(current); + + // Process the references of the current assembly + foreach (AssemblyNameReference reference in references) + { + AssemblyDefinition referencedAssembly = resolver.Resolve(reference); + if (referencedAssembly != null) + ProcessAssembly(referencedAssembly); + } + } + } + static void ProcessMirrorAssemblyClasses(AssemblyDefinition CurrentAssembly, IAssemblyResolver resolver, Logger Log, Writers writers, Readers readers, ref bool WeavingFailed) { // find Mirror.dll in assembly's references. diff --git a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta index c14d6fa..cd06284 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs index df44f20..ca2d7b9 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs @@ -8,7 +8,7 @@ public static class RpcProcessor { public static MethodDefinition ProcessRpcInvoke(WeaverTypes weaverTypes, Writers writers, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed) { - string rpcName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, md); + string rpcName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, md); MethodDefinition rpc = new MethodDefinition(rpcName, MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, weaverTypes.Import(typeof(void))); @@ -82,6 +82,11 @@ public static MethodDefinition ProcessRpcCall(WeaverTypes weaverTypes, Writers w worker.Emit(OpCodes.Ldarg_0); // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, channel); diff --git a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta index 22375ba..c47fb64 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/RpcProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs index 50df598..a0b7739 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs @@ -25,7 +25,7 @@ static bool ProcessSiteMethod(WeaverTypes weaverTypes, Logger Log, MethodDefinit { if (md.Name == ".cctor" || md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || - md.Name.StartsWith(Weaver.InvokeRpcPrefix)) + md.Name.StartsWith(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix)) return false; if (md.IsAbstract) @@ -123,7 +123,16 @@ static void InjectGuardParameters(MethodDefinition md, ILProcessor worker, Instr ParameterDefinition param = md.Parameters[index]; if (param.IsOut) { - TypeReference elementType = param.ParameterType.GetElementType(); + // this causes IL2CPP build issues with generic out parameters: + // https://github.com/MirrorNetworking/Mirror/issues/3482 + // TypeReference elementType = param.ParameterType.GetElementType(); + // + // instead we need to use ElementType not GetElementType() + // GetElementType() will get the element type of the inner elementType + // which will return wrong type for arrays and generic + // credit: JamesFrowen + ByReferenceType byRefType = (ByReferenceType)param.ParameterType; + TypeReference elementType = byRefType.ElementType; md.Body.Variables.Add(new VariableDefinition(elementType)); md.Body.InitLocals = true; diff --git a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta index 5a5451d..697a431 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/ServerClientAttributeProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta index 22f976e..97e66ae 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs index 2cec8a4..8143e86 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs @@ -16,7 +16,7 @@ public static List FindSyncObjectsFields(Writers writers, Reade foreach (FieldDefinition fd in td.Fields) { - if (fd.FieldType.IsGenericParameter) + if (fd.FieldType.IsGenericParameter || fd.ContainsGenericParameter) { // can't call .Resolve on generic ones continue; diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta index 0efe434..1408ab3 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs index 0a6f376..73a9526 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs @@ -11,7 +11,7 @@ namespace Mirror.Weaver public static class SyncVarAttributeAccessReplacer { // process the module - public static void Process(ModuleDefinition moduleDef, SyncVarAccessLists syncVarAccessLists) + public static void Process(Logger Log, ModuleDefinition moduleDef, SyncVarAccessLists syncVarAccessLists) { DateTime startTime = DateTime.Now; @@ -20,31 +20,31 @@ public static void Process(ModuleDefinition moduleDef, SyncVarAccessLists syncVa { if (td.IsClass) { - ProcessClass(syncVarAccessLists, td); + ProcessClass(Log, syncVarAccessLists, td); } } Console.WriteLine($" ProcessSitesModule {moduleDef.Name} elapsed time:{(DateTime.Now - startTime)}"); } - static void ProcessClass(SyncVarAccessLists syncVarAccessLists, TypeDefinition td) + static void ProcessClass(Logger Log, SyncVarAccessLists syncVarAccessLists, TypeDefinition td) { //Console.WriteLine($" ProcessClass {td}"); // process all methods in this class foreach (MethodDefinition md in td.Methods) { - ProcessMethod(syncVarAccessLists, md); + ProcessMethod(Log, syncVarAccessLists, md); } // processes all nested classes in this class recursively foreach (TypeDefinition nested in td.NestedTypes) { - ProcessClass(syncVarAccessLists, nested); + ProcessClass(Log, syncVarAccessLists, nested); } } - static void ProcessMethod(SyncVarAccessLists syncVarAccessLists, MethodDefinition md) + static void ProcessMethod(Logger Log, SyncVarAccessLists syncVarAccessLists, MethodDefinition md) { // process all references to replaced members with properties //Log.Warning($" ProcessSiteMethod {md}"); @@ -52,7 +52,7 @@ static void ProcessMethod(SyncVarAccessLists syncVarAccessLists, MethodDefinitio // skip static constructor, "MirrorProcessed", "InvokeUserCode_" if (md.Name == ".cctor" || md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || - md.Name.StartsWith(Weaver.InvokeRpcPrefix)) + md.Name.StartsWith(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix)) return; // skip abstract @@ -67,32 +67,64 @@ static void ProcessMethod(SyncVarAccessLists syncVarAccessLists, MethodDefinitio for (int i = 0; i < md.Body.Instructions.Count;) { Instruction instr = md.Body.Instructions[i]; - i += ProcessInstruction(syncVarAccessLists, md, instr, i); + i += ProcessInstruction(Log, syncVarAccessLists, md, instr, i); } } } - static int ProcessInstruction(SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, int iCount) + static int ProcessInstruction(Logger Log, SyncVarAccessLists syncVarAccessLists, MethodDefinition md, Instruction instr, int iCount) { // stfld (sets value of a field)? - if (instr.OpCode == OpCodes.Stfld && instr.Operand is FieldDefinition opFieldst) + if (instr.OpCode == OpCodes.Stfld) { - ProcessSetInstruction(syncVarAccessLists, md, instr, opFieldst); + // operand is a FieldDefinition in the same assembly? + if (instr.Operand is FieldDefinition opFieldst) + { + ProcessSetInstruction(syncVarAccessLists, md, instr, opFieldst); + } + // operand is a FieldReference in another assembly? + // this is not supported just yet. + // compilation error is better than silently failing SyncVar serialization at runtime. + // https://github.com/MirrorNetworking/Mirror/issues/3525 + else if (instr.Operand is FieldReference opFieldstRef) + { + // resolve it from the other assembly + FieldDefinition field = opFieldstRef.Resolve(); + + // [SyncVar]? + if (field.HasCustomAttribute()) + { + // ILPostProcessor would need to Process() the assembly's + // references before processing this one. + // we can not control the order. + // instead, Log an error to suggest adding a SetSyncVar(value) function. + // this is a very easy solution for a very rare edge case. + Log.Error($"'[SyncVar] {opFieldstRef.DeclaringType.Name}.{opFieldstRef.Name}' in '{field.Module.Name}' is modified by '{md.FullName}' in '{md.Module.Name}'. Modifying a [SyncVar] from another assembly is not supported. Please add a: 'public void Set{opFieldstRef.Name}(value) {{ this.{opFieldstRef.Name} = value; }}' method in '{opFieldstRef.DeclaringType.Name}' and call this function from '{md.FullName}' instead."); + } + } } // ldfld (load value of a field)? - if (instr.OpCode == OpCodes.Ldfld && instr.Operand is FieldDefinition opFieldld) + if (instr.OpCode == OpCodes.Ldfld) { - // this instruction gets the value of a field. cache the field reference. - ProcessGetInstruction(syncVarAccessLists, md, instr, opFieldld); + // operand is a FieldDefinition in the same assembly? + if (instr.Operand is FieldDefinition opFieldld) + { + // this instruction gets the value of a field. cache the field reference. + ProcessGetInstruction(syncVarAccessLists, md, instr, opFieldld); + } } // ldflda (load field address aka reference) - if (instr.OpCode == OpCodes.Ldflda && instr.Operand is FieldDefinition opFieldlda) + if (instr.OpCode == OpCodes.Ldflda) { - // watch out for initobj instruction - // see https://github.com/vis2k/Mirror/issues/696 - return ProcessLoadAddressInstruction(syncVarAccessLists, md, instr, opFieldlda, iCount); + // operand is a FieldDefinition in the same assembly? + if (instr.Operand is FieldDefinition opFieldlda) + { + // watch out for initobj instruction + // see https://github.com/vis2k/Mirror/issues/696 + return ProcessLoadAddressInstruction(syncVarAccessLists, md, instr, opFieldlda, iCount); + } } // we processed one instruction (instr) diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta index e8c2500..ce80ec2 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeAccessReplacer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs index a5f95cd..76e3ac7 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs @@ -46,15 +46,26 @@ public MethodDefinition GetHookMethod(TypeDefinition td, FieldDefinition syncVar return FindHookMethod(td, syncVar, hookFunctionName, ref WeavingFailed); } + // Create a field definition for a field that will store the Action delegate instance for the syncvar hook method (only instantiate delegate once) + public FieldDefinition CreateNewActionFieldDefinitionFromHookMethod(FieldDefinition syncVarField) + { + TypeReference actionRef = assembly.MainModule.ImportReference(typeof(Action<,>)); + GenericInstanceType syncVarHookActionDelegateType = actionRef.MakeGenericInstanceType(syncVarField.FieldType, syncVarField.FieldType); + string syncVarHookDelegateFieldName = $"_Mirror_SyncVarHookDelegate_{syncVarField.Name}"; + return new FieldDefinition(syncVarHookDelegateFieldName, FieldAttributes.Public, syncVarHookActionDelegateType); + } + // push hook from GetHookMethod() onto the stack as a new Action. // allows for reuse without handling static/virtual cases every time. + // perf warning: it is recommended to use this method only when generating IL to create a new Action() in order to store it into a field + // avoid using this to emit IL to instantiate a new action instance every single time one is needed for the same method public void GenerateNewActionFromHookMethod(FieldDefinition syncVar, ILProcessor worker, MethodDefinition hookMethod) { // IL_000a: ldarg.0 // IL_000b: ldftn instance void Mirror.Examples.Tanks.Tank::ExampleHook(int32, int32) // IL_0011: newobj instance void class [netstandard]System.Action`2::.ctor(object, native int) - // we support static hook sand instance hooks. + // we support static hooks and instance hooks. if (hookMethod.IsStatic) { // for static hooks, we need to push 'null' first. @@ -95,15 +106,23 @@ public void GenerateNewActionFromHookMethod(FieldDefinition syncVar, ILProcessor // call 'new Action()' constructor to convert the function to an action // we need to make an instance of the generic Action. - // - // TODO this allocates a new 'Action' for every SyncVar hook call. - // we should allocate it once and store it somewhere in the future. - // hooks are only called on the client though, so it's not too bad for now. TypeReference actionRef = assembly.MainModule.ImportReference(typeof(Action<,>)); GenericInstanceType genericInstance = actionRef.MakeGenericInstanceType(syncVar.FieldType, syncVar.FieldType); worker.Emit(OpCodes.Newobj, weaverTypes.ActionT_T.MakeHostInstanceGeneric(assembly.MainModule, genericInstance)); } + // generates CIL to set an Action instance field to a new Action(hookMethod) + // this.hookDelegate = new Action(HookMethod); + public void GenerateSyncVarHookDelegateInitializer(ILProcessor worker, FieldDefinition syncVar, FieldDefinition hookDelegate, MethodDefinition hookMethod) + { + // push this + worker.Emit(OpCodes.Ldarg_0); + // push new Action(hookMethod) + GenerateNewActionFromHookMethod(syncVar, worker, hookMethod); + // set field + worker.Emit(OpCodes.Stfld, hookDelegate); + } + MethodDefinition FindHookMethod(TypeDefinition td, FieldDefinition syncVar, string hookFunctionName, ref bool WeavingFailed) { List methods = td.GetMethods(hookFunctionName); @@ -203,7 +222,9 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference); worker.Emit(OpCodes.Ret); } - else if (fd.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { // return this.GetSyncVarNetworkBehaviour(ref field, uint netId); // this. @@ -240,7 +261,7 @@ public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string origina // } // // the setter used to be manually IL generated, but we moved it to C# :) - public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition fd, string originalName, long dirtyBit, FieldDefinition netFieldId, ref bool WeavingFailed) + public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition fd, string originalName, long dirtyBit, FieldDefinition netFieldId, Dictionary syncVarHookDelegates, ref bool WeavingFailed) { //Create the set method MethodDefinition set = new MethodDefinition($"set_Network{originalName}", MethodAttributes.Public | @@ -302,11 +323,17 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition // push the dirty bit for this SyncVar worker.Emit(OpCodes.Ldc_I8, dirtyBit); - // hook? then push 'new Action(Hook)' onto stack + // hook? then push 'this.HookDelegate' onto stack MethodDefinition hookMethod = GetHookMethod(td, fd, ref WeavingFailed); if (hookMethod != null) { - GenerateNewActionFromHookMethod(fd, worker, hookMethod); + // Create the field that will store a single instance of the hook as a delegate (field will be set in constructor) + FieldDefinition hookActionDelegateField = CreateNewActionFieldDefinitionFromHookMethod(fd); + syncVarHookDelegates[fd] = (hookActionDelegateField, hookMethod); + + // push this.hookActionDelegateField + worker.Emit(OpCodes.Ldarg_0); + worker.Emit(OpCodes.Ldfld, hookActionDelegateField); } // otherwise push 'null' as hook else @@ -331,10 +358,9 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition worker.Emit(OpCodes.Ldflda, netIdFieldReference); worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_NetworkIdentity); } - // TODO this only uses the persistent netId for types DERIVED FROM NB. - // not if the type is just 'NetworkBehaviour'. - // this is what original implementation did too. fix it after. - else if (fd.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { // NetworkIdentity setter needs one more parameter: netId field ref // (actually its a NetworkBehaviourSyncVar type) @@ -361,18 +387,20 @@ public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition return set; } - public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVarNetIds, long dirtyBit, ref bool WeavingFailed) + public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVarNetIds, Dictionary syncVarHookDelegates, long dirtyBit, ref bool WeavingFailed) { string originalName = fd.Name; // GameObject/NetworkIdentity SyncVars have a new field for netId FieldDefinition netIdField = null; // NetworkBehaviour has different field type than other NetworkIdentityFields - if (fd.FieldType.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + if (fd.FieldType.IsDerivedFrom() || fd.FieldType.Is()) { netIdField = new FieldDefinition($"___{fd.Name}NetId", FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed - weaverTypes.Import()); + weaverTypes.Import()); netIdField.DeclaringType = td; syncVarNetIds[fd] = netIdField; @@ -388,7 +416,7 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVars, Dictionary syncVarNetIds) ProcessSyncVars(TypeDefinition td, ref bool WeavingFailed) + public (List syncVars, Dictionary syncVarNetIds, Dictionary syncVarHookDelegates) ProcessSyncVars(TypeDefinition td, ref bool WeavingFailed) { List syncVars = new List(); Dictionary syncVarNetIds = new Dictionary(); + Dictionary syncVarHookDelegates = new Dictionary(); // the mapping of dirtybits to sync-vars is implicit in the order of the fields here. this order is recorded in m_replacementProperties. // start assigning syncvars at the place the base class stopped, if any @@ -442,13 +471,6 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary SyncVarLimit) @@ -475,9 +497,19 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary fields + foreach((FieldDefinition hookDelegateInstanceField, MethodDefinition) entry in syncVarHookDelegates.Values) + { + td.Fields.Add(entry.hookDelegateInstanceField); + } + + // include parent class syncvars + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3457 + int parentSyncVarCount = syncVarAccessLists.GetSyncVarStart(td.BaseType.FullName); + syncVarAccessLists.SetNumSyncVars(td.FullName, parentSyncVarCount + syncVars.Count); + + return (syncVars, syncVarNetIds, syncVarHookDelegates); } } } diff --git a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta index 982f768..3b77329 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/SyncVarAttributeProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs index 8afba94..8d56040 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs @@ -9,13 +9,21 @@ public static class TargetRpcProcessor // helper functions to check if the method has a NetworkConnection parameter public static bool HasNetworkConnectionParameter(MethodDefinition md) { - return md.Parameters.Count > 0 && - md.Parameters[0].ParameterType.Is(); + if (md.Parameters.Count > 0) + { + // we need to allow both NetworkConnection, and inheriting types. + // NetworkBehaviour.SendTargetRpc takes a NetworkConnection parameter. + // fixes https://github.com/vis2k/Mirror/issues/3290 + TypeReference type = md.Parameters[0].ParameterType; + return type.Is() || + type.IsDerivedFrom(); + } + return false; } public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed) { - string trgName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, md); + string trgName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, md); MethodDefinition rpc = new MethodDefinition(trgName, MethodAttributes.Family | MethodAttributes.Static | @@ -34,16 +42,25 @@ public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, R // NetworkConnection parameter is optional if (HasNetworkConnectionParameter(md)) { - // on server, the NetworkConnection parameter is a connection to client. - // when the rpc is invoked on the client, it still has the same - // function signature. we pass in the connection to server, - // which is cleaner than just passing null) - //NetworkClient.readyconnection + // TargetRpcs are sent from server to client. + // on server, we currently support two types: + // TargetRpc(NetworkConnection) + // TargetRpc(NetworkConnectionToClient) + // however, it's always a connection to client. + // in the future, only NetworkConnectionToClient will be supported. + // explicit typing helps catch issues at compile time. + // + // on client, InvokeTargetRpc calls the original code. + // we need to fill in the NetworkConnection parameter. + // NetworkClient.connection is always a connection to server. // - // TODO - // a) .connectionToServer = best solution. no doubt. - // b) NetworkClient.connection for now. add TODO to not use static later. - worker.Emit(OpCodes.Call, weaverTypes.NetworkClientConnectionReference); + // we used to pass NetworkClient.connection as the TargetRpc parameter. + // which caused: https://github.com/MirrorNetworking/Mirror/issues/3455 + // when the parameter is defined as a NetworkConnectionToClient. + // + // a client's connection never fits into a NetworkConnectionToClient. + // we need to always pass null here. + worker.Emit(OpCodes.Ldnull); } // process reader parameters and skip first one if first one is NetworkConnection @@ -122,6 +139,11 @@ public static MethodDefinition ProcessTargetRpcCall(WeaverTypes weaverTypes, Wri } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions worker.Emit(OpCodes.Ldstr, md.FullName); + // pass the function hash so we don't have to compute it at runtime + // otherwise each GetStableHash call requires O(N) complexity. + // noticeable for long function names: + // https://github.com/MirrorNetworking/Mirror/issues/3375 + worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode()); // writer worker.Emit(OpCodes.Ldloc_0); worker.Emit(OpCodes.Ldc_I4, targetRpcAttr.GetField("channel", 0)); diff --git a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta index 0ff7cc5..f4019ff 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Readers.cs b/Assets/Mirror/Editor/Weaver/Readers.cs index fa888c9..875f8b7 100644 --- a/Assets/Mirror/Editor/Weaver/Readers.cs +++ b/Assets/Mirror/Editor/Weaver/Readers.cs @@ -33,12 +33,24 @@ public Readers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinit internal void Register(TypeReference dataType, MethodReference methodReference) { - if (readFuncs.ContainsKey(dataType)) + // sometimes we define multiple read methods for the same type. + // for example: + // ReadInt() // alwasy writes 4 bytes: should be available to the user for binary protocols etc. + // ReadVarInt() // varint compression: we may want Weaver to always use this for minimal bandwidth + // give the user a way to define the weaver prefered one if two exists: + // "[WeaverPriority]" attribute is automatically detected and prefered. + MethodDefinition methodDefinition = methodReference.Resolve(); + bool priority = methodDefinition.HasCustomAttribute(); + // if (priority) Log.Warning($"Weaver: Registering priority Read<{dataType.FullName}> with {methodReference.FullName}.", methodReference); + + // Weaver sometimes calls Register for multiple times because we resolve assemblies multiple times. + // if the function name is the same: always use the latest one. + // if the function name differes: use the priority one. + if (readFuncs.TryGetValue(dataType, out MethodReference existingMethod) && // if it was already defined + existingMethod.FullName != methodReference.FullName && // and this one is a different name + !priority) // and it's not the priority one { - // TODO enable this again later. - // Reader has some obsolete functions that were renamed. - // Don't want weaver warnings for all of them. - //Log.Warning($"Registering a Read method for {dataType.FullName} when one already exists", methodReference); + return; // then skip } // we need to import type when we Initialize Readers so import here in case it is used anywhere else @@ -113,7 +125,16 @@ MethodReference GenerateReader(TypeReference variableReference, ref bool Weaving return GenerateReadCollection(variableReference, elementType, nameof(NetworkReaderExtensions.ReadList), ref WeavingFailed); } - else if (variableReference.IsDerivedFrom()) + else if (variableDefinition.Is(typeof(HashSet<>))) + { + GenericInstanceType genericInstance = (GenericInstanceType)variableReference; + TypeReference elementType = genericInstance.GenericArguments[0]; + + return GenerateReadCollection(variableReference, elementType, nameof(NetworkReaderExtensions.ReadHashSet), ref WeavingFailed); + } + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + else if (variableReference.IsDerivedFrom() || variableReference.Is()) { return GetNetworkBehaviourReader(variableReference); } diff --git a/Assets/Mirror/Editor/Weaver/Readers.cs.meta b/Assets/Mirror/Editor/Weaver/Readers.cs.meta index 838ff59..5831615 100644 --- a/Assets/Mirror/Editor/Weaver/Readers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Readers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Readers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Resolvers.cs b/Assets/Mirror/Editor/Weaver/Resolvers.cs index a9d551b..0af32ca 100644 --- a/Assets/Mirror/Editor/Weaver/Resolvers.cs +++ b/Assets/Mirror/Editor/Weaver/Resolvers.cs @@ -42,6 +42,38 @@ public static MethodReference ResolveMethod(TypeReference t, AssemblyDefinition return null; } + public static FieldReference ResolveField(TypeReference tr, AssemblyDefinition assembly, Logger Log, string name, ref bool WeavingFailed) + { + if (tr == null) + { + Log.Error($"Cannot resolve Field {name} without a class"); + WeavingFailed = true; + return null; + } + FieldReference field = ResolveField(tr, assembly, Log, m => m.Name == name, ref WeavingFailed); + if (field == null) + { + Log.Error($"Field not found with name {name} in type {tr.Name}", tr); + WeavingFailed = true; + } + return field; + } + + public static FieldReference ResolveField(TypeReference t, AssemblyDefinition assembly, Logger Log, System.Func predicate, ref bool WeavingFailed) + { + foreach (FieldDefinition fieldRef in t.Resolve().Fields) + { + if (predicate(fieldRef)) + { + return assembly.MainModule.ImportReference(fieldRef); + } + } + + Log.Error($"Field not found in type {t.Name}", t); + WeavingFailed = true; + return null; + } + public static MethodReference TryResolveMethodInParents(TypeReference tr, AssemblyDefinition assembly, string name) { if (tr == null) diff --git a/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta b/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta index f4f6602..49235c1 100644 --- a/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Resolvers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Resolvers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta index 9a4da44..31a2ed3 100644 --- a/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta +++ b/Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 6905230c3c4c4e158760065a93380e83 -timeCreated: 1629348618 \ No newline at end of file +timeCreated: 1629348618 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/SyncVarAccessLists.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta index 890b4dc..ec97f9d 100644 --- a/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta +++ b/Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/TypeReferenceComparer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef index 4566bb2..987382c 100644 --- a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef +++ b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef @@ -2,7 +2,7 @@ "name": "Unity.Mirror.CodeGen", "rootNamespace": "", "references": [ - "Mirror" + "GUID:30817c1a0e6d646d99c048fc403f5979" ], "includePlatforms": [ "Editor" diff --git a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta index b65a0cd..5fc0fe1 100644 --- a/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta +++ b/Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef.meta @@ -5,3 +5,10 @@ AssemblyDefinitionImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Unity.Mirror.CodeGen.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Weaver.cs b/Assets/Mirror/Editor/Weaver/Weaver.cs index 2644e68..3aef7b8 100644 --- a/Assets/Mirror/Editor/Weaver/Weaver.cs +++ b/Assets/Mirror/Editor/Weaver/Weaver.cs @@ -2,14 +2,14 @@ using System.Collections.Generic; using System.Diagnostics; using Mono.CecilX; +using Mono.CecilX.Cil; +using Mono.CecilX.Rocks; namespace Mirror.Weaver { // not static, because ILPostProcessor is multithreaded internal class Weaver { - public const string InvokeRpcPrefix = "InvokeUserCode_"; - // generated code class public const string GeneratedCodeNamespace = "Mirror"; public const string GeneratedCodeClassName = "GeneratedNetworkCode"; @@ -118,7 +118,17 @@ bool WeaveModule(ModuleDefinition moduleDefinition) Stopwatch watch = Stopwatch.StartNew(); watch.Start(); - foreach (TypeDefinition td in moduleDefinition.Types) + // ModuleDefinition.Types only finds top level types. + // GetAllTypes recursively finds all nested types as well. + // fixes nested types not being weaved, for example: + // class Parent { // ModuleDefinition.Types finds this + // class Child { // .Types.NestedTypes finds this + // class GrandChild {} // only GetAllTypes finds this too + // } + // } + // note this is not about inheritance, only about type definitions. + // see test: NetworkBehaviourTests.DeeplyNested() + foreach (TypeDefinition td in moduleDefinition.GetAllTypes()) { if (td.IsClass && td.BaseType.CanBeResolved()) { @@ -142,6 +152,16 @@ void CreateGeneratedCodeClass() weaverTypes.Import()); } + void ToggleWeaverFuse() + { + // // find Weaved() function + MethodDefinition func = weaverTypes.weaverFuseMethod.Resolve(); + // // change return 0 to return 1 + + ILProcessor worker = func.Body.GetILProcessor(); + func.Body.Instructions[0] = worker.Create(OpCodes.Ldc_I4_1); + } + // Weave takes an AssemblyDefinition to be compatible with both old and // new weavers: // * old takes a filepath, new takes a in-memory byte[] @@ -214,7 +234,7 @@ public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out b if (modified) { - SyncVarAttributeAccessReplacer.Process(moduleDefinition, syncVarAccessLists); + SyncVarAttributeAccessReplacer.Process(Log, moduleDefinition, syncVarAccessLists); // add class that holds read/write functions moduleDefinition.Types.Add(GeneratedCodeClass); @@ -228,6 +248,12 @@ public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out b //CurrentAssembly.Write(new WriterParameters{ WriteSymbols = true }); } + // if weaving succeeded, switch on the Weaver Fuse in Mirror.dll + if (CurrentAssembly.Name.Name == MirrorAssemblyName) + { + ToggleWeaverFuse(); + } + return true; } catch (Exception e) diff --git a/Assets/Mirror/Editor/Weaver/Weaver.cs.meta b/Assets/Mirror/Editor/Weaver/Weaver.cs.meta index 0ea2dfe..2eaf569 100644 --- a/Assets/Mirror/Editor/Weaver/Weaver.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Weaver.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Weaver.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta index 68643b2..4ef61d6 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta +++ b/Assets/Mirror/Editor/Weaver/WeaverExceptions.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/WeaverExceptions.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs index 173af58..aa0d42d 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs @@ -10,7 +10,7 @@ public class WeaverTypes { public MethodReference ScriptableObjectCreateInstanceMethod; - public MethodReference NetworkBehaviourDirtyBitsReference; + public FieldReference NetworkBehaviourDirtyBitsReference; public MethodReference GetWriterReference; public MethodReference ReturnWriterReference; @@ -53,6 +53,9 @@ public class WeaverTypes public MethodReference readNetworkBehaviourGeneric; + public TypeReference weaverFuseType; + public MethodReference weaverFuseMethod; + // attributes public TypeDefinition initializeOnLoadMethodAttribute; public TypeDefinition runtimeInitializeOnLoadMethodAttribute; @@ -75,30 +78,19 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail TypeReference ActionType = Import(typeof(Action<,>)); ActionT_T = Resolvers.ResolveMethod(ActionType, assembly, Log, ".ctor", ref WeavingFailed); + weaverFuseType = Import(typeof(WeaverFuse)); + weaverFuseMethod = Resolvers.ResolveMethod(weaverFuseType, assembly, Log, "Weaved", ref WeavingFailed); + TypeReference NetworkServerType = Import(typeof(NetworkServer)); NetworkServerGetActive = Resolvers.ResolveMethod(NetworkServerType, assembly, Log, "get_active", ref WeavingFailed); + TypeReference NetworkClientType = Import(typeof(NetworkClient)); NetworkClientGetActive = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_active", ref WeavingFailed); - - TypeReference RemoteCallDelegateType = Import(); - RemoteCallDelegateConstructor = Resolvers.ResolveMethod(RemoteCallDelegateType, assembly, Log, ".ctor", ref WeavingFailed); + NetworkClientConnectionReference = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_connection", ref WeavingFailed); TypeReference NetworkBehaviourType = Import(); - TypeReference RemoteProcedureCallsType = Import(typeof(RemoteCalls.RemoteProcedureCalls)); - - TypeReference ScriptableObjectType = Import(); - - ScriptableObjectCreateInstanceMethod = Resolvers.ResolveMethod( - ScriptableObjectType, assembly, Log, - md => md.Name == "CreateInstance" && md.HasGenericParameters, - ref WeavingFailed); - NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, assembly, "syncVarDirtyBits"); - TypeReference NetworkWriterPoolType = Import(typeof(NetworkWriterPool)); - GetWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Get", ref WeavingFailed); - ReturnWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Return", ref WeavingFailed); - - NetworkClientConnectionReference = Resolvers.ResolveMethod(NetworkClientType, assembly, Log, "get_connection", ref WeavingFailed); + NetworkBehaviourDirtyBitsReference = Resolvers.ResolveField(NetworkBehaviourType, assembly, Log, "syncVarDirtyBits", ref WeavingFailed); generatedSyncVarSetter = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter", ref WeavingFailed); generatedSyncVarSetter_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_GameObject", ref WeavingFailed); @@ -114,9 +106,25 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkIdentity", ref WeavingFailed); getSyncVarNetworkBehaviourReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkBehaviour", ref WeavingFailed); + sendCommandInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendCommandInternal", ref WeavingFailed); + sendRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendRPCInternal", ref WeavingFailed); + sendTargetRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendTargetRPCInternal", ref WeavingFailed); + + InitSyncObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "InitSyncObject", ref WeavingFailed); + + TypeReference RemoteProcedureCallsType = Import(typeof(RemoteCalls.RemoteProcedureCalls)); registerCommandReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterCommand", ref WeavingFailed); registerRpcReference = Resolvers.ResolveMethod(RemoteProcedureCallsType, assembly, Log, "RegisterRpc", ref WeavingFailed); + TypeReference RemoteCallDelegateType = Import(); + RemoteCallDelegateConstructor = Resolvers.ResolveMethod(RemoteCallDelegateType, assembly, Log, ".ctor", ref WeavingFailed); + + TypeReference ScriptableObjectType = Import(); + ScriptableObjectCreateInstanceMethod = Resolvers.ResolveMethod( + ScriptableObjectType, assembly, Log, + md => md.Name == "CreateInstance" && md.HasGenericParameters, + ref WeavingFailed); + TypeReference unityDebug = Import(typeof(UnityEngine.Debug)); // these have multiple methods with same name, so need to check parameters too logErrorReference = Resolvers.ResolveMethod(unityDebug, assembly, Log, md => @@ -133,11 +141,10 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail TypeReference typeType = Import(typeof(Type)); getTypeFromHandleReference = Resolvers.ResolveMethod(typeType, assembly, Log, "GetTypeFromHandle", ref WeavingFailed); - sendCommandInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendCommandInternal", ref WeavingFailed); - sendRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendRPCInternal", ref WeavingFailed); - sendTargetRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendTargetRPCInternal", ref WeavingFailed); - InitSyncObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "InitSyncObject", ref WeavingFailed); + TypeReference NetworkWriterPoolType = Import(typeof(NetworkWriterPool)); + GetWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Get", ref WeavingFailed); + ReturnWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, assembly, Log, "Return", ref WeavingFailed); TypeReference readerExtensions = Import(typeof(NetworkReaderExtensions)); readNetworkBehaviourGeneric = Resolvers.ResolveMethod(readerExtensions, assembly, Log, (md => diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta index d71c33f..ea2cec8 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs.meta @@ -1,3 +1,10 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 2585961bf7fe4c10a9143f4087efdf6f -timeCreated: 1596486854 \ No newline at end of file +timeCreated: 1596486854 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/WeaverTypes.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Weaver/Writers.cs b/Assets/Mirror/Editor/Weaver/Writers.cs index 51d514e..31a4ac7 100644 --- a/Assets/Mirror/Editor/Weaver/Writers.cs +++ b/Assets/Mirror/Editor/Weaver/Writers.cs @@ -33,12 +33,24 @@ public Writers(AssemblyDefinition assembly, WeaverTypes weaverTypes, TypeDefinit public void Register(TypeReference dataType, MethodReference methodReference) { - if (writeFuncs.ContainsKey(dataType)) + // sometimes we define multiple write methods for the same type. + // for example: + // WriteInt() // alwasy writes 4 bytes: should be available to the user for binary protocols etc. + // WriteVarInt() // varint compression: we may want Weaver to always use this for minimal bandwidth + // give the user a way to define the weaver prefered one if two exists: + // "[WeaverPriority]" attribute is automatically detected and prefered. + MethodDefinition methodDefinition = methodReference.Resolve(); + bool priority = methodDefinition.HasCustomAttribute(); + // if (priority) Log.Warning($"Weaver: Registering priority Write<{dataType.FullName}> with {methodReference.FullName}.", methodReference); + + // Weaver sometimes calls Register for multiple times because we resolve assemblies multiple times. + // if the function name is the same: always use the latest one. + // if the function name differes: use the priority one. + if (writeFuncs.TryGetValue(dataType, out MethodReference existingMethod) && // if it was already defined + existingMethod.FullName != methodReference.FullName && // and this one is a different name + !priority) // and it's not the priority one { - // TODO enable this again later. - // Writer has some obsolete functions that were renamed. - // Don't want weaver warnings for all of them. - //Log.Warning($"Registering a Write method for {dataType.FullName} when one already exists", methodReference); + return; // then skip } // we need to import type when we Initialize Writers so import here in case it is used anywhere else @@ -114,8 +126,17 @@ MethodReference GenerateWriter(TypeReference variableReference, ref bool Weaving return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteList), ref WeavingFailed); } + if (variableReference.Is(typeof(HashSet<>))) + { + GenericInstanceType genericInstance = (GenericInstanceType)variableReference; + TypeReference elementType = genericInstance.GenericArguments[0]; + + return GenerateCollectionWriter(variableReference, elementType, nameof(NetworkWriterExtensions.WriteHashSet), ref WeavingFailed); + } - if (variableReference.IsDerivedFrom()) + // handle both NetworkBehaviour and inheritors. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 + if (variableReference.IsDerivedFrom() || variableReference.Is()) { return GetNetworkBehaviourWriter(variableReference); } diff --git a/Assets/Mirror/Editor/Weaver/Writers.cs.meta b/Assets/Mirror/Editor/Weaver/Writers.cs.meta index 3769f7f..1bec2a8 100644 --- a/Assets/Mirror/Editor/Weaver/Writers.cs.meta +++ b/Assets/Mirror/Editor/Weaver/Writers.cs.meta @@ -9,3 +9,10 @@ MonoImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Weaver/Writers.cs + uploadId: 736421 diff --git a/Assets/Mirror/Editor/Welcome.cs b/Assets/Mirror/Editor/Welcome.cs new file mode 100644 index 0000000..49bd379 --- /dev/null +++ b/Assets/Mirror/Editor/Welcome.cs @@ -0,0 +1,23 @@ +// Shows either a welcome message, only once per session. +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + static class Welcome + { + [InitializeOnLoadMethod] + static void OnInitializeOnLoad() + { + // InitializeOnLoad is called on start and after each rebuild, + // but we only want to show this once per editor session. + if (!SessionState.GetBool("MIRROR_WELCOME", false)) + { + SessionState.SetBool("MIRROR_WELCOME", true); + Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, "Mirror | mirror-networking.com | discord.gg/N9QVxbM"); + } + } + } +} +#endif diff --git a/Assets/Mirror/Editor/Welcome.cs.meta b/Assets/Mirror/Editor/Welcome.cs.meta new file mode 100644 index 0000000..82dd027 --- /dev/null +++ b/Assets/Mirror/Editor/Welcome.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 180619c3887314f56bf396769c0a23ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Editor/Welcome.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE b/Assets/Mirror/LICENSE similarity index 90% rename from Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE rename to Assets/Mirror/LICENSE index 0330370..63a97b2 100644 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE +++ b/Assets/Mirror/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2021 Mirror Networking (vis2k, FakeByte) +Copyright (c) 2015, Unity Technologies +Copyright (c) 2019, Mischa, Paul, Chris, Robin, Stephen and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Assets/Mirror/LICENSE.meta b/Assets/Mirror/LICENSE.meta new file mode 100644 index 0000000..a3285f2 --- /dev/null +++ b/Assets/Mirror/LICENSE.meta @@ -0,0 +1,9 @@ +guid: E3D46192E4CB96FDDE36997FE62D5973 +fileFormatVersion: 2 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/LICENSE + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta b/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta index 9477cb6..a02e246 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/License.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/License.txt + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta index d5555bf..91e5737 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta @@ -90,3 +90,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta index 3ab420f..322faf6 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta @@ -90,3 +90,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta index aff0237..09ecb6b 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta @@ -90,3 +90,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll + uploadId: 736421 diff --git a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta index f87dc69..2b667c6 100644 --- a/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta +++ b/Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta @@ -92,3 +92,10 @@ PluginImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll + uploadId: 736421 diff --git a/Assets/Mirror/Presets.meta b/Assets/Mirror/Presets.meta new file mode 100644 index 0000000..b8cf771 --- /dev/null +++ b/Assets/Mirror/Presets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 80d00a982048947fa93f2be2ea402b4e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Presets/Network Transform (Reliable).meta b/Assets/Mirror/Presets/Network Transform (Reliable).meta new file mode 100644 index 0000000..c710730 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable).meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 599f312ab09244b8e9b5c475e2634d26 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset new file mode 100644 index 0000000..23f5f11 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset.meta new file mode 100644 index 0000000..841f1b9 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: d2265a8e4f13f4cabada48c26038cd61 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset new file mode 100644 index 0000000..c7dcc7d --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Casual + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset.meta new file mode 100644 index 0000000..233119d --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: bdd9c4732b76c4a6e955711cbf5123de +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Casual.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset new file mode 100644 index 0000000..029c618 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Responsive + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset.meta new file mode 100644 index 0000000..32b858d --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: b6ef89bc6332145cabf562c53295145a +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ClientAuth-Responsive.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset new file mode 100644 index 0000000..a267f8e --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ServerAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: 8ff3ba0becae47b8b9381191598957c8, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChangeCorrectionMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionPrecision + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scalePrecision + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset.meta new file mode 100644 index 0000000..23f4f73 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 13adbacdaa535458cb4f558cccd3de6f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Reliable)/ServerAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable).meta b/Assets/Mirror/Presets/Network Transform (Unreliable).meta new file mode 100644 index 0000000..561aec5 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable).meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb1ce23d9d0fa494d919aefb1f1fc1d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset new file mode 100644 index 0000000..4df0198 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 3 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset.meta new file mode 100644 index 0000000..6650728 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 0a20d07504f3744079937213136ef7a3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset new file mode 100644 index 0000000..6a83c1c --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Casual + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 4 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset.meta new file mode 100644 index 0000000..ad1d531 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 11dbda2b964884128b2bd209f9e5cead +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Casual.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset new file mode 100644 index 0000000..88ee71c --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ClientAuth-Responsive + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 2 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset.meta new file mode 100644 index 0000000..e3dc9ae --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 393416c7c2b2f4366be81f82886c280c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ClientAuth-Responsive.preset + uploadId: 736421 diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset new file mode 100644 index 0000000..f77a015 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset @@ -0,0 +1,123 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!181963792 &2655988077585873504 +Preset: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: ServerAuth-Balanced + m_TargetType: + m_NativeTypeID: 114 + m_ManagedTypePPtr: {fileID: 11500000, guid: a553cb17010b2403e8523b558bffbc14, + type: 3} + m_ManagedTypeFallback: + m_Properties: + - target: {fileID: 0} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorHideFlags + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: m_EditorClassIdentifier + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncDirection + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncMode + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncInterval + value: 0.05 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: target + value: + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncPosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: syncScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: onlySyncOnChange + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: compressRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolatePosition + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateRotation + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: interpolateScale + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: coordinateSpace + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: timelineOffset + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showGizmos + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: showOverlay + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.r + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.g + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.b + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: overlayColor.a + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: bufferResetMultiplier + value: 3 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: positionSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: rotationSensitivity + value: 0.01 + objectReference: {fileID: 0} + - target: {fileID: 0} + propertyPath: scaleSensitivity + value: 0.01 + objectReference: {fileID: 0} diff --git a/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset.meta b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset.meta new file mode 100644 index 0000000..17ca248 --- /dev/null +++ b/Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset.meta @@ -0,0 +1,15 @@ +fileFormatVersion: 2 +guid: 66a33afb7bec84e30b79a8673719ee3d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2655988077585873504 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Presets/Network Transform (Unreliable)/ServerAuth-Balanced.preset + uploadId: 736421 diff --git a/Assets/Mirror/Readme.txt.meta b/Assets/Mirror/Readme.txt.meta index d52ccce..3be4e9e 100644 --- a/Assets/Mirror/Readme.txt.meta +++ b/Assets/Mirror/Readme.txt.meta @@ -5,3 +5,10 @@ TextScriptImporter: userData: assetBundleName: assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Readme.txt + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Empty.meta b/Assets/Mirror/Runtime/Empty.meta deleted file mode 100644 index e702402..0000000 --- a/Assets/Mirror/Runtime/Empty.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: a99666a026b14cf6ba1a2b65946b1b27 -timeCreated: 1615288671 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs b/Assets/Mirror/Runtime/Empty/ClientScene.cs deleted file mode 100644 index 0d1b96e..0000000 --- a/Assets/Mirror/Runtime/Empty/ClientScene.cs +++ /dev/null @@ -1 +0,0 @@ -// moved into NetworkClient on 2021-03-07 diff --git a/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta b/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta deleted file mode 100644 index 82b617e..0000000 --- a/Assets/Mirror/Runtime/Empty/ClientScene.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 96fc7967f813e4960b9119d7c2118494 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud.meta b/Assets/Mirror/Runtime/Empty/Cloud.meta deleted file mode 100644 index e2c44de..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 73a9bb2dacafa8141bce8feef34e33a7 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs b/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta deleted file mode 100644 index 9279c0c..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiConnector.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 8bdb99a29e179d14cb0acc43f175d9ad -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs b/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta deleted file mode 100644 index 98a4c11..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ApiUpdater.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1f6e5d5acb5879f45a2235ae0f44dc92 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs b/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta deleted file mode 100644 index a6fc272..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Ball.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b4e9cc0829b13e54594a80883836bda7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs b/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta deleted file mode 100644 index b914a33..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BallManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9cc796972dc396a42ba3686bd952e329 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta deleted file mode 100644 index f66b84e..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/BaseApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 70f563b7a7210ae43bbcde5cb7721a94 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs b/Assets/Mirror/Runtime/Empty/Cloud/Events.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta deleted file mode 100644 index 150d85b..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Events.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c7c472a3ea1bc4348bd5a0b05bf7cc3b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs b/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta deleted file mode 100644 index 6bf6291..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Extensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 97501e783fc67a4459b15d10e6c63563 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs b/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta deleted file mode 100644 index f1149a9..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ICoroutineRunner.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 43472c60a7c72e54eafe559290dd0fc6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs b/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta deleted file mode 100644 index 966c503..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IRequestCreator.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b80b95532a9d6e8418aa676a261e4f69 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs b/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta deleted file mode 100644 index 7cb2a59..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/IUnityEqualCheck.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 05185b973ba389a4588fc8a99c75a4f6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs b/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta deleted file mode 100644 index 4b7219b..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/InstantiateNetworkManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: dbabb497385c20346a3c8bda4ae69508 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs b/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta deleted file mode 100644 index 2c04009..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/JsonStructs.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 0688c0fdae5376e4ea74d5c3904eed17 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta deleted file mode 100644 index 519876d..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6f0311899162c5b49a3c11fa9bd9c133 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta deleted file mode 100644 index a9d32ea..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerBaseApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b6838f9df45594d48873518cbb75b329 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta deleted file mode 100644 index 306bf7c..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerClientApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d49649fb32cb96b46b10f013b38a4b50 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta deleted file mode 100644 index 7e206f1..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerJson.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a963606335eae0f47abe7ecb5fd028ea -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs b/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta deleted file mode 100644 index 82e23fd..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ListServerServerApi.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 675f0d0fd4e82b04290c4d30c8d78ede -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs b/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta deleted file mode 100644 index 5984ce3..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Logger.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 457ba2df6cb6e1542996c17c715ee81b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta deleted file mode 100644 index 86775df..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 95bebb8e810e2954485291a26324f7d5 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta deleted file mode 100644 index 5c4294f..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/NetworkManagerListServerPong.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 068feff770f710141afa4a90063a5e6c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs b/Assets/Mirror/Runtime/Empty/Cloud/Player.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta deleted file mode 100644 index 1c85828..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/Player.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 2b6cfd54b79bb464dbc6ae7f331ed45f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs b/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta deleted file mode 100644 index 4a22565..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuickListServerDebug.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 07d1ea5260bc06e4d831c4b61d494bff -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs b/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta deleted file mode 100644 index 67341ea..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/QuitButtonHUD.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 76dab753e7255254687cd57985d8d675 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs b/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta deleted file mode 100644 index eb139af..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/RequestCreator.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: cfaa626443cc7c94eae138a2e3a04d7c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs b/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta deleted file mode 100644 index 74c6a0f..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: bfc354d4a7f63ca45a653bf5d479afa0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta deleted file mode 100644 index f7fe4f2..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUI.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ed11184fcffcdc04c9850d82c8014926 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs deleted file mode 100644 index 2f11787..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 diff --git a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta b/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta deleted file mode 100644 index d8857e8..0000000 --- a/Assets/Mirror/Runtime/Empty/Cloud/ServerListUIItem.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c67eda1b451338a428df87fda1e3a7c9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs b/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta b/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta deleted file mode 100644 index 8742197..0000000 --- a/Assets/Mirror/Runtime/Empty/DotNetCompatibility.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b307f850ccbbe450295acf24d70e5c28 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs b/Assets/Mirror/Runtime/Empty/FallbackTransport.cs deleted file mode 100644 index 57f3344..0000000 --- a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-05-13 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta b/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta deleted file mode 100644 index 509a58f..0000000 --- a/Assets/Mirror/Runtime/Empty/FallbackTransport.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 330c9aab13d2d42069c6ebbe582b73ca -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs b/Assets/Mirror/Runtime/Empty/LogFactory.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFactory.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta b/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta deleted file mode 100644 index 0715501..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFactory.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 353c7c9e14e82f349b1679112050b196 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs b/Assets/Mirror/Runtime/Empty/LogFilter.cs deleted file mode 100644 index 391c5bd..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFilter.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-03-08 diff --git a/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta b/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta deleted file mode 100644 index aab4fa0..0000000 --- a/Assets/Mirror/Runtime/Empty/LogFilter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f6928b080072948f7b2909b4025fcc79 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging.meta b/Assets/Mirror/Runtime/Empty/Logging.meta deleted file mode 100644 index 867da74..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 63d647500ca1bfa4a845bc1f4cff9dcc -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs b/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta deleted file mode 100644 index 329c6eb..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/ConsoleColorLogHandler.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 2a9618569c20a504aa86feb5913c70e9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs b/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta deleted file mode 100644 index 81b33e9..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/EditorLogSettingsLoader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a39aa1e48aa54eb4e964f0191c1dcdce -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs b/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta deleted file mode 100644 index acf3b63..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogFactory.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d06522432d5a44e1587967a4731cd279 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs b/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs deleted file mode 100644 index 264a1cd..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs +++ /dev/null @@ -1,2 +0,0 @@ -// removed 2021-02-16 - diff --git a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta deleted file mode 100644 index 90c4e4d..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/LogSettings.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 633889a39717fde4fa28dd6b948dfac7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs b/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta deleted file mode 100644 index 221a61b..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkHeadlessLogger.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c7627623f2b9fad4484082517cd73e67 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs b/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta b/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta deleted file mode 100644 index 2f7ecdf..0000000 --- a/Assets/Mirror/Runtime/Empty/Logging/NetworkLogSettings.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ac6e8eccf4b6f4dc7b24c276ef47fde8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta deleted file mode 100644 index 7c7d6cf..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkMatchChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1020a74962faada4b807ac5dc053a4cf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs deleted file mode 100644 index 712833c..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta deleted file mode 100644 index fee7725..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkOwnerChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 25fd0c51bbe07c140bc30978b91e9182 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta deleted file mode 100644 index c5aa112..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkProximityChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1731d8de2d0c84333b08ebe1e79f4118 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs b/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta deleted file mode 100644 index b451655..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkSceneChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b7fdb599e1359924bad6255660370252 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs b/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs deleted file mode 100644 index 3797620..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2022-01-06 diff --git a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta b/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta deleted file mode 100644 index f71b7be..0000000 --- a/Assets/Mirror/Runtime/Empty/NetworkVisibility.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c08f1a030234d49d391d7223a8592f15 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs b/Assets/Mirror/Runtime/Empty/StringHash.cs deleted file mode 100644 index 39b95f7..0000000 --- a/Assets/Mirror/Runtime/Empty/StringHash.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-16 diff --git a/Assets/Mirror/Runtime/Empty/StringHash.cs.meta b/Assets/Mirror/Runtime/Empty/StringHash.cs.meta deleted file mode 100644 index 6198581..0000000 --- a/Assets/Mirror/Runtime/Empty/StringHash.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 733f020f9b76d453da841089579fd7a7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs b/Assets/Mirror/Runtime/ExponentialMovingAverage.cs deleted file mode 100644 index 64b91e1..0000000 --- a/Assets/Mirror/Runtime/ExponentialMovingAverage.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Mirror -{ - // implementation of N-day EMA - // it calculates an exponential moving average roughly equivalent to the last n observations - // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average - public class ExponentialMovingAverage - { - readonly float alpha; - bool initialized; - - public double Value; - public double Var; - - public ExponentialMovingAverage(int n) - { - // standard N-day EMA alpha calculation - alpha = 2.0f / (n + 1); - } - - public void Add(double newValue) - { - // simple algorithm for EMA described here: - // https://en.wikipedia.org/wiki/Moving_average#Exponentially_weighted_moving_variance_and_standard_deviation - if (initialized) - { - double delta = newValue - Value; - Value += alpha * delta; - Var = (1 - alpha) * (Var + alpha * delta * delta); - } - else - { - Value = newValue; - initialized = true; - } - } - } -} diff --git a/Assets/Mirror/Runtime/Extensions.cs b/Assets/Mirror/Runtime/Extensions.cs deleted file mode 100644 index 3d285e9..0000000 --- a/Assets/Mirror/Runtime/Extensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace Mirror -{ - public static class Extensions - { - // string.GetHashCode is not guaranteed to be the same on all machines, but - // we need one that is the same on all machines. simple and stupid: - public static int GetStableHashCode(this string text) - { - unchecked - { - int hash = 23; - foreach (char c in text) - hash = hash * 31 + c; - return hash; - } - } - - // previously in DotnetCompatibility.cs - // leftover from the UNET days. supposedly for windows store? - internal static string GetMethodName(this Delegate func) - { -#if NETFX_CORE - return func.GetMethodInfo().Name; -#else - return func.Method.Name; -#endif - } - - // helper function to copy to List - // C# only provides CopyTo(T[]) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CopyTo(this IEnumerable source, List destination) - { - // foreach allocates. use AddRange. - destination.AddRange(source); - } - -#if !UNITY_2021_OR_NEWER - // Unity 2019 / 2020 don't have Queue.TryDeque which we need for batching. - public static bool TryDequeue(this Queue source, out T element) - { - if (source.Count > 0) - { - element = source.Dequeue(); - return true; - } - - element = default; - return false; - } -#endif - } -} diff --git a/Assets/Mirror/Runtime/InterestManagement.cs b/Assets/Mirror/Runtime/InterestManagement.cs deleted file mode 100644 index ab149c3..0000000 --- a/Assets/Mirror/Runtime/InterestManagement.cs +++ /dev/null @@ -1,99 +0,0 @@ -// interest management component for custom solutions like -// distance based, spatial hashing, raycast based, etc. -using System.Collections.Generic; -using UnityEngine; - -namespace Mirror -{ - [DisallowMultipleComponent] - [HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")] - public abstract class InterestManagement : MonoBehaviour - { - // Awake configures InterestManagement in NetworkServer/Client - // Do NOT check for active server or client here. - // Awake must always set the static aoi references. - void Awake() - { - if (NetworkServer.aoi == null) - { - NetworkServer.aoi = this; - } - else Debug.LogError($"Only one InterestManagement component allowed. {NetworkServer.aoi.GetType()} has been set up already."); - - if (NetworkClient.aoi == null) - { - NetworkClient.aoi = this; - } - else Debug.LogError($"Only one InterestManagement component allowed. {NetworkClient.aoi.GetType()} has been set up already."); - } - - [ServerCallback] - public virtual void Reset() {} - - // Callback used by the visibility system to determine if an observer - // (player) can see the NetworkIdentity. If this function returns true, - // the network connection will be added as an observer. - // conn: Network connection of a player. - // returns True if the player can see this object. - public abstract bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver); - - // rebuild observers for the given NetworkIdentity. - // Server will automatically spawn/despawn added/removed ones. - // newObservers: cached hashset to put the result into - // initialize: true if being rebuilt for the first time - // - // IMPORTANT: - // => global rebuild would be more simple, BUT - // => local rebuild is way faster for spawn/despawn because we can - // simply rebuild a select NetworkIdentity only - // => having both .observers and .observing is necessary for local - // rebuilds - // - // in other words, this is the perfect solution even though it's not - // completely simple (due to .observers & .observing). - // - // Mirror maintains .observing automatically in the background. best of - // both worlds without any worrying now! - public abstract void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers); - - // helper function to trigger a full rebuild. - // most implementations should call this in a certain interval. - // some might call this all the time, or only on team changes or - // scene changes and so on. - // - // IMPORTANT: check if NetworkServer.active when using Update()! - [ServerCallback] - protected void RebuildAll() - { - foreach (NetworkIdentity identity in NetworkServer.spawned.Values) - { - NetworkServer.RebuildObservers(identity, false); - } - } - - // Callback used by the visibility system for objects on a host. - // Objects on a host (with a local client) cannot be disabled or - // destroyed when they are not visible to the local client. So this - // function is called to allow custom code to hide these objects. A - // typical implementation will disable renderer components on the - // object. This is only called on local clients on a host. - // => need the function in here and virtual so people can overwrite! - // => not everyone wants to hide renderers! - [ServerCallback] - public virtual void SetHostVisibility(NetworkIdentity identity, bool visible) - { - foreach (Renderer rend in identity.GetComponentsInChildren()) - rend.enabled = visible; - } - - /// Called on the server when a new networked object is spawned. - // (useful for 'only rebuild if changed' interest management algorithms) - [ServerCallback] - public virtual void OnSpawned(NetworkIdentity identity) {} - - /// Called on the server when a networked object is destroyed. - // (useful for 'only rebuild if changed' interest management algorithms) - [ServerCallback] - public virtual void OnDestroyed(NetworkIdentity identity) {} - } -} diff --git a/Assets/Mirror/Runtime/LocalConnectionToClient.cs b/Assets/Mirror/Runtime/LocalConnectionToClient.cs deleted file mode 100644 index 67c9649..0000000 --- a/Assets/Mirror/Runtime/LocalConnectionToClient.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace Mirror -{ - // a server's connection TO a LocalClient. - // sending messages on this connection causes the client's handler function to be invoked directly - public class LocalConnectionToClient : NetworkConnectionToClient - { - internal LocalConnectionToServer connectionToServer; - - public LocalConnectionToClient() : base(LocalConnectionId) {} - - public override string address => "localhost"; - - // Send stage two: serialized NetworkMessage as ArraySegment - internal override void Send(ArraySegment segment, int channelId = Channels.Reliable) - { - // get a writer to copy the message into since the segment is only - // valid until returning. - // => pooled writer will be returned to pool when dequeuing. - // => WriteBytes instead of WriteArraySegment because the latter - // includes a 4 bytes header. we just want to write raw. - //Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}"); - NetworkWriterPooled writer = NetworkWriterPool.Get(); - writer.WriteBytes(segment.Array, segment.Offset, segment.Count); - connectionToServer.queue.Enqueue(writer); - } - - // true because local connections never timeout - internal override bool IsAlive(float timeout) => true; - - internal void DisconnectInternal() - { - // set not ready and handle clientscene disconnect in any case - // (might be client or host mode here) - isReady = false; - RemoveFromObservingsObservers(); - } - - /// Disconnects this connection. - public override void Disconnect() - { - DisconnectInternal(); - connectionToServer.DisconnectInternal(); - } - } -} diff --git a/Assets/Mirror/Runtime/MessagePacking.cs b/Assets/Mirror/Runtime/MessagePacking.cs deleted file mode 100644 index af7fca6..0000000 --- a/Assets/Mirror/Runtime/MessagePacking.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Mirror -{ - // message packing all in one place, instead of constructing headers in all - // kinds of different places - // - // MsgType (2 bytes) - // Content (ContentSize bytes) - public static class MessagePacking - { - // message header size - public const int HeaderSize = sizeof(ushort); - - // max message content size (without header) calculation for convenience - // -> Transport.GetMaxPacketSize is the raw maximum - // -> Every message gets serialized into <> - // -> Every serialized message get put into a batch with a header - public static int MaxContentSize - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Transport.activeTransport.GetMaxPacketSize() - - HeaderSize - - Batcher.HeaderSize; - } - - // paul: 16 bits is enough to avoid collisions - // - keeps the message size small - // - in case of collisions, Mirror will display an error - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ushort GetId() where T : struct, NetworkMessage => - (ushort)(typeof(T).FullName.GetStableHashCode() & 0xFFFF); - - // pack message before sending - // -> NetworkWriter passed as arg so that we can use .ToArraySegment - // and do an allocation free send before recycling it. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Pack(T message, NetworkWriter writer) - where T : struct, NetworkMessage - { - ushort msgType = GetId(); - writer.WriteUShort(msgType); - - // serialize message into writer - writer.Write(message); - } - - // unpack message after receiving - // -> pass NetworkReader so it's less strange if we create it in here - // and pass it upwards. - // -> NetworkReader will point at content afterwards! - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Unpack(NetworkReader messageReader, out ushort msgType) - { - // read message type - try - { - msgType = messageReader.ReadUShort(); - return true; - } - catch (System.IO.EndOfStreamException) - { - msgType = 0; - return false; - } - } - - // version for handlers with channelId - // inline! only exists for 20-30 messages and they call it all the time. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication) - where T : struct, NetworkMessage - where C : NetworkConnection - => (conn, reader, channelId) => - { - // protect against DOS attacks if attackers try to send invalid - // data packets to crash the server/client. there are a thousand - // ways to cause an exception in data handling: - // - invalid headers - // - invalid message ids - // - invalid data causing exceptions - // - negative ReadBytesAndSize prefixes - // - invalid utf8 strings - // - etc. - // - // let's catch them all and then disconnect that connection to avoid - // further attacks. - T message = default; - // record start position for NetworkDiagnostics because reader might contain multiple messages if using batching - int startPos = reader.Position; - try - { - if (requireAuthentication && !conn.isAuthenticated) - { - // message requires authentication, but the connection was not authenticated - Debug.LogWarning($"Closing connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet"); - conn.Disconnect(); - return; - } - - //Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}"); - - // if it is a value type, just use default(T) - // otherwise allocate a new instance - message = reader.Read(); - } - catch (Exception exception) - { - Debug.LogError($"Closed connection: {conn}. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}"); - conn.Disconnect(); - return; - } - finally - { - int endPos = reader.Position; - // TODO: Figure out the correct channel - NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos); - } - - // user handler exception should not stop the whole server - try - { - // user implemented handler - handler((C)conn, message, channelId); - } - catch (Exception e) - { - Debug.LogError($"Disconnecting connId={conn.connectionId} to prevent exploits from an Exception in MessageHandler: {e.GetType().Name} {e.Message}\n{e.StackTrace}"); - conn.Disconnect(); - } - }; - - // version for handlers without channelId - // TODO obsolete this some day to always use the channelId version. - // all handlers in this version are wrapped with 1 extra action. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static NetworkMessageDelegate WrapHandler(Action handler, bool requireAuthentication) - where T : struct, NetworkMessage - where C : NetworkConnection - { - // wrap action as channelId version, call original - void Wrapped(C conn, T msg, int _) => handler(conn, msg); - return WrapHandler((Action) Wrapped, requireAuthentication); - } - } -} diff --git a/Assets/Mirror/Runtime/Messages.cs b/Assets/Mirror/Runtime/Messages.cs deleted file mode 100644 index d3816f8..0000000 --- a/Assets/Mirror/Runtime/Messages.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using UnityEngine; - -namespace Mirror -{ - public struct ReadyMessage : NetworkMessage {} - - public struct NotReadyMessage : NetworkMessage {} - - public struct AddPlayerMessage : NetworkMessage {} - - public struct SceneMessage : NetworkMessage - { - public string sceneName; - // Normal = 0, LoadAdditive = 1, UnloadAdditive = 2 - public SceneOperation sceneOperation; - public bool customHandling; - } - - public enum SceneOperation : byte - { - Normal, - LoadAdditive, - UnloadAdditive - } - - public struct CommandMessage : NetworkMessage - { - public uint netId; - public byte componentIndex; - public int functionHash; - // the parameters for the Cmd function - // -> ArraySegment to avoid unnecessary allocations - public ArraySegment payload; - } - - public struct RpcMessage : NetworkMessage - { - public uint netId; - public byte componentIndex; - public int functionHash; - // the parameters for the Cmd function - // -> ArraySegment to avoid unnecessary allocations - public ArraySegment payload; - } - - 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; - public ulong sceneId; - // If sceneId != 0 then it is used instead of assetId - public Guid assetId; - // Local position - public Vector3 position; - // Local rotation - public Quaternion rotation; - // Local scale - public Vector3 scale; - // serialized component data - // ArraySegment to avoid unnecessary allocations - public ArraySegment payload; - } - - public struct ChangeOwnerMessage : NetworkMessage - { - public uint netId; - public bool isOwner; - public bool isLocalPlayer; - } - - public struct ObjectSpawnStartedMessage : NetworkMessage {} - - public struct ObjectSpawnFinishedMessage : NetworkMessage {} - - public struct ObjectDestroyMessage : NetworkMessage - { - public uint netId; - } - - public struct ObjectHideMessage : NetworkMessage - { - public uint netId; - } - - public struct EntityStateMessage : NetworkMessage - { - public uint netId; - // the serialized component data - // -> ArraySegment to avoid unnecessary allocations - public ArraySegment payload; - } - - // A client sends this message to the server - // to calculate RTT and synchronize time - public struct NetworkPingMessage : NetworkMessage - { - public double clientTime; - - public NetworkPingMessage(double value) - { - clientTime = value; - } - } - - // The server responds with this message - // The client can use this to calculate RTT and sync time - public struct NetworkPongMessage : NetworkMessage - { - public double clientTime; - public double serverTime; - } -} diff --git a/Assets/Mirror/Runtime/Mirror.asmdef.meta b/Assets/Mirror/Runtime/Mirror.asmdef.meta deleted file mode 100644 index 202009b..0000000 --- a/Assets/Mirror/Runtime/Mirror.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 30817c1a0e6d646d99c048fc403f5979 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs b/Assets/Mirror/Runtime/NetworkConnectionToClient.cs deleted file mode 100644 index 4cb56f5..0000000 --- a/Assets/Mirror/Runtime/NetworkConnectionToClient.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace Mirror -{ - public class NetworkConnectionToClient : NetworkConnection - { - public override string address => - Transport.activeTransport.ServerGetClientAddress(connectionId); - - /// NetworkIdentities that this connection can see - // TODO move to server's NetworkConnectionToClient? - public new readonly HashSet observing = new HashSet(); - - /// All NetworkIdentities owned by this connection. Can be main player, pets, etc. - // IMPORTANT: this needs to be , not . - // fixes a bug where DestroyOwnedObjects wouldn't find the - // netId anymore: https://github.com/vis2k/Mirror/issues/1380 - // Works fine with NetworkIdentity pointers though. - public new readonly HashSet clientOwnedObjects = new HashSet(); - - // unbatcher - public Unbatcher unbatcher = new Unbatcher(); - - public NetworkConnectionToClient(int networkConnectionId) - : base(networkConnectionId) {} - - // Send stage three: hand off to transport - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => - Transport.activeTransport.ServerSend(connectionId, segment, channelId); - - /// Disconnects this connection. - public override void Disconnect() - { - // set not ready and handle clientscene disconnect in any case - // (might be client or host mode here) - isReady = false; - Transport.activeTransport.ServerDisconnect(connectionId); - - // IMPORTANT: NetworkConnection.Disconnect() is NOT called for - // voluntary disconnects from the other end. - // -> so all 'on disconnect' cleanup code needs to be in - // OnTransportDisconnect, where it's called for both voluntary - // and involuntary disconnects! - } - - internal void AddToObserving(NetworkIdentity netIdentity) - { - observing.Add(netIdentity); - - // spawn identity for this conn - NetworkServer.ShowForConnection(netIdentity, this); - } - - internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed) - { - observing.Remove(netIdentity); - - if (!isDestroyed) - { - // hide identity for this conn - NetworkServer.HideForConnection(netIdentity, this); - } - } - - internal void RemoveFromObservingsObservers() - { - foreach (NetworkIdentity netIdentity in observing) - { - netIdentity.RemoveObserver(this); - } - observing.Clear(); - } - - internal void AddOwnedObject(NetworkIdentity obj) - { - clientOwnedObjects.Add(obj); - } - - internal void RemoveOwnedObject(NetworkIdentity obj) - { - clientOwnedObjects.Remove(obj); - } - - internal void DestroyOwnedObjects() - { - // create a copy because the list might be modified when destroying - HashSet tmp = new HashSet(clientOwnedObjects); - foreach (NetworkIdentity netIdentity in tmp) - { - if (netIdentity != null) - { - NetworkServer.Destroy(netIdentity.gameObject); - } - } - - // clear the hashset because we destroyed them all - clientOwnedObjects.Clear(); - } - } -} diff --git a/Assets/Mirror/Runtime/NetworkReaderPooled.cs b/Assets/Mirror/Runtime/NetworkReaderPooled.cs deleted file mode 100644 index fcfa792..0000000 --- a/Assets/Mirror/Runtime/NetworkReaderPooled.cs +++ /dev/null @@ -1,22 +0,0 @@ -// "NetworkReaderPooled" instead of "PooledNetworkReader" to group files, for -// easier IDE workflow and more elegant code. -using System; - -namespace Mirror -{ - [Obsolete("PooledNetworkReader was renamed to NetworkReaderPooled. It's cleaner & slightly easier to use.")] - public sealed class PooledNetworkReader : NetworkReaderPooled - { - internal PooledNetworkReader(byte[] bytes) : base(bytes) {} - internal PooledNetworkReader(ArraySegment segment) : base(segment) {} - } - - /// Pooled NetworkReader, automatically returned to pool when using 'using' - // TODO make sealed again after removing obsolete NetworkReaderPooled! - public class NetworkReaderPooled : NetworkReader, IDisposable - { - internal NetworkReaderPooled(byte[] bytes) : base(bytes) {} - internal NetworkReaderPooled(ArraySegment segment) : base(segment) {} - public void Dispose() => NetworkReaderPool.Return(this); - } -} diff --git a/Assets/Mirror/Runtime/NetworkTime.cs b/Assets/Mirror/Runtime/NetworkTime.cs deleted file mode 100644 index 1721524..0000000 --- a/Assets/Mirror/Runtime/NetworkTime.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using UnityEngine; -#if !UNITY_2020_3_OR_NEWER -using Stopwatch = System.Diagnostics.Stopwatch; -#endif - -namespace Mirror -{ - /// Synchronizes server time to clients. - public static class NetworkTime - { - /// Ping message frequency, used to calculate network time and RTT - public static float PingFrequency = 2.0f; - - /// Average out the last few results from Ping - public static int PingWindowSize = 10; - - static double lastPingTime; - - static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(10); - static ExponentialMovingAverage _offset = new ExponentialMovingAverage(10); - - // the true offset guaranteed to be in this range - static double offsetMin = double.MinValue; - static double offsetMax = double.MaxValue; - - /// Returns double precision clock time _in this system_, unaffected by the network. -#if UNITY_2020_3_OR_NEWER - public static double localTime - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Time.timeAsDouble; - } -#else - // need stopwatch for older Unity versions, but it's quite slow. - // CAREFUL: unlike Time.time, this is not a FRAME time. - // it changes during the frame too. - static readonly Stopwatch stopwatch = new Stopwatch(); - static NetworkTime() => stopwatch.Start(); - public static double localTime => stopwatch.Elapsed.TotalSeconds; -#endif - - /// The time in seconds since the server started. - // - // I measured the accuracy of float and I got this: - // for the same day, accuracy is better than 1 ms - // after 1 day, accuracy goes down to 7 ms - // after 10 days, accuracy is 61 ms - // after 30 days , accuracy is 238 ms - // after 60 days, accuracy is 454 ms - // in other words, if the server is running for 2 months, - // and you cast down to float, then the time will jump in 0.4s intervals. - // - // TODO consider using Unbatcher's remoteTime for NetworkTime - public static double time - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => localTime - _offset.Value; - } - - /// Time measurement variance. The higher, the less accurate the time is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double timeVariance => _offset.Var; - - /// Time standard deviation. The highe, the less accurate the time is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double timeStandardDeviation => Math.Sqrt(timeVariance); - - /// Clock difference in seconds between the client and the server. Always 0 on server. - public static double offset => _offset.Value; - - /// Round trip time (in seconds) that it takes a message to go client->server->client. - public static double rtt => _rtt.Value; - - /// Round trip time variance. The higher, the less accurate the rtt is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double rttVariance => _rtt.Var; - - /// Round trip time standard deviation. The higher, the less accurate the rtt is. - // TODO does this need to be public? user should only need NetworkTime.time - public static double rttStandardDeviation => Math.Sqrt(rttVariance); - - // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload - [UnityEngine.RuntimeInitializeOnLoadMethod] - public static void ResetStatics() - { - PingFrequency = 2.0f; - PingWindowSize = 10; - lastPingTime = 0; - _rtt = new ExponentialMovingAverage(PingWindowSize); - _offset = new ExponentialMovingAverage(PingWindowSize); - offsetMin = double.MinValue; - offsetMax = double.MaxValue; -#if !UNITY_2020_3_OR_NEWER - stopwatch.Restart(); -#endif - } - - internal static void UpdateClient() - { - // localTime (double) instead of Time.time for accuracy over days - if (localTime - lastPingTime >= PingFrequency) - { - NetworkPingMessage pingMessage = new NetworkPingMessage(localTime); - NetworkClient.Send(pingMessage, Channels.Unreliable); - lastPingTime = localTime; - } - } - - // executed at the server when we receive a ping message - // reply with a pong containing the time from the client - // and time from the server - internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMessage message) - { - // Debug.Log($"OnPingServerMessage conn:{conn}"); - NetworkPongMessage pongMessage = new NetworkPongMessage - { - clientTime = message.clientTime, - serverTime = localTime - }; - conn.Send(pongMessage, Channels.Unreliable); - } - - // Executed at the client when we receive a Pong message - // find out how long it took since we sent the Ping - // and update time offset - internal static void OnClientPong(NetworkPongMessage message) - { - double now = localTime; - - // how long did this message take to come back - double newRtt = now - message.clientTime; - _rtt.Add(newRtt); - - // the difference in time between the client and the server - // but subtract half of the rtt to compensate for latency - // half of rtt is the best approximation we have - double newOffset = now - newRtt * 0.5f - message.serverTime; - - double newOffsetMin = now - newRtt - message.serverTime; - double newOffsetMax = now - message.serverTime; - offsetMin = Math.Max(offsetMin, newOffsetMin); - offsetMax = Math.Min(offsetMax, newOffsetMax); - - if (_offset.Value < offsetMin || _offset.Value > offsetMax) - { - // the old offset was offrange, throw it away and use new one - _offset = new ExponentialMovingAverage(PingWindowSize); - _offset.Add(newOffset); - } - else if (newOffset >= offsetMin || newOffset <= offsetMax) - { - // new offset looks reasonable, add to the average - _offset.Add(newOffset); - } - } - } -} diff --git a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs b/Assets/Mirror/Runtime/NetworkWriterExtensions.cs deleted file mode 100644 index cf0954e..0000000 --- a/Assets/Mirror/Runtime/NetworkWriterExtensions.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using UnityEngine; - -namespace Mirror -{ - // Mirror's Weaver automatically detects all NetworkWriter function types, - // but they do all need to be extensions. - public static class NetworkWriterExtensions - { - // cache encoding instead of creating it with BinaryWriter each time - // 1000 readers before: 1MB GC, 30ms - // 1000 readers after: 0.8MB GC, 18ms - static readonly UTF8Encoding encoding = new UTF8Encoding(false, true); - static readonly byte[] stringBuffer = new byte[NetworkWriter.MaxStringLength]; - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteByteNullable(this NetworkWriter writer, byte? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteSByteNullable(this NetworkWriter writer, sbyte? value) => writer.WriteBlittableNullable(value); - - // char is not blittable. convert to ushort. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteBlittable((ushort)value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteCharNullable(this NetworkWriter writer, char? value) => writer.WriteBlittableNullable((ushort?)value); - - // bool is not blittable. convert to byte. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteBool(this NetworkWriter writer, bool value) => writer.WriteBlittable((byte)(value ? 1 : 0)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteBoolNullable(this NetworkWriter writer, bool? value) => writer.WriteBlittableNullable(value.HasValue ? ((byte)(value.Value ? 1 : 0)) : new byte?()); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteShort(this NetworkWriter writer, short value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteShortNullable(this NetworkWriter writer, short? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUShort(this NetworkWriter writer, ushort value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUShortNullable(this NetworkWriter writer, ushort? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteInt(this NetworkWriter writer, int value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteIntNullable(this NetworkWriter writer, int? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUInt(this NetworkWriter writer, uint value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUIntNullable(this NetworkWriter writer, uint? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteLong(this NetworkWriter writer, long value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteLongNullable(this NetworkWriter writer, long? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value); - - [StructLayout(LayoutKind.Explicit)] - internal struct UIntDouble - { - [FieldOffset(0)] - public double doubleValue; - - [FieldOffset(0)] - public ulong longValue; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteDouble(this NetworkWriter writer, double value) - { - // DEBUG: try to find the exact value that fails. - //UIntDouble convert = new UIntDouble{doubleValue = value}; - //Debug.Log($"=> NetworkWriter.WriteDouble: {value} => 0x{convert.longValue:X8}"); - - - writer.WriteBlittable(value); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteDoubleNullable(this NetworkWriter writer, double? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteString(this NetworkWriter writer, string value) - { - // write 0 for null support, increment real size by 1 - // (note: original HLAPI would write "" for null strings, but if a - // string is null on the server then it should also be null - // on the client) - if (value == null) - { - writer.WriteUShort(0); - return; - } - - // write string with same method as NetworkReader - // convert to byte[] - int size = encoding.GetBytes(value, 0, value.Length, stringBuffer, 0); - - // check if within max size - if (size >= NetworkWriter.MaxStringLength) - { - throw new IndexOutOfRangeException($"NetworkWriter.Write(string) too long: {size}. Limit: {NetworkWriter.MaxStringLength}"); - } - - // write size and bytes - writer.WriteUShort(checked((ushort)(size + 1))); - writer.WriteBytes(stringBuffer, 0, size); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteBytesAndSizeSegment(this NetworkWriter writer, ArraySegment buffer) - { - writer.WriteBytesAndSize(buffer.Array, buffer.Offset, buffer.Count); - } - - // Weaver needs a write function with just one byte[] parameter - // (we don't name it .Write(byte[]) because it's really a WriteBytesAndSize since we write size / null info too) - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer) - { - // buffer might be null, so we can't use .Length in that case - writer.WriteBytesAndSize(buffer, 0, buffer != null ? buffer.Length : 0); - } - - // for byte arrays with dynamic size, where the reader doesn't know how many will come - // (like an inventory with different items etc.) - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer, int offset, int count) - { - // null is supported because [SyncVar]s might be structs with null byte[] arrays - // write 0 for null array, increment normal size by 1 to save bandwidth - // (using size=-1 for null would limit max size to 32kb instead of 64kb) - if (buffer == null) - { - writer.WriteUInt(0u); - return; - } - writer.WriteUInt(checked((uint)count) + 1u); - writer.WriteBytes(buffer, offset, count); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteArraySegment(this NetworkWriter writer, ArraySegment segment) - { - int length = segment.Count; - writer.WriteInt(length); - for (int i = 0; i < length; i++) - { - writer.Write(segment.Array[segment.Offset + i]); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector2(this NetworkWriter writer, Vector2 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector2Nullable(this NetworkWriter writer, Vector2? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector3(this NetworkWriter writer, Vector3 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector3Nullable(this NetworkWriter writer, Vector3? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector4(this NetworkWriter writer, Vector4 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector4Nullable(this NetworkWriter writer, Vector4? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector2IntNullable(this NetworkWriter writer, Vector2Int? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteVector3IntNullable(this NetworkWriter writer, Vector3Int? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteColor(this NetworkWriter writer, Color value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteColorNullable(this NetworkWriter writer, Color? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteColor32(this NetworkWriter writer, Color32 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteColor32Nullable(this NetworkWriter writer, Color32? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteRect(this NetworkWriter writer, Rect value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteRectNullable(this NetworkWriter writer, Rect? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WritePlane(this NetworkWriter writer, Plane value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WritePlaneNullable(this NetworkWriter writer, Plane? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteRay(this NetworkWriter writer, Ray value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteRayNullable(this NetworkWriter writer, Ray? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteGuid(this NetworkWriter writer, Guid value) - { - byte[] data = value.ToByteArray(); - writer.WriteBytes(data, 0, data.Length); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteGuidNullable(this NetworkWriter writer, Guid? value) - { - writer.WriteBool(value.HasValue); - if (value.HasValue) - writer.WriteGuid(value.Value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value) - { - if (value == null) - { - writer.WriteUInt(0); - return; - } - - // users might try to use unspawned / prefab GameObjects in - // rpcs/cmds/syncvars/messages. they would be null on the other - // end, and it might not be obvious why. let's make it obvious. - // https://github.com/vis2k/Mirror/issues/2060 - // - // => warning (instead of exception) because we also use a warning - // if a GameObject doesn't have a NetworkIdentity component etc. - if (value.netId == 0) - Debug.LogWarning($"Attempted to serialize unspawned GameObject: {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc."); - - writer.WriteUInt(value.netId); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value) - { - if (value == null) - { - writer.WriteUInt(0); - return; - } - writer.WriteUInt(value.netId); - writer.WriteByte((byte)value.ComponentIndex); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteTransform(this NetworkWriter writer, Transform value) - { - if (value == null) - { - writer.WriteUInt(0); - return; - } - NetworkIdentity identity = value.GetComponent(); - if (identity != null) - { - writer.WriteUInt(identity.netId); - } - else - { - Debug.LogWarning($"NetworkWriter {value} has no NetworkIdentity"); - writer.WriteUInt(0); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteGameObject(this NetworkWriter writer, GameObject value) - { - if (value == null) - { - writer.WriteUInt(0); - return; - } - - // warn if the GameObject doesn't have a NetworkIdentity, - NetworkIdentity identity = value.GetComponent(); - if (identity == null) - Debug.LogWarning($"NetworkWriter {value} has no NetworkIdentity"); - - // serialize the correct amount of data in any case to make sure - // that the other end can read the expected amount of data too. - writer.WriteNetworkIdentity(identity); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteList(this NetworkWriter writer, List list) - { - if (list is null) - { - writer.WriteInt(-1); - return; - } - writer.WriteInt(list.Count); - for (int i = 0; i < list.Count; i++) - writer.Write(list[i]); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteArray(this NetworkWriter writer, T[] array) - { - if (array is null) - { - writer.WriteInt(-1); - return; - } - writer.WriteInt(array.Length); - for (int i = 0; i < array.Length; i++) - writer.Write(array[i]); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteUri(this NetworkWriter writer, Uri uri) - { - writer.WriteString(uri?.ToString()); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D) - { - writer.WriteArray(texture2D.GetPixels32()); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteSprite(this NetworkWriter writer, Sprite sprite) - { - writer.WriteTexture2D(sprite.texture); - writer.WriteRect(sprite.rect); - writer.WriteVector2(sprite.pivot); - } - } -} diff --git a/Assets/Mirror/Runtime/NetworkWriterPooled.cs b/Assets/Mirror/Runtime/NetworkWriterPooled.cs deleted file mode 100644 index ce113bc..0000000 --- a/Assets/Mirror/Runtime/NetworkWriterPooled.cs +++ /dev/null @@ -1,17 +0,0 @@ -// "NetworkWriterPooled" instead of "PooledNetworkWriter" to group files, for -// easier IDE workflow and more elegant code. -using System; - -namespace Mirror -{ - // DEPRECATED 2022-03-10 - [Obsolete("PooledNetworkWriter was renamed to NetworkWriterPooled. It's cleaner & slightly easier to use.")] - public sealed class PooledNetworkWriter : NetworkWriterPooled {} - - /// Pooled NetworkWriter, automatically returned to pool when using 'using' - // TODO make sealed again after removing obsolete NetworkWriterPooled! - public class NetworkWriterPooled : NetworkWriter, IDisposable - { - public void Dispose() => NetworkWriterPool.Return(this); - } -} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs b/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs deleted file mode 100644 index fbf2c24..0000000 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/Snapshot.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Snapshot interface so we can reuse it for all kinds of systems. -// for example, NetworkTransform, NetworkRigidbody, CharacterController etc. -// NOTE: we use '' and 'where T : Snapshot' to avoid boxing. -// List would cause allocations through boxing. -namespace Mirror -{ - public interface Snapshot - { - // snapshots have two timestamps: - // -> the remote timestamp (when it was sent by the remote) - // used to interpolate. - // -> the local timestamp (when we received it) - // used to know if the first two snapshots are old enough to start. - // - // IMPORTANT: the timestamp does _NOT_ need to be sent over the - // network. simply get it from batching. - double remoteTimestamp { get; set; } - double localTimestamp { get; set; } - } -} diff --git a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs b/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs deleted file mode 100644 index bc685d7..0000000 --- a/Assets/Mirror/Runtime/SnapshotInterpolation/SnapshotInterpolation.cs +++ /dev/null @@ -1,325 +0,0 @@ -// snapshot interpolation algorithms only, -// independent from Unity/NetworkTransform/MonoBehaviour/Mirror/etc. -// the goal is to remove all the magic from it. -// => a standalone snapshot interpolation algorithm -// => that can be simulated with unit tests easily -// -// BOXING: in C#, uses does not box! passing the interface would box! -using System; -using System.Collections.Generic; - -namespace Mirror -{ - public static class SnapshotInterpolation - { - // insert into snapshot buffer if newer than first entry - // this should ALWAYS be used when inserting into a snapshot buffer! - public static void InsertIfNewEnough(T snapshot, SortedList buffer) - where T : Snapshot - { - // we need to drop any snapshot which is older ('<=') - // the snapshots we are already working with. - double timestamp = snapshot.remoteTimestamp; - - // if size == 1, then only add snapshots that are newer. - // for example, a snapshot before the first one might have been - // lagging. - if (buffer.Count == 1 && - timestamp <= buffer.Values[0].remoteTimestamp) - return; - - // for size >= 2, we are already interpolating between the first two - // so only add snapshots that are newer than the second entry. - // aka the 'ACB' problem: - // if we have a snapshot A at t=0 and C at t=2, - // we start interpolating between them. - // if suddenly B at t=1 comes in unexpectely, - // we should NOT suddenly steer towards B. - if (buffer.Count >= 2 && - timestamp <= buffer.Values[1].remoteTimestamp) - return; - - // otherwise sort it into the list - // an UDP messages might arrive twice sometimes. - // SortedList throws if key already exists, so check. - if (!buffer.ContainsKey(timestamp)) - buffer.Add(timestamp, snapshot); - } - - // helper function to check if we have 'bufferTime' worth of snapshots - // to start. - // - // glenn fiedler article: - // "Now for the trick with snapshots. What we do is instead of - // immediately rendering snapshot data received is that we buffer - // snapshots for a short amount of time in an interpolation buffer. - // This interpolation buffer holds on to snapshots for a period of time - // such that you have not only the snapshot you want to render but also, - // statistically speaking, you are very likely to have the next snapshot - // as well." - // - // => 'statistically' implies that we always wait for a fixed amount - // aka LOCAL TIME has passed. - // => it does NOT imply to wait for a remoteTime span of bufferTime. - // that would not be 'statistically'. it would be 'exactly'. - public static bool HasAmountOlderThan(SortedList buffer, double threshold, int amount) - where T : Snapshot => - buffer.Count >= amount && - buffer.Values[amount - 1].localTimestamp <= threshold; - - // for convenience, hide the 'bufferTime worth of snapshots' check in an - // easy to use function. this way we can have several conditions etc. - public static bool HasEnough(SortedList buffer, double time, double bufferTime) - where T : Snapshot => - // two snapshots with local time older than threshold? - HasAmountOlderThan(buffer, time - bufferTime, 2); - - // sometimes we need to know if it's still safe to skip past the first - // snapshot. - public static bool HasEnoughWithoutFirst(SortedList buffer, double time, double bufferTime) - where T : Snapshot => - // still two snapshots with local time older than threshold if - // we remove the first one? (in other words, need three older) - HasAmountOlderThan(buffer, time - bufferTime, 3); - - // calculate catchup. - // the goal is to buffer 'bufferTime' snapshots. - // for whatever reason, we might see growing buffers. - // in which case we should speed up to avoid ever growing delay. - // -> everything after 'threshold' is multiplied by 'multiplier' - public static double CalculateCatchup(SortedList buffer, int catchupThreshold, double catchupMultiplier) - where T : Snapshot - { - // NOTE: we count ALL buffer entires > threshold as excess. - // not just the 'old enough' ones. - // if buffer keeps growing, we have to catch up no matter what. - int excess = buffer.Count - catchupThreshold; - return excess > 0 ? excess * catchupMultiplier : 0; - } - - // get first & second buffer entries and delta between them. - // helper function because we use this several times. - // => assumes at least two entries in buffer. - public static void GetFirstSecondAndDelta(SortedList buffer, out T first, out T second, out double delta) - where T : Snapshot - { - // get first & second - first = buffer.Values[0]; - second = buffer.Values[1]; - - // delta between first & second is needed a lot - delta = second.remoteTimestamp - first.remoteTimestamp; - } - - // the core snapshot interpolation algorithm. - // for a given remoteTime, interpolationTime and buffer, - // we tick the snapshot simulation once. - // => it's the same one on server and client - // => should be called every Update() depending on authority - // - // time: LOCAL time since startup in seconds. like Unity's Time.time. - // deltaTime: Time.deltaTime from Unity. parameter for easier tests. - // interpolationTime: time in interpolation. moved along deltaTime. - // between [0, delta] where delta is snapshot - // B.timestamp - A.timestamp. - // IMPORTANT: - // => we use actual time instead of a relative - // t [0,1] because overshoot is easier to handle. - // if relative t overshoots but next snapshots are - // further apart than the current ones, it's not - // obvious how to calculate it. - // => for example, if t = 3 every time we skip we would have to - // make sure to adjust the subtracted value relative to the - // skipped delta. way too complex. - // => actual time can overshoot without problems. - // we know it's always by actual time. - // bufferTime: time in seconds that we buffer snapshots. - // buffer: our buffer of snapshots. - // Compute() assumes full integrity of the snapshots. - // for example, when interpolating between A=0 and C=2, - // make sure that you don't add B=1 between A and C if that - // snapshot arrived after we already started interpolating. - // => InsertIfNewEnough needs to protect against the 'ACB' problem - // catchupThreshold: amount of buffer entries after which we start to - // accelerate to catch up. - // if 'bufferTime' is 'sendInterval * 3', then try - // a value > 3 like 6. - // catchupMultiplier: catchup by % per additional excess buffer entry - // over the amount of 'catchupThreshold'. - // Interpolate: interpolates one snapshot to another, returns the result - // T Interpolate(T from, T to, double t); - // => needs to be Func instead of a function in the Snapshot - // interface because that would require boxing. - // => make sure to only allocate that function once. - // - // returns - // 'true' if it spit out a snapshot to apply. - // 'false' means computation moved along, but nothing to apply. - public static bool Compute( - double time, - double deltaTime, - ref double interpolationTime, - double bufferTime, - SortedList buffer, - int catchupThreshold, - float catchupMultiplier, - Func Interpolate, - out T computed) - where T : Snapshot - { - // we buffer snapshots for 'bufferTime' - // for example: - // * we buffer for 3 x sendInterval = 300ms - // * the idea is to wait long enough so we at least have a few - // snapshots to interpolate between - // * we process anything older 100ms immediately - // - // IMPORTANT: snapshot timestamps are _remote_ time - // we need to interpolate and calculate buffer lifetimes based on it. - // -> we don't know remote's current time - // -> NetworkTime.time fluctuates too much, that's no good - // -> we _could_ calculate an offset when the first snapshot arrives, - // but if there was high latency then we'll always calculate time - // with high latency - // -> at any given time, we are interpolating from snapshot A to B - // => seems like A.timestamp += deltaTime is a good way to do it - - computed = default; - //Debug.Log($"{name} snapshotbuffer={buffer.Count}"); - - // do we have enough buffered to start interpolating? - if (!HasEnough(buffer, time, bufferTime)) - return false; - - // multiply deltaTime by catchup. - // for example, assuming a catch up of 50%: - // - deltaTime = 1s => 1.5s - // - deltaTime = 0.1s => 0.15s - // in other words, variations in deltaTime don't matter. - // simply multiply. that's just how time works. - // (50% catch up means 0.5, so we multiply by 1.5) - // - // if '0' catchup then we multiply by '1', which changes nothing. - // (faster branch prediction) - double catchup = CalculateCatchup(buffer, catchupThreshold, catchupMultiplier); - deltaTime *= (1 + catchup); - - // interpolationTime starts at 0 and we add deltaTime to move - // along the interpolation. - // - // ONLY while we have snapshots to interpolate. - // otherwise we might increase it to infinity which would lead - // to skipping the next snapshots entirely. - // - // IMPORTANT: interpolationTime as actual time instead of - // t [0,1] allows us to overshoot and subtract easily. - // if t was [0,1], and we overshoot by 0.1, that's a - // RELATIVE overshoot for the delta between B.time - A.time. - // => if the next C.time - B.time is not the same delta, - // then the relative overshoot would speed up or slow - // down the interpolation! CAREFUL. - // - // IMPORTANT: we NEVER add deltaTime to 'time'. - // 'time' is already NOW. that's how Unity works. - interpolationTime += deltaTime; - - // get first & second & delta - GetFirstSecondAndDelta(buffer, out T first, out T second, out double delta); - - // reached goal and have more old enough snapshots in buffer? - // then skip and move to next. - // for example, if we have snapshots at t=1,2,3 - // and we are at interpolationTime = 2.5, then - // we should skip the first one, subtract delta and interpolate - // between 2,3 instead. - // - // IMPORTANT: we only ever use old enough snapshots. - // if we wouldn't check for old enough, then we would - // move to the next one, interpolate a little bit, - // and then in next compute() wait again because it - // wasn't old enough yet. - while (interpolationTime >= delta && - HasEnoughWithoutFirst(buffer, time, bufferTime)) - { - // subtract exactly delta from interpolation time - // instead of setting to '0', where we would lose the - // overshoot part and see jitter again. - // - // IMPORTANT: subtracting delta TIME works perfectly. - // subtracting '1' from a ratio of t [0,1] would - // leave the overshoot as relative between the - // next delta. if next delta is different, then - // overshoot would be bigger than planned and - // speed up the interpolation. - interpolationTime -= delta; - //Debug.LogWarning($"{name} overshot and is now at: {interpolationTime}"); - - // remove first, get first, second & delta again after change. - buffer.RemoveAt(0); - GetFirstSecondAndDelta(buffer, out first, out second, out delta); - - // NOTE: it's worth consider spitting out all snapshots - // that we skipped, in case someone still wants to move - // along them to avoid physics collisions. - // * for NetworkTransform it's unnecessary as we always - // set transform.position, which can go anywhere. - // * for CharacterController it's worth considering - } - - // interpolationTime is actual time, NOT a 't' ratio [0,1]. - // we need 't' between [0,1] relative. - // InverseLerp calculates just that. - // InverseLerp CLAMPS between [0,1] and DOES NOT extrapolate! - // => we already skipped ahead as many as possible above. - // => we do NOT extrapolate for the reasons below. - // - // IMPORTANT: - // we should NOT extrapolate & predict while waiting for more - // snapshots as this would introduce a whole range of issues: - // * player might be extrapolated WAY out if we wait for long - // * player might be extrapolated behind walls - // * once we receive a new snapshot, we would interpolate - // not from the last valid position, but from the - // extrapolated position. this could be ANYWHERE. the - // player might get stuck in walls, etc. - // => we are NOT doing client side prediction & rollback here - // => we are simply interpolating with known, valid positions - // - // SEE TEST: Compute_Step5_OvershootWithoutEnoughSnapshots_NeverExtrapolates() - double t = Mathd.InverseLerp(first.remoteTimestamp, second.remoteTimestamp, first.remoteTimestamp + interpolationTime); - //Debug.Log($"InverseLerp({first.remoteTimestamp:F2}, {second.remoteTimestamp:F2}, {first.remoteTimestamp} + {interpolationTime:F2}) = {t:F2} snapshotbuffer={buffer.Count}"); - - // interpolate snapshot, return true to indicate we computed one - computed = Interpolate(first, second, t); - - // interpolationTime: - // overshooting is ONLY allowed for smooth transitions when - // immediately moving to the NEXT snapshot afterwards. - // - // if there is ANY break, for example: - // * reached second snapshot and waiting for more - // * reached second snapshot and next one isn't old enough yet - // - // then we SHOULD NOT overshoot because: - // * increasing interpolationTime by deltaTime while waiting - // would make it grow HUGE to 100+. - // * once we have more snapshots, we would skip most of them - // instantly instead of actually interpolating through them. - // - // in other words: cap time if we WOULDN'T have enough after removing - if (!HasEnoughWithoutFirst(buffer, time, bufferTime)) - { - // interpolationTime is always from 0..delta. - // so we cap it at delta. - // DO NOT cap it at second.remoteTimestamp. - // (that's why when interpolating the third parameter is - // first.time + interpolationTime) - // => covered with test: - // Compute_Step5_OvershootWithEnoughSnapshots_NextIsntOldEnough() - interpolationTime = Math.Min(interpolationTime, delta); - } - - return true; - } - } -} diff --git a/Assets/Mirror/Runtime/SyncVar.cs b/Assets/Mirror/Runtime/SyncVar.cs deleted file mode 100644 index 5c1790b..0000000 --- a/Assets/Mirror/Runtime/SyncVar.cs +++ /dev/null @@ -1,148 +0,0 @@ -// SyncVar to make [SyncVar] weaving easier. -// -// we can possibly move a lot of complex logic out of weaver: -// * set dirty bit -// * calling the hook -// * hook guard in host mode -// * GameObject/NetworkIdentity internal netId storage -// -// here is the plan: -// 1. develop SyncVar along side [SyncVar] -// 2. internally replace [SyncVar]s with SyncVar -// 3. eventually obsolete [SyncVar] -// -// downsides: -// - generic types don't show in Unity Inspector -// -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Mirror -{ - // 'class' so that we can track it in SyncObjects list, and iterate it for - // de/serialization. - [Serializable] - public class SyncVar : SyncObject, IEquatable - { - // Unity 2020+ can show [SerializeField] in inspector. - // (only if SyncVar isn't readonly though) - [SerializeField] T _Value; - - // Value property with hooks - // virtual for SyncFieldNetworkIdentity netId trick etc. - public virtual T Value - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _Value; - set - { - // only if value changed. otherwise don't dirty/hook. - // we have .Equals(T), simply reuse it here. - if (!Equals(value)) - { - // set value, set dirty bit - T old = _Value; - _Value = value; - OnDirty(); - - // Value.set calls the hook if changed. - // calling Value.set from within the hook would call the - // hook again and deadlock. prevent it with hookGuard. - // (see test: Hook_Set_DoesntDeadlock) - if (!hookGuard && - // original [SyncVar] only calls hook on clients. - // let's keep it for consistency for now - // TODO remove check & dependency in the future. - // use isClient/isServer in the hook instead. - NetworkClient.active) - { - hookGuard = true; - InvokeCallback(old, value); - hookGuard = false; - } - } - } - } - - // OnChanged Callback. - // named 'Callback' for consistency with SyncList etc. - // needs to be public so we can assign it in OnStartClient. - // (ctor passing doesn't work, it can only take static functions) - // assign via: field.Callback += ...! - public event Action Callback; - - // OnCallback is responsible for calling the callback. - // this is necessary for inheriting classes like SyncVarGameObject, - // where the netIds should be converted to GOs and call the GO hook. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected virtual void InvokeCallback(T oldValue, T newValue) => - Callback?.Invoke(oldValue, newValue); - - // Value.set calls the hook if changed. - // calling Value.set from within the hook would call the hook again and - // deadlock. prevent it with a simple 'are we inside the hook' bool. - bool hookGuard; - - public override void ClearChanges() {} - public override void Reset() {} - - // ctor from value and OnChanged hook. - // it was always called 'hook'. let's keep naming for convenience. - public SyncVar(T value) - { - // recommend explicit GameObject, NetworkIdentity, NetworkBehaviour - // with persistent netId method - if (this is SyncVar) - Debug.LogWarning($"Use explicit {nameof(SyncVarGameObject)} class instead of {nameof(SyncVar)}. It stores netId internally for persistence."); - - if (this is SyncVar) - Debug.LogWarning($"Use explicit {nameof(SyncVarNetworkIdentity)} class instead of {nameof(SyncVar)}. It stores netId internally for persistence."); - - if (this is SyncVar) - Debug.LogWarning($"Use explicit SyncVarNetworkBehaviour class instead of {nameof(SyncVar)}. It stores netId internally for persistence."); - - _Value = value; - } - - // NOTE: copy ctor is unnecessary. - // SyncVars are readonly and only initialized by 'Value' once. - - // implicit conversion: int value = SyncVar - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator T(SyncVar field) => field.Value; - - // implicit conversion: SyncVar = value - // even if SyncVar is readonly, it's still useful: SyncVar = 1; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator SyncVar(T value) => new SyncVar(value); - - // serialization (use .Value instead of _Value so hook is called!) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnSerializeAll(NetworkWriter writer) => writer.Write(Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnSerializeDelta(NetworkWriter writer) => writer.Write(Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnDeserializeAll(NetworkReader reader) => Value = reader.Read(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnDeserializeDelta(NetworkReader reader) => Value = reader.Read(); - - // IEquatable should compare Value. - // SyncVar should act invisibly like [SyncVar] before. - // this way we can do SyncVar health == 0 etc. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(T other) => - // from NetworkBehaviour.SyncVarEquals: - // EqualityComparer method avoids allocations. - // otherwise would have to be :IEquatable (not all structs are) - EqualityComparer.Default.Equals(Value, other); - - // ToString should show Value. - // SyncVar should act invisibly like [SyncVar] before. - public override string ToString() => Value.ToString(); - } -} diff --git a/Assets/Mirror/Runtime/SyncVar.cs.meta b/Assets/Mirror/Runtime/SyncVar.cs.meta deleted file mode 100644 index fffb472..0000000 --- a/Assets/Mirror/Runtime/SyncVar.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 5e87cb681af8459fbbb1f467e1c7632c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncVarGameObject.cs b/Assets/Mirror/Runtime/SyncVarGameObject.cs deleted file mode 100644 index 81961c2..0000000 --- a/Assets/Mirror/Runtime/SyncVarGameObject.cs +++ /dev/null @@ -1,145 +0,0 @@ -// persistent GameObject SyncField which stores .netId internally. -// this is necessary for cases like a player's target. -// the target might run in and out of visibility range and become 'null'. -// but the 'netId' remains and will always point to the monster if around. -// -// NOTE that SyncFieldNetworkIdentity is faster (no .gameObject/GetComponent<>)! -// -// original Weaver code with netId workaround: -/* - // USER: - [SyncVar(hook = "OnTargetChanged")] - public GameObject target; - - // WEAVER: - private uint ___targetNetId; - - public GameObject Networktarget - { - get - { - return GetSyncVarGameObject(___targetNetId, ref target); - } - [param: In] - set - { - if (!NetworkBehaviour.SyncVarGameObjectEqual(value, ___targetNetId)) - { - GameObject networktarget = Networktarget; - SetSyncVarGameObject(value, ref target, 1uL, ref ___targetNetId); - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL)) - { - SetSyncVarHookGuard(1uL, value: true); - OnTargetChanged(networktarget, value); - SetSyncVarHookGuard(1uL, value: false); - } - } - } - } - - private void OnTargetChanged(GameObject old, GameObject value) - { - } -*/ -using System; -using System.Runtime.CompilerServices; -using UnityEngine; - -namespace Mirror -{ - // SyncField only stores an uint netId. - // while providing .spawned lookup for convenience. - // NOTE: server always knows all spawned. consider caching the field again. - public class SyncVarGameObject : SyncVar - { - // .spawned lookup from netId overwrites base uint .Value - public new GameObject Value - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => GetGameObject(base.Value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => base.Value = GetNetId(value); - } - - // OnChanged Callback is for . - // Let's also have one for - public new event Action Callback; - - // overwrite CallCallback to use the GameObject version instead - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void InvokeCallback(uint oldValue, uint newValue) => - Callback?.Invoke(GetGameObject(oldValue), GetGameObject(newValue)); - - // ctor - // 'value = null' so we can do: - // SyncVarGameObject = new SyncVarGameObject() - // instead of - // SyncVarGameObject = new SyncVarGameObject(null); - public SyncVarGameObject(GameObject value = null) - : base(GetNetId(value)) {} - - // helper function to get netId from GameObject (if any) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static uint GetNetId(GameObject go) - { - if (go != null) - { - NetworkIdentity identity = go.GetComponent(); - return identity != null ? identity.netId : 0; - } - return 0; - } - - // helper function to get GameObject from netId (if spawned) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static GameObject GetGameObject(uint netId) - { - NetworkIdentity spawned = Utils.GetSpawnedInServerOrClient(netId); - return spawned != null ? spawned.gameObject : null; - } - - // implicit conversion: GameObject value = SyncFieldGameObject - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator GameObject(SyncVarGameObject field) => field.Value; - - // implicit conversion: SyncFieldGameObject = value - // even if SyncField is readonly, it's still useful: SyncFieldGameObject = target; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator SyncVarGameObject(GameObject value) => new SyncVarGameObject(value); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(SyncVarGameObject a, SyncVarGameObject b) => - a.Value == b.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(SyncVarGameObject a, SyncVarGameObject b) => !(a == b); - - // NOTE: overloading all == operators blocks '== null' checks with an - // "ambiguous invocation" error. that's good. this way user code like - // "player.target == null" won't compile instead of silently failing! - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(SyncVarGameObject a, GameObject b) => - a.Value == b; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(SyncVarGameObject a, GameObject b) => !(a == b); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(GameObject a, SyncVarGameObject b) => - a == b.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(GameObject a, SyncVarGameObject b) => !(a == b); - - // if we overwrite == operators, we also need to overwrite .Equals. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override bool Equals(object obj) => obj is SyncVarGameObject value && this == value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => Value.GetHashCode(); - } -} diff --git a/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta b/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta deleted file mode 100644 index 4e924f0..0000000 --- a/Assets/Mirror/Runtime/SyncVarGameObject.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 84da90dae05442e3a149753c9b25ae98 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs b/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs deleted file mode 100644 index 623b890..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs +++ /dev/null @@ -1,168 +0,0 @@ -// persistent NetworkBehaviour SyncField which stores netId and component index. -// this is necessary for cases like a player's target. -// the target might run in and out of visibility range and become 'null'. -// but the 'netId' remains and will always point to the monster if around. -// (we also store the component index because GameObject can have multiple -// NetworkBehaviours of same type) -// -// original Weaver code was broken because it didn't store by netId. -using System; -using System.Runtime.CompilerServices; - -namespace Mirror -{ - // SyncField needs an uint netId and a byte componentIndex. - // we use an ulong SyncField internally to store both. - // while providing .spawned lookup for convenience. - // NOTE: server always knows all spawned. consider caching the field again. - // to support abstract NetworkBehaviour and classes inheriting from it. - // => hooks can be OnHook(Monster, Monster) instead of OnHook(NB, NB) - // => implicit cast can be to/from Monster instead of only NetworkBehaviour - // => Weaver needs explicit types for hooks too, not just OnHook(NB, NB) - public class SyncVarNetworkBehaviour : SyncVar - where T : NetworkBehaviour - { - // .spawned lookup from netId overwrites base uint .Value - public new T Value - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ULongToNetworkBehaviour(base.Value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => base.Value = NetworkBehaviourToULong(value); - } - - // OnChanged Callback is for . - // Let's also have one for - public new event Action Callback; - - // overwrite CallCallback to use the NetworkIdentity version instead - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void InvokeCallback(ulong oldValue, ulong newValue) => - Callback?.Invoke(ULongToNetworkBehaviour(oldValue), ULongToNetworkBehaviour(newValue)); - - // ctor - // 'value = null' so we can do: - // SyncVarNetworkBehaviour = new SyncVarNetworkBehaviour() - // instead of - // SyncVarNetworkBehaviour = new SyncVarNetworkBehaviour(null); - public SyncVarNetworkBehaviour(T value = null) - : base(NetworkBehaviourToULong(value)) {} - - // implicit conversion: NetworkBehaviour value = SyncFieldNetworkBehaviour - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator T(SyncVarNetworkBehaviour field) => field.Value; - - // implicit conversion: SyncFieldNetworkBehaviour = value - // even if SyncField is readonly, it's still useful: SyncFieldNetworkBehaviour = target; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator SyncVarNetworkBehaviour(T value) => new SyncVarNetworkBehaviour(value); - - // NOTE: overloading all == operators blocks '== null' checks with an - // "ambiguous invocation" error. that's good. this way user code like - // "player.target == null" won't compile instead of silently failing! - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(SyncVarNetworkBehaviour a, SyncVarNetworkBehaviour b) => - a.Value == b.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(SyncVarNetworkBehaviour a, SyncVarNetworkBehaviour b) => !(a == b); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(SyncVarNetworkBehaviour a, NetworkBehaviour b) => - a.Value == b; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(SyncVarNetworkBehaviour a, NetworkBehaviour b) => !(a == b); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(SyncVarNetworkBehaviour a, T b) => - a.Value == b; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(SyncVarNetworkBehaviour a, T b) => !(a == b); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(NetworkBehaviour a, SyncVarNetworkBehaviour b) => - a == b.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(NetworkBehaviour a, SyncVarNetworkBehaviour b) => !(a == b); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(T a, SyncVarNetworkBehaviour b) => - a == b.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(T a, SyncVarNetworkBehaviour b) => !(a == b); - - // if we overwrite == operators, we also need to overwrite .Equals. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override bool Equals(object obj) => obj is SyncVarNetworkBehaviour value && this == value; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => Value.GetHashCode(); - - // helper functions to get/set netId, componentIndex from ulong - // netId on the 4 left bytes. compIndex on the right most byte. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ulong Pack(uint netId, byte componentIndex) => - (ulong)netId << 32 | componentIndex; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void Unpack(ulong value, out uint netId, out byte componentIndex) - { - netId = (uint)(value >> 32); - componentIndex = (byte)(value & 0xFF); - } - - // helper function to find/get NetworkBehaviour to ulong (netId/compIndex) - static T ULongToNetworkBehaviour(ulong value) - { - // unpack ulong to netId, componentIndex - Unpack(value, out uint netId, out byte componentIndex); - - // find spawned NetworkIdentity by netId - NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId); - - // get the nth component - return identity != null ? (T)identity.NetworkBehaviours[componentIndex] : null; - } - - static ulong NetworkBehaviourToULong(T value) - { - // pack netId, componentIndex to ulong - return value != null ? Pack(value.netId, (byte)value.ComponentIndex) : 0; - } - - // Serialize should only write 4+1 bytes, not 8 bytes ulong - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnSerializeAll(NetworkWriter writer) - { - Unpack(base.Value, out uint netId, out byte componentIndex); - writer.WriteUInt(netId); - writer.WriteByte(componentIndex); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnSerializeDelta(NetworkWriter writer) => - OnSerializeAll(writer); - - // Deserialize should only write 4+1 bytes, not 8 bytes ulong - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnDeserializeAll(NetworkReader reader) - { - uint netId = reader.ReadUInt(); - byte componentIndex = reader.ReadByte(); - base.Value = Pack(netId, componentIndex); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override void OnDeserializeDelta(NetworkReader reader) => - OnDeserializeAll(reader); - } -} diff --git a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta b/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta deleted file mode 100644 index 0ceab50..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkBehaviour.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c0fff77f1a624ba8ad6e4bdef6c14a8b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs b/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs deleted file mode 100644 index 0f3fdf2..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs +++ /dev/null @@ -1,118 +0,0 @@ -// persistent NetworkIdentity SyncField which stores .netId internally. -// this is necessary for cases like a player's target. -// the target might run in and out of visibility range and become 'null'. -// but the 'netId' remains and will always point to the monster if around. -// -// original Weaver code with netId workaround: -/* - // USER: - [SyncVar(hook = "OnTargetChanged")] - public NetworkIdentity target; - - // WEAVER GENERATED: - private uint ___targetNetId; - - public NetworkIdentity Networktarget - { - get - { - return GetSyncVarNetworkIdentity(___targetNetId, ref target); - } - [param: In] - set - { - if (!SyncVarNetworkIdentityEqual(value, ___targetNetId)) - { - NetworkIdentity networktarget = Networktarget; - SetSyncVarNetworkIdentity(value, ref target, 1uL, ref ___targetNetId); - if (NetworkServer.localClientActive && !GetSyncVarHookGuard(1uL)) - { - SetSyncVarHookGuard(1uL, value: true); - OnTargetChanged(networktarget, value); - SetSyncVarHookGuard(1uL, value: false); - } - } - } - } -*/ -using System; -using System.Runtime.CompilerServices; - -namespace Mirror -{ - // SyncField only stores an uint netId. - // while providing .spawned lookup for convenience. - // NOTE: server always knows all spawned. consider caching the field again. - public class SyncVarNetworkIdentity : SyncVar - { - // .spawned lookup from netId overwrites base uint .Value - public new NetworkIdentity Value - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Utils.GetSpawnedInServerOrClient(base.Value); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => base.Value = value != null ? value.netId : 0; - } - - // OnChanged Callback is for . - // Let's also have one for - public new event Action Callback; - - // overwrite CallCallback to use the NetworkIdentity version instead - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void InvokeCallback(uint oldValue, uint newValue) => - Callback?.Invoke(Utils.GetSpawnedInServerOrClient(oldValue), Utils.GetSpawnedInServerOrClient(newValue)); - - // ctor - // 'value = null' so we can do: - // SyncVarNetworkIdentity = new SyncVarNetworkIdentity() - // instead of - // SyncVarNetworkIdentity = new SyncVarNetworkIdentity(null); - public SyncVarNetworkIdentity(NetworkIdentity value = null) - : base(value != null ? value.netId : 0) {} - - // implicit conversion: NetworkIdentity value = SyncFieldNetworkIdentity - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator NetworkIdentity(SyncVarNetworkIdentity field) => field.Value; - - // implicit conversion: SyncFieldNetworkIdentity = value - // even if SyncField is readonly, it's still useful: SyncFieldNetworkIdentity = target; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator SyncVarNetworkIdentity(NetworkIdentity value) => new SyncVarNetworkIdentity(value); - - // NOTE: overloading all == operators blocks '== null' checks with an - // "ambiguous invocation" error. that's good. this way user code like - // "player.target == null" won't compile instead of silently failing! - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(SyncVarNetworkIdentity a, SyncVarNetworkIdentity b) => - a.Value == b.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(SyncVarNetworkIdentity a, SyncVarNetworkIdentity b) => !(a == b); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(SyncVarNetworkIdentity a, NetworkIdentity b) => - a.Value == b; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(SyncVarNetworkIdentity a, NetworkIdentity b) => !(a == b); - - // == operator for comparisons like Player.target==monster - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(NetworkIdentity a, SyncVarNetworkIdentity b) => - a == b.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(NetworkIdentity a, SyncVarNetworkIdentity b) => !(a == b); - - // if we overwrite == operators, we also need to overwrite .Equals. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override bool Equals(object obj) => obj is SyncVarNetworkIdentity value && this == value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => Value.GetHashCode(); - } -} diff --git a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta b/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta deleted file mode 100644 index 53271d4..0000000 --- a/Assets/Mirror/Runtime/SyncVarNetworkIdentity.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1f9a6d4d2741477999ad9588261870fe -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta b/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta deleted file mode 100644 index dedea2f..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/MirrorTransport.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7bdb797750d0a490684410110bf48192 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta deleted file mode 100644 index 1d70e80..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/KCP.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 6806a62c384838046a3c66c44f06d75f -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta deleted file mode 100644 index 2a07daa..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/VERSION.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: ed3f2cf1bbf1b4d53a6f2c103d311f71 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta deleted file mode 100644 index 42f163f..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/ErrorCode.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 3abbeffc1d794f11a45b7fcf110353f5 -timeCreated: 1652320712 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs deleted file mode 100644 index 6115bc8..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Net.Sockets; - -namespace kcp2k -{ - public static class Extensions - { - // 100k attempts of 1 KB increases = default + 100 MB max - public static void SetReceiveBufferToOSLimit(this Socket socket, int stepSize = 1024, int attempts = 100_000) - { - // setting a too large size throws a socket exception. - // so let's keep increasing until we encounter it. - for (int i = 0; i < attempts; ++i) - { - // increase in 1 KB steps - try { socket.ReceiveBufferSize += stepSize; } - catch (SocketException) { break; } - } - } - - // 100k attempts of 1 KB increases = default + 100 MB max - public static void SetSendBufferToOSLimit(this Socket socket, int stepSize = 1024, int attempts = 100_000) - { - // setting a too large size throws a socket exception. - // so let's keep increasing until we encounter it. - for (int i = 0; i < attempts; ++i) - { - // increase in 1 KB steps - try { socket.SendBufferSize += stepSize; } - catch (SocketException) { break; } - } - } - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta deleted file mode 100644 index 36d3193..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/Extensions.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: c0649195e5ba4fcf8e0e1231fee7d5f6 -timeCreated: 1641701011 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta deleted file mode 100644 index 2721025..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpChannel.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 9e852b2532fb248d19715cfebe371db3 -timeCreated: 1610081248 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs deleted file mode 100644 index 58249e7..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs +++ /dev/null @@ -1,148 +0,0 @@ -// kcp client logic abstracted into a class. -// for use in Mirror, DOTSNET, testing, etc. -using System; - -namespace kcp2k -{ - public class KcpClient - { - // events - public Action OnConnected; - public Action, KcpChannel> OnData; - public Action OnDisconnected; - // error callback instead of logging. - // allows libraries to show popups etc. - // (string instead of Exception for ease of use and to avoid user panic) - public Action OnError; - - // state - public KcpClientConnection connection; - public bool connected; - - public KcpClient(Action OnConnected, - Action, - KcpChannel> OnData, - Action OnDisconnected, - Action OnError) - { - this.OnConnected = OnConnected; - this.OnData = OnData; - this.OnDisconnected = OnDisconnected; - this.OnError = OnError; - } - - // CreateConnection can be overwritten for where-allocation: - // https://github.com/vis2k/where-allocation - protected virtual KcpClientConnection CreateConnection() => - new KcpClientConnection(); - - public void Connect(string address, - ushort port, - bool noDelay, - uint interval, - int fastResend = 0, - bool congestionWindow = true, - uint sendWindowSize = Kcp.WND_SND, - uint receiveWindowSize = Kcp.WND_RCV, - int timeout = KcpConnection.DEFAULT_TIMEOUT, - uint maxRetransmits = Kcp.DEADLINK, - bool maximizeSendReceiveBuffersToOSLimit = false) - { - if (connected) - { - Log.Warning("KCP: client already connected!"); - return; - } - - // create connection - connection = CreateConnection(); - - // setup events - connection.OnAuthenticated = () => - { - Log.Info($"KCP: OnClientConnected"); - connected = true; - OnConnected(); - }; - connection.OnData = (message, channel) => - { - //Log.Debug($"KCP: OnClientData({BitConverter.ToString(message.Array, message.Offset, message.Count)})"); - OnData(message, channel); - }; - connection.OnDisconnected = () => - { - Log.Info($"KCP: OnClientDisconnected"); - connected = false; - connection = null; - OnDisconnected(); - }; - connection.OnError = (error, reason) => - { - OnError(error, reason); - }; - - // connect - connection.Connect(address, - port, - noDelay, - interval, - fastResend, - congestionWindow, - sendWindowSize, - receiveWindowSize, - timeout, - maxRetransmits, - maximizeSendReceiveBuffersToOSLimit); - } - - public void Send(ArraySegment segment, KcpChannel channel) - { - if (connected) - { - connection.SendData(segment, channel); - } - else Log.Warning("KCP: can't send because client not connected!"); - } - - public void Disconnect() - { - // only if connected - // otherwise we end up in a deadlock because of an open Mirror bug: - // https://github.com/vis2k/Mirror/issues/2353 - if (connected) - { - // call Disconnect and let the connection handle it. - // DO NOT set it to null yet. it needs to be updated a few more - // times first. let the connection handle it! - connection?.Disconnect(); - } - } - - // process incoming messages. should be called before updating the world. - public void TickIncoming() - { - // recv on socket first, then process incoming - // (even if we didn't receive anything. need to tick ping etc.) - // (connection is null if not active) - connection?.RawReceive(); - connection?.TickIncoming(); - } - - // process outgoing messages. should be called after updating the world. - public void TickOutgoing() - { - // process outgoing - // (connection is null if not active) - connection?.TickOutgoing(); - } - - // process incoming and outgoing for convenience - // => ideally call ProcessIncoming() before updating the world and - // ProcessOutgoing() after updating the world for minimum latency - public void Tick() - { - TickIncoming(); - TickOutgoing(); - } - } -} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta deleted file mode 100644 index e55306b..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClient.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 6aa069a28ed24fedb533c102d9742b36 -timeCreated: 1603786960 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs deleted file mode 100644 index a843a8d..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace kcp2k -{ - public class KcpClientConnection : KcpConnection - { - // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even - // if MaxMessageSize is larger. kcp always sends in MTU - // segments and having a buffer smaller than MTU would - // silently drop excess data. - // => we need the MTU to fit channel + message! - readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; - - // helper function to resolve host to IPAddress - public static bool ResolveHostname(string hostname, out IPAddress[] addresses) - { - try - { - // NOTE: dns lookup is blocking. this can take a second. - addresses = Dns.GetHostAddresses(hostname); - return addresses.Length >= 1; - } - catch (SocketException exception) - { - Log.Info($"Failed to resolve host: {hostname} reason: {exception}"); - addresses = null; - return false; - } - } - - // EndPoint & Receive functions can be overwritten for where-allocation: - // https://github.com/vis2k/where-allocation - // NOTE: Client's SendTo doesn't allocate, don't need a virtual. - protected virtual void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) => - remoteEndPoint = new IPEndPoint(addresses[0], port); - - protected virtual int ReceiveFrom(byte[] buffer) => - socket.ReceiveFrom(buffer, ref remoteEndPoint); - - // if connections drop under heavy load, increase to OS limit. - // if still not enough, increase the OS limit. - void ConfigureSocketBufferSizes(bool maximizeSendReceiveBuffersToOSLimit) - { - if (maximizeSendReceiveBuffersToOSLimit) - { - // log initial size for comparison. - // remember initial size for log comparison - int initialReceive = socket.ReceiveBufferSize; - int initialSend = socket.SendBufferSize; - - socket.SetReceiveBufferToOSLimit(); - socket.SetSendBufferToOSLimit(); - Log.Info($"KcpClient: RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x) increased to OS limits!"); - } - // otherwise still log the defaults for info. - else Log.Info($"KcpClient: RecvBuf = {socket.ReceiveBufferSize} SendBuf = {socket.SendBufferSize}. If connections drop under heavy load, enable {nameof(maximizeSendReceiveBuffersToOSLimit)} to increase it to OS limit. If they still drop, increase the OS limit."); - } - - public void Connect(string host, - ushort port, - bool noDelay, - uint interval = Kcp.INTERVAL, - int fastResend = 0, - bool congestionWindow = true, - uint sendWindowSize = Kcp.WND_SND, - uint receiveWindowSize = Kcp.WND_RCV, - int timeout = DEFAULT_TIMEOUT, - uint maxRetransmits = Kcp.DEADLINK, - bool maximizeSendReceiveBuffersToOSLimit = false) - { - Log.Info($"KcpClient: connect to {host}:{port}"); - - // try resolve host name - if (ResolveHostname(host, out IPAddress[] addresses)) - { - // create remote endpoint - CreateRemoteEndPoint(addresses, port); - - // create socket - socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); - - // configure buffer sizes - ConfigureSocketBufferSizes(maximizeSendReceiveBuffersToOSLimit); - - // connect - socket.Connect(remoteEndPoint); - - // set up kcp - SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout, maxRetransmits); - - // client should send handshake to server as very first message - SendHandshake(); - - RawReceive(); - } - // otherwise call OnDisconnected to let the user know. - else - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {host}"); - OnDisconnected(); - } - } - - // call from transport update - public void RawReceive() - { - try - { - if (socket != null) - { - while (socket.Poll(0, SelectMode.SelectRead)) - { - int msgLength = ReceiveFrom(rawReceiveBuffer); - // IMPORTANT: detect if buffer was too small for the - // received msgLength. otherwise the excess - // data would be silently lost. - // (see ReceiveFrom documentation) - if (msgLength <= rawReceiveBuffer.Length) - { - //Log.Debug($"KCP: client raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); - RawInput(rawReceiveBuffer, msgLength); - } - else - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidReceive, $"KCP ClientConnection: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting."); - Disconnect(); - } - } - } - } - // this is fine, the socket might have been closed in the other end - catch (SocketException ex) - { - // the other end closing the connection is not an 'error'. - // but connections should never just end silently. - // at least log a message for easier debugging. - Log.Info($"KCP ClientConnection: looks like the other end has closed the connection. This is fine: {ex}"); - Disconnect(); - } - } - - protected override void Dispose() - { - socket.Close(); - socket = null; - } - - protected override void RawSend(byte[] data, int length) - { - socket.Send(data, length, SocketFlags.None); - } - } -} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta deleted file mode 100644 index 3369918..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpClientConnection.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 96512e74aa8214a6faa8a412a7a07877 -timeCreated: 1602601237 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs deleted file mode 100644 index e5bc0f3..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs +++ /dev/null @@ -1,668 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; - -namespace kcp2k -{ - enum KcpState { Connected, Authenticated, Disconnected } - - public abstract class KcpConnection - { - protected Socket socket; - protected EndPoint remoteEndPoint; - internal Kcp kcp; - - // kcp can have several different states, let's use a state machine - KcpState state = KcpState.Disconnected; - - public Action OnAuthenticated; - public Action, KcpChannel> OnData; - public Action OnDisconnected; - // error callback instead of logging. - // allows libraries to show popups etc. - // (string instead of Exception for ease of use and to avoid user panic) - public Action OnError; - - // If we don't receive anything these many milliseconds - // then consider us disconnected - public const int DEFAULT_TIMEOUT = 10000; - public int timeout = DEFAULT_TIMEOUT; - uint lastReceiveTime; - - // internal time. - // StopWatch offers ElapsedMilliSeconds and should be more precise than - // Unity's time.deltaTime over long periods. - readonly Stopwatch refTime = new Stopwatch(); - - // we need to subtract the channel byte from every MaxMessageSize - // calculation. - // we also need to tell kcp to use MTU-1 to leave space for the byte. - const int CHANNEL_HEADER_SIZE = 1; - - // reliable channel (= kcp) MaxMessageSize so the outside knows largest - // allowed message to send. the calculation in Send() is not obvious at - // all, so let's provide the helper here. - // - // kcp does fragmentation, so max message is way larger than MTU. - // - // -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD - // -> Send() checks if fragment count < rcv_wnd, so we use rcv_wnd - 1. - // NOTE that original kcp has a bug where WND_RCV default is used - // instead of configured rcv_wnd, limiting max message size to 144 KB - // https://github.com/skywind3000/kcp/pull/291 - // we fixed this in kcp2k. - // -> we add 1 byte KcpHeader enum to each message, so -1 - // - // IMPORTANT: max message is MTU * rcv_wnd, in other words it completely - // fills the receive window! due to head of line blocking, - // all other messages have to wait while a maxed size message - // is being delivered. - // => in other words, DO NOT use max size all the time like - // for batching. - // => sending UNRELIABLE max message size most of the time is - // best for performance (use that one for batching!) - static int ReliableMaxMessageSize_Unconstrained(uint rcv_wnd) => (Kcp.MTU_DEF - Kcp.OVERHEAD - CHANNEL_HEADER_SIZE) * ((int)rcv_wnd - 1) - 1; - - // kcp encodes 'frg' as 1 byte. - // max message size can only ever allow up to 255 fragments. - // WND_RCV gives 127 fragments. - // WND_RCV * 2 gives 255 fragments. - // so we can limit max message size by limiting rcv_wnd parameter. - public static int ReliableMaxMessageSize(uint rcv_wnd) => - ReliableMaxMessageSize_Unconstrained(Math.Min(rcv_wnd, Kcp.FRG_MAX)); - - // unreliable max message size is simply MTU - channel header size - public const int UnreliableMaxMessageSize = Kcp.MTU_DEF - CHANNEL_HEADER_SIZE; - - // buffer to receive kcp's processed messages (avoids allocations). - // IMPORTANT: this is for KCP messages. so it needs to be of size: - // 1 byte header + MaxMessageSize content - byte[] kcpMessageBuffer;// = new byte[1 + ReliableMaxMessageSize]; - - // send buffer for handing user messages to kcp for processing. - // (avoids allocations). - // IMPORTANT: needs to be of size: - // 1 byte header + MaxMessageSize content - byte[] kcpSendBuffer;// = new byte[1 + ReliableMaxMessageSize]; - - // raw send buffer is exactly MTU. - byte[] rawSendBuffer = new byte[Kcp.MTU_DEF]; - - // send a ping occasionally so we don't time out on the other end. - // for example, creating a character in an MMO could easily take a - // minute of no data being sent. which doesn't mean we want to time out. - // same goes for slow paced card games etc. - public const int PING_INTERVAL = 1000; - uint lastPingTime; - - // if we send more than kcp can handle, we will get ever growing - // send/recv buffers and queues and minutes of latency. - // => if a connection can't keep up, it should be disconnected instead - // to protect the server under heavy load, and because there is no - // point in growing to gigabytes of memory or minutes of latency! - // => 2k isn't enough. we reach 2k when spawning 4k monsters at once - // easily, but it does recover over time. - // => 10k seems safe. - // - // note: we have a ChokeConnectionAutoDisconnects test for this too! - internal const int QueueDisconnectThreshold = 10000; - - // getters for queue and buffer counts, used for debug info - public int SendQueueCount => kcp.snd_queue.Count; - public int ReceiveQueueCount => kcp.rcv_queue.Count; - public int SendBufferCount => kcp.snd_buf.Count; - public int ReceiveBufferCount => kcp.rcv_buf.Count; - - // maximum send rate per second can be calculated from kcp parameters - // source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html - // - // KCP can send/receive a maximum of WND*MTU per interval. - // multiple by 1000ms / interval to get the per-second rate. - // - // example: - // WND(32) * MTU(1400) = 43.75KB - // => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s - // - // returns bytes/second! - public uint MaxSendRate => - kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval; - - public uint MaxReceiveRate => - kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval; - - // SetupKcp creates and configures a new KCP instance. - // => useful to start from a fresh state every time the client connects - // => NoDelay, interval, wnd size are the most important configurations. - // let's force require the parameters so we don't forget it anywhere. - protected void SetupKcp(bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT, uint maxRetransmits = Kcp.DEADLINK) - { - // set up kcp over reliable channel (that's what kcp is for) - kcp = new Kcp(0, RawSendReliable); - // set nodelay. - // note that kcp uses 'nocwnd' internally so we negate the parameter - kcp.SetNoDelay(noDelay ? 1u : 0u, interval, fastResend, !congestionWindow); - kcp.SetWindowSize(sendWindowSize, receiveWindowSize); - - // IMPORTANT: high level needs to add 1 channel byte to each raw - // message. so while Kcp.MTU_DEF is perfect, we actually need to - // tell kcp to use MTU-1 so we can still put the header into the - // message afterwards. - kcp.SetMtu(Kcp.MTU_DEF - CHANNEL_HEADER_SIZE); - - // set maximum retransmits (aka dead_link) - kcp.dead_link = maxRetransmits; - - // create message buffers AFTER window size is set - // see comments on buffer definition for the "+1" part - kcpMessageBuffer = new byte[1 + ReliableMaxMessageSize(receiveWindowSize)]; - kcpSendBuffer = new byte[1 + ReliableMaxMessageSize(receiveWindowSize)]; - - this.timeout = timeout; - state = KcpState.Connected; - - refTime.Start(); - } - - void HandleTimeout(uint time) - { - // note: we are also sending a ping regularly, so timeout should - // only ever happen if the connection is truly gone. - if (time >= lastReceiveTime + timeout) - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.Timeout, $"KCP: Connection timed out after not receiving any message for {timeout}ms. Disconnecting."); - Disconnect(); - } - } - - void HandleDeadLink() - { - // kcp has 'dead_link' detection. might as well use it. - if (kcp.state == -1) - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.Timeout, $"KCP Connection dead_link detected: a message was retransmitted {kcp.dead_link} times without ack. Disconnecting."); - Disconnect(); - } - } - - // send a ping occasionally in order to not time out on the other end. - void HandlePing(uint time) - { - // enough time elapsed since last ping? - if (time >= lastPingTime + PING_INTERVAL) - { - // ping again and reset time - //Log.Debug("KCP: sending ping..."); - SendPing(); - lastPingTime = time; - } - } - - void HandleChoked() - { - // disconnect connections that can't process the load. - // see QueueSizeDisconnect comments. - // => include all of kcp's buffers and the unreliable queue! - int total = kcp.rcv_queue.Count + kcp.snd_queue.Count + - kcp.rcv_buf.Count + kcp.snd_buf.Count; - if (total >= QueueDisconnectThreshold) - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.Congestion, - $"KCP: disconnecting connection because it can't process data fast enough.\n" + - $"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" + - $"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" + - $"* Or perhaps the network is simply too slow on our end, or on the other end."); - - // let's clear all pending sends before disconnting with 'Bye'. - // otherwise a single Flush in Disconnect() won't be enough to - // flush thousands of messages to finally deliver 'Bye'. - // this is just faster and more robust. - kcp.snd_queue.Clear(); - - Disconnect(); - } - } - - // reads the next reliable message type & content from kcp. - // -> to avoid buffering, unreliable messages call OnData directly. - bool ReceiveNextReliable(out KcpHeader header, out ArraySegment message) - { - int msgSize = kcp.PeekSize(); - if (msgSize > 0) - { - // only allow receiving up to buffer sized messages. - // otherwise we would get BlockCopy ArgumentException anyway. - if (msgSize <= kcpMessageBuffer.Length) - { - // receive from kcp - int received = kcp.Receive(kcpMessageBuffer, msgSize); - if (received >= 0) - { - // extract header & content without header - header = (KcpHeader)kcpMessageBuffer[0]; - message = new ArraySegment(kcpMessageBuffer, 1, msgSize - 1); - lastReceiveTime = (uint)refTime.ElapsedMilliseconds; - return true; - } - else - { - // if receive failed, close everything - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidReceive, $"Receive failed with error={received}. closing connection."); - Disconnect(); - } - } - // we don't allow sending messages > Max, so this must be an - // attacker. let's disconnect to avoid allocation attacks etc. - else - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidReceive, $"KCP: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection."); - Disconnect(); - } - } - - message = default; - header = KcpHeader.Disconnect; - return false; - } - - void TickIncoming_Connected(uint time) - { - // detect common events & ping - HandleTimeout(time); - HandleDeadLink(); - HandlePing(time); - HandleChoked(); - - // any reliable kcp message received? - if (ReceiveNextReliable(out KcpHeader header, out ArraySegment message)) - { - // message type FSM. no default so we never miss a case. - switch (header) - { - case KcpHeader.Handshake: - { - // we were waiting for a handshake. - // it proves that the other end speaks our protocol. - Log.Info("KCP: received handshake"); - state = KcpState.Authenticated; - OnAuthenticated?.Invoke(); - break; - } - case KcpHeader.Ping: - { - // ping keeps kcp from timing out. do nothing. - break; - } - case KcpHeader.Data: - case KcpHeader.Disconnect: - { - // everything else is not allowed during handshake! - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidReceive, $"KCP: received invalid header {header} while Connected. Disconnecting the connection."); - Disconnect(); - break; - } - } - } - } - - void TickIncoming_Authenticated(uint time) - { - // detect common events & ping - HandleTimeout(time); - HandleDeadLink(); - HandlePing(time); - HandleChoked(); - - // process all received messages - while (ReceiveNextReliable(out KcpHeader header, out ArraySegment message)) - { - // message type FSM. no default so we never miss a case. - switch (header) - { - case KcpHeader.Handshake: - { - // should never receive another handshake after auth - Log.Warning($"KCP: received invalid header {header} while Authenticated. Disconnecting the connection."); - Disconnect(); - break; - } - case KcpHeader.Data: - { - // call OnData IF the message contained actual data - if (message.Count > 0) - { - //Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}"); - OnData?.Invoke(message, KcpChannel.Reliable); - } - // empty data = attacker, or something went wrong - else - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidReceive, "KCP: received empty Data message while Authenticated. Disconnecting the connection."); - Disconnect(); - } - break; - } - case KcpHeader.Ping: - { - // ping keeps kcp from timing out. do nothing. - break; - } - case KcpHeader.Disconnect: - { - // disconnect might happen - Log.Info("KCP: received disconnect message"); - Disconnect(); - break; - } - } - } - } - - public void TickIncoming() - { - uint time = (uint)refTime.ElapsedMilliseconds; - - try - { - switch (state) - { - case KcpState.Connected: - { - TickIncoming_Connected(time); - break; - } - case KcpState.Authenticated: - { - TickIncoming_Authenticated(time); - break; - } - case KcpState.Disconnected: - { - // do nothing while disconnected - break; - } - } - } - catch (SocketException exception) - { - // this is ok, the connection was closed - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); - Disconnect(); - } - catch (ObjectDisposedException exception) - { - // fine, socket was closed - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); - Disconnect(); - } - catch (Exception exception) - { - // unexpected - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.Unexpected, $"KCP Connection: unexpected Exception: {exception}"); - Disconnect(); - } - } - - public void TickOutgoing() - { - uint time = (uint)refTime.ElapsedMilliseconds; - - try - { - switch (state) - { - case KcpState.Connected: - case KcpState.Authenticated: - { - // update flushes out messages - kcp.Update(time); - break; - } - case KcpState.Disconnected: - { - // do nothing while disconnected - break; - } - } - } - catch (SocketException exception) - { - // this is ok, the connection was closed - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); - Disconnect(); - } - catch (ObjectDisposedException exception) - { - // fine, socket was closed - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.ConnectionClosed, $"KCP Connection: Disconnecting because {exception}. This is fine."); - Disconnect(); - } - catch (Exception exception) - { - // unexpected - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.Unexpected, $"KCP Connection: unexpected exception: {exception}"); - Disconnect(); - } - } - - public void RawInput(byte[] buffer, int msgLength) - { - // parse channel - if (msgLength > 0) - { - byte channel = buffer[0]; - switch (channel) - { - case (byte)KcpChannel.Reliable: - { - // input into kcp, but skip channel byte - int input = kcp.Input(buffer, 1, msgLength - 1); - if (input != 0) - { - Log.Warning($"Input failed with error={input} for buffer with length={msgLength - 1}"); - } - break; - } - case (byte)KcpChannel.Unreliable: - { - // ideally we would queue all unreliable messages and - // then process them in ReceiveNext() together with the - // reliable messages, but: - // -> queues/allocations/pools are slow and complex. - // -> DOTSNET 10k is actually slower if we use pooled - // unreliable messages for transform messages. - // - // DOTSNET 10k benchmark: - // reliable-only: 170 FPS - // unreliable queued: 130-150 FPS - // unreliable direct: 183 FPS(!) - // - // DOTSNET 50k benchmark: - // reliable-only: FAILS (queues keep growing) - // unreliable direct: 18-22 FPS(!) - // - // -> all unreliable messages are DATA messages anyway. - // -> let's skip the magic and call OnData directly if - // the current state allows it. - if (state == KcpState.Authenticated) - { - ArraySegment message = new ArraySegment(buffer, 1, msgLength - 1); - OnData?.Invoke(message, KcpChannel.Unreliable); - - // set last receive time to avoid timeout. - // -> we do this in ANY case even if not enabled. - // a message is a message. - // -> we set last receive time for both reliable and - // unreliable messages. both count. - // otherwise a connection might time out even - // though unreliable were received, but no - // reliable was received. - lastReceiveTime = (uint)refTime.ElapsedMilliseconds; - } - else - { - // should never happen - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidReceive, $"KCP: received unreliable message in state {state}. Disconnecting the connection."); - Disconnect(); - } - break; - } - default: - { - // not a valid channel. random data or attacks. - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidReceive, $"Disconnecting connection because of invalid channel header: {channel}"); - Disconnect(); - break; - } - } - } - } - - // raw send puts the data into the socket - protected abstract void RawSend(byte[] data, int length); - - // raw send called by kcp - void RawSendReliable(byte[] data, int length) - { - // copy channel header, data into raw send buffer, then send - rawSendBuffer[0] = (byte)KcpChannel.Reliable; - Buffer.BlockCopy(data, 0, rawSendBuffer, 1, length); - RawSend(rawSendBuffer, length + 1); - } - - void SendReliable(KcpHeader header, ArraySegment content) - { - // 1 byte header + content needs to fit into send buffer - if (1 + content.Count <= kcpSendBuffer.Length) // TODO - { - // copy header, content (if any) into send buffer - kcpSendBuffer[0] = (byte)header; - if (content.Count > 0) - Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count); - - // send to kcp for processing - int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count); - if (sent < 0) - { - Log.Warning($"Send failed with error={sent} for content with length={content.Count}"); - } - } - // otherwise content is larger than MaxMessageSize. let user know! - else Log.Error($"Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={ReliableMaxMessageSize(kcp.rcv_wnd)}"); - } - - void SendUnreliable(ArraySegment message) - { - // message size needs to be <= unreliable max size - if (message.Count <= UnreliableMaxMessageSize) - { - // copy channel header, data into raw send buffer, then send - rawSendBuffer[0] = (byte)KcpChannel.Unreliable; - Buffer.BlockCopy(message.Array, message.Offset, rawSendBuffer, 1, message.Count); - RawSend(rawSendBuffer, message.Count + 1); - } - // otherwise content is larger than MaxMessageSize. let user know! - else Log.Error($"Failed to send unreliable message of size {message.Count} because it's larger than UnreliableMaxMessageSize={UnreliableMaxMessageSize}"); - } - - // server & client need to send handshake at different times, so we need - // to expose the function. - // * client should send it immediately. - // * server should send it as reply to client's handshake, not before - // (server should not reply to random internet messages with handshake) - // => handshake info needs to be delivered, so it goes over reliable. - public void SendHandshake() - { - Log.Info("KcpConnection: sending Handshake to other end!"); - SendReliable(KcpHeader.Handshake, default); - } - - public void SendData(ArraySegment data, KcpChannel channel) - { - // sending empty segments is not allowed. - // nobody should ever try to send empty data. - // it means that something went wrong, e.g. in Mirror/DOTSNET. - // let's make it obvious so it's easy to debug. - if (data.Count == 0) - { - // pass error to user callback. no need to log it manually. - OnError(ErrorCode.InvalidSend, "KcpConnection: tried sending empty message. This should never happen. Disconnecting."); - Disconnect(); - return; - } - - switch (channel) - { - case KcpChannel.Reliable: - SendReliable(KcpHeader.Data, data); - break; - case KcpChannel.Unreliable: - SendUnreliable(data); - break; - } - } - - // ping goes through kcp to keep it from timing out, so it goes over the - // reliable channel. - void SendPing() => SendReliable(KcpHeader.Ping, default); - - // disconnect info needs to be delivered, so it goes over reliable - void SendDisconnect() => SendReliable(KcpHeader.Disconnect, default); - - protected virtual void Dispose() {} - - // disconnect this connection - public void Disconnect() - { - // only if not disconnected yet - if (state == KcpState.Disconnected) - return; - - // send a disconnect message - if (socket.Connected) - { - try - { - SendDisconnect(); - kcp.Flush(); - } - catch (SocketException) - { - // this is ok, the connection was already closed - } - catch (ObjectDisposedException) - { - // this is normal when we stop the server - // the socket is stopped so we can't send anything anymore - // to the clients - - // the clients will eventually timeout and realize they - // were disconnected - } - } - - // set as Disconnected, call event - Log.Info("KCP Connection: Disconnected."); - state = KcpState.Disconnected; - OnDisconnected?.Invoke(); - } - - // get remote endpoint - public EndPoint GetRemoteEndPoint() => remoteEndPoint; - } -} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta deleted file mode 100644 index fa5dcff..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpConnection.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 3915c7c62b72d4dc2a9e4e76c94fc484 -timeCreated: 1602600432 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs deleted file mode 100644 index bc4b047..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace kcp2k -{ - // header for messages processed by kcp. - // this is NOT for the raw receive messages(!) because handshake/disconnect - // need to be sent reliably. it's not enough to have those in rawreceive - // because those messages might get lost without being resent! - public enum KcpHeader : byte - { - // don't react on 0x00. might help to filter out random noise. - Handshake = 0x01, - // ping goes over reliable & KcpHeader for now. could go over reliable - // too. there is no real difference except that this is easier because - // we already have a KcpHeader for reliable messages. - // ping is only used to keep it alive, so latency doesn't matter. - Ping = 0x02, - Data = 0x03, - Disconnect = 0x04 - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta deleted file mode 100644 index 9e81c94..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpHeader.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 91b5edac31224a49bd76f960ae018942 -timeCreated: 1610081248 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs deleted file mode 100644 index 5e48688..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs +++ /dev/null @@ -1,375 +0,0 @@ -// kcp server logic abstracted into a class. -// for use in Mirror, DOTSNET, testing, etc. -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; - -namespace kcp2k -{ - public class KcpServer - { - // events - public Action OnConnected; - public Action, KcpChannel> OnData; - public Action OnDisconnected; - // error callback instead of logging. - // allows libraries to show popups etc. - // (string instead of Exception for ease of use and to avoid user panic) - public Action OnError; - - // socket configuration - // DualMode uses both IPv6 and IPv4. not all platforms support it. - // (Nintendo Switch, etc.) - public bool DualMode; - // too small send/receive buffers might cause connection drops under - // heavy load. using the OS max size can make a difference already. - public bool MaximizeSendReceiveBuffersToOSLimit; - - // kcp configuration - // NoDelay is recommended to reduce latency. This also scales better - // without buffers getting full. - public bool NoDelay; - // KCP internal update interval. 100ms is KCP default, but a lower - // interval is recommended to minimize latency and to scale to more - // networked entities. - public uint Interval; - // KCP fastresend parameter. Faster resend for the cost of higher - // bandwidth. - public int FastResend; - // KCP 'NoCongestionWindow' is false by default. here we negate it for - // ease of use. This can be disabled for high scale games if connections - // choke regularly. - public bool CongestionWindow; - // KCP window size can be modified to support higher loads. - // for example, Mirror Benchmark requires: - // 128, 128 for 4k monsters - // 512, 512 for 10k monsters - // 8192, 8192 for 20k monsters - public uint SendWindowSize; - public uint ReceiveWindowSize; - // timeout in milliseconds - public int Timeout; - // maximum retransmission attempts until dead_link - public uint MaxRetransmits; - - // state - protected Socket socket; - EndPoint newClientEP; - - // IMPORTANT: raw receive buffer always needs to be of 'MTU' size, even - // if MaxMessageSize is larger. kcp always sends in MTU - // segments and having a buffer smaller than MTU would - // silently drop excess data. - // => we need the mtu to fit channel + message! - readonly byte[] rawReceiveBuffer = new byte[Kcp.MTU_DEF]; - - // connections where connectionId is EndPoint.GetHashCode - public Dictionary connections = new Dictionary(); - - public KcpServer(Action OnConnected, - Action, KcpChannel> OnData, - Action OnDisconnected, - Action OnError, - bool DualMode, - bool NoDelay, - uint Interval, - int FastResend = 0, - bool CongestionWindow = true, - uint SendWindowSize = Kcp.WND_SND, - uint ReceiveWindowSize = Kcp.WND_RCV, - int Timeout = KcpConnection.DEFAULT_TIMEOUT, - uint MaxRetransmits = Kcp.DEADLINK, - bool MaximizeSendReceiveBuffersToOSLimit = false) - { - this.OnConnected = OnConnected; - this.OnData = OnData; - this.OnDisconnected = OnDisconnected; - this.OnError = OnError; - this.DualMode = DualMode; - this.NoDelay = NoDelay; - this.Interval = Interval; - this.FastResend = FastResend; - this.CongestionWindow = CongestionWindow; - this.SendWindowSize = SendWindowSize; - this.ReceiveWindowSize = ReceiveWindowSize; - this.Timeout = Timeout; - this.MaxRetransmits = MaxRetransmits; - this.MaximizeSendReceiveBuffersToOSLimit = MaximizeSendReceiveBuffersToOSLimit; - - // create newClientEP either IPv4 or IPv6 - newClientEP = DualMode - ? new IPEndPoint(IPAddress.IPv6Any, 0) - : new IPEndPoint(IPAddress.Any, 0); - } - - public bool IsActive() => socket != null; - - // if connections drop under heavy load, increase to OS limit. - // if still not enough, increase the OS limit. - void ConfigureSocketBufferSizes() - { - if (MaximizeSendReceiveBuffersToOSLimit) - { - // log initial size for comparison. - // remember initial size for log comparison - int initialReceive = socket.ReceiveBufferSize; - int initialSend = socket.SendBufferSize; - - socket.SetReceiveBufferToOSLimit(); - socket.SetSendBufferToOSLimit(); - Log.Info($"KcpServer: RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x) increased to OS limits!"); - } - // otherwise still log the defaults for info. - else Log.Info($"KcpServer: RecvBuf = {socket.ReceiveBufferSize} SendBuf = {socket.SendBufferSize}. If connections drop under heavy load, enable {nameof(MaximizeSendReceiveBuffersToOSLimit)} to increase it to OS limit. If they still drop, increase the OS limit."); - } - - public void Start(ushort port) - { - // only start once - if (socket != null) - { - Log.Warning("KCP: server already started!"); - } - - // listen - if (DualMode) - { - // IPv6 socket with DualMode - socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - socket.DualMode = true; - socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port)); - } - else - { - // IPv4 socket - socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.Bind(new IPEndPoint(IPAddress.Any, port)); - } - - // configure socket buffer size. - ConfigureSocketBufferSizes(); - } - - public void Send(int connectionId, ArraySegment segment, KcpChannel channel) - { - if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) - { - connection.SendData(segment, channel); - } - } - - public void Disconnect(int connectionId) - { - if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) - { - connection.Disconnect(); - } - } - - // expose the whole IPEndPoint, not just the IP address. some need it. - public IPEndPoint GetClientEndPoint(int connectionId) - { - if (connections.TryGetValue(connectionId, out KcpServerConnection connection)) - { - return (connection.GetRemoteEndPoint() as IPEndPoint); - } - return null; - } - - // EndPoint & Receive functions can be overwritten for where-allocation: - // https://github.com/vis2k/where-allocation - protected virtual int ReceiveFrom(byte[] buffer, out int connectionHash) - { - // NOTE: ReceiveFrom allocates. - // we pass our IPEndPoint to ReceiveFrom. - // receive from calls newClientEP.Create(socketAddr). - // IPEndPoint.Create always returns a new IPEndPoint. - // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 - int read = socket.ReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref newClientEP); - - // calculate connectionHash from endpoint - // NOTE: IPEndPoint.GetHashCode() allocates. - // it calls m_Address.GetHashCode(). - // m_Address is an IPAddress. - // GetHashCode() allocates for IPv6: - // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 - // - // => using only newClientEP.Port wouldn't work, because - // different connections can have the same port. - connectionHash = newClientEP.GetHashCode(); - return read; - } - - protected virtual KcpServerConnection CreateConnection() => - new KcpServerConnection(socket, newClientEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmits); - - // process incoming messages. should be called before updating the world. - HashSet connectionsToRemove = new HashSet(); - public void TickIncoming() - { - while (socket != null && socket.Poll(0, SelectMode.SelectRead)) - { - try - { - // receive - int msgLength = ReceiveFrom(rawReceiveBuffer, out int connectionId); - //Log.Info($"KCP: server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}"); - - // IMPORTANT: detect if buffer was too small for the received - // msgLength. otherwise the excess data would be - // silently lost. - // (see ReceiveFrom documentation) - if (msgLength <= rawReceiveBuffer.Length) - { - // is this a new connection? - if (!connections.TryGetValue(connectionId, out KcpServerConnection connection)) - { - // create a new KcpConnection based on last received - // EndPoint. can be overwritten for where-allocation. - connection = CreateConnection(); - - // DO NOT add to connections yet. only if the first message - // is actually the kcp handshake. otherwise it's either: - // * random data from the internet - // * or from a client connection that we just disconnected - // but that hasn't realized it yet, still sending data - // from last session that we should absolutely ignore. - // - // - // TODO this allocates a new KcpConnection for each new - // internet connection. not ideal, but C# UDP Receive - // already allocated anyway. - // - // expecting a MAGIC byte[] would work, but sending the raw - // UDP message without kcp's reliability will have low - // probability of being received. - // - // for now, this is fine. - - // setup authenticated event that also adds to connections - connection.OnAuthenticated = () => - { - // only send handshake to client AFTER we received his - // handshake in OnAuthenticated. - // we don't want to reply to random internet messages - // with handshakes each time. - connection.SendHandshake(); - - // add to connections dict after being authenticated. - connections.Add(connectionId, connection); - Log.Info($"KCP: server added connection({connectionId})"); - - // setup Data + Disconnected events only AFTER the - // handshake. we don't want to fire OnServerDisconnected - // every time we receive invalid random data from the - // internet. - - // setup data event - connection.OnData = (message, channel) => - { - // call mirror event - //Log.Info($"KCP: OnServerDataReceived({connectionId}, {BitConverter.ToString(message.Array, message.Offset, message.Count)})"); - OnData.Invoke(connectionId, message, channel); - }; - - // setup disconnected event - connection.OnDisconnected = () => - { - // flag for removal - // (can't remove directly because connection is updated - // and event is called while iterating all connections) - connectionsToRemove.Add(connectionId); - - // call mirror event - Log.Info($"KCP: OnServerDisconnected({connectionId})"); - OnDisconnected(connectionId); - }; - - // setup error event - connection.OnError = (error, reason) => - { - OnError(connectionId, error, reason); - }; - - // finally, call mirror OnConnected event - Log.Info($"KCP: OnServerConnected({connectionId})"); - OnConnected(connectionId); - }; - - // now input the message & process received ones - // connected event was set up. - // tick will process the first message and adds the - // connection if it was the handshake. - connection.RawInput(rawReceiveBuffer, msgLength); - connection.TickIncoming(); - - // again, do not add to connections. - // if the first message wasn't the kcp handshake then - // connection will simply be garbage collected. - } - // existing connection: simply input the message into kcp - else - { - connection.RawInput(rawReceiveBuffer, msgLength); - } - } - else - { - Log.Error($"KCP Server: message of size {msgLength} does not fit into buffer of size {rawReceiveBuffer.Length}. The excess was silently dropped. Disconnecting connectionId={connectionId}."); - Disconnect(connectionId); - } - } - // this is fine, the socket might have been closed in the other end - catch (SocketException ex) - { - // the other end closing the connection is not an 'error'. - // but connections should never just end silently. - // at least log a message for easier debugging. - Log.Info($"KCP ClientConnection: looks like the other end has closed the connection. This is fine: {ex}"); - } - } - - // process inputs for all server connections - // (even if we didn't receive anything. need to tick ping etc.) - foreach (KcpServerConnection connection in connections.Values) - { - connection.TickIncoming(); - } - - // remove disconnected connections - // (can't do it in connection.OnDisconnected because Tick is called - // while iterating connections) - foreach (int connectionId in connectionsToRemove) - { - connections.Remove(connectionId); - } - connectionsToRemove.Clear(); - } - - // process outgoing messages. should be called after updating the world. - public void TickOutgoing() - { - // flush all server connections - foreach (KcpServerConnection connection in connections.Values) - { - connection.TickOutgoing(); - } - } - - // process incoming and outgoing for convenience. - // => ideally call ProcessIncoming() before updating the world and - // ProcessOutgoing() after updating the world for minimum latency - public void Tick() - { - TickIncoming(); - TickOutgoing(); - } - - public void Stop() - { - socket?.Close(); - socket = null; - } - } -} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta deleted file mode 100644 index ef720d4..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServer.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 9759159c6589494a9037f5e130a867ed -timeCreated: 1603787747 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs deleted file mode 100644 index a902865..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace kcp2k -{ - public class KcpServerConnection : KcpConnection - { - // Constructor & Send functions can be overwritten for where-allocation: - // https://github.com/vis2k/where-allocation - public KcpServerConnection(Socket socket, EndPoint remoteEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT, uint maxRetransmits = Kcp.DEADLINK) - { - this.socket = socket; - this.remoteEndPoint = remoteEndPoint; - SetupKcp(noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout, maxRetransmits); - } - - protected override void RawSend(byte[] data, int length) - { - socket.SendTo(data, 0, length, SocketFlags.None, remoteEndPoint); - } - } -} diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta deleted file mode 100644 index 10d9803..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 80a9b1ce9a6f14abeb32bfa9921d097b -timeCreated: 1602601483 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta deleted file mode 100644 index 4cbc909..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 0b320ff06046474eae7bce7240ea478c -timeCreated: 1626430641 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs deleted file mode 100644 index b3e1b27..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs +++ /dev/null @@ -1,24 +0,0 @@ -// where-allocation version of KcpClientConnection. -// may not be wanted on all platforms, so it's an extra optional class. -using System.Net; -using WhereAllocation; - -namespace kcp2k -{ - public class KcpClientConnectionNonAlloc : KcpClientConnection - { - IPEndPointNonAlloc reusableEP; - - protected override void CreateRemoteEndPoint(IPAddress[] addresses, ushort port) - { - // create reusableEP with same address family as remoteEndPoint. - // otherwise ReceiveFrom_NonAlloc couldn't use it. - reusableEP = new IPEndPointNonAlloc(addresses[0], port); - base.CreateRemoteEndPoint(addresses, port); - } - - // where-allocation nonalloc recv - protected override int ReceiveFrom(byte[] buffer) => - socket.ReceiveFrom_NonAlloc(buffer, reusableEP); - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta deleted file mode 100644 index 9d4a42e..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientConnectionNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 4c1b235bbe054706bef6d092f361006e -timeCreated: 1626430539 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs deleted file mode 100644 index 2417408..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs +++ /dev/null @@ -1,20 +0,0 @@ -// where-allocation version of KcpClientConnectionNonAlloc. -// may not be wanted on all platforms, so it's an extra optional class. -using System; - -namespace kcp2k -{ - public class KcpClientNonAlloc : KcpClient - { - public KcpClientNonAlloc(Action OnConnected, - Action, KcpChannel> OnData, - Action OnDisconnected, - Action OnError) - : base(OnConnected, OnData, OnDisconnected, OnError) - { - } - - protected override KcpClientConnection CreateConnection() => - new KcpClientConnectionNonAlloc(); - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta deleted file mode 100644 index 266dafb..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpClientNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 2cf0ccf7d551480bb5af08fcbe169f84 -timeCreated: 1626435264 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs deleted file mode 100644 index 7986bea..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs +++ /dev/null @@ -1,25 +0,0 @@ -// where-allocation version of KcpServerConnection. -// may not be wanted on all platforms, so it's an extra optional class. -using System.Net; -using System.Net.Sockets; -using WhereAllocation; - -namespace kcp2k -{ - public class KcpServerConnectionNonAlloc : KcpServerConnection - { - IPEndPointNonAlloc reusableSendEndPoint; - - public KcpServerConnectionNonAlloc(Socket socket, EndPoint remoteEndpoint, IPEndPointNonAlloc reusableSendEndPoint, bool noDelay, uint interval = Kcp.INTERVAL, int fastResend = 0, bool congestionWindow = true, uint sendWindowSize = Kcp.WND_SND, uint receiveWindowSize = Kcp.WND_RCV, int timeout = DEFAULT_TIMEOUT, uint maxRetransmits = Kcp.DEADLINK) - : base(socket, remoteEndpoint, noDelay, interval, fastResend, congestionWindow, sendWindowSize, receiveWindowSize, timeout, maxRetransmits) - { - this.reusableSendEndPoint = reusableSendEndPoint; - } - - protected override void RawSend(byte[] data, int length) - { - // where-allocation nonalloc send - socket.SendTo_NonAlloc(data, 0, length, SocketFlags.None, reusableSendEndPoint); - } - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta deleted file mode 100644 index 383fe02..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerConnectionNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 4e1b74cc224b4c83a0f6c8d8da9090ab -timeCreated: 1626430608 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs deleted file mode 100644 index 001a64b..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs +++ /dev/null @@ -1,77 +0,0 @@ -// where-allocation version of KcpServer. -// may not be wanted on all platforms, so it's an extra optional class. -using System; -using System.Net; -using System.Net.Sockets; -using WhereAllocation; - -namespace kcp2k -{ - public class KcpServerNonAlloc : KcpServer - { - IPEndPointNonAlloc reusableClientEP; - - public KcpServerNonAlloc(Action OnConnected, - Action, KcpChannel> OnData, - Action OnDisconnected, - Action OnError, - bool DualMode, - bool NoDelay, - uint Interval, - int FastResend = 0, - bool CongestionWindow = true, - uint SendWindowSize = Kcp.WND_SND, - uint ReceiveWindowSize = Kcp.WND_RCV, - int Timeout = KcpConnection.DEFAULT_TIMEOUT, - uint MaxRetransmits = Kcp.DEADLINK, - bool MaximizeSendReceiveBuffersToOSLimit = false) - : base(OnConnected, - OnData, - OnDisconnected, - OnError, - DualMode, - NoDelay, - Interval, - FastResend, - CongestionWindow, - SendWindowSize, - ReceiveWindowSize, - Timeout, - MaxRetransmits, - MaximizeSendReceiveBuffersToOSLimit) - { - // create reusableClientEP either IPv4 or IPv6 - reusableClientEP = DualMode - ? new IPEndPointNonAlloc(IPAddress.IPv6Any, 0) - : new IPEndPointNonAlloc(IPAddress.Any, 0); - } - - protected override int ReceiveFrom(byte[] buffer, out int connectionHash) - { - // where-allocation nonalloc ReceiveFrom. - int read = socket.ReceiveFrom_NonAlloc(buffer, 0, buffer.Length, SocketFlags.None, reusableClientEP); - SocketAddress remoteAddress = reusableClientEP.temp; - - // where-allocation nonalloc GetHashCode - connectionHash = remoteAddress.GetHashCode(); - return read; - } - - protected override KcpServerConnection CreateConnection() - { - // IPEndPointNonAlloc is reused all the time. - // we can't store that as the connection's endpoint. - // we need a new copy! - IPEndPoint newClientEP = reusableClientEP.DeepCopyIPEndPoint(); - - // for allocation free sending, we also need another - // IPEndPointNonAlloc... - IPEndPointNonAlloc reusableSendEP = new IPEndPointNonAlloc(newClientEP.Address, newClientEP.Port); - - // create a new KcpConnection NonAlloc version - // -> where-allocation IPEndPointNonAlloc is reused. - // need to create a new one from the temp address. - return new KcpServerConnectionNonAlloc(socket, newClientEP, reusableSendEP, NoDelay, Interval, FastResend, CongestionWindow, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmits); - } - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta deleted file mode 100644 index a878cc1..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/highlevel/NonAlloc/KcpServerNonAlloc.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 54b8398dcd544c8a93bcad846214cc40 -timeCreated: 1626432191 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta deleted file mode 100644 index 6b442a9..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/kcp/AssemblyInfo.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: aec6a15ac7bd43129317ea1f01f19782 -timeCreated: 1602665988 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta deleted file mode 100644 index 5c72cf0..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: e9de45e025f26411bbb52d1aefc8d5a5 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta deleted file mode 100644 index 4fadbdf..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/LICENSE.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: a857d4e863bbf4a7dba70bc2cd1b5949 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta deleted file mode 100644 index 6878ad8..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 6b7f3f8e8fa16475bbe48a8e9fbe800b -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs deleted file mode 100644 index 246a5d1..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("where-allocations.Tests")] \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta deleted file mode 100644 index 1edb254..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/AssemblyInfo.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 158a96a7489b450485a8b06a13328871 -timeCreated: 1622356221 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs deleted file mode 100644 index fcf18f6..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace WhereAllocation -{ - public static class Extensions - { - // always pass the same IPEndPointNonAlloc instead of allocating a new - // one each time. - // - // use IPEndPointNonAlloc.temp to get the latest SocketAdddress written - // by ReceiveFrom_Internal! - // - // IMPORTANT: .temp will be overwritten in next call! - // hash or manually copy it if you need to store it, e.g. - // when adding a new connection. - public static int ReceiveFrom_NonAlloc( - this Socket socket, - byte[] buffer, - int offset, - int size, - SocketFlags socketFlags, - IPEndPointNonAlloc remoteEndPoint) - { - // call ReceiveFrom with IPEndPointNonAlloc. - // need to wrap this in ReceiveFrom_NonAlloc because it's not - // obvious that IPEndPointNonAlloc.Create does NOT create a new - // IPEndPoint. it saves the result in IPEndPointNonAlloc.temp! - EndPoint casted = remoteEndPoint; - return socket.ReceiveFrom(buffer, offset, size, socketFlags, ref casted); - } - - // same as above, different parameters - public static int ReceiveFrom_NonAlloc(this Socket socket, byte[] buffer, IPEndPointNonAlloc remoteEndPoint) - { - EndPoint casted = remoteEndPoint; - return socket.ReceiveFrom(buffer, ref casted); - } - - // SendTo allocates too: - // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L2240 - // -> the allocation is in EndPoint.Serialize() - // NOTE: technically this function isn't necessary. - // could just pass IPEndPointNonAlloc. - // still good for strong typing. - public static int SendTo_NonAlloc( - this Socket socket, - byte[] buffer, - int offset, - int size, - SocketFlags socketFlags, - IPEndPointNonAlloc remoteEndPoint) - { - EndPoint casted = remoteEndPoint; - return socket.SendTo(buffer, offset, size, socketFlags, casted); - } - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta deleted file mode 100644 index c4fa54d..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/Extensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9e801942544d44d65808fb250623fe25 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs deleted file mode 100644 index 65eb453..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; - -namespace WhereAllocation -{ - public class IPEndPointNonAlloc : IPEndPoint - { - // Two steps to remove allocations in ReceiveFrom_Internal: - // - // 1.) remoteEndPoint.Serialize(): - // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1733 - // -> creates an EndPoint for ReceiveFrom_Internal to write into - // -> it's never read from: - // ReceiveFrom_Internal passes it to native: - // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1885 - // native recv populates 'sockaddr* from' with the remote address: - // https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recvfrom - // -> can NOT be null. bricks both Unity and Unity Hub otherwise. - // -> it seems as if Serialize() is only called to avoid allocating - // a 'new SocketAddress' in ReceiveFrom. it's up to the EndPoint. - // - // 2.) EndPoint.Create(SocketAddress): - // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761 - // -> SocketAddress is the remote's address that we want to return - // -> to avoid 'new EndPoint(SocketAddress), it seems up to the user - // to decide how to create a new EndPoint via .Create - // -> SocketAddress is the object that was returned by Serialize() - // - // in other words, all we need is an extra SocketAddress field that we - // can pass to ReceiveFrom_Internal to write the result into. - // => callers can then get the result from the extra field! - // => no allocations - // - // IMPORTANT: remember that IPEndPointNonAlloc is always the same object - // and never changes. only the helper field is changed. - public SocketAddress temp; - - // constructors simply create the field once by calling the base method. - // (our overwritten method would create anything new) - public IPEndPointNonAlloc(long address, int port) : base(address, port) - { - temp = base.Serialize(); - } - public IPEndPointNonAlloc(IPAddress address, int port) : base(address, port) - { - temp = base.Serialize(); - } - - // Serialize simply returns it - public override SocketAddress Serialize() => temp; - - // Create doesn't need to create anything. - // SocketAddress object is already the one we returned in Serialize(). - // ReceiveFrom_Internal simply wrote into it. - public override EndPoint Create(SocketAddress socketAddress) - { - // original IPEndPoint.Create validates: - if (socketAddress.Family != AddressFamily) - throw new ArgumentException($"Unsupported socketAddress.AddressFamily: {socketAddress.Family}. Expected: {AddressFamily}"); - if (socketAddress.Size < 8) - throw new ArgumentException($"Unsupported socketAddress.Size: {socketAddress.Size}. Expected: <8"); - - // double check to guarantee that ReceiveFrom actually did write - // into our 'temp' field. just in case that's ever changed. - if (socketAddress != temp) - { - // well this is fun. - // in the latest mono from the above github links, - // the result of Serialize() is passed as 'ref' so ReceiveFrom - // does in fact write into it. - // - // in Unity 2019 LTS's mono version, it does create a new one - // each time. this is from ILSpy Receive_From: - // - // SocketPal.CheckDualModeReceiveSupport(this); - // ValidateBlockingMode(); - // if (NetEventSource.IsEnabled) - // { - // NetEventSource.Info(this, $"SRC{LocalEndPoint} size:{size} remoteEP:{remoteEP}", "ReceiveFrom"); - // } - // EndPoint remoteEP2 = remoteEP; - // System.Net.Internals.SocketAddress socketAddress = SnapshotAndSerialize(ref remoteEP2); - // System.Net.Internals.SocketAddress socketAddress2 = IPEndPointExtensions.Serialize(remoteEP2); - // int bytesTransferred; - // SocketError socketError = SocketPal.ReceiveFrom(_handle, buffer, offset, size, socketFlags, socketAddress.Buffer, ref socketAddress.InternalSize, out bytesTransferred); - // SocketException ex = null; - // if (socketError != 0) - // { - // ex = new SocketException((int)socketError); - // UpdateStatusAfterSocketError(ex); - // if (NetEventSource.IsEnabled) - // { - // NetEventSource.Error(this, ex, "ReceiveFrom"); - // } - // if (ex.SocketErrorCode != SocketError.MessageSize) - // { - // throw ex; - // } - // } - // if (!socketAddress2.Equals(socketAddress)) - // { - // try - // { - // remoteEP = remoteEP2.Create(socketAddress); - // } - // catch - // { - // } - // if (_rightEndPoint == null) - // { - // _rightEndPoint = remoteEP2; - // } - // } - // if (ex != null) - // { - // throw ex; - // } - // if (NetEventSource.IsEnabled) - // { - // NetEventSource.DumpBuffer(this, buffer, offset, size, "ReceiveFrom"); - // NetEventSource.Exit(this, bytesTransferred, "ReceiveFrom"); - // } - // return bytesTransferred; - // - - // so until they upgrade their mono version, we are stuck with - // some allocations. - // - // for now, let's pass the newly created on to our temp so at - // least we reuse it next time. - temp = socketAddress; - - // SocketAddress.GetHashCode() depends on SocketAddress.m_changed. - // ReceiveFrom only sets the buffer, it does not seem to set m_changed. - // we need to reset m_changed for two reasons: - // * if m_changed is false, GetHashCode() returns the cahced m_hash - // which is '0'. that would be a problem. - // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L262 - // * if we have a cached m_hash, but ReceiveFrom modified the buffer - // then the GetHashCode() should change too. so we need to reset - // either way. - // - // the only way to do that is by _actually_ modifying the buffer: - // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/SocketAddress.cs#L99 - // so let's do that. - // -> unchecked in case it's byte.Max - unchecked - { - temp[0] += 1; - temp[0] -= 1; - } - - // make sure this worked. - // at least throw an Exception to make it obvious if the trick does - // not work anymore, in case ReceiveFrom is ever changed. - if (temp.GetHashCode() == 0) - throw new Exception($"SocketAddress GetHashCode() is 0 after ReceiveFrom. Does the m_changed trick not work anymore?"); - - // in the future, enable this again: - //throw new Exception($"Socket.ReceiveFrom(): passed SocketAddress={socketAddress} but expected {temp}. This should never happen. Did ReceiveFrom() change?"); - } - - // ReceiveFrom sets seed_endpoint to the result of Create(): - // https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1764 - // so let's return ourselves at least. - // (seed_endpoint only seems to matter for BeginSend etc.) - return this; - } - - // we need to overwrite GetHashCode() for two reasons. - // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPEndPoint.cs#L160 - // * it uses m_Address. but our true SocketAddress is in m_temp. - // m_Address might not be set at all. - // * m_Address.GetHashCode() allocates: - // https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699 - public override int GetHashCode() => temp.GetHashCode(); - - // helper function to create an ACTUAL new IPEndPoint from this. - // server needs it to store new connections as unique IPEndPoints. - public IPEndPoint DeepCopyIPEndPoint() - { - // we need to create a new IPEndPoint from 'temp' SocketAddress. - // there is no 'new IPEndPoint(SocketAddress) constructor. - // so we need to be a bit creative... - - // allocate a placeholder IPAddress to copy - // our SocketAddress into. - // -> needs to be the same address family. - IPAddress ipAddress; - if (temp.Family == AddressFamily.InterNetworkV6) - ipAddress = IPAddress.IPv6Any; - else if (temp.Family == AddressFamily.InterNetwork) - ipAddress = IPAddress.Any; - else - throw new Exception($"Unexpected SocketAddress family: {temp.Family}"); - - // allocate a placeholder IPEndPoint - // with the needed size form IPAddress. - // (the real class. not NonAlloc) - IPEndPoint placeholder = new IPEndPoint(ipAddress, 0); - - // the real IPEndPoint's .Create function can create a new IPEndPoint - // copy from a SocketAddress. - return (IPEndPoint)placeholder.Create(temp); - } - } -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta deleted file mode 100644 index ef424ba..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/IPEndPointNonAlloc.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: af0279d15e39b484792394f1d3cad4d9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta deleted file mode 100644 index ce96c63..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/Scripts/where-allocations.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 63c380d6dae6946209ed0832388a657c -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION deleted file mode 100644 index 8341d28..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION +++ /dev/null @@ -1,2 +0,0 @@ -V0.1 [2021-06-01] -- initial release \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta b/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta deleted file mode 100644 index 67ab688..0000000 --- a/Assets/Mirror/Runtime/Transports/KCP/kcp2k/where-allocation/VERSION.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: f1256cadc037546ccb66071784fce137 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs b/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs deleted file mode 100644 index 0d0503d..0000000 --- a/Assets/Mirror/Runtime/Transports/MultiplexTransport.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System; -using System.Text; -using UnityEngine; - -namespace Mirror -{ - // a transport that can listen to multiple underlying transport at the same time - [DisallowMultipleComponent] - public class MultiplexTransport : Transport - { - public Transport[] transports; - - Transport available; - - public void Awake() - { - if (transports == null || transports.Length == 0) - { - Debug.LogError("Multiplex transport requires at least 1 underlying transport"); - } - } - - public override void ClientEarlyUpdate() - { - foreach (Transport transport in transports) - { - transport.ClientEarlyUpdate(); - } - } - - public override void ServerEarlyUpdate() - { - foreach (Transport transport in transports) - { - transport.ServerEarlyUpdate(); - } - } - - public override void ClientLateUpdate() - { - foreach (Transport transport in transports) - { - transport.ClientLateUpdate(); - } - } - - public override void ServerLateUpdate() - { - foreach (Transport transport in transports) - { - transport.ServerLateUpdate(); - } - } - - void OnEnable() - { - foreach (Transport transport in transports) - { - transport.enabled = true; - } - } - - void OnDisable() - { - foreach (Transport transport in transports) - { - transport.enabled = false; - } - } - - public override bool Available() - { - // available if any of the transports is available - foreach (Transport transport in transports) - { - if (transport.Available()) - { - return true; - } - } - return false; - } - - #region Client - - public override void ClientConnect(string address) - { - foreach (Transport transport in transports) - { - if (transport.Available()) - { - available = transport; - transport.OnClientConnected = OnClientConnected; - transport.OnClientDataReceived = OnClientDataReceived; - transport.OnClientError = OnClientError; - transport.OnClientDisconnected = OnClientDisconnected; - transport.ClientConnect(address); - return; - } - } - throw new ArgumentException("No transport suitable for this platform"); - } - - public override void ClientConnect(Uri uri) - { - foreach (Transport transport in transports) - { - if (transport.Available()) - { - try - { - available = transport; - transport.OnClientConnected = OnClientConnected; - transport.OnClientDataReceived = OnClientDataReceived; - transport.OnClientError = OnClientError; - transport.OnClientDisconnected = OnClientDisconnected; - transport.ClientConnect(uri); - return; - } - catch (ArgumentException) - { - // transport does not support the schema, just move on to the next one - } - } - } - throw new ArgumentException("No transport suitable for this platform"); - } - - public override bool ClientConnected() - { - return (object)available != null && available.ClientConnected(); - } - - public override void ClientDisconnect() - { - if ((object)available != null) - available.ClientDisconnect(); - } - - public override void ClientSend(ArraySegment segment, int channelId) - { - available.ClientSend(segment, channelId); - } - - #endregion - - #region Server - // connection ids get mapped to base transports - // if we have 3 transports, then - // transport 0 will produce connection ids [0, 3, 6, 9, ...] - // transport 1 will produce connection ids [1, 4, 7, 10, ...] - // transport 2 will produce connection ids [2, 5, 8, 11, ...] - int FromBaseId(int transportId, int connectionId) - { - return connectionId * transports.Length + transportId; - } - - int ToBaseId(int connectionId) - { - return connectionId / transports.Length; - } - - int ToTransportId(int connectionId) - { - return connectionId % transports.Length; - } - - void AddServerCallbacks() - { - // wire all the base transports to my events - for (int i = 0; i < transports.Length; i++) - { - // this is required for the handlers, if I use i directly - // then all the handlers will use the last i - int locali = i; - Transport transport = transports[i]; - - transport.OnServerConnected = (baseConnectionId => - { - OnServerConnected.Invoke(FromBaseId(locali, baseConnectionId)); - }); - - transport.OnServerDataReceived = (baseConnectionId, data, channel) => - { - OnServerDataReceived.Invoke(FromBaseId(locali, baseConnectionId), data, channel); - }; - - transport.OnServerError = (baseConnectionId, error) => - { - OnServerError.Invoke(FromBaseId(locali, baseConnectionId), error); - }; - transport.OnServerDisconnected = baseConnectionId => - { - OnServerDisconnected.Invoke(FromBaseId(locali, baseConnectionId)); - }; - } - } - - // for now returns the first uri, - // should we return all available uris? - public override Uri ServerUri() - { - return transports[0].ServerUri(); - } - - - public override bool ServerActive() - { - // avoid Linq.All allocations - foreach (Transport transport in transports) - { - if (!transport.ServerActive()) - { - return false; - } - } - return true; - } - - public override string ServerGetClientAddress(int connectionId) - { - int baseConnectionId = ToBaseId(connectionId); - int transportId = ToTransportId(connectionId); - return transports[transportId].ServerGetClientAddress(baseConnectionId); - } - - public override void ServerDisconnect(int connectionId) - { - int baseConnectionId = ToBaseId(connectionId); - int transportId = ToTransportId(connectionId); - transports[transportId].ServerDisconnect(baseConnectionId); - } - - public override void ServerSend(int connectionId, ArraySegment segment, int channelId) - { - int baseConnectionId = ToBaseId(connectionId); - int transportId = ToTransportId(connectionId); - - for (int i = 0; i < transports.Length; ++i) - { - if (i == transportId) - { - transports[i].ServerSend(baseConnectionId, segment, channelId); - } - } - } - - public override void ServerStart() - { - foreach (Transport transport in transports) - { - AddServerCallbacks(); - transport.ServerStart(); - } - } - - public override void ServerStop() - { - foreach (Transport transport in transports) - { - transport.ServerStop(); - } - } - #endregion - - public override int GetMaxPacketSize(int channelId = 0) - { - // finding the max packet size in a multiplex environment has to be - // done very carefully: - // * servers run multiple transports at the same time - // * different clients run different transports - // * there should only ever be ONE true max packet size for everyone, - // otherwise a spawn message might be sent to all tcp sockets, but - // be too big for some udp sockets. that would be a debugging - // nightmare and allow for possible exploits and players on - // different platforms seeing a different game state. - // => the safest solution is to use the smallest max size for all - // transports. that will never fail. - int mininumAllowedSize = int.MaxValue; - foreach (Transport transport in transports) - { - int size = transport.GetMaxPacketSize(channelId); - mininumAllowedSize = Mathf.Min(size, mininumAllowedSize); - } - return mininumAllowedSize; - } - - public override void Shutdown() - { - foreach (Transport transport in transports) - { - transport.Shutdown(); - } - } - - public override string ToString() - { - StringBuilder builder = new StringBuilder(); - foreach (Transport transport in transports) - { - builder.AppendLine(transport.ToString()); - } - return builder.ToString().Trim(); - } - } -} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta deleted file mode 100644 index bc43099..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/CHANGELOG.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: b0ef23ac1c6a62546bbad5529b3bfdad -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs deleted file mode 100644 index 4b7bce5..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/Common/Log.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using UnityEngine; -using Conditional = System.Diagnostics.ConditionalAttribute; - -namespace Mirror.SimpleWeb -{ - public static class Log - { - // used for Conditional - const string SIMPLEWEB_LOG_ENABLED = nameof(SIMPLEWEB_LOG_ENABLED); - const string DEBUG = nameof(DEBUG); - - public enum Levels - { - none = 0, - error = 1, - warn = 2, - info = 3, - verbose = 4, - } - - public static ILogger logger = Debug.unityLogger; - public static Levels level = Levels.none; - - public static string BufferToString(byte[] buffer, int offset = 0, int? length = null) - { - return BitConverter.ToString(buffer, offset, length ?? buffer.Length); - } - - [Conditional(SIMPLEWEB_LOG_ENABLED)] - public static void DumpBuffer(string label, byte[] buffer, int offset, int length) - { - if (level < Levels.verbose) - return; - - logger.Log(LogType.Log, $"VERBOSE: {label}: {BufferToString(buffer, offset, length)}"); - } - - [Conditional(SIMPLEWEB_LOG_ENABLED)] - public static void DumpBuffer(string label, ArrayBuffer arrayBuffer) - { - if (level < Levels.verbose) - return; - - logger.Log(LogType.Log, $"VERBOSE: {label}: {BufferToString(arrayBuffer.array, 0, arrayBuffer.count)}"); - } - - [Conditional(SIMPLEWEB_LOG_ENABLED)] - public static void Verbose(string msg, bool showColor = true) - { - if (level < Levels.verbose) - return; - - if (showColor) - logger.Log(LogType.Log, $"VERBOSE: {msg}"); - else - logger.Log(LogType.Log, $"VERBOSE: {msg}"); - } - - [Conditional(SIMPLEWEB_LOG_ENABLED)] - public static void Info(string msg, bool showColor = true) - { - if (level < Levels.info) - return; - - if (showColor) - logger.Log(LogType.Log, $"INFO: {msg}"); - else - logger.Log(LogType.Log, $"INFO: {msg}"); - } - - /// - /// An expected Exception was caught, useful for debugging but not important - /// - /// - /// - [Conditional(SIMPLEWEB_LOG_ENABLED)] - public static void InfoException(Exception e) - { - if (level < Levels.info) - return; - - logger.Log(LogType.Log, $"INFO_EXCEPTION: {e.GetType().Name} Message: {e.Message}\n{e.StackTrace}\n\n"); - } - - [Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)] - public static void Warn(string msg, bool showColor = true) - { - if (level < Levels.warn) - return; - - if (showColor) - logger.Log(LogType.Warning, $"WARN: {msg}"); - else - logger.Log(LogType.Warning, $"WARN: {msg}"); - } - - [Conditional(SIMPLEWEB_LOG_ENABLED), Conditional(DEBUG)] - public static void Error(string msg, bool showColor = true) - { - if (level < Levels.error) - return; - - if (showColor) - logger.Log(LogType.Error, $"ERROR: {msg}"); - else - logger.Log(LogType.Error, $"ERROR: {msg}"); - } - - public static void Exception(Exception e) - { - // always log Exceptions - logger.Log(LogType.Error, $"EXCEPTION: {e.GetType().Name} Message: {e.Message}\n{e.StackTrace}\n\n"); - } - } -} diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta deleted file mode 100644 index 8ece59e..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/LICENSE.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 0a0cf751b4a201242ac60b4adbc54657 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta deleted file mode 100644 index b63fe39..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/README.txt.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 0e3971d5783109f4d9ce93c7a689d701 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef deleted file mode 100644 index 3687c5d..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "SimpleWebTransport", - "references": [ - "Mirror" - ], - "optionalUnityReferences": [], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [] -} \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta b/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta deleted file mode 100644 index 99755b6..0000000 --- a/Assets/Mirror/Runtime/Transports/SimpleWebTransport/SimpleWebTransport.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 3b5390adca4e2bb4791cb930316d6f3e -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta deleted file mode 100644 index 1bc9652..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 885e89897e3a03241827ab7a14fe5fa0 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs deleted file mode 100644 index 4f7722a..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-04 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta deleted file mode 100644 index 304866f..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Logger.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: aa8d703f0b73f4d6398b76812719b68b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs deleted file mode 100644 index 4f7722a..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-04 \ No newline at end of file diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta deleted file mode 100644 index 5937bb9..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/Message.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: aedf812e9637b4f92a35db1aedca8c92 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs deleted file mode 100644 index 7899911..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-04 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta deleted file mode 100644 index f3a9310..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/SafeQueue.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 8fc06e2fb29854a0c9e90c0188d36a08 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs deleted file mode 100644 index 85dece4..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs +++ /dev/null @@ -1 +0,0 @@ -// removed 2021-02-04 diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta deleted file mode 100644 index 77c885d..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Empty/ThreadExtensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 64df4eaebe4ff9a43a9fb318c3e8e321 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta deleted file mode 100644 index 4d7664e..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/LICENSE.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 0ba11103b95fd4721bffbb08440d5b8e -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta deleted file mode 100644 index 572c127..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/Telepathy.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 725ee7191c021de4dbf9269590ded755 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta b/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta deleted file mode 100644 index 04c1c8a..0000000 --- a/Assets/Mirror/Runtime/Transports/Telepathy/Telepathy/VERSION.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: d942af06608be434dbeeaa58207d20bd -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Mirror/Runtime/Utils.cs b/Assets/Mirror/Runtime/Utils.cs deleted file mode 100644 index d39ed98..0000000 --- a/Assets/Mirror/Runtime/Utils.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using UnityEngine; - -namespace Mirror -{ - // Handles network messages on client and server - public delegate void NetworkMessageDelegate(NetworkConnection conn, NetworkReader reader, int channelId); - - // Handles requests to spawn objects on the client - public delegate GameObject SpawnDelegate(Vector3 position, Guid assetId); - - public delegate GameObject SpawnHandlerDelegate(SpawnMessage msg); - - // Handles requests to unspawn objects on the client - public delegate void UnSpawnDelegate(GameObject spawned); - - // channels are const ints instead of an enum so people can add their own - // channels (can't extend an enum otherwise). - // - // note that Mirror is slowly moving towards quake style networking which - // will only require reliable for handshake, and unreliable for the rest. - // so eventually we can change this to an Enum and transports shouldn't - // add custom channels anymore. - public static class Channels - { - public const int Reliable = 0; // ordered - public const int Unreliable = 1; // unordered - } - - public static class Utils - { - public static uint GetTrueRandomUInt() - { - // use Crypto RNG to avoid having time based duplicates - using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) - { - byte[] bytes = new byte[4]; - rng.GetBytes(bytes); - return BitConverter.ToUInt32(bytes, 0); - } - } - - public static bool IsPrefab(GameObject obj) - { -#if UNITY_EDITOR - return UnityEditor.PrefabUtility.IsPartOfPrefabAsset(obj); -#else - return false; -#endif - } - - public static bool IsSceneObjectWithPrefabParent(GameObject gameObject, out GameObject prefab) - { - prefab = null; - -#if UNITY_EDITOR - if (!UnityEditor.PrefabUtility.IsPartOfPrefabInstance(gameObject)) - { - return false; - } - prefab = UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(gameObject); -#endif - - if (prefab == null) - { - Debug.LogError($"Failed to find prefab parent for scene object [name:{gameObject.name}]"); - return false; - } - return true; - } - - // is a 2D point in screen? (from ummorpg) - // (if width = 1024, then indices from 0..1023 are valid (=1024 indices) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPointInScreen(Vector2 point) => - 0 <= point.x && point.x < Screen.width && - 0 <= point.y && point.y < Screen.height; - - // pretty print bytes as KB/MB/GB/etc. from DOTSNET - // long to support > 2GB - // divides by floats to return "2.5MB" etc. - public static string PrettyBytes(long bytes) - { - // bytes - if (bytes < 1024) - return $"{bytes} B"; - // kilobytes - else if (bytes < 1024L * 1024L) - return $"{(bytes / 1024f):F2} KB"; - // megabytes - else if (bytes < 1024 * 1024L * 1024L) - return $"{(bytes / (1024f * 1024f)):F2} MB"; - // gigabytes - return $"{(bytes / (1024f * 1024f * 1024f)):F2} GB"; - } - - // universal .spawned function - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static NetworkIdentity GetSpawnedInServerOrClient(uint netId) - { - // server / host mode: use the one from server. - // host mode has access to all spawned. - if (NetworkServer.active) - { - NetworkServer.spawned.TryGetValue(netId, out NetworkIdentity entry); - return entry; - } - - // client - if (NetworkClient.active) - { - NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity entry); - return entry; - } - - return null; - } - } -} diff --git a/Assets/Mirror/ScriptTemplates.meta b/Assets/Mirror/ScriptTemplates.meta new file mode 100644 index 0000000..0dfbf44 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8c00361129d75a941a732ef88e326a4f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt new file mode 100644 index 0000000..887e76f --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt @@ -0,0 +1,365 @@ +using System; +using UnityEngine; +using UnityEngine.SceneManagement; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-manager + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkManager.html +*/ + +public class #SCRIPTNAME# : NetworkManager +{ + // You can adjust the parameters of the Actions below to suit your needs and pass the values through the Invoke() method. + + public event Action OnStartAction; + public event Action OnDestroyAction; + + public event Action OnApplicationQuitAction; + + public event Action ServerChangeSceneAction; + public event Action OnServerChangeSceneAction; + public event Action OnServerSceneChangedAction; + public event Action OnClientChangeSceneAction; + public event Action OnClientSceneChangedAction; + + public event Action OnServerConnectAction; + public event Action OnServerReadyAction; + public event Action OnServerAddPlayerAction; + public event Action OnServerDisconnectAction; + public event Action OnServerErrorAction; + public event Action OnServerTransportExceptionAction; + + public event Action OnClientConnectAction; + public event Action OnClientDisconnectAction; + public event Action OnClientNotReadyAction; + public event Action OnClientErrorAction; + public event Action OnClientTransportExceptionAction; + + public event Action OnStartServerAction; + public event Action OnStopServerAction; + public event Action OnStartHostAction; + public event Action OnStopHostAction; + public event Action OnStartClientAction; + public event Action OnStopClientAction; + + // Overrides the base singleton so we don't have to cast to this type everywhere. + public static new #SCRIPTNAME# singleton => (#SCRIPTNAME#)NetworkManager.singleton; + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Awake() + { + base.Awake(); + + // Example of adding a handler for the OnStartAction + // Multiple handlers can be added for actions + // Use -= to remove handlers + // Set the action to null to remove all handlers + OnStartAction += OnStartedActionHandler; + } + + /// + /// Example handler for OnStartAction + /// + /// Handlers can be assigned from, and exist, in any script + public void OnStartedActionHandler() + { + Debug.Log("#SCRIPTNAME#.OnStartAction invoked"); + } + + #region Unity Callbacks + + public override void OnValidate() + { + base.OnValidate(); + } + + /// + /// Runs on both Server and Client + /// Networking is NOT initialized when this fires + /// + public override void Start() + { + OnStartAction?.Invoke(); + base.Start(); + } + + /// + /// Runs on both Server and Client + /// + public override void LateUpdate() + { + base.LateUpdate(); + } + + /// + /// Runs on both Server and Client + /// + public override void OnDestroy() + { + OnDestroyAction?.Invoke(); + base.OnDestroy(); + } + + #endregion + + #region Start & Stop + + /// + /// Set the frame rate for a headless server. + /// Override if you wish to disable the behavior or set your own tick rate. + /// + public override void ConfigureHeadlessFrameRate() + { + base.ConfigureHeadlessFrameRate(); + } + + /// + /// called when quitting the application by closing the window / pressing stop in the editor + /// + public override void OnApplicationQuit() + { + OnApplicationQuitAction?.Invoke(); + base.OnApplicationQuit(); + } + + #endregion + + #region Scene Management + + /// + /// This causes the server to switch scenes and sets the networkSceneName. + /// Clients that connect to this server will automatically switch to this scene. This is called automatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene. + /// + /// + public override void ServerChangeScene(string newSceneName) + { + ServerChangeSceneAction?.Invoke(newSceneName); + base.ServerChangeScene(newSceneName); + } + + /// + /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed + /// This allows server to do work / cleanup / prep before the scene changes. + /// + /// Name of the scene that's about to be loaded + public override void OnServerChangeScene(string newSceneName) + { + OnServerChangeSceneAction?.Invoke(newSceneName); + } + + /// + /// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene(). + /// + /// The name of the new scene. + public override void OnServerSceneChanged(string sceneName) + { + OnServerSceneChangedAction?.Invoke(sceneName); + } + + /// + /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed + /// This allows client to do work / cleanup / prep before the scene changes. + /// + /// Name of the scene that's about to be loaded + /// Scene operation that's about to happen + /// true to indicate that scene loading will be handled through overrides + public override void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) + { + OnClientChangeSceneAction?.Invoke(newSceneName, sceneOperation, customHandling); + } + + /// + /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. + /// Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists. + /// + public override void OnClientSceneChanged() + { + OnClientSceneChangedAction?.Invoke(); + base.OnClientSceneChanged(); + } + + #endregion + + #region Server System Callbacks + + /// + /// Called on the server when a new client connects. + /// Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server. + /// + /// Connection from client. + public override void OnServerConnect(NetworkConnectionToClient conn) + { + OnServerConnectAction?.Invoke(conn); + } + + /// + /// Called on the server when a client is ready. + /// The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process. + /// + /// Connection from client. + public override void OnServerReady(NetworkConnectionToClient conn) + { + OnServerReadyAction?.Invoke(conn); + base.OnServerReady(conn); + } + + /// + /// Called on the server when a client adds a new player with ClientScene.AddPlayer. + /// The default implementation for this function creates a new player object from the playerPrefab. + /// + /// Connection from client. + public override void OnServerAddPlayer(NetworkConnectionToClient conn) + { + OnServerAddPlayerAction?.Invoke(conn); + base.OnServerAddPlayer(conn); + } + + /// + /// Called on the server when a client disconnects. + /// This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected. + /// + /// Connection from client. + public override void OnServerDisconnect(NetworkConnectionToClient conn) + { + OnServerDisconnectAction?.Invoke(conn); + base.OnServerDisconnect(conn); + } + + /// + /// Called on server when transport raises an error. + /// NetworkConnection may be null. + /// + /// Connection of the client...may be null + /// TransportError enum + /// String message of the error. + public override void OnServerError(NetworkConnectionToClient conn, TransportError transportError, string message) + { + OnServerErrorAction?.Invoke(conn, transportError, message); + } + + /// + /// Called on server when transport raises an exception. + /// NetworkConnection may be null. + /// + /// Connection of the client...may be null + /// Exception thrown from the Transport. + public override void OnServerTransportException(NetworkConnectionToClient conn, Exception exception) + { + OnServerTransportExceptionAction?.Invoke(conn, exception); + } + + #endregion + + #region Client System Callbacks + + /// + /// Called on the client when connected to a server. + /// The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects. + /// + public override void OnClientConnect() + { + OnClientConnectAction?.Invoke(); + base.OnClientConnect(); + } + + /// + /// Called on clients when disconnected from a server. + /// This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects. + /// + public override void OnClientDisconnect() + { + OnClientDisconnectAction?.Invoke(); + } + + /// + /// Called on clients when a servers tells the client it is no longer ready. + /// This is commonly used when switching scenes. + /// + public override void OnClientNotReady() + { + OnClientNotReadyAction?.Invoke(); + } + + /// + /// Called on client when transport raises an error. + /// + /// TransportError enum. + /// String message of the error. + public override void OnClientError(TransportError transportError, string message) + { + OnClientErrorAction?.Invoke(transportError, message); + } + + /// + /// Called on client when transport raises an exception. + /// + /// Exception thrown from the Transport. + public override void OnClientTransportException(Exception exception) + { + OnClientTransportExceptionAction?.Invoke(exception); + } + + #endregion + + #region Start & Stop Callbacks + + // Since there are multiple versions of StartServer, StartClient and StartHost, to reliably customize + // their functionality, users would need override all the versions. Instead these callbacks are invoked + // from all versions, so users only need to implement this one case. + + /// + /// This is invoked when a server is started - including when a host is started. + /// StartServer has multiple signatures, but they all cause this hook to be called. + /// + public override void OnStartServer() + { + OnStartServerAction?.Invoke(); + } + + /// + /// This is called when a server is stopped - including when a host is stopped. + /// + public override void OnStopServer() + { + OnStopServerAction?.Invoke(); + } + + /// + /// This is invoked when a host is started. + /// StartHost has multiple signatures, but they all cause this hook to be called. + /// + public override void OnStartHost() + { + OnStartHostAction?.Invoke(); + } + + /// + /// This is called when a host is stopped. + /// + public override void OnStopHost() + { + OnStopHostAction?.Invoke(); + } + + /// + /// This is invoked when the client is started. + /// + public override void OnStartClient() + { + OnStartClientAction?.Invoke(); + } + + /// + /// This is called when a client is stopped. + /// + public override void OnStopClient() + { + OnStopClientAction?.Invoke(); + } + + #endregion +} diff --git a/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt.meta b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt.meta new file mode 100644 index 0000000..ff0a39a --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 54c5bb307a5d80d4180010c653b2c085 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/51-Mirror__Network Manager With Actions-NewNetworkManagerWithActions.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt new file mode 100644 index 0000000..9c4fc57 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Mirror; +using UnityEngine; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/components/network-authenticators + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkAuthenticator.html +*/ + +public class #SCRIPTNAME# : NetworkAuthenticator +{ + #region Messages + + public struct AuthRequestMessage : NetworkMessage { } + + public struct AuthResponseMessage : NetworkMessage { } + + #endregion + + #region Server + + /// + /// Called on server from StartServer to initialize the Authenticator + /// Server message handlers should be registered in this method. + /// + public override void OnStartServer() + { + // register a handler for the authentication request we expect from client + NetworkServer.RegisterHandler(OnAuthRequestMessage, false); + } + + /// + /// Called on server from OnServerConnectInternal when a client needs to authenticate + /// + /// Connection to client. + public override void OnServerAuthenticate(NetworkConnectionToClient conn) { } + + /// + /// Called on server when the client's AuthRequestMessage arrives + /// + /// Connection to client. + /// The message payload + public void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg) + { + AuthResponseMessage authResponseMessage = new AuthResponseMessage(); + conn.Send(authResponseMessage); + + // Accept the successful authentication + ServerAccept(conn); + } + + /// + /// Called when server stops, used to unregister message handlers if needed. + /// + public override void OnStopServer() + { + // Unregister the handler for the authentication request + NetworkServer.UnregisterHandler(); + } + + #endregion + + #region Client + + /// + /// Called on client from StartClient to initialize the Authenticator + /// Client message handlers should be registered in this method. + /// + public override void OnStartClient() + { + // register a handler for the authentication response we expect from server + NetworkClient.RegisterHandler(OnAuthResponseMessage, false); + } + + /// + /// Called on client from OnClientConnectInternal when a client needs to authenticate + /// + public override void OnClientAuthenticate() + { + AuthRequestMessage authRequestMessage = new AuthRequestMessage(); + NetworkClient.Send(authRequestMessage); + } + + /// + /// Called on client when the server's AuthResponseMessage arrives + /// + /// The message payload + public void OnAuthResponseMessage(AuthResponseMessage msg) + { + // Authentication has been accepted + ClientAccept(); + } + + /// + /// Called when client stops, used to unregister message handlers if needed. + /// + public override void OnStopClient() + { + // Unregister the handler for the authentication response + NetworkClient.UnregisterHandler(); + } + + #endregion +} diff --git a/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta new file mode 100644 index 0000000..9e44168 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 65ea3c30df1c9f949bb5d6c76e549280 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/52-Mirror__Network Authenticator-NewNetworkAuthenticator.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt new file mode 100644 index 0000000..5998b8f --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/guides/networkbehaviour + API Reference: https://mirror-networking.com/docs/api/Mirror.NetworkBehaviour.html +*/ + +public class #SCRIPTNAME# : NetworkBehaviour +{ + // You can adjust the parameters of the Actions below to suit your needs and pass the values through the Invoke() method. + + public event Action OnStartServerAction; + public event Action OnStopServerAction; + public event Action OnStartClientAction; + public event Action OnStopClientAction; + public event Action OnStartLocalPlayerAction; + public event Action OnStopLocalPlayerAction; + public event Action OnStartAuthorityAction; + public event Action OnStopAuthorityAction; + + #region Unity Callbacks + + /// + /// Add your validation code here after the base.OnValidate(); call. + /// + protected override void OnValidate() + { + base.OnValidate(); + } + + // NOTE: Do not put objects in DontDestroyOnLoad (DDOL) in Awake. You can do that in Start instead. + void Awake() + { + // Example of adding a handler for OnStartServerAction + // Multiple handlers can be added for actions + // Use -= to remove handlers + // Set the action to null to remove all handlers + OnStartServerAction += OnStartServerHandler; + } + + void Start() + { + } + + /// + /// Example handler for OnStartServerAction + /// + /// Handlers can be assigned from, and exist, in any script + public void OnStartServerHandler(NetworkIdentity identity) + { + Debug.Log($"#SCRIPTNAME#.OnStartServerAction invoked for {identity}"); + } + + #endregion + + #region Start & Stop Callbacks + + /// + /// This is invoked for NetworkBehaviour objects when they become active on the server. + /// This could be triggered by NetworkServer.Listen() for objects in the scene, or by NetworkServer.Spawn() for objects that are dynamically created. + /// This will be called for objects on a "host" as well as for object on a dedicated server. + /// + public override void OnStartServer() + { + OnStartServerAction?.Invoke(netIdentity); + } + + /// + /// Invoked on the server when the object is unspawned + /// Useful for saving object data in persistent storage + /// + public override void OnStopServer() + { + OnStopServerAction?.Invoke(netIdentity); + } + + /// + /// Called on every NetworkBehaviour when it is activated on a client. + /// Objects on the host have this function called, as there is a local client on the host. The values of SyncVars on object are guaranteed to be initialized correctly with the latest state from the server when this function is called on the client. + /// + public override void OnStartClient() + { + OnStartClientAction?.Invoke(); + } + + /// + /// This is invoked on clients when the server has caused this object to be destroyed. + /// This can be used as a hook to invoke effects or do client specific cleanup. + /// + public override void OnStopClient() + { + OnStopClientAction?.Invoke(); + } + + /// + /// Called when the local player object has been set up. + /// This happens after OnStartClient(), as it is triggered by an ownership message from the server. This is an appropriate place to activate components or functionality that should only be active for the local player, such as cameras and input. + /// + public override void OnStartLocalPlayer() + { + OnStartLocalPlayerAction?.Invoke(); + } + + /// + /// Called when the local player object is being stopped. + /// This happens before OnStopClient(), as it may be triggered by an ownership message from the server, or because the player object is being destroyed. This is an appropriate place to deactivate components or functionality that should only be active for the local player, such as cameras and input. + /// + public override void OnStopLocalPlayer() + { + OnStopLocalPlayerAction?.Invoke(); + } + + /// + /// This is invoked on behaviours that have authority, based on context and NetworkIdentity.hasAuthority. + /// This is called after OnStartServer and before OnStartClient. + /// When AssignClientAuthority is called on the server, this will be called on the client that owns the object. When an object is spawned with NetworkServer.Spawn with a NetworkConnectionToClient parameter included, this will be called on the client that owns the object. + /// + public override void OnStartAuthority() + { + OnStartAuthorityAction?.Invoke(); + } + + /// + /// This is invoked on behaviours when authority is removed. + /// When NetworkIdentity.RemoveClientAuthority is called on the server, this will be called on the client that owns the object. + /// + public override void OnStopAuthority() + { + OnStopAuthorityAction?.Invoke(); + } + + #endregion +} diff --git a/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt.meta b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt.meta new file mode 100644 index 0000000..1420e20 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 847fec635407d6740979cc0da3b87d40 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/53-Mirror__Network Behaviour With Actions-NewNetworkBehaviourWithActions.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt new file mode 100644 index 0000000..5523ac9 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using UnityEngine; +using Mirror; + +/* + Documentation: https://mirror-networking.gitbook.io/docs/guides/interest-management + API Reference: https://mirror-networking.com/docs/api/Mirror.InterestManagement.html +*/ + +// NOTE: Attach this component to the same object as your Network Manager. + +public class #SCRIPTNAME# : InterestManagement +{ + /// + /// Callback used by the visibility system to determine if an observer (client) can see the NetworkIdentity. + /// If this function returns true, the network connection will be added as an observer. + /// + /// Object to be observed (or not) by a client + /// Network Connection of a client. + /// True if the client can see this object. + [ServerCallback] + public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver) + { + // Default behaviour of making the identity object visible to all clients. + // Replace this code with your own logic as appropriate. + return true; + } + + /// + /// Callback used by the visibility system to determine if an observer (client) can see the NetworkIdentity. + /// Add connections to newObservers that should see the identity object. + /// + /// Object to be observed (or not) by clients + /// cached hashset to put the result into + [ServerCallback] + public override void OnRebuildObservers(NetworkIdentity identity, HashSet newObservers) + { + // Default behaviour of making the identity object visible to all clients. + // Replace this code with your own logic as appropriate. + foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values) + newObservers.Add(conn); + } + + /// + /// Called on the server when a new networked object is spawned. + /// + /// NetworkIdentity of the object being spawned + [ServerCallback] + public override void OnSpawned(NetworkIdentity identity) { } + + /// + /// Called on the server when a networked object is destroyed. + /// + /// NetworkIdentity of the object being destroyed + [ServerCallback] + public override void OnDestroyed(NetworkIdentity identity) { } + + /// + /// Callback used by the visibility system for objects on a host. + /// Objects on a host (with a local client) cannot be disabled or destroyed when + /// they are not visible to the local client, so this function is called to allow + /// custom code to hide these objects. + /// A typical implementation will disable renderer components on the object. + /// This is only called on local clients on a host. + /// + /// NetworkIdentity of the object being considered for visibility + /// True if the identity object should be visible to the host client + [ServerCallback] + public override void SetHostVisibility(NetworkIdentity identity, bool visible) + { + base.SetHostVisibility(identity, visible); + } + + /// + /// Called by NetworkServer in Initialize and Shutdown + /// + [ServerCallback] + public override void ResetState() { } + + [ServerCallback] + void Update() + { + // Here is where you'd need to evaluate if observers need to be rebuilt, + // either for a specific object, a subset of objects, or all objects. + + // Review the code in the various Interest Management components + // included with Mirror for inspiration: + // - Distance Interest Management + // - Spatial Hash Interest Management + // - Scene Interest Management + // - Match Interest Management + // - Team Interest Management + } +} diff --git a/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta new file mode 100644 index 0000000..d0f3108 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 81cfade18750ef748be6886f764e1e7d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/54-Mirror__Custom Interest Management-CustomInterestManagement.cs.txt + uploadId: 736421 diff --git a/Assets/Mirror/ScriptTemplates/Editor.meta b/Assets/Mirror/ScriptTemplates/Editor.meta new file mode 100644 index 0000000..c9b29c6 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 96993aced46357f4d9dbda89e40eabad +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs new file mode 100644 index 0000000..0b7b78a --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs @@ -0,0 +1,42 @@ +using UnityEditor; +using UnityEngine; + +[InitializeOnLoad] +public class MoveToAssetsFolder +{ + const string FirstTimeKey = "MOVE_SCRIPT_TEMPLATES_HAS_RUN"; + const string targetFolder = "ScriptTemplates"; + const string targetPath = "Assets/ScriptTemplates"; + + static MoveToAssetsFolder() + { + if (!SessionState.GetBool(FirstTimeKey, false)) + { + FindAndMoveScriptTemplatesFolder(); + SessionState.SetBool(FirstTimeKey, true); + } + } + + static void FindAndMoveScriptTemplatesFolder() + { + string[] guids = AssetDatabase.FindAssets(targetFolder, null); + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + + // Check if it's a folder and not some random asset + if (AssetDatabase.IsValidFolder(path)) + { + // Ensure exact match of the name and that it's not in the Assets folder already + string folderName = System.IO.Path.GetFileName(path); + if (folderName == targetFolder && !path.StartsWith(targetPath)) + { + AssetDatabase.MoveAsset(path, targetPath); + Debug.LogFormat(LogType.Log, LogOption.NoStacktrace, null, $"Moved {targetFolder} to Assets folder."); + } + } + } + + AssetDatabase.Refresh(); + } +} diff --git a/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs.meta b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs.meta new file mode 100644 index 0000000..44f1113 --- /dev/null +++ b/Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e383132f744a8c1448807b71fa31f7ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/ScriptTemplates/Editor/MoveToAssetsFolder.cs + uploadId: 736421 diff --git a/Assets/Mirror/Runtime/Transports.meta b/Assets/Mirror/Transports.meta similarity index 77% rename from Assets/Mirror/Runtime/Transports.meta rename to Assets/Mirror/Transports.meta index fc29442..72fd20d 100644 --- a/Assets/Mirror/Runtime/Transports.meta +++ b/Assets/Mirror/Transports.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 7825d46cd73fe47938869eb5427b40fa +guid: d7d3068968d80427e8cc256e22b0b0b5 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Mirror/Transports/Edgegap.meta b/Assets/Mirror/Transports/Edgegap.meta new file mode 100644 index 0000000..39cd5b6 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2d2959d363903444bae4333db12a9ea1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta new file mode 100644 index 0000000..5621c20 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 447b4ad1a3db7cf4fa5a0709d297ba9b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs new file mode 100644 index 0000000..74945e1 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections; +using System.Threading; +using Mirror; +using UnityEngine; +using Random = UnityEngine.Random; +namespace Edgegap +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")] + public class EdgegapLobbyKcpTransport : EdgegapKcpTransport + { + [Header("Lobby Settings")] + [Tooltip("URL to the Edgegap lobby service, automatically filled in after completing the creation process via button below (or enter manually)")] + public string lobbyUrl; + [Tooltip("How long to wait for the relay to be assigned after starting a lobby")] + public float lobbyWaitTimeout = 60; + + public LobbyApi Api; + private LobbyCreateRequest? _request; + private string _lobbyId; + private string _playerId; + private TransportStatus _status = TransportStatus.Offline; + public enum TransportStatus + { + Offline, + CreatingLobby, + StartingLobby, + JoiningLobby, + WaitingRelay, + Connecting, + Connected, + Error, + } + public TransportStatus Status + { + get + { + if (!NetworkClient.active && !NetworkServer.active) + { + return TransportStatus.Offline; + } + if (_status == TransportStatus.Connecting) + { + if (NetworkServer.active) + { + switch (((EdgegapKcpServer)this.server).state) + { + case ConnectionState.Valid: + return TransportStatus.Connected; + case ConnectionState.Invalid: + case ConnectionState.SessionTimeout: + case ConnectionState.Error: + return TransportStatus.Error; + } + } + else if (NetworkClient.active) + { + switch (((EdgegapKcpClient)this.client).connectionState) + { + case ConnectionState.Valid: + return TransportStatus.Connected; + case ConnectionState.Invalid: + case ConnectionState.SessionTimeout: + case ConnectionState.Error: + return TransportStatus.Error; + } + } + } + return _status; + } + } + + protected override void Awake() + { + base.Awake(); + Api = new LobbyApi(lobbyUrl); + } + + private void Reset() + { + this.relayGUI = false; + } + + public override void ServerStart() + { + if (!_request.HasValue) + { + throw new Exception("No lobby request set. Call SetServerLobbyParams"); + } + _status = TransportStatus.CreatingLobby; + Api.CreateLobby(_request.Value, lobby => + { + _lobbyId = lobby.lobby_id; + _status = TransportStatus.StartingLobby; + Api.StartLobby(new LobbyIdRequest(_lobbyId), () => + { + StartCoroutine(WaitForLobbyRelay(_lobbyId, true)); + }, error => + { + _status = TransportStatus.Error; + string errorMsg = $"Could not start lobby: {error}"; + Debug.LogError(errorMsg); + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + }); + }, + error => + { + _status = TransportStatus.Error; + string errorMsg = $"Couldn't create lobby: {error}"; + Debug.LogError(errorMsg); + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + }); + } + + public override void ServerStop() + { + base.ServerStop(); + + Api.DeleteLobby(_lobbyId, () => + { + // yay + }, error => + { + OnServerError?.Invoke(0, TransportError.Unexpected, $"Failed to delete lobby: {error}"); + }); + } + + public override void ClientDisconnect() + { + base.ClientDisconnect(); + // this gets called for host mode as well + if (!NetworkServer.active) + { + Api.LeaveLobby(new LobbyJoinOrLeaveRequest + { + player = new LobbyJoinOrLeaveRequest.Player + { + id = _playerId + }, + lobby_id = _lobbyId + }, () => + { + // yay + }, error => + { + string errorMsg = $"Failed to leave lobby: {error}"; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + Debug.LogError(errorMsg); + }); + } + } + + public override void ClientConnect(string address) + { + _lobbyId = address; + _playerId = RandomPlayerId(); + _status = TransportStatus.JoiningLobby; + Api.JoinLobby(new LobbyJoinOrLeaveRequest + { + player = new LobbyJoinOrLeaveRequest.Player + { + id = _playerId, + }, + lobby_id = address + }, () => + { + StartCoroutine(WaitForLobbyRelay(_lobbyId, false)); + }, error => + { + _status = TransportStatus.Offline; + string errorMsg = $"Failed to join lobby: {error}"; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + Debug.LogError(errorMsg); + OnClientDisconnected?.Invoke(); + }); + } + + private IEnumerator WaitForLobbyRelay(string lobbyId, bool forServer) + { + _status = TransportStatus.WaitingRelay; + double startTime = NetworkTime.localTime; + bool running = true; + while (running) + { + if (NetworkTime.localTime - startTime >= lobbyWaitTimeout) + { + _status = TransportStatus.Error; + string errorMsg = "Timed out waiting for lobby."; + Debug.LogError(errorMsg); + if (forServer) + { + _status = TransportStatus.Error; + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + _status = TransportStatus.Error; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + yield break; + } + bool waitingForResponse = true; + Api.GetLobby(lobbyId, lobby => + { + waitingForResponse = false; + if (string.IsNullOrEmpty(lobby.assignment.ip)) + { + // no lobby deployed yet, have the outer loop retry + return; + } + relayAddress = lobby.assignment.ip; + foreach (Lobby.Port aport in lobby.assignment.ports) + { + if (aport.protocol == "UDP") + { + if (aport.name == "server") + { + relayGameServerPort = (ushort)aport.port; + + } + else if (aport.name == "client") + { + relayGameClientPort = (ushort)aport.port; + } + } + } + bool found = false; + foreach (Lobby.Player player in lobby.players) + { + if (player.id == _playerId) + { + userId = player.authorization_token; + sessionId = lobby.assignment.authorization_token; + found = true; + break; + } + } + running = false; + if (!found) + { + string errorMsg = $"Couldn't find my player ({_playerId})"; + Debug.LogError(errorMsg); + + if (forServer) + { + _status = TransportStatus.Error; + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + _status = TransportStatus.Error; + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + return; + } + _status = TransportStatus.Connecting; + if (forServer) + { + base.ServerStart(); + } + else + { + base.ClientConnect(""); + } + }, error => + { + running = false; + waitingForResponse = false; + _status = TransportStatus.Error; + string errorMsg = $"Failed to get lobby info: {error}"; + Debug.LogError(errorMsg); + if (forServer) + { + OnServerError?.Invoke(0, TransportError.Unexpected, errorMsg); + ServerStop(); + } + else + { + OnClientError?.Invoke(TransportError.Unexpected, errorMsg); + ClientDisconnect(); + } + }); + while (waitingForResponse) + { + yield return null; + } + yield return new WaitForSeconds(0.2f); + } + } + private static string RandomPlayerId() + { + return $"mirror-player-{Random.Range(1, int.MaxValue)}"; + } + + public void SetServerLobbyParams(string lobbyName, int capacity) + { + SetServerLobbyParams(new LobbyCreateRequest + { + player = new LobbyCreateRequest.Player + { + id = RandomPlayerId(), + }, + annotations = new LobbyCreateRequest.Annotation[] + { + }, + capacity = capacity, + is_joinable = true, + name = lobbyName, + tags = new string[] + { + } + }); + } + + public void SetServerLobbyParams(LobbyCreateRequest request) + { + _playerId = request.player.id; + _request = request; + } + + private void OnDestroy() + { + // attempt to clean up lobbies, if active + if (NetworkServer.active) + { + ServerStop(); + // Absolutely make sure there's time for the network request to hit edgegap servers. + // sorry. this can go once the lobby service can timeout lobbies itself + Thread.Sleep(300); + } + else if (NetworkClient.active) + { + ClientDisconnect(); + // Absolutely make sure there's time for the network request to hit edgegap servers. + // sorry. this can go once the lobby service can timeout lobbies itself + Thread.Sleep(300); + } + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta new file mode 100644 index 0000000..56c239c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fa9d4c3f48a245ed89f122f44e1e81ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/EdgegapLobbyKcpTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs new file mode 100644 index 0000000..c73a707 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace Edgegap +{ + // Implements the edgegap lobby api: https://docs.edgegap.com/docs/lobby/functions + public class LobbyApi + { + [Header("Lobby Config")] + public string LobbyUrl; + public LobbyBrief[] Lobbies; + + public LobbyApi(string url) + { + LobbyUrl = url; + } + + + + private static UnityWebRequest SendJson(string url, T data, string method = "POST") + { + string body = JsonUtility.ToJson(data); + UnityWebRequest request = new UnityWebRequest(url, method); + request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body)); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Accept", "application/json"); + request.SetRequestHeader("Content-Type", "application/json"); + return request; + } + + private static bool CheckErrorResponse(UnityWebRequest request, Action onError) + { +#if UNITY_2020_3_OR_NEWER + if (request.result != UnityWebRequest.Result.Success) + { + // how I hate http libs that think they need to be smart and handle status code errors. + if (request.result != UnityWebRequest.Result.ProtocolError || request.responseCode == 0) + { + onError?.Invoke(request.error); + return true; + } + } +#else + if (request.isNetworkError) + { + onError?.Invoke(request.error); + return true; + } +#endif + if (request.responseCode < 200 || request.responseCode >= 300) + { + onError?.Invoke($"non-200 status code: {request.responseCode}. Body:\n {request.downloadHandler.text}"); + return true; + } + return false; + } + + public void RefreshLobbies(Action onLoaded, Action onError) + { + UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies"); + request.SendWebRequest().completed += operation => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + ListLobbiesResponse lobbies = JsonUtility.FromJson(request.downloadHandler.text); + Lobbies = lobbies.data; + onLoaded?.Invoke(lobbies.data); + } + }; + } + + public void CreateLobby(LobbyCreateRequest createData, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies", createData); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + Lobby lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void UpdateLobby(string lobbyId, LobbyUpdateRequest updateData, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", updateData, "PATCH"); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyBrief lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void GetLobby(string lobbyId, Action onResponse, Action onError) + { + UnityWebRequest request = UnityWebRequest.Get($"{LobbyUrl}/lobbies/{lobbyId}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + Lobby lobby = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(lobby); + } + }; + } + + public void JoinLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:join", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void LeaveLobby(LobbyJoinOrLeaveRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:leave", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void StartLobby(LobbyIdRequest data, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies:start", data); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + public void DeleteLobby(string lobbyId, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson($"{LobbyUrl}/lobbies/{lobbyId}", "", "DELETE"); + request.SetRequestHeader("Content-Type", "application/json"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + onResponse?.Invoke(); + } + }; + } + + struct CreateLobbyServiceRequest + { + public string name; + } + public struct LobbyServiceResponse + { + public string name; + public string url; + public string status; + } + + public static void TrimApiKey(ref string apiKey) + { + if (apiKey == null) + { + return; + } + if (apiKey.StartsWith("token ")) + { + apiKey = apiKey.Substring("token ".Length); + } + apiKey = apiKey.Trim(); + } + + public static void CreateAndDeployLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + // try to get the lobby first + GetLobbyService(apiKey, name, response => + { + if (response == null) + { + CreateLobbyService(apiKey, name, onResponse, onError); + } + else if (!string.IsNullOrEmpty(response.Value.url)) + { + onResponse(response.Value); + } + else + { + DeployLobbyService(apiKey, name, onResponse, onError); + } + }, onError); + } + + private static void CreateLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + UnityWebRequest request = SendJson("https://api.edgegap.com/v1/lobbies", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + DeployLobbyService(apiKey, name, onResponse, onError); + } + }; + } + + public static void GetLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + var request = UnityWebRequest.Get($"https://api.edgegap.com/v1/lobbies/{name}"); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (request.responseCode == 404) + { + onResponse(null); + return; + } + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse(response); + } + }; + } + + public static void TerminateLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + TrimApiKey(ref apiKey); + + var request = SendJson("https://api.edgegap.com/v1/lobbies:terminate", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(response); + } + }; + } + private static void DeployLobbyService(string apiKey, string name, Action onResponse, Action onError) + { + var request = SendJson("https://api.edgegap.com/v1/lobbies:deploy", new CreateLobbyServiceRequest + { + name = name + }); + request.SetRequestHeader("Authorization", $"token {apiKey}"); + request.SendWebRequest().completed += (op) => + { + using (request) + { + if (CheckErrorResponse(request, onError)) return; + LobbyServiceResponse response = JsonUtility.FromJson(request.downloadHandler.text); + onResponse?.Invoke(response); + } + }; + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta new file mode 100644 index 0000000..7ad55b0 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 64510fc75d0d75f4185fec1cf4d12206 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyApi.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs new file mode 100644 index 0000000..7aee63c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs @@ -0,0 +1,138 @@ +using System; +using System.Threading; +using UnityEditor; +using UnityEngine; +#if UNITY_EDITOR +namespace Edgegap +{ + public class LobbyServiceCreateDialogue : EditorWindow + { + public Action onLobby; + public bool waitingCreate; + public bool waitingStatus; + private string _name; + private string _key; + private string _lastStatus; + + private void Awake() + { + minSize = maxSize = new Vector2(450, 300); + titleContent = new GUIContent("Edgegap Lobby Service Setup"); + } + + private void OnGUI() + { + if (waitingCreate) + { + EditorGUILayout.LabelField("Waiting for lobby to create . . . "); + return; + } + if (waitingStatus) + { + EditorGUILayout.LabelField("Waiting for lobby to deploy . . . "); + EditorGUILayout.LabelField($"Latest status: {_lastStatus}"); + return; + } + _key = EditorGUILayout.TextField("Edgegap API key", _key); + LobbyApi.TrimApiKey(ref _key); + EditorGUILayout.HelpBox(new GUIContent("Your API key won't be saved.")); + if (GUILayout.Button("I have no api key?")) + { + Application.OpenURL("https://app.edgegap.com/user-settings?tab=tokens"); + } + EditorGUILayout.Separator(); + EditorGUILayout.HelpBox("There's currently a bug where lobby names longer than 5 characters can fail to deploy correctly and will return a \"503 Service Temporarily Unavailable\"\nIt's recommended to limit your lobby names to 4-5 characters for now", UnityEditor.MessageType.Warning); + _name = EditorGUILayout.TextField("Lobby Name", _name); + EditorGUILayout.HelpBox(new GUIContent("The lobby name is your games identifier for the lobby service")); + + if (GUILayout.Button("Create")) + { + if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name)) + { + EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok"); + } + else + { + waitingCreate = true; + Repaint(); + + LobbyApi.CreateAndDeployLobbyService(_key.Trim(), _name.Trim(), res => + { + waitingCreate = false; + waitingStatus = true; + _lastStatus = res.status; + RefreshStatus(); + Repaint(); + }, error => + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) the lobby service:\n\n{error}", "Ok"); + waitingCreate = false; + }); + return; + } + + } + + if (GUILayout.Button("Cancel")) + Close(); + + EditorGUILayout.HelpBox(new GUIContent("Note: If you forgot your lobby url simply re-create it with the same name!\nIt will re-use the existing lobby service")); + EditorGUILayout.Separator(); + EditorGUILayout.Separator(); + + + if (GUILayout.Button("Terminate existing deploy")) + { + + if (string.IsNullOrWhiteSpace(_key) || string.IsNullOrWhiteSpace(_name)) + { + EditorUtility.DisplayDialog("Error", "Key and Name can't be empty.", "Ok"); + } + else + { + LobbyApi.TerminateLobbyService(_key.Trim(), _name.Trim(), res => + { + EditorUtility.DisplayDialog("Success", $"The lobby service will start terminating (shutting down the deploy) now", "Ok"); + }, error => + { + EditorUtility.DisplayDialog("Failed to terminate lobby", $"The following error happened while trying to terminate the lobby service:\n\n{error}", "Ok"); + }); + } + } + EditorGUILayout.HelpBox(new GUIContent("Done with your lobby?\nEnter the same name as creation to shut it down")); + } + private void RefreshStatus() + { + // Stop if window is closed + if (!this) + { + return; + } + LobbyApi.GetLobbyService(_key, _name, res => + { + if (!res.HasValue) + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The lobby seems to have vanished while waiting for it to deploy.", "Ok"); + waitingStatus = false; + Repaint(); + return; + } + if (!string.IsNullOrEmpty(res.Value.url)) + { + onLobby(res.Value.url); + Close(); + return; + } + _lastStatus = res.Value.status; + Repaint(); + Thread.Sleep(100); // :( but this is a lazy editor script, its fiiine + RefreshStatus(); + }, error => + { + EditorUtility.DisplayDialog("Failed to create lobby", $"The following error happened while trying to create (&deploy) a lobby:\n\n{error}", "Ok"); + waitingStatus = false; + }); + } + } +} +#endif diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta new file mode 100644 index 0000000..3ef32f3 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 25579cc004424981bf0b05bcec65df0a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyServiceCreateDialogue.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs new file mode 100644 index 0000000..3d9aa32 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Reflection; +using kcp2k; +using UnityEditor; +using UnityEngine; +#if UNITY_EDITOR +namespace Edgegap +{ + [CustomEditor(typeof(EdgegapLobbyKcpTransport))] + public class EncryptionTransportInspector : UnityEditor.Editor + { + SerializedProperty lobbyUrlProperty; + SerializedProperty lobbyWaitTimeoutProperty; + private List kcpProperties = new List(); + + + // Assuming proper SerializedProperty definitions for properties + // Add more SerializedProperty fields related to different modes as needed + + void OnEnable() + { + lobbyUrlProperty = serializedObject.FindProperty("lobbyUrl"); + lobbyWaitTimeoutProperty = serializedObject.FindProperty("lobbyWaitTimeout"); + // Get public fields from KcpTransport + kcpProperties.Clear(); + FieldInfo[] fields = typeof(KcpTransport).GetFields(BindingFlags.Public | BindingFlags.Instance); + foreach (var field in fields) + { + SerializedProperty prop = serializedObject.FindProperty(field.Name); + if (prop == null) + { + // callbacks have no property + continue; + } + kcpProperties.Add(prop); + } + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + EditorGUILayout.PropertyField(lobbyUrlProperty); + if (GUILayout.Button("Create & Deploy Lobby")) + { + var input = CreateInstance(); + input.onLobby = (url) => + { + lobbyUrlProperty.stringValue = url; + serializedObject.ApplyModifiedProperties(); + }; + input.ShowUtility(); + } + EditorGUILayout.PropertyField(lobbyWaitTimeoutProperty); + EditorGUILayout.Separator(); + foreach (SerializedProperty prop in kcpProperties) + { + EditorGUILayout.PropertyField(prop); + } + serializedObject.ApplyModifiedProperties(); + } + } +} + +#endif diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta new file mode 100644 index 0000000..9c04c2e --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 7d7cc53263184754a4682335440df515 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/LobbyTransportInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta new file mode 100644 index 0000000..feda681 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b9b459cf5e084bdd8b196df849a2c519 +timeCreated: 1709953502 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs new file mode 100644 index 0000000..dd8ac68 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#functions + [Serializable] + public struct ListLobbiesResponse + { + public int count; + public LobbyBrief[] data; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta new file mode 100644 index 0000000..c2b9160 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fdb37041d9464f8c90ac86942b940565 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/ListLobbiesResponse.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs new file mode 100644 index 0000000..c08c899 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs @@ -0,0 +1,45 @@ +using System; +using UnityEngine; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#getting-a-specific-lobbys-information + [Serializable] + public struct Lobby + { + [Serializable] + public struct Player + { + public uint authorization_token; + public string id; + public bool is_host; + } + + [Serializable] + public struct Port + { + public string name; + public int port; + public string protocol; + } + + [Serializable] + public struct Assignment + { + public uint authorization_token; + public string host; + public string ip; + public Port[] ports; + } + + public Assignment assignment; + public string name; + public string lobby_id; + public bool is_joinable; + public bool is_started; + public int player_count; + public int capacity; + public int available_slots => capacity - player_count; + public string[] tags; + public Player[] players; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta new file mode 100644 index 0000000..3b4e187 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 64db55f096cd4ace83e1aa1c0c0588f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/Lobby.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs new file mode 100644 index 0000000..1e927e3 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs @@ -0,0 +1,17 @@ +using System; +namespace Edgegap +{ + // Brief lobby data, returned by the list function + [Serializable] + public struct LobbyBrief + { + public string lobby_id; + public string name; + public bool is_joinable; + public bool is_started; + public int player_count; + public int capacity; + public int available_slots => capacity - player_count; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta new file mode 100644 index 0000000..4bf80d8 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 6018ece006144e719c6b3f0d4e256d7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyBrief.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs new file mode 100644 index 0000000..2ed819d --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs @@ -0,0 +1,27 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#creating-a-new-lobby + [Serializable] + public struct LobbyCreateRequest + { + [Serializable] + public struct Player + { + public string id; + } + [Serializable] + public struct Annotation + { + public bool inject; + public string key; + public string value; + } + public Annotation[] annotations; // todo + public int capacity; + public bool is_joinable; + public string name; + public Player player; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta new file mode 100644 index 0000000..7b0c8cb --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4040c1adafc3449eaebd3bd22aa3ff26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyCreateRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs new file mode 100644 index 0000000..3647979 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs @@ -0,0 +1,14 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions/#starting-a-lobby + [Serializable] + public struct LobbyIdRequest + { + public string lobby_id; + public LobbyIdRequest(string lobbyId) + { + lobby_id = lobbyId; + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta new file mode 100644 index 0000000..1e79270 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 219c7fba8724473caf170c6254e6dc45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyIdRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs new file mode 100644 index 0000000..7c2115f --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs @@ -0,0 +1,17 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby + // https://docs.edgegap.com/docs/lobby/functions#leaving-a-lobby + [Serializable] + public struct LobbyJoinOrLeaveRequest + { + [Serializable] + public struct Player + { + public string id; + } + public string lobby_id; + public Player player; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta new file mode 100644 index 0000000..a888de8 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 4091d555e62341f0ac30479952d517aa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyJoinOrLeaveRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs new file mode 100644 index 0000000..3b8b53c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs @@ -0,0 +1,12 @@ +using System; +namespace Edgegap +{ + // https://docs.edgegap.com/docs/lobby/functions#updating-a-lobby + [Serializable] + public struct LobbyUpdateRequest + { + public int capacity; + public bool is_joinable; + public string[] tags; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta new file mode 100644 index 0000000..c233f83 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: ee158bc379f44cdf9904578f37a5e7a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapLobby/Models/LobbyUpdateRequest.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta new file mode 100644 index 0000000..34ff726 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 62c28e855fc644011b4079c268b46b71 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs new file mode 100644 index 0000000..ecec9f7 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs @@ -0,0 +1,141 @@ +// overwrite RawSend/Receive +using System; +using System.Net.Sockets; +using Mirror; +using UnityEngine; +using kcp2k; + +namespace Edgegap +{ + public class EdgegapKcpClient : KcpClient + { + // need buffer larger than KcpClient.rawReceiveBuffer to add metadata + readonly byte[] relayReceiveBuffer; + + // authentication + public uint userId; + public uint sessionId; + public ConnectionState connectionState = ConnectionState.Disconnected; + + // ping + double lastPingTime; + + public EdgegapKcpClient( + Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + : base(OnConnected, OnData, OnDisconnected, OnError, config) + { + relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead]; + } + + // custom start function with relay parameters; connects udp client. + public void Connect(string relayAddress, ushort relayPort, uint userId, uint sessionId) + { + // reset last state + connectionState = ConnectionState.Checking; + this.userId = userId; + this.sessionId = sessionId; + + // reuse base connect + base.Connect(relayAddress, relayPort); + } + + // parse metadata, then pass to kcp + protected override bool RawReceive(out ArraySegment segment) + { + segment = default; + if (socket == null) return false; + + try + { + if (socket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment content)) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(content)) + { + // parse message type + if (reader.Remaining == 0) + { + Debug.LogWarning($"EdgegapClient: message of {content.Count} is too small to parse."); + return false; + } + byte messageType = reader.ReadByte(); + + // handle message type + switch (messageType) + { + case (byte)MessageType.Ping: + { + // parse state + if (reader.Remaining < 1) return false; + ConnectionState last = connectionState; + connectionState = (ConnectionState)reader.ReadByte(); + + // log state changes for debugging. + if (connectionState != last) Debug.Log($"EdgegapClient: state updated to: {connectionState}"); + + // return true indicates Mirror to keep checking + // for further messages. + return true; + } + case (byte)MessageType.Data: + { + segment = reader.ReadBytesSegment(reader.Remaining); + return true; + } + // wrong message type. return false, don't throw. + default: return false; + } + } + } + } + catch (SocketException e) + { + Log.Info($"EdgegapClient: looks like the other end has closed the connection. This is fine: {e}"); + Disconnect(); + } + + return false; + } + + protected override void RawSend(ArraySegment data) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Data); + writer.WriteBytes(data.Array, data.Offset, data.Count); + base.RawSend(writer); + } + } + + void SendPing() + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Ping); + base.RawSend(writer); + } + } + + public override void TickOutgoing() + { + if (connected) + { + // ping every interval for keepalive & handshake + if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval) + { + SendPing(); + lastPingTime = NetworkTime.localTime; + } + } + + base.TickOutgoing(); + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs.meta new file mode 100644 index 0000000..28915cf --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: a0d6fba7098f4ea3949d0195e8276adc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpClient.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs new file mode 100644 index 0000000..cc87b14 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs @@ -0,0 +1,203 @@ +using System; +using System.Net; +using System.Net.Sockets; +using Mirror; +using UnityEngine; +using kcp2k; + +namespace Edgegap +{ + public class EdgegapKcpServer : KcpServer + { + // need buffer larger than KcpClient.rawReceiveBuffer to add metadata + readonly byte[] relayReceiveBuffer; + + // authentication + public uint userId; + public uint sessionId; + public ConnectionState state = ConnectionState.Disconnected; + + // server is an UDP client talking to relay + protected Socket relaySocket; + public EndPoint remoteEndPoint; + + // ping + double lastPingTime; + + // custom 'active'. while connected to relay + bool relayActive; + + public EdgegapKcpServer( + Action OnConnected, + Action, KcpChannel> OnData, + Action OnDisconnected, + Action OnError, + KcpConfig config) + // TODO don't call base. don't listen to local UdpServer at all? + : base(OnConnected, OnData, OnDisconnected, OnError, config) + { + relayReceiveBuffer = new byte[config.Mtu + Protocol.Overhead]; + } + + public override bool IsActive() => relayActive; + + // custom start function with relay parameters; connects udp client. + public void Start(string relayAddress, ushort relayPort, uint userId, uint sessionId) + { + // reset last state + state = ConnectionState.Checking; + this.userId = userId; + this.sessionId = sessionId; + + // try resolve host name + if (!Common.ResolveHostname(relayAddress, out IPAddress[] addresses)) + { + OnError(0, ErrorCode.DnsResolve, $"Failed to resolve host: {relayAddress}"); + return; + } + + // create socket + remoteEndPoint = new IPEndPoint(addresses[0], relayPort); + relaySocket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + relaySocket.Blocking = false; + + // configure buffer sizes + Common.ConfigureSocketBuffers(relaySocket, config.RecvBufferSize, config.SendBufferSize); + + // bind to endpoint for Send/Receive instead of SendTo/ReceiveFrom + relaySocket.Connect(remoteEndPoint); + relayActive = true; + } + + public override void Stop() + { + relayActive = false; + } + + protected override bool RawReceiveFrom(out ArraySegment segment, out int connectionId) + { + segment = default; + connectionId = 0; + + if (relaySocket == null) return false; + + try + { + // TODO need separate buffer. don't write into result yet. only payload + + if (relaySocket.ReceiveNonBlocking(relayReceiveBuffer, out ArraySegment content)) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(content)) + { + // parse message type + if (reader.Remaining == 0) + { + Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse header."); + return false; + } + byte messageType = reader.ReadByte(); + + // handle message type + switch (messageType) + { + case (byte)MessageType.Ping: + { + // parse state + if (reader.Remaining < 1) return false; + ConnectionState last = state; + state = (ConnectionState)reader.ReadByte(); + + // log state changes for debugging. + if (state != last) Debug.Log($"EdgegapServer: state updated to: {state}"); + + // return true indicates Mirror to keep checking + // for further messages. + return true; + } + case (byte)MessageType.Data: + { + // parse connectionId and payload + if (reader.Remaining <= 4) + { + Debug.LogWarning($"EdgegapServer: message of {content.Count} is too small to parse connId."); + return false; + } + + connectionId = reader.ReadInt(); + segment = reader.ReadBytesSegment(reader.Remaining); + // Debug.Log($"EdgegapServer: receiving from connId={connectionId}: {segment.ToHexString()}"); + return true; + } + // wrong message type. return false, don't throw. + default: return false; + } + } + } + } + catch (SocketException e) + { + Log.Info($"EdgegapServer: looks like the other end has closed the connection. This is fine: {e}"); + } + return false; + } + + protected override void RawSend(int connectionId, ArraySegment data) + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // Debug.Log($"EdgegapServer: sending to connId={connectionId}: {data.ToHexString()}"); + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Data); + writer.WriteInt(connectionId); + writer.WriteBytes(data.Array, data.Offset, data.Count); + ArraySegment message = writer; + + try + { + relaySocket.SendNonBlocking(message); + } + catch (SocketException e) + { + Log.Error($"KcpRleayServer: RawSend failed: {e}"); + } + } + } + + void SendPing() + { + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + writer.WriteUInt(userId); + writer.WriteUInt(sessionId); + writer.WriteByte((byte)MessageType.Ping); + ArraySegment message = writer; + + try + { + relaySocket.SendNonBlocking(message); + } + catch (SocketException e) + { + Debug.LogWarning($"EdgegapServer: failed to ping. perhaps the relay isn't running? {e}"); + } + } + } + + public override void TickOutgoing() + { + if (relayActive) + { + // ping every interval for keepalive & handshake + if (NetworkTime.localTime >= lastPingTime + Protocol.PingInterval) + { + SendPing(); + lastPingTime = NetworkTime.localTime; + } + } + + // base processing + base.TickOutgoing(); + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs.meta new file mode 100644 index 0000000..42a969c --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fd8551078397248b0848950352c208ee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpServer.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs new file mode 100644 index 0000000..e0a4100 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs @@ -0,0 +1,162 @@ +// edgegap relay transport. +// reuses KcpTransport with custom KcpServer/Client. + +//#if MIRROR <- commented out because MIRROR isn't defined on first import yet +using System; +using System.Text.RegularExpressions; +using UnityEngine; +using Mirror; +using kcp2k; + +namespace Edgegap +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/edgegap-transports/edgegap-relay")] + public class EdgegapKcpTransport : KcpTransport + { + [Header("Relay")] + public string relayAddress = "127.0.0.1"; + public ushort relayGameServerPort = 8888; + public ushort relayGameClientPort = 9999; + + // mtu for kcp transport. respects relay overhead. + public const int MaxPayload = Kcp.MTU_DEF - Protocol.Overhead; + + [Header("Relay")] + public bool relayGUI = true; + public uint userId = 11111111; + public uint sessionId = 22222222; + + // helper + internal static String ReParse(String cmd, String pattern, String defaultValue) + { + Match match = Regex.Match(cmd, pattern); + return match.Success ? match.Groups[1].Value : defaultValue; + } + + protected override void Awake() + { + // logging + // Log.Info should use Debug.Log if enabled, or nothing otherwise + // (don't want to spam the console on headless servers) + if (debugLog) + Log.Info = Debug.Log; + else + Log.Info = _ => {}; + Log.Warning = Debug.LogWarning; + Log.Error = Debug.LogError; + + // create config from serialized settings. + // with MaxPayload as max size to respect relay overhead. + config = new KcpConfig(DualMode, RecvBufferSize, SendBufferSize, MaxPayload, NoDelay, Interval, FastResend, false, SendWindowSize, ReceiveWindowSize, Timeout, MaxRetransmit); + + // client (NonAlloc version is not necessary anymore) + client = new EdgegapKcpClient( + () => OnClientConnected.Invoke(), + (message, channel) => OnClientDataReceived.Invoke(message, FromKcpChannel(channel)), + () => OnClientDisconnected?.Invoke(), // may be null in StopHost(): https://github.com/MirrorNetworking/Mirror/issues/3708 + (error, reason) => OnClientError.Invoke(ToTransportError(error), reason), + config + ); + + // server + server = new EdgegapKcpServer( + (connectionId, endPoint) => OnServerConnectedWithAddress.Invoke(connectionId, endPoint.PrettyAddress()), + (connectionId, message, channel) => OnServerDataReceived.Invoke(connectionId, message, FromKcpChannel(channel)), + (connectionId) => OnServerDisconnected.Invoke(connectionId), + (connectionId, error, reason) => OnServerError.Invoke(connectionId, ToTransportError(error), reason), + config); + + if (statisticsLog) + InvokeRepeating(nameof(OnLogStatistics), 1, 1); + + Debug.Log("EdgegapTransport initialized!"); + } + + protected override void OnValidate() + { + // show max message sizes in inspector for convenience. + // 'config' isn't available in edit mode yet, so use MTU define. + ReliableMaxMessageSize = KcpPeer.ReliableMaxMessageSize(MaxPayload, ReceiveWindowSize); + UnreliableMaxMessageSize = KcpPeer.UnreliableMaxMessageSize(MaxPayload); + } + + // client overwrites to use EdgegapClient instead of KcpClient + public override void ClientConnect(string address) + { + // connect to relay address:port instead of the expected server address + EdgegapKcpClient client = (EdgegapKcpClient)this.client; + client.userId = userId; + client.sessionId = sessionId; + client.connectionState = ConnectionState.Checking; // reset from last time + client.Connect(relayAddress, relayGameClientPort); + } + public override void ClientConnect(Uri uri) + { + if (uri.Scheme != Scheme) + throw new ArgumentException($"Invalid url {uri}, use {Scheme}://host:port instead", nameof(uri)); + + // connect to relay address:port instead of the expected server address + EdgegapKcpClient client = (EdgegapKcpClient)this.client; + client.Connect(relayAddress, relayGameClientPort, userId, sessionId); + } + + // server overwrites to use EdgegapServer instead of KcpServer + public override void ServerStart() + { + // start the server + EdgegapKcpServer server = (EdgegapKcpServer)this.server; + server.Start(relayAddress, relayGameServerPort, userId, sessionId); + } + + void OnGUIRelay() + { + // if (server.IsActive()) return; + + GUILayout.BeginArea(new Rect(300, 30, 200, 100)); + + GUILayout.BeginHorizontal(); + GUILayout.Label("SessionId:"); + sessionId = Convert.ToUInt32(GUILayout.TextField(sessionId.ToString())); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + GUILayout.Label("UserId:"); + userId = Convert.ToUInt32(GUILayout.TextField(userId.ToString())); + GUILayout.EndHorizontal(); + + if (NetworkServer.active) + { + EdgegapKcpServer server = (EdgegapKcpServer)this.server; + GUILayout.BeginHorizontal(); + GUILayout.Label("State:"); + GUILayout.Label(server.state.ToString()); + GUILayout.EndHorizontal(); + } + else if (NetworkClient.active) + { + EdgegapKcpClient client = (EdgegapKcpClient)this.client; + GUILayout.BeginHorizontal(); + GUILayout.Label("State:"); + GUILayout.Label(client.connectionState.ToString()); + GUILayout.EndHorizontal(); + } + + GUILayout.EndArea(); + } + + // base OnGUI only shows in editor & development builds. + // here we always show it because we need the sessionid & userid buttons. +#pragma warning disable CS0109 + new void OnGUI() + { +#if UNITY_EDITOR || DEVELOPMENT_BUILD + base.OnGUI(); +#endif + if (relayGUI) OnGUIRelay(); + } + + public override string ToString() => "Edgegap Kcp Transport"; + } +#pragma warning restore CS0109 +} +//#endif MIRROR <- commented out because MIRROR isn't defined on first import yet diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs.meta new file mode 100644 index 0000000..66be633 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: c2d1e0e17f753449798fa27474d6b86b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/EdgegapKcpTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs new file mode 100644 index 0000000..58ba478 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs @@ -0,0 +1,29 @@ +// relay protocol definitions +namespace Edgegap +{ + public enum ConnectionState : byte + { + Disconnected = 0, // until the user calls connect() + Checking = 1, // recently connected, validation in progress + Valid = 2, // validation succeeded + Invalid = 3, // validation rejected by tower + SessionTimeout = 4, // session owner timed out + Error = 5, // other error + } + + public enum MessageType : byte + { + Ping = 1, + Data = 2 + } + + public static class Protocol + { + // MTU: relay adds up to 13 bytes of metadata in the worst case. + public const int Overhead = 13; + + // ping interval should be between 100 ms and 1 second. + // faster ping gives faster authentication, but higher bandwidth. + public const float PingInterval = 0.5f; + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs.meta new file mode 100644 index 0000000..900ad7d --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: eac30312ba61470b849e368af3c3b0e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/Protocol.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md new file mode 100644 index 0000000..51add6e --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md @@ -0,0 +1,20 @@ +# Edgegap Relay for Mirror +Documentation: https://docs.edgegap.com/docs/distributed-relay-manager/ + +## Prerequisites +- Unity project set up with the Mirror networking library installed + - Supported Versions: [Mirror](https://assetstore.unity.com/packages/tools/network/mirror-129321) and [Mirror LTS](https://assetstore.unity.com/packages/tools/network/mirror-lts-102631) +- EdgegapTransport module downloaded and extracted + +## Steps +1. Open your Unity project and navigate to the "Assets" folder. +2. Locate the "Mirror" folder within "Assets" and open it. +3. Within the "Mirror" folder, open the "Transports" folder. +4. Drag and drop the "Unity" folder from the extracted EdgegapTransport files into the "Transports" folder. +5. Open your NetworkManager script in the Unity Editor and navigate to the "Inspector" panel. +6. In the "Inspector" panel, locate the "Network Manager" component and click the "+" button next to the "Transport" property. +7. In the "Add Component" menu that appears, select "Edgegap Transport" to add it to the NetworkManager. +8. Drag the newly added "Edgegap Transport" component into the "Transport" property in the "Inspector" panel. + +## Notes +- The EdgegapTransport module is only compatible with Mirror and Mirror LTS versions. diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta new file mode 100644 index 0000000..1302efd --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 8ade7c960d8fe4e94970ddd88ede3bca +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/README.md + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs new file mode 100644 index 0000000..670c507 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs @@ -0,0 +1,25 @@ +// parse session_id and user_id from command line args. +// mac: "open mirror.app --args session_id=123 user_id=456" +using System; +using UnityEngine; + +namespace Edgegap +{ + public class RelayCredentialsFromArgs : MonoBehaviour + { + void Awake() + { + String cmd = Environment.CommandLine; + + // parse session_id via regex + String sessionId = EdgegapKcpTransport.ReParse(cmd, "session_id=(\\d+)", "111111"); + String userID = EdgegapKcpTransport.ReParse(cmd, "user_id=(\\d+)", "222222"); + Debug.Log($"Parsed sessionId: {sessionId} user_id: {userID}"); + + // configure transport + EdgegapKcpTransport transport = GetComponent(); + transport.sessionId = UInt32.Parse(sessionId); + transport.userId = UInt32.Parse(userID); + } + } +} diff --git a/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs.meta b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs.meta new file mode 100644 index 0000000..c373ec3 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e9ec7091b26c4d3882f4b42f10f9b8c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 3ea6ff15cda674a57b0c7c8b7dc1878c, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/EdgegapRelay/RelayCredentialsFromArgs.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Edgegap/edgegap.png b/Assets/Mirror/Transports/Edgegap/edgegap.png new file mode 100644 index 0000000..b52c295 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/edgegap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9a2bb9d1b247129cfcd1e8b41400fed37eb423ad47a2baba5e2270f67f8ce13 +size 4347 diff --git a/Assets/Mirror/Transports/Edgegap/edgegap.png.meta b/Assets/Mirror/Transports/Edgegap/edgegap.png.meta new file mode 100644 index 0000000..2be5b45 --- /dev/null +++ b/Assets/Mirror/Transports/Edgegap/edgegap.png.meta @@ -0,0 +1,130 @@ +fileFormatVersion: 2 +guid: 3ea6ff15cda674a57b0c7c8b7dc1878c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 0 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 16 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Edgegap/edgegap.png + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption.meta b/Assets/Mirror/Transports/Encryption.meta new file mode 100644 index 0000000..db711f4 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 741b3c7e5d0842049ff50a2f6e27ca12 +timeCreated: 1708015148 diff --git a/Assets/Mirror/Transports/Encryption/Editor.meta b/Assets/Mirror/Transports/Encryption/Editor.meta new file mode 100644 index 0000000..7111250 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0d3cd9d7d6e84a578f7e4b384ff813f1 +timeCreated: 1708793986 diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef new file mode 100644 index 0000000..0ba9c76 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "EncryptionTransportEditor", + "rootNamespace": "", + "references": [ + "GUID:627104647b9c04b4ebb8978a92ecac63" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta new file mode 100644 index 0000000..bbe2e8c --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 4c9c7b0ef83e6e945b276d644816a489 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportEditor.asmdef + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs new file mode 100644 index 0000000..8ffb428 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs @@ -0,0 +1,90 @@ +using UnityEditor; +using UnityEngine; + +namespace Mirror.Transports.Encryption +{ + [CustomEditor(typeof(EncryptionTransport), true)] + public class EncryptionTransportInspector : UnityEditor.Editor + { + SerializedProperty innerProperty; + SerializedProperty clientValidatesServerPubKeyProperty; + SerializedProperty clientTrustedPubKeySignaturesProperty; + SerializedProperty serverKeypairPathProperty; + SerializedProperty serverLoadKeyPairFromFileProperty; + + // Assuming proper SerializedProperty definitions for properties + // Add more SerializedProperty fields related to different modes as needed + + void OnEnable() + { + innerProperty = serializedObject.FindProperty("Inner"); + clientValidatesServerPubKeyProperty = serializedObject.FindProperty("ClientValidateServerPubKey"); + clientTrustedPubKeySignaturesProperty = serializedObject.FindProperty("ClientTrustedPubKeySignatures"); + serverKeypairPathProperty = serializedObject.FindProperty("ServerKeypairPath"); + serverLoadKeyPairFromFileProperty = serializedObject.FindProperty("ServerLoadKeyPairFromFile"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // Draw default inspector for the parent class + DrawDefaultInspector(); + EditorGUILayout.LabelField("Encryption Settings", EditorStyles.boldLabel); + if (innerProperty != null) + { + EditorGUILayout.LabelField("Common", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(innerProperty); + EditorGUILayout.Separator(); + } + // Client Section + EditorGUILayout.LabelField("Client", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Validating the servers public key is essential for complete man-in-the-middle (MITM) safety, but might not be feasible for all modes of hosting.", MessageType.Info); + EditorGUILayout.PropertyField(clientValidatesServerPubKeyProperty, new GUIContent("Validate Server Public Key")); + + EncryptionTransport.ValidationMode validationMode = (EncryptionTransport.ValidationMode)clientValidatesServerPubKeyProperty.enumValueIndex; + + switch (validationMode) + { + case EncryptionTransport.ValidationMode.List: + EditorGUILayout.PropertyField(clientTrustedPubKeySignaturesProperty); + break; + case EncryptionTransport.ValidationMode.Callback: + EditorGUILayout.HelpBox("Please set the EncryptionTransport.onClientValidateServerPubKey at runtime.", MessageType.Info); + break; + } + + EditorGUILayout.Separator(); + // Server Section + EditorGUILayout.LabelField("Server", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(serverLoadKeyPairFromFileProperty, new GUIContent("Load Keypair From File")); + if (serverLoadKeyPairFromFileProperty.boolValue) + { + EditorGUILayout.PropertyField(serverKeypairPathProperty, new GUIContent("Keypair File Path")); + } + if(GUILayout.Button("Generate Key Pair")) + { + EncryptionCredentials keyPair = EncryptionCredentials.Generate(); + string path = EditorUtility.SaveFilePanel("Select where to save the keypair", "", "server-keys.json", "json"); + if (!string.IsNullOrEmpty(path)) + { + keyPair.SaveToFile(path); + EditorUtility.DisplayDialog("KeyPair Saved", $"Successfully saved the keypair.\nThe fingerprint is {keyPair.PublicKeyFingerprint}, you can also retrieve it from the saved json file at any point.", "Ok"); + if (validationMode == EncryptionTransport.ValidationMode.List) + { + if (EditorUtility.DisplayDialog("Add key to trusted list?", "Do you also want to add the generated key to the trusted list?", "Yes", "No")) + { + clientTrustedPubKeySignaturesProperty.arraySize++; + clientTrustedPubKeySignaturesProperty.GetArrayElementAtIndex(clientTrustedPubKeySignaturesProperty.arraySize - 1).stringValue = keyPair.PublicKeyFingerprint; + } + } + } + } + + serializedObject.ApplyModifiedProperties(); + } + + [CustomEditor(typeof(ThreadedEncryptionKcpTransport), true)] + class EncryptionThreadedTransportInspector : EncryptionTransportInspector {} + } +} diff --git a/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta new file mode 100644 index 0000000..d7bb0c1 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 871580d2094a46139279d651cec92b5d +timeCreated: 1708794004 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/Editor/EncryptionTransportInspector.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs new file mode 100644 index 0000000..f91ad60 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs @@ -0,0 +1,554 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Mirror.BouncyCastle.Crypto; +using Mirror.BouncyCastle.Crypto.Agreement; +using Mirror.BouncyCastle.Crypto.Digests; +using Mirror.BouncyCastle.Crypto.Generators; +using Mirror.BouncyCastle.Crypto.Modes; +using Mirror.BouncyCastle.Crypto.Parameters; +using UnityEngine.Profiling; + +namespace Mirror.Transports.Encryption +{ + public class EncryptedConnection + { + // 256-bit key + const int KeyLength = 32; + // 512-bit salt for the key derivation function + const int HkdfSaltSize = KeyLength * 2; + + // Info tag for the HKDF, this just adds more entropy + static readonly byte[] HkdfInfo = Encoding.UTF8.GetBytes("Mirror/EncryptionTransport"); + + // fixed size of the unique per-packet nonce. Defaults to 12 bytes/96 bits (not recommended to be changed) + const int NonceSize = 12; + + // this is the size of the "checksum" included in each encrypted payload + // 16 bytes/128 bytes is the recommended value for best security + // can be reduced to 12 bytes for a small space savings, but makes encryption slightly weaker. + // Setting it lower than 12 bytes is not recommended + const int MacSizeBytes = 16; + + const int MacSizeBits = MacSizeBytes * 8; + + // How much metadata overhead we have for regular packets + public const int Overhead = sizeof(OpCodes) + MacSizeBytes + NonceSize; + + // After how many seconds of not receiving a handshake packet we should time out + const double DurationTimeout = 2; // 2s + + // After how many seconds to assume the last handshake packet got lost and to resend another one + const double DurationResend = 0.05; // 50ms + + + // Static fields for allocation efficiency, makes this not thread safe + // It'd be as easy as using ThreadLocal though to fix that + + // Set up a global cipher instance, it is initialised/reset before use + // (AesFastEngine used to exist, but was removed due to side channel issues) + // use AesUtilities.CreateEngine here as it'll pick the hardware accelerated one if available (which is will not be unless on .net core) + static readonly ThreadLocal Cipher = new ThreadLocal(() => new GcmBlockCipher(AesUtilities.CreateEngine())); + + // Set up a global HKDF with a SHA-256 digest + static readonly ThreadLocal Hkdf = new ThreadLocal(() => new HkdfBytesGenerator(new Sha256Digest())); + + // Global byte array to store nonce sent by the remote side, they're used immediately after + static readonly ThreadLocal ReceiveNonce = new ThreadLocal(() => new byte[NonceSize]); + + // Buffer for the remote salt, as bouncycastle needs to take a byte[] *rolls eyes* + static readonly ThreadLocal TMPRemoteSaltBuffer = new ThreadLocal(() => new byte[HkdfSaltSize]); + + // buffer for encrypt/decrypt operations, resized larger as needed + static ThreadLocal TMPCryptBuffer = new ThreadLocal(() => new byte[2048]); + + // packet headers + enum OpCodes : byte + { + // start at 1 to maybe filter out random noise + Data = 1, + HandshakeStart = 2, + HandshakeAck = 3, + HandshakeFin = 4 + } + + enum State + { + // Waiting for a handshake to arrive + // this is for _sendsFirst: + // - false: OpCodes.HandshakeStart + // - true: Opcodes.HandshakeAck + WaitingHandshake, + + // Waiting for a handshake reply/acknowledgement to arrive + // this is for _sendsFirst: + // - false: OpCodes.HandshakeFine + // - true: Opcodes.Data (implicitly) + WaitingHandshakeReply, + + // Both sides have confirmed the keys are exchanged and data can be sent freely + Ready + } + + State state = State.WaitingHandshake; + + // Key exchange confirmed and data can be sent freely + public bool IsReady => state == State.Ready; + // Callback to send off encrypted data + readonly Action, int> send; + // Callback when received data has been decrypted + readonly Action, int> receive; + // Callback when the connection becomes ready + readonly Action ready; + // On-error callback, disconnect expected + readonly Action error; + // Optional callback to validate the remotes public key, validation on one side is necessary to ensure MITM resistance + // (usually client validates the server key) + readonly Func validateRemoteKey; + // Our asymmetric credentials for the initial DH exchange + EncryptionCredentials credentials; + readonly byte[] hkdfSalt; + NetworkReader _tmpReader = new NetworkReader(new ArraySegment()); + + // After no handshake packet in this many seconds, the handshake fails + double handshakeTimeout; + // When to assume the last handshake packet got lost and to resend another one + double nextHandshakeResend; + + + // we can reuse the _cipherParameters here since the nonce is stored as the byte[] reference we pass in + // so we can update it without creating a new AeadParameters instance + // this might break in the future! (will cause bad data) + byte[] nonce = new byte[NonceSize]; + AeadParameters cipherParametersEncrypt; + AeadParameters cipherParametersDecrypt; + + + /* + * Specifies if we send the first key, then receive ack, then send fin + * Or the opposite if set to false + * + * The client does this, since the fin is not acked explicitly, but by receiving data to decrypt + */ + readonly bool sendsFirst; + + public EncryptedConnection(EncryptionCredentials credentials, + bool isClient, + Action, int> sendAction, + Action, int> receiveAction, + Action readyAction, + Action errorAction, + Func validateRemoteKey = null) + { + this.credentials = credentials; + sendsFirst = isClient; + if (!sendsFirst) + // salt is controlled by the server + hkdfSalt = GenerateSecureBytes(HkdfSaltSize); + send = sendAction; + receive = receiveAction; + ready = readyAction; + error = errorAction; + this.validateRemoteKey = validateRemoteKey; + } + + // Generates a random starting nonce + static byte[] GenerateSecureBytes(int size) + { + byte[] bytes = new byte[size]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + rng.GetBytes(bytes); + + return bytes; + } + + + public void OnReceiveRaw(ArraySegment data, int channel) + { + if (data.Count < 1) + { + error(TransportError.Unexpected, "Received empty packet"); + return; + } + + _tmpReader.SetBuffer(data); + OpCodes opcode = (OpCodes)_tmpReader.ReadByte(); + switch (opcode) + { + case OpCodes.Data: + // first sender ready is implicit when data is received + if (sendsFirst && state == State.WaitingHandshakeReply) + SetReady(); + else if (!IsReady) + error(TransportError.Unexpected, "Unexpected data while not ready."); + + if (_tmpReader.Remaining < Overhead) + { + error(TransportError.Unexpected, "received data packet smaller than metadata size"); + return; + } + + ArraySegment ciphertext = _tmpReader.ReadBytesSegment(_tmpReader.Remaining - NonceSize); + _tmpReader.ReadBytes(ReceiveNonce.Value, NonceSize); + + Profiler.BeginSample("EncryptedConnection.Decrypt"); + ArraySegment plaintext = Decrypt(ciphertext); + Profiler.EndSample(); + if (plaintext.Count == 0) + // error + return; + receive(plaintext, channel); + break; + case OpCodes.HandshakeStart: + if (sendsFirst) + { + error(TransportError.Unexpected, "Received HandshakeStart packet, we don't expect this."); + return; + } + + if (state == State.WaitingHandshakeReply) + // this is fine, packets may arrive out of order + return; + + state = State.WaitingHandshakeReply; + ResetTimeouts(); + CompleteExchange(_tmpReader.ReadBytesSegment(_tmpReader.Remaining), hkdfSalt); + SendHandshakeAndPubKey(OpCodes.HandshakeAck); + break; + case OpCodes.HandshakeAck: + if (!sendsFirst) + { + error(TransportError.Unexpected, "Received HandshakeAck packet, we don't expect this."); + return; + } + + if (IsReady) + // this is fine, packets may arrive out of order + return; + + if (state == State.WaitingHandshakeReply) + // this is fine, packets may arrive out of order + return; + + + state = State.WaitingHandshakeReply; + ResetTimeouts(); + _tmpReader.ReadBytes(TMPRemoteSaltBuffer.Value, HkdfSaltSize); + CompleteExchange(_tmpReader.ReadBytesSegment(_tmpReader.Remaining), TMPRemoteSaltBuffer.Value); + SendHandshakeFin(); + break; + case OpCodes.HandshakeFin: + if (sendsFirst) + { + error(TransportError.Unexpected, "Received HandshakeFin packet, we don't expect this."); + return; + } + + if (IsReady) + // this is fine, packets may arrive out of order + return; + + if (state != State.WaitingHandshakeReply) + { + error(TransportError.Unexpected, + "Received HandshakeFin packet, we didn't expect this yet."); + return; + } + + SetReady(); + + break; + default: + error(TransportError.InvalidReceive, $"Unhandled opcode {(byte)opcode:x}"); + break; + } + } + + void SetReady() + { + // done with credentials, null out the reference + credentials = null; + + state = State.Ready; + ready(); + } + + void ResetTimeouts() + { + handshakeTimeout = 0; + nextHandshakeResend = -1; + } + + public void Send(ArraySegment data, int channel) + { + using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get()) + { + writer.WriteByte((byte)OpCodes.Data); + Profiler.BeginSample("EncryptedConnection.Encrypt"); + ArraySegment encrypted = Encrypt(data); + Profiler.EndSample(); + + if (encrypted.Count == 0) + // error + return; + writer.WriteBytes(encrypted.Array, 0, encrypted.Count); + // write nonce after since Encrypt will update it + writer.WriteBytes(nonce, 0, NonceSize); + send(writer.ToArraySegment(), channel); + } + } + + ArraySegment Encrypt(ArraySegment plaintext) + { + if (plaintext.Count == 0) + // Invalid + return new ArraySegment(); + // Need to make the nonce unique again before encrypting another message + UpdateNonce(); + // Re-initialize the cipher with our cached parameters + Cipher.Value.Init(true, cipherParametersEncrypt); + + // Calculate the expected output size, this should always be input size + mac size + int outSize = Cipher.Value.GetOutputSize(plaintext.Count); +#if UNITY_EDITOR + // expecting the outSize to be input size + MacSize + if (outSize != plaintext.Count + MacSizeBytes) + throw new Exception($"Encrypt: Unexpected output size (Expected {plaintext.Count + MacSizeBytes}, got {outSize}"); +#endif + // Resize the static buffer to fit + byte[] cryptBuffer = TMPCryptBuffer.Value; + EnsureSize(ref cryptBuffer, outSize); + TMPCryptBuffer.Value = cryptBuffer; + + int resultLen; + try + { + // Run the plain text through the cipher, ProcessBytes will only process full blocks + resultLen = + Cipher.Value.ProcessBytes(plaintext.Array, plaintext.Offset, plaintext.Count, cryptBuffer, 0); + // Then run any potentially remaining partial blocks through with DoFinal (and calculate the mac) + resultLen += Cipher.Value.DoFinal(cryptBuffer, resultLen); + } + // catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types + // + catch (Exception e) + { + error(TransportError.Unexpected, $"Unexpected exception while encrypting {e.GetType()}: {e.Message}"); + return new ArraySegment(); + } +#if UNITY_EDITOR + // expecting the result length to match the previously calculated input size + MacSize + if (resultLen != outSize) + throw new Exception($"Encrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})"); +#endif + return new ArraySegment(cryptBuffer, 0, resultLen); + } + + ArraySegment Decrypt(ArraySegment ciphertext) + { + if (ciphertext.Count <= MacSizeBytes) + { + error(TransportError.Unexpected, $"Received too short data packet (min {{MacSizeBytes + 1}}, got {ciphertext.Count})"); + // Invalid + return new ArraySegment(); + } + // Re-initialize the cipher with our cached parameters + Cipher.Value.Init(false, cipherParametersDecrypt); + + // Calculate the expected output size, this should always be input size - mac size + int outSize = Cipher.Value.GetOutputSize(ciphertext.Count); +#if UNITY_EDITOR + // expecting the outSize to be input size - MacSize + if (outSize != ciphertext.Count - MacSizeBytes) + throw new Exception($"Decrypt: Unexpected output size (Expected {ciphertext.Count - MacSizeBytes}, got {outSize}"); +#endif + + byte[] cryptBuffer = TMPCryptBuffer.Value; + EnsureSize(ref cryptBuffer, outSize); + TMPCryptBuffer.Value = cryptBuffer; + + int resultLen; + try + { + // Run the ciphertext through the cipher, ProcessBytes will only process full blocks + resultLen = + Cipher.Value.ProcessBytes(ciphertext.Array, ciphertext.Offset, ciphertext.Count, cryptBuffer, 0); + // Then run any potentially remaining partial blocks through with DoFinal (and calculate/check the mac) + resultLen += Cipher.Value.DoFinal(cryptBuffer, resultLen); + } + // catch all Exception's since BouncyCastle is fairly noisy with both standard and their own exception types + catch (Exception e) + { + error(TransportError.Unexpected, $"Unexpected exception while decrypting {e.GetType()}: {e.Message}. This usually signifies corrupt data"); + return new ArraySegment(); + } +#if UNITY_EDITOR + // expecting the result length to match the previously calculated input size + MacSize + if (resultLen != outSize) + throw new Exception($"Decrypt: resultLen did not match outSize (expected {outSize}, got {resultLen})"); +#endif + return new ArraySegment(cryptBuffer, 0, resultLen); + } + + void UpdateNonce() + { + // increment the nonce by one + // we need to ensure the nonce is *always* unique and not reused + // easiest way to do this is by simply incrementing it + for (int i = 0; i < NonceSize; i++) + { + nonce[i]++; + if (nonce[i] != 0) + break; + } + } + + static void EnsureSize(ref byte[] buffer, int size) + { + if (buffer.Length < size) + // double buffer to avoid constantly resizing by a few bytes + Array.Resize(ref buffer, Math.Max(size, buffer.Length * 2)); + } + + void SendHandshakeAndPubKey(OpCodes opcode) + { + using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get()) + { + writer.WriteByte((byte)opcode); + if (opcode == OpCodes.HandshakeAck) + writer.WriteBytes(hkdfSalt, 0, HkdfSaltSize); + writer.WriteBytes(credentials.PublicKeySerialized, 0, credentials.PublicKeySerialized.Length); + send(writer.ToArraySegment(), Channels.Unreliable); + } + } + + void SendHandshakeFin() + { + using (ConcurrentNetworkWriterPooled writer = ConcurrentNetworkWriterPool.Get()) + { + writer.WriteByte((byte)OpCodes.HandshakeFin); + send(writer.ToArraySegment(), Channels.Unreliable); + } + } + + void CompleteExchange(ArraySegment remotePubKeyRaw, byte[] salt) + { + AsymmetricKeyParameter remotePubKey; + try + { + remotePubKey = EncryptionCredentials.DeserializePublicKey(remotePubKeyRaw); + } + catch (Exception e) + { + error(TransportError.Unexpected, $"Failed to deserialize public key of remote. {e.GetType()}: {e.Message}"); + return; + } + + if (validateRemoteKey != null) + { + PubKeyInfo info = new PubKeyInfo + { + Fingerprint = EncryptionCredentials.PubKeyFingerprint(remotePubKeyRaw), + Serialized = remotePubKeyRaw, + Key = remotePubKey + }; + if (!validateRemoteKey(info)) + { + error(TransportError.Unexpected, $"Remote public key (fingerprint: {info.Fingerprint}) failed validation. "); + return; + } + } + + // Calculate a common symmetric key from our private key and the remotes public key + // This gives us the same key on the other side, with our public key and their remote + // It's like magic, but with math! + ECDHBasicAgreement ecdh = new ECDHBasicAgreement(); + ecdh.Init(credentials.PrivateKey); + byte[] sharedSecret; + try + { + sharedSecret = ecdh.CalculateAgreement(remotePubKey).ToByteArrayUnsigned(); + } + catch + (Exception e) + { + error(TransportError.Unexpected, $"Failed to calculate the ECDH key exchange. {e.GetType()}: {e.Message}"); + return; + } + + if (salt.Length != HkdfSaltSize) + { + error(TransportError.Unexpected, $"Salt is expected to be {HkdfSaltSize} bytes long, got {salt.Length}."); + return; + } + + Hkdf.Value.Init(new HkdfParameters(sharedSecret, salt, HkdfInfo)); + + // Allocate a buffer for the output key + byte[] keyRaw = new byte[KeyLength]; + + // Generate the output keying material + Hkdf.Value.GenerateBytes(keyRaw, 0, keyRaw.Length); + + KeyParameter key = new KeyParameter(keyRaw); + + // generate a starting nonce + nonce = GenerateSecureBytes(NonceSize); + + // we pass in the nonce array once (as it's stored by reference) so we can cache the AeadParameters instance + // instead of creating a new one each encrypt/decrypt + cipherParametersEncrypt = new AeadParameters(key, MacSizeBits, nonce); + cipherParametersDecrypt = new AeadParameters(key, MacSizeBits, ReceiveNonce.Value); + } + + /** + * non-ready connections need to be ticked for resending key data over unreliable + */ + public void TickNonReady(double time) + { + if (IsReady) + return; + + // Timeout reset + if (handshakeTimeout == 0) + handshakeTimeout = time + DurationTimeout; + else if (time > handshakeTimeout) + { + error?.Invoke(TransportError.Timeout, $"Timed out during {state}, this probably just means the other side went away which is fine."); + return; + } + + // Timeout reset + if (nextHandshakeResend < 0) + { + nextHandshakeResend = time + DurationResend; + return; + } + + if (time < nextHandshakeResend) + // Resend isn't due yet + return; + + nextHandshakeResend = time + DurationResend; + switch (state) + { + case State.WaitingHandshake: + if (sendsFirst) + SendHandshakeAndPubKey(OpCodes.HandshakeStart); + + break; + case State.WaitingHandshakeReply: + if (sendsFirst) + SendHandshakeFin(); + else + SendHandshakeAndPubKey(OpCodes.HandshakeAck); + + break; + case State.Ready: // IsReady is checked above & early-returned + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta new file mode 100644 index 0000000..84c136a --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptedConnection.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 28f3ac4ff1d346a895d0b4ff714fb57b +timeCreated: 1708111337 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/EncryptedConnection.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs new file mode 100644 index 0000000..5834366 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs @@ -0,0 +1,119 @@ +using System; +using System.IO; +using Mirror.BouncyCastle.Asn1.Pkcs; +using Mirror.BouncyCastle.Asn1.X509; +using Mirror.BouncyCastle.Crypto; +using Mirror.BouncyCastle.Crypto.Digests; +using Mirror.BouncyCastle.Crypto.Generators; +using Mirror.BouncyCastle.X509; +using Mirror.BouncyCastle.Crypto.Parameters; +using Mirror.BouncyCastle.Pkcs; +using Mirror.BouncyCastle.Security; +using UnityEngine; + +namespace Mirror.Transports.Encryption +{ + public class EncryptionCredentials + { + const int PrivateKeyBits = 256; + // don't actually need to store this currently + // but we'll need to for loading/saving from file maybe? + // public ECPublicKeyParameters PublicKey; + + // The serialized public key, in DER format + public byte[] PublicKeySerialized; + public ECPrivateKeyParameters PrivateKey; + public string PublicKeyFingerprint; + + EncryptionCredentials() {} + + // TODO: load from file + public static EncryptionCredentials Generate() + { + var generator = new ECKeyPairGenerator(); + generator.Init(new KeyGenerationParameters(new SecureRandom(), PrivateKeyBits)); + AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair(); + var serialized = SerializePublicKey((ECPublicKeyParameters)keyPair.Public); + return new EncryptionCredentials + { + // see fields above + // PublicKey = (ECPublicKeyParameters)keyPair.Public, + PublicKeySerialized = serialized, + PublicKeyFingerprint = PubKeyFingerprint(new ArraySegment(serialized)), + PrivateKey = (ECPrivateKeyParameters)keyPair.Private + }; + } + + public static byte[] SerializePublicKey(AsymmetricKeyParameter publicKey) + { + // apparently the best way to transmit this public key over the network is to serialize it as a DER + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(publicKey); + return publicKeyInfo.ToAsn1Object().GetDerEncoded(); + } + + public static AsymmetricKeyParameter DeserializePublicKey(ArraySegment pubKey) => + // And then we do this to deserialize from the DER (from above) + // the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted + // to a byte[] first and then shoved through a MemoryStream + PublicKeyFactory.CreateKey(new MemoryStream(pubKey.Array, pubKey.Offset, pubKey.Count, false)); + + public static byte[] SerializePrivateKey(AsymmetricKeyParameter privateKey) + { + // Serialize privateKey as a DER + PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKey); + return privateKeyInfo.ToAsn1Object().GetDerEncoded(); + } + + public static AsymmetricKeyParameter DeserializePrivateKey(ArraySegment privateKey) => + // And then we do this to deserialize from the DER (from above) + // the "new MemoryStream" actually saves an allocation, since otherwise the ArraySegment would be converted + // to a byte[] first and then shoved through a MemoryStream + PrivateKeyFactory.CreateKey(new MemoryStream(privateKey.Array, privateKey.Offset, privateKey.Count, false)); + + public static string PubKeyFingerprint(ArraySegment publicKeyBytes) + { + Sha256Digest digest = new Sha256Digest(); + byte[] hash = new byte[digest.GetDigestSize()]; + digest.BlockUpdate(publicKeyBytes.Array, publicKeyBytes.Offset, publicKeyBytes.Count); + digest.DoFinal(hash, 0); + + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } + + public void SaveToFile(string path) + { + string json = JsonUtility.ToJson(new SerializedPair + { + PublicKeyFingerprint = PublicKeyFingerprint, + PublicKey = Convert.ToBase64String(PublicKeySerialized), + PrivateKey= Convert.ToBase64String(SerializePrivateKey(PrivateKey)) + }); + File.WriteAllText(path, json); + } + + public static EncryptionCredentials LoadFromFile(string path) + { + string json = File.ReadAllText(path); + SerializedPair serializedPair = JsonUtility.FromJson(json); + + byte[] publicKeyBytes = Convert.FromBase64String(serializedPair.PublicKey); + byte[] privateKeyBytes = Convert.FromBase64String(serializedPair.PrivateKey); + + if (serializedPair.PublicKeyFingerprint != PubKeyFingerprint(new ArraySegment(publicKeyBytes))) + throw new Exception("Saved public key fingerprint does not match public key."); + return new EncryptionCredentials + { + PublicKeySerialized = publicKeyBytes, + PublicKeyFingerprint = serializedPair.PublicKeyFingerprint, + PrivateKey = (ECPrivateKeyParameters) DeserializePrivateKey(new ArraySegment(privateKeyBytes)) + }; + } + + class SerializedPair + { + public string PublicKeyFingerprint; + public string PublicKey; + public string PrivateKey; + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta new file mode 100644 index 0000000..16dbc7e --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: af6ae5f74f9548588cba5731643fabaf +timeCreated: 1708139579 +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/EncryptionCredentials.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs new file mode 100644 index 0000000..3bb8d1a --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using Mirror.BouncyCastle.Crypto; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Serialization; + +namespace Mirror.Transports.Encryption +{ + [HelpURL("https://mirror-networking.gitbook.io/docs/manual/transports/encryption-transport")] + public class EncryptionTransport : Transport, PortTransport + { + public override bool IsEncrypted => true; + public override string EncryptionCipher => "AES256-GCM"; + [FormerlySerializedAs("inner")] + [HideInInspector] + public Transport Inner; + + public ushort Port + { + get + { + if (Inner is PortTransport portTransport) + return portTransport.Port; + + Debug.LogError($"EncryptionTransport can't get Port because {Inner} is not a PortTransport"); + return 0; + } + set + { + if (Inner is PortTransport portTransport) + { + portTransport.Port = value; + return; + } + Debug.LogError($"EncryptionTransport can't set Port because {Inner} is not a PortTransport"); + } + } + + public enum ValidationMode + { + Off, + List, + Callback + } + + [FormerlySerializedAs("clientValidateServerPubKey")] + [HideInInspector] + public ValidationMode ClientValidateServerPubKey; + [FormerlySerializedAs("clientTrustedPubKeySignatures")] + [HideInInspector] + [Tooltip("List of public key fingerprints the client will accept")] + public string[] ClientTrustedPubKeySignatures; + public Func OnClientValidateServerPubKey; + [FormerlySerializedAs("serverLoadKeyPairFromFile")] + [HideInInspector] + public bool ServerLoadKeyPairFromFile; + [FormerlySerializedAs("serverKeypairPath")] + [HideInInspector] + public string ServerKeypairPath = "./server-keys.json"; + + EncryptedConnection client; + + readonly Dictionary serverConnections = new Dictionary(); + + readonly List serverPendingConnections = + new List(); + + EncryptionCredentials credentials; + public string EncryptionPublicKeyFingerprint => credentials?.PublicKeyFingerprint; + public byte[] EncryptionPublicKey => credentials?.PublicKeySerialized; + + void ServerRemoveFromPending(EncryptedConnection con) + { + for (int i = 0; i < serverPendingConnections.Count; i++) + if (serverPendingConnections[i] == con) + { + // remove by swapping with last + int lastIndex = serverPendingConnections.Count - 1; + serverPendingConnections[i] = serverPendingConnections[lastIndex]; + serverPendingConnections.RemoveAt(lastIndex); + break; + } + } + + void HandleInnerServerDisconnected(int connId) + { + if (serverConnections.TryGetValue(connId, out EncryptedConnection con)) + { + ServerRemoveFromPending(con); + serverConnections.Remove(connId); + } + OnServerDisconnected?.Invoke(connId); + } + + void HandleInnerServerError(int connId, TransportError type, string msg) => OnServerError?.Invoke(connId, type, $"inner: {msg}"); + + void HandleInnerServerDataReceived(int connId, ArraySegment data, int channel) + { + if (serverConnections.TryGetValue(connId, out EncryptedConnection c)) + c.OnReceiveRaw(data, channel); + } + + void HandleInnerServerConnected(int connId) => HandleInnerServerConnected(connId, Inner.ServerGetClientAddress(connId)); + + void HandleInnerServerConnected(int connId, string clientRemoteAddress) + { + Debug.Log($"[EncryptionTransport] New connection #{connId} from {clientRemoteAddress}"); + EncryptedConnection ec = null; + ec = new EncryptedConnection( + credentials, + false, + (segment, channel) => Inner.ServerSend(connId, segment, channel), + (segment, channel) => OnServerDataReceived?.Invoke(connId, segment, channel), + () => + { + Debug.Log($"[EncryptionTransport] Connection #{connId} is ready"); + // ReSharper disable once AccessToModifiedClosure + ServerRemoveFromPending(ec); + OnServerConnectedWithAddress?.Invoke(connId, clientRemoteAddress); + }, + (type, msg) => + { + OnServerError?.Invoke(connId, type, msg); + ServerDisconnect(connId); + }); + serverConnections.Add(connId, ec); + serverPendingConnections.Add(ec); + } + + void HandleInnerClientDisconnected() + { + client = null; + OnClientDisconnected?.Invoke(); + } + + void HandleInnerClientError(TransportError arg1, string arg2) => OnClientError?.Invoke(arg1, $"inner: {arg2}"); + + void HandleInnerClientDataReceived(ArraySegment data, int channel) => client?.OnReceiveRaw(data, channel); + + void HandleInnerClientConnected() => + client = new EncryptedConnection( + credentials, + true, + (segment, channel) => Inner.ClientSend(segment, channel), + (segment, channel) => OnClientDataReceived?.Invoke(segment, channel), + () => + { + OnClientConnected?.Invoke(); + }, + (type, msg) => + { + OnClientError?.Invoke(type, msg); + ClientDisconnect(); + }, + HandleClientValidateServerPubKey); + + bool HandleClientValidateServerPubKey(PubKeyInfo pubKeyInfo) + { + switch (ClientValidateServerPubKey) + { + case ValidationMode.Off: + return true; + case ValidationMode.List: + return Array.IndexOf(ClientTrustedPubKeySignatures, pubKeyInfo.Fingerprint) >= 0; + case ValidationMode.Callback: + return OnClientValidateServerPubKey(pubKeyInfo); + default: + throw new ArgumentOutOfRangeException(); + } + } + + void Awake() => + // check if encryption via hardware acceleration is supported. + // this can be useful to know for low end devices. + // + // hardware acceleration requires netcoreapp3.0 or later: + // https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/AesUtilities.cs#L18 + // because AesEngine_x86 requires System.Runtime.Intrinsics.X86: + // https://github.com/bcgit/bc-csharp/blob/449940429c57686a6fcf6bfbb4d368dec19d906e/crypto/src/crypto/engines/AesEngine_X86.cs + // which Unity does not support yet. + Debug.Log($"EncryptionTransport: IsHardwareAccelerated={AesUtilities.IsHardwareAccelerated}"); + + public override bool Available() => Inner.Available(); + + public override bool ClientConnected() => client != null && client.IsReady; + + public override void ClientConnect(string address) + { + switch (ClientValidateServerPubKey) + { + case ValidationMode.Off: + break; + case ValidationMode.List: + if (ClientTrustedPubKeySignatures == null || ClientTrustedPubKeySignatures.Length == 0) + { + OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to List, but the clientTrustedPubKeySignatures list is empty."); + return; + } + break; + case ValidationMode.Callback: + if (OnClientValidateServerPubKey == null) + { + OnClientError?.Invoke(TransportError.Unexpected, "Validate Server Public Key is set to Callback, but the onClientValidateServerPubKey handler is not set"); + return; + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + credentials = EncryptionCredentials.Generate(); + Inner.OnClientConnected = HandleInnerClientConnected; + Inner.OnClientDataReceived = HandleInnerClientDataReceived; + Inner.OnClientDataSent = (bytes, channel) => OnClientDataSent?.Invoke(bytes, channel); + Inner.OnClientError = HandleInnerClientError; + Inner.OnClientDisconnected = HandleInnerClientDisconnected; + Inner.ClientConnect(address); + } + + public override void ClientSend(ArraySegment segment, int channelId = Channels.Reliable) => + client?.Send(segment, channelId); + + public override void ClientDisconnect() => Inner.ClientDisconnect(); + + public override Uri ServerUri() => Inner.ServerUri(); + + public override bool ServerActive() => Inner.ServerActive(); + + public override void ServerStart() + { + if (ServerLoadKeyPairFromFile) + credentials = EncryptionCredentials.LoadFromFile(ServerKeypairPath); + else + credentials = EncryptionCredentials.Generate(); +#pragma warning disable CS0618 // Type or member is obsolete + Inner.OnServerConnected = HandleInnerServerConnected; +#pragma warning restore CS0618 // Type or member is obsolete + Inner.OnServerConnectedWithAddress = HandleInnerServerConnected; + Inner.OnServerDataReceived = HandleInnerServerDataReceived; + Inner.OnServerDataSent = (connId, bytes, channel) => OnServerDataSent?.Invoke(connId, bytes, channel); + Inner.OnServerError = HandleInnerServerError; + Inner.OnServerDisconnected = HandleInnerServerDisconnected; + Inner.ServerStart(); + } + + public override void ServerSend(int connectionId, ArraySegment segment, int channelId = Channels.Reliable) + { + if (serverConnections.TryGetValue(connectionId, out EncryptedConnection connection) && connection.IsReady) + connection.Send(segment, channelId); + } + + public override void ServerDisconnect(int connectionId) => + // cleanup is done via inners disconnect event + Inner.ServerDisconnect(connectionId); + + public override string ServerGetClientAddress(int connectionId) => Inner.ServerGetClientAddress(connectionId); + + public override void ServerStop() => Inner.ServerStop(); + + public override int GetMaxPacketSize(int channelId = Channels.Reliable) => + Inner.GetMaxPacketSize(channelId) - EncryptedConnection.Overhead; + + public override int GetBatchThreshold(int channelId = Channels.Reliable) => Inner.GetBatchThreshold(channelId) - EncryptedConnection.Overhead; + + public override void Shutdown() => Inner.Shutdown(); + + public override void ClientEarlyUpdate() => Inner.ClientEarlyUpdate(); + + public override void ClientLateUpdate() + { + Inner.ClientLateUpdate(); + Profiler.BeginSample("EncryptionTransport.ServerLateUpdate"); + client?.TickNonReady(NetworkTime.localTime); + Profiler.EndSample(); + } + + public override void ServerEarlyUpdate() => Inner.ServerEarlyUpdate(); + + public override void ServerLateUpdate() + { + Inner.ServerLateUpdate(); + Profiler.BeginSample("EncryptionTransport.ServerLateUpdate"); + // Reverse iteration as entries can be removed while updating + for (int i = serverPendingConnections.Count - 1; i >= 0; i--) + serverPendingConnections[i].TickNonReady(NetworkTime.time); + Profiler.EndSample(); + } + } +} diff --git a/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta new file mode 100644 index 0000000..3ad4787 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/EncryptionTransport.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 0aa135acc32a4383ae9a5817f018cb06 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/EncryptionTransport.cs + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/Plugins.meta b/Assets/Mirror/Transports/Encryption/Plugins.meta new file mode 100644 index 0000000..bc110a7 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4048f5ff245dfa34abec0a401364e7c0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle.meta b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle.meta new file mode 100644 index 0000000..e064e80 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 31ff83bf6d2e72542adcbe2c21383f4a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md new file mode 100644 index 0000000..92e7901 --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md @@ -0,0 +1,15 @@ +Version with renamed namespaces to avoid conflicts lives here: https://github.com/MirrorNetworking/bc-csharp + +Copyright (c) 2000-2024 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org). +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sub license, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this +permission notice shall be included in all copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.** diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md.meta b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md.meta new file mode 100644 index 0000000..d93eeac --- /dev/null +++ b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 2b45a99b5583cda419e1f1ec943fec4b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 129321 + packageName: Mirror + packageVersion: 96.0.1 + assetPath: Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/LICENSE.md + uploadId: 736421 diff --git a/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll b/Assets/Mirror/Transports/Encryption/Plugins/BouncyCastle/Mirror.BouncyCastle.Cryptography.dll new file mode 100644 index 0000000000000000000000000000000000000000..5838952e5945d6356e40e4462608f62c65c5a454 GIT binary patch literal 6856192 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%0D3eqrlbcbH|$v3MJ%oC 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%0DPbJbC6@r8Z3F0}TmYCU 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

Yc=_~Ew`BYvt}i+nZT~@yeRe>`K*WaR!nZK_ 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Ξfq1K#M7|1Lrbd%5$)De-91DfYw1Lylo&Kg^olhTd()p3>G7%JeYsR( z`7o@or@b)N*!!lr#mc8SL^{=hOv?u&LR@0T%R~pX2a{6>tGEwe+tus{>SZa__e4mJ8UD~d#0u1Pl*So;~Z04={|VZqCPtC8EHqtK%y@-YrBo; zjFzTf7nXClc)IqE)Yc%Qls%L|?5({gwOv8~a@&$s#TBTwH#?g{7qnD)s#qCZM#|1k zNA$fu`XbKw>ss25c5qBf?w6J~=r6sWFdN^g*LwLk5&fj4wiwA=Vi(579nCuQmH2%$ zPkQ0`nc~6GccdM+dW*Di2+@6A_MMedV$|c}6~*IQ3;L+@ym;_2R{Gq^Tty@X)6x?C zsrA0vDkav>wtAx_LLD+l+v;o<4_-PXt(=3=$-UJWnSo<^ZxP-~QAC|@a~??91?XQs zw>;!s9yx`cmX>$i=9vE7vIE!hx@;==!j_Yvr#Whe8wxvKO?>J8i;RPJ4~kx4#ER>9 zbDYGb<_)P={j!F~K}$ z=Om`1J4=ZqIHzN^2gmkGZramDj?QRAlusXRkGQbqY5l~%HqVJyi+`4wcEUNsqpA9G z$!7j)srQv1iJsE-p7~IEe<9A$VwxrCE1t%zkqFfoEw$B#PHC-|*hln!un}5{bDCq- z(MDt7YSFuiUf{BS*<3v5WAQl=o^tOuei>in)^%*hnB?{j8$nb_vud+M?@n_$7k)qw znm%eOwdG7AdY@bq*3ixddlRImtE`f^)XpR9wJjp0Pmga{>UzJ~D!#ljT%^wL;;Vc5 z=;4CGN?jppsM$e`UGDun{iO~}2Bt+V;63~NLq&;s{0rZ*(e~cLIo(=I z##PIoq_(u>C2uT5u4A7o<(H9orKCe$ zbRB0#il_HlNOX(tkrMaEOFkH~SL}U-I(q1`(~yU_ZFMpJxL42B5U(y^>~g(1-V^D{ z<1#wmpD1PhO%m~Y7!jPC@>Y^|w5lb(6hKV5XSTm6tlJo@PP8vR=Ego+8naIPvz(fgGE|aN6xYXIJemQ(#Y8ZwdHTo4$iFEw@Z5~BXe^duReyo zR;|}{y7XK6SBXYzb^;kVPD{gQh=27_g5y_pphT#{DDm{CRpN^?)S*eb?2rlK)#>*| zFKrD`S!Cc%@M6vc&2xyyyPS?uhhZDv{j%_!gkHaxo*m zAgraOugIu-XM=d!_7l9_P0yG6Y(6;EU5=jIF8#46ok;seiT9-=67d&NFUP*ZOp#7c zl@gR$T0VW2WV2i)#Fqn@4R~y{sxPsh-Css*@m&&^?REuqxhwAG_k;Ok@>~{VG`2}UkvLewp<{e_S?LCY9$5NrvQrn*VA`N|3>KNOTOm6Szi{<-eWyJRC zBxn5o%2MJj7m;zA6ZwiQztKaiWWg$fw)97bjlz+ev1zEu?#pYNzZ^|acxZkUN-eJ$_a~SPx zrFLJ5#xSfrxJ4V#`+T%uVX^Z1R*@d-DA!S+G!~zOL1fPJ^KFdDL|yOx0BjslrPx`26= z;}?&7#a7N@HsCC<7jp{FzcZgAI;Jh(87{FujrGt;&CbS(l_C43L|L?#SNh*!jI-qz z?V7(YuA?}n>#r7m-xZv#ATS{+0!Ms=HwFXHp=yM(_vEfz^4+cWxYf? z-_vGHj81ObWXz_gwf9r)Zpy>m(jP^J5S`aYFBXw{*Jl@L`1fL^>=4K3O-8*ufB2(` zF6pxOuSkhM_c?S;vvkG9tGCuk{_BjkaeGrF6{*M+iR5|c@ws2Ws6#ikl}?B$pHV$A z4){7W`_EEa3al+SLi3Pwe$geyV~*kYHJUAT#NHOE#$=fdh9iq|zwKKkz3^!V@wsGg z@oIi8IojTyp}i|hy>l^FaGYx+@Ydv0k5h~;**9el4_&8GK|Em-dl?AY+S=}Sa|y*l`j^kCBAVz~xp zNnVSlMDFD4t%W!jIFb`Flema~NvBCS@&4Lfu~P1kjOP?{Zo8D=*`I73Lm3&%0m-<6X53`=ms2n>RAvBTAxuehSy! z9PyVjiBuD7t`z#{d5lS}tp={GI6@7PH@Jk0ma~7aLYwt|nkPpih%T2nyG>d-9=V;z zWLAs~F0rbRwDQgiGS8$zpYw>gildy-k0HZxT*hPkrP2O9X|K2LAjk6BqHrMX-HJ1X zJ-vgKG?$%;xs2;gk1@oSr)6*`qqbKXD^>QnKqpzv-KZ(^^rsjeua6>K~mY3$&gpwPixjn21-Bh+oCH;(5O?;>TX4UMqSyDU8p(R!U6!)e^md zmql6yE4j2UbLtA4j7-gW=*kMBy!zq?I|eOd82K*Rg8mIQz(= z?0v5dlGl=;KiKE01teD0+DNY+TI0}@y57C5$y`e@aUDSA#XS!s29Tv%ZHve;_LDqrL3$@I)R ziAx$-=4_h@dw6{G*cP!uIFERozI;PQXIz4mXf;OSJPhj%o*_QMZXS=sZv7={>oM1I z44OU=%fDd#!ZH07SBqTlC9KW3<^3`H@j0{b6Ny2dk`jZf$UdAWC!jyLY#GcNT*svL za@})#k7FW!30aCWaE60YVll=udr+aA)SDWal}FQzz0!_?m{Zu7y)$K{bsG`qdOxTn zR?cI6&#{_}xr}$&x|IROJ!h`J3Q4^yah7py^X$Bp;U_Y4#VucaPM%lv-axM7{=GO_>Ycn=a`6nz zwbgWOHJ*@2zJ_ZYt|Px)XTOc}gRN|xAi1gFyV9bPdnNXbF|%{oPxA;nhnbFB-VmdX z$M^?pWOTj`?>QPNGRoYu17{SEjf}O#=R$VXd}Nly`S*`yZJY1|QElB05Az4tHWs@E zY$Y33k<41WDz)8wQqI+TedOr&7Si$`G3#>1e0#01=2OI%{%NGdvsg*<_&c9L`g|Pr zhZ<;KPU6~yqxMcF(c6kx@%rE}&Q-ojz1UCmM!hFK$6%cDJ)1R{A=vv`7}>n5I~Jpz zEl(IIkwmCWBJGb+3MbE2@bd#h-<#BDm zy*doB;u3TIl&msxzW6d5`GEC)!>&P79fN~dX|Ys#gm^juYXOd0XSAG4?0j4-|Jh1b zP{nMfu8BE?`|=xH_i)>)WER#PvppZZv%#TOy50-h#pjV2Yi5QxBzjYE?lQX*;9K9i z#AK}h*vc1=iC4|zBqt8S`ihTMoFtw$Js>$dCFU(|QBu3()gY;O@C_o)W$&RsIDYl2 zI@DR$du*t%&8UM%@hfvBrkgN_a)b&L6}>O|i*yw&=Mhmmhh)|?TO~qSZC+cC%*s|8 zJtbFn>z|g9SgnB6(RY=UO+X*<`1^W;wET^c;#FR(hj{#z#yP_+`t=e~cip0^o5bFP z*?8ZJW~opw>#d(B*Num;8_HvCD)K$AWIo5bglCrn=nwWe(|C#VvKit_u8d-DC1z)~ z{06cp_iBso!nO{Uvc=KUyteHMCw&a&aDc0 zcXMUdvyFjjc%)k_}|jn#Do$VeQ6 z6Ijc$y}Iy)Te-5o^zYYcC4NJuNGpHCInC|;p^4o2S@6E-eUwScj>hVS>&Rng?79WT zmwvq^tK7kgj6F#DtDMR6$4C!$aizq2Pl@G$SOxLu{241%UO(4Ethkj41*GNo(F@Gx z+8!)ALs-j$lJ7r2_GS<2+cm@Z(x~Tu1K@RlwyE1Z-3Ax@WPe{a5;>v+7&%kKs_1eny zaz-^vDwZEZ47iR&TnDg~^oXCS7jr0&oVHg*%7ylFW|-AW^u9nY;Z@%b^cKhO-Pgq4 zu@Ms8BAcZ|8pMk0_$HmqYB8I|`)O;$s|Ge(#NwKS{maxt>gbzYtYpL01bccPXB3}D zwUM9s+HaI?hx!T)$K?v5!5-|#dCq04BL;l*G$O?8r>BK2L&RU>ek&+Pzpf=#K7?g6 zB9IBqy?3l**-FRv#M7!c&$%7@Fk;!$>RA7A)NY{n*{eDkB#WlR^#@xSiVVk6Qj8{^ z7qVp#DfwgK>F4NG_T^S8IWH&TdYa4LMP}fuyIvKgM5p0WZ)&XX*@F?twmdVHC?l@jjECYqy{T@5iP^BzcZ{`>u@1XS;~KXR+Spky9V}nd{BfOZF4D zVMWGcEeTfUe6%+9Ew~*G6UF=NYsKfQ>%^~gG?_s`V|GFbqJQ8D&>$n|XVXx{vZMh&uBCkrDV!gp7Xq8yGiY&?*J$pVW zTX?5P@!du5F3?) zC&S#$k$eXdd!H9Q$WlhkzkFTx+|Rd}jqUB+81m8Tx5?|!k*(xM0zp6Ahb zu)^)QqNQPNr5!Kfx9c}Fdt;BppiOC_pTY3E&n`rF_0fN_CnX~EPxjI_#k&p=E4)_( zR_=FF(!W}M@NX@D6YGIOksa{+PDpGy2Yx~O2C|4p9~|;&v6g;qhVQsR&!c+PeY&ODt1tP~MYFYYJxU9E z9(8hjDx>?iV-cT<>7(Zgi&T3we+k5+jh%g3uC4Sd>(d4;73}KL7A>8`yQ$vSQX~9| z^Fu9F+~w0gEp5Xut|KhrchT&9e!Lw)d5^c()Zli+Qcb)wL@|7yZVtCHmelWcRUhGf z6d&OW#@gpE6Y)Kf?8^I+Xm2Vl{ip4{Gs2_d+LwQ7Wwb@tv-^}y*Y>D(;_eU|@swpdbcBl!y}(|><-VqWEQ z%xHYI5M>=2wbP+;XdAxY@fqH^qNRf=Tw1G-RxD3cRkI~WIG;irzvk$zj~=${3f=<1 zV<_<=nWORcXkPrrJv+Q|D2tWuZ};hgoQm~;Jtak`;bE6*p|%KJ--freXelM6CXh-} znTGgQ5A^ZQ7j-|CVMas8>eTk)9@UNXdS`8j_($Z6_JUWT^u?HVnU;b6f zO;^fsCd^G&pBE`I*QMIXD!J+D_T0a@Df2>+k{#vmr+C!34`)J;KCd8|tNkg~^C%@& z+^YAHUlN7=_&7@reOAULSC7fo*Mu#?Z}53EJ?dyIxjVU6V`+n#asJs}ef&C(Js8yh zZ!*yN>U~5)N%%d8^`NEo;0^1+UG(pzT&m9{;#kd^UT{doo{viG9g^5PB(V=mYf?I< z#3fkavj5ad@%is703NY2V>|RSekG|`gw~t<=TK_QYOFWyC6OwvXNhO=ZWtRG>1Gy; zdp%0j+oDv2B9sMdix;0$(r9EFHG_rUW-vWDp7TYCmT|4I(r0P$w;~-sAV)V{5$U-d zA{`zrdUdd-=CZkvgIMY?hox|Bf4t<-XkA;X0V1{NE7B0d!X>((-Z)+2!5TbstfjH7 zg?)QWq+yo5v`_T5-xeuJJ<;p3LD*TGLu^mAjipv-8%s}|5NYW>DKRRUu&U>TEvX~y z>(e4-d`FIwl~jb-a%o(jv(yz4V(HjTk@nhp-8N#cWkimqe?!=?J0ku0xk!`V6}^?# zt8VB^uH$|+k?u_u={wuIE`a#+~+heRUFT3Km`m9ALHx0b)O(jQjZW9>DuY_=`C z!AjYzRNhMIN{aVS*s>#VR&ZS2x9p3E_?*wSsHByK-jt(@Y>8*h(Qw4SNQqq%9r1b? zYb@ z`~cU3mZl&Ju=JvBM{VQ-W}gNk)x(tyvk}&p zYSx!6)`MfVMTe(Ji5a#~ za+CVriMkFAvr$Q0I{JiS*Kx%+_itxe6`TtTbf3?%(p$J|#4M*}L##C09!+jp4=Xjc zQc){Swo*qcy=Wx7T@kCGMabZIAMH21^5uE(EqIlYc4x+}A0&^C<2_FswR9F&BI+B0 z9;HIdw`x|yO7d1^w(^0t@*1ps^fxR2s&`oHb-}f>sts?(TgSe{(&clWd`WboYoyCh z$Zo(_yLpbsbsDZ>R%0guZ$hlg>vo^=mGo$^Wi8@lD~(OxBSd)4~cBjqVes=fc7soMCcs*NR88%u9? z7ir`~k+x!8$45^kh|~c4iOf1DakLamS;lz8J-C0DtH)3*4gQHoL@ez%kCh~Rj->;( zf9XY!zSeynEj!khi0Tzv;~UG-62lI-|HYoNa#z>U@}WahuxlMldCb_w8`Z59H4-0a zJv99tvj6F5@W`Xd9(nY!M;=Z8$fKDbc{Il(kLG^l(E^V=TKJJiOFZ&u zS$p)K&iJPvd9=zSkJfzT(RzPJiUlS@!7P&oY%k;`Py4$gzATcftCZ=Y`5wWG?fN!}$ByzafXQ zG;1k;@dxjK#oJ-{eU=-HY$Ck%?42xnGZyDEYnr|faE_B6^ zL5ksPWZ9MUGv2_(zMM{w443kxOXaoRKOI$X`%wEs_-aLCnJ-_vAkxy`#maH)GPTlH z9w_hb@ElV!pWcP^_ZS@0F}N`mU*FJDTYNW& zrTKs1UB{X&PvIHqlGSTw^*URLoHUyVO5lG?>lESA1s!n-|q zo8NmJA-qWuJBz&Iuqv-hMX*B}OU@#ncA(TCSvU9Y5k;?_yoY*mf+1 zk9NZ~$zC0o7FMc_eGq0pVoj~2a0%t}a_cio%4e38&nzjQSyDc;q($^0ZgF(K zj_{KMP2&D(Z2VK4_gH(gti4aIz020#f7%~Mti89ay;IiSFV^0F+Ha?=y|vcfC2Nnc z2IiLkr~P}w+S_gI{b}u`wf6qgad6+-`_$S?jx{=amDAe$PsdBd?BzCzJ7w)G8WPq3mCL9gi<3bd)bF zD0T=v7)v!?aVfv9qmVE6vD1y0yQ)uh=CWb`&YZ*VW>L0fTz2dzDJguk%QcsN*WPc& z{S+nfn&u?*_)f!VNX)KiruH6gSawgd^zZ}YFX)ZTXq4iJ~OiwlwGRVdQu{mj++v( zBxPes%Hr!ZMdiCP;whzg}_rd z4&}n?hVM!r{h3F5lem$)McVeENExQcn)?1l86|J!khSO+>tz&gsLmyPdJo?`Q0I(K z5A%9d9kuzi?FPQGjkY<|A7{k`Fx)ji=hHbY9l~4xlrJ7N!LC(WtgrrlC+=D8ee_OT z`9-`lhNoGg5}wbgruEh)H@juD(nt;!c$>eJh-Y?;jVHG;@7LC4Pmjef zP_*btJbtQ^ z^9EmQe1077ABPp5Q-=N@NxVPF?_jTgf_pz|kB{=Vdj9KCj5<6ip~eR84jtn05=%Q1 zd>ZwCP|1YHf?Xxpykg@2QjPwHLiiuiM_pxU|3eQKMj>h-{YZ*6b628L=DVByk zi|_D4Vk_@L%7`bL@T?ys?ydjr@%x`?Ws|tGrTLCTlQ{L1fLfPCE&u072Ud_ZaH?)1 zJ&j!V@9q6>De>Ql+W&K9|KEu2f5-d(S^qc-e5kX)v&aJbAcZr?q%yc42-Y%g-Cf*o z)6#?ou9m`jH&?p!r9S$ZmEzuJ7CkGX64!eMC3szYK}%JT!;}=&`|n7x)F0ms;xBy2 z*G0MBA9Zb;(cata0VKQ#BvfxKeTF-QDZv`&!ZDhVX(;YaX2ic8>Z-|PO|pZyG&Mx> zgZQ)~L<)lhbRtAbfkY^EEa7i*%;U3ND(w;_qwsoi@EuB>HDoN_$lgQiT+OZON_1XEU420wqa=AW zAw_U&jZ#wvLy|Y+wx^*dG~uWYMyWJZCPY*W(^7Ly#?kTL@huvVbaan(bh}h5(oxU6 zs;+S)ZBIuBHHrG1fz;dIh%P(ga|Y^@Pmx6HVMbbHh!Fi_P83p>XHmrzK5Yldi7$CZ z$zG5rsFNn+Xd0e(Jq(hMzA~f^&h8T+`KfjhRcaj7gwEHR4Af*Ym3t501O+L8_gyNT z$#kqc?t_CAq?sWdRq9FFQZ%Y_8{{cE%S4}FsuhLkk|wh<{d^w#=+G%dziPr!bxJ9c zw3xP|OT~Z`rUD_78l)(mQPnyzt8o3O&Yj}ao(c6!<5MQ+6sI>eNu*Z?_>@zVcQl!3 z{V7hHxRfS8ddkk85Xp;DC8$&hRaautTyMBk2&5!EZAhWxxSx+>C8-V*XI4c#r>u1b z7?SxnJns#iQWO^=<)BlF9vC|xH1u@dC`~aXxh^?ROH&p@Tx5V}QK~fMH>AELr3_g( z*QEt1@yjRV3bs>ZmuE<8L%v(#Qgv;o8xtoVMuN&nrD?b!k1r51(UiJ3jN@FI-ZrFj zIUygIQuA_iW-CnxjLzA79@W)#oiL=tV&q{>E*mn!l6%I^owoRTsn&_X{=3RQ3qaaw zvdEB+pZ86~&KNSXI-bSWI#&%@XvwdJbVXi$LF>5K#aEVJvLv-83C@QoHC*eIGh_jD z;y^ItsLrMYdgm#hW`LBXw>3#j8i%uTo+cZaIM3jV*9$c{YDjH70i)tvmM$@|IjSs8 zDXaY<$x-F#uptG$N7q7RHAs2#E3(cS=qOTw78~;LNBmL& zq$2GIk!>JP({V$Jqu&*IhOTIm;Ou(crQIOU64daQ;OvLx1dvM9P?K>~?rZGAfmEge zn#`iyAjdV?VaT@gKAi@sLi;u0%yto^D%}VX^;Ws+RQwrL*JS$PfJe7MYEXMaB!)F; z5tEo*wOzWWb(S0Q+F~K=G>OgyHRz5Z66cy!4oy?pPi4qIV5cT+G(_dYptM7^=&T_s zXR5ktQ@Kj);lZUYrGQRts%6NDzdT9@QiocHbd;UCw4^eZQaU+6>d|^ba$%)W5Tris zXJT_y13IEfG-o!T3!03hqliywlxjc)su2DrQ~rba>KRBwDr-o#=Ge0VX+*U{I;vD- z@~d(wrPC0k2^C^uYi~-WLPTZArc^yd+MrZ3+M`Kg(vEw4>IBl94r&rTQ<~Ft*0H0v zIeFDo?L3~lGGEFN=HU-EBZM^)L3mz$*QxCjNUesOOxp6ZA;}0k zGI4pf6lQpvp?sH-88dppo*Q%Z7g2byAZB=>fp zc}7Rhoes2`iOoM9sD3S7yUjn((M&@m|2#(zab2wXB71&bA3jHw>nW0ubW~099b;YTlF^ZtcBO0$bX|7c+m(tKa&rmyX;-Rj$WQpyk4ns4sj()}ywR0b8zOn5 z8+~Pn#HSn0Z4_-+1Y$Q@6e6iLS+2=As)L9vLrc3;w#KT|I7$oh4oDBGZOC+x_dt5m zd`%|P{!AWi(`1z)zvuF4H%Kqq5zMxV}_M>m)ciuv$KkQhjNiCW(%$miy6GLu5tWk3KY{U44(PqEtWn+>jJo z@H8Drf4Xdltg#2s@0vtcXamV@ru^a2;y@ZmX*7wh>jqISP3F*R1%3J*mIu)QO=eM} z;V$`QaD7G9n=3nWXgF3f$w7wF=bFr-!gwwvEyyssq)BwuJ)CZ^j$N+~r|#|8&L;d; zCNoM6rwJjF2V^A850PRZFVZ?gCQtXNBFI>}5hC?LCQz~V%JM8aoCjaF2AM>SG?_y` ze1~iU@-nqIq*!^E`h!fNI77a}I%*imRN80Anfq9`fV@H{40#`RrfTw?As?d^Z-Bf? zw+xw{(xsIkGw4r4x?&Bm31k+f=)i6NDwRtgg3O^O4H=Df1PKWHyWAwedo_WXPcDcy1S^7E-h}h2{%jkwCv#8Wyj3`Y? zb=3aonQ?jwJo8LdLqyGt%c*&YxMdw$K?#~9Cas4Zl%yal>98R(@2sTn4UskfN_uEW z&PBKfs7qz$k@@`{ z+7=>eeqT#RHHpsV>*%T>GMm3kWx8m89LfCashTFy*QgKb9<9Q=BVq(YhM(SZmH}qgWSl&pZ3>k#^s}RV0G+T%n^P6ae zAu_@@(+)#qgm0lkhSXV)D^1whN*4{8kp<@k$TqrbNDQuJo(0)X$-AmnOeRHYfqX!@ z3{hu6Baq!Rm5DtIKBSpU>}v2sS|1{AmihNA>47vNOPaRO#K1$o2Ei2LkB#}yn$Y799sj(qPFjp!MkI)4} zBpRR5uOTuPI>#yK@vreYK{-QY8gx!kDMM7m=7OB2Eg`Z9^Ay+XUt@TI<{Kgz@&Y9oB2oR8J~u?h&v*2_Au@h0(mg{YLtdgt zZ>~!+kS(~XU5$2Kru>FXM!2oEHqahJ9M~6^EqGC__$zc;&E1 z?YwMAaom|yc9J>qA)@Rgch(qEvkaa9f=&u&haqJ^l%15$K~2U{GFVQfNv;7ZVza2p zx42#gN#&H#B$|64b1G=U{goFwk2&30CnhDXg`WaR?esGw3rI0dh8vO@mdk*oaV8p4 z7^I>mvkm#s=%jTP8}hl)N$YIXgvUk|luGAh7^r-Xj*awAc1@x_r*{g4h^i~S^SdUA zb~iqQ;|@}GCdVY*;OSkX49;VQ{Q%C zZh73PXUGl^m46<0nj3N*dpl}mW^&pa@)w92nVFn!hV*ZXHw3`v%+4S~CW184B+ihr z-{99ss6C4_#gI2ZQfo5H5H)($bzD|wfgx)2s+C?=XPF@_U|FqOvN;nqiPc~<_ zAsNs5+Mn#sr-sCWD1Wj$Um3E%oN+mv3x>RF&bSkUQnIsFZJ6QvZ%?~K=E7LB-!>kE(q&Rj!kkHGUqAWu5W4M|_qqp2WIIY%{#j_@MR zS0OSNIz^oR!) zEaogQ2}&PqeR1zE1i7DH;k%J;uYIJ*pK1ETU_31`0{nT<|K=ZGOs8l95P zmxiQ1!*@?gIp++?3!?ITDd&nIUtpi44BA}U`N@#$AnGhA?c6h@WdYWK10z&?CdUj0 zQ95Ouq=x*liuZ`hIvEYgvf9@ps;ra8kQN2mPC2KLAtOPQopO$Pv#qjx4*jm~{gro~ zG2|Ds@+|Mv)`UmY21KlaN*?%2Oewun(aAMR5$@%!(5dK@(qtC3!A|Kekf)uqhOE5m zQv%2{PO;I^QmRj%b)F3obwyUmsUITBa%HEJCbMYg1)q+9RB<+jbX2LT&WD=BQ^#$% zI}M#`j(R79y4R-2d64SPjZmowj@5MT7@d+2@dO)mYC2iRaH$%2gUW4?I!=iY`5mO5 zQzt~+^4M{8I)z9wkVej;5XlJA!ucvho&ag*WP34M*OMR}oVJFJKn6PXL!=JKVCVS|X#z6T85JUJK!!VWHJL@p zFoURQjCA&dh>FH&=SGMqe_nJlja9XCDOLMer+SE}+Q&HqLPXU*!C4d{s`g3FK~3UG z?W%PKdD+Po$L&%(Z#_V!IL$+(FUVA9N{9>vneOZgkkwHB@`f|h5OueGGsv6HvJm+I z?DTBL69ZRS0Qo~WT|sKM7{)B<~$6MZ$RF5QcYkFRqdBRmOHsZgC8=QL~k{RSZC(}gs zN9p7O+3b`Hk%A!Go%V)QIe@1aLEd*JhDb$_ozA)tsRpv!IT0fDKt6IFhDbAzy-uD< zT)VQ|4rHHG-;ftOV~m09cj7{10LVdSONfjFIpkaok?|m(IhiLb%d=?PT|86a6CHOd zYZ6cUYq&H6I$t;=LgaOjlg`EvQLQ-TTs34bt~6EeeC?EanSEAdF?7y2afS>@hif#D zZ`5WW{^CjLYy>&y)DMyOK`uD+4cTq{`OY~OBFdjjPRvxcqjZ!%mmT%>W@TBCk3p_F zV+=Wmdu?j;UUxQzh#HwcIF~e;LrUi}l)B-ho5prp*T<6vAUB;@L*AO|(M6D-oMItz z4dj+n*$~2e>$iSBIQaTXb!AHVYH0d(#-%R|Jg zh$~I!Mu?;U`OPW*3i~`@8Q!=C^1yl8kf|6gu^@jqXAL<8%f&#*{XRs>fjI6hLo)p0 zQdJP&y=TZ@x0wVkP3O8?+?%KaorpWvkU>*CO=8@}ud+_Vi7vH(PBQnfA&sD;ND8;f zYf2~D_Ec_9P2%a3Pdw@fomB4h5a|t)*4<@DVsE^60VK0~DMZGBWOLKZP?qOV5jOy4x^adST*o?3 zx>G_V)3f*{v-_1HYCJ1{3b_|TMAcQ;O*@M%507CxMcgbQqI8P72fg8O-h zD4mM#SwmFBUQU5q|E@oq+od9=$TRNKhNx&LJI}h+LqyrBE)MMLRScBhAk(y8KJ zG(<&X3rJP>mk{{?q`K?P;o4O+luk`IU5F^1+HP)5qS2`9KCMags-&)4Jw((KTy@b!k6HV|Sw=x$#?x^BCRD++Buz zieJn;nT)7~d)1Kgn0LNLsg~}85IGOh+D$cAwR8?Co$o=~x=)124Ui6Q8748wTlv%i zmOHu67@g86rR;Qa8yk}R0sGU%ecq6J3z>9vhZ*7;o$l^bLtYr>(l4;n(~S?2dmz2s zw={{yxwpGPlXz0S{1JrBq+CR4yQ$yMga+aL0ZgtMGRl%63t4BM?-`wk zhUCHUL}zl3#~ZQ(tu*WK2J>>PlZ69?zJ ziwt?8g-dl&*8=yTA$L0Z6jUWz=uTM*RhmGz@Vf{VjW^v}hGhQOFUx1=Gjpsv(7xZE9L z$iu>z-@6j6a7V76S#@C*Fz@Nk1WG(`G?Y;q?Wa;`hxX92Rsy|9H#Rl+Ph z0ptU>=T;^Qs&l{ZbQ26|pPMc3bmwkkoo+w-G!3Obba!oM@&I>pR9$=BAs=X>_tocu z>~n81p*E=NO^`%)%uX)#$Kzb;pj-SCCf8fxt~7K$b?X~419hnpb;uoINa{7b3;LNm z(U6YlK~?)_?!!-&o#@$g%q?_CTeds@$J|m(C?4m|yC`+c9b$;`XBWsR_u9#S_2;yE z!;sSGodeK0YJ!ubU!v^IijlKe966FNOIJ!eE#0;^bOaQ9P4>S zuDFLUG8tv!e9awliOGlXP}#ZRj{cs>&XwGQKf0?miRPA@?k-KD(YWcJ)npt!1Ao4T zottio>ujfFC%iEkp3lUX&d9Q1TsM;%f$3sNb{)~4vL{#mSyb||RE25FF>ebLB>Q6PV zwIklIUE<`K7XY4ey>ImCP(t!~6EWvYbe5EV;?V_E!z>t|8K2HN3wJk-n(u zrTAUjNh*C&)62$$s;uK#xTaV50oPR)^Rk+S>v%OaiS}1ruThAoh}HAjgouh*1Mm3| zQ4wq8^$igfu_oRKO``E>?u}y|Qn^lzs21KUA<_}0T6pmxqGq&~-jWdM3Y}KoULnYk zN~eu?#t@ZPmF2eH14C4XWRm`&YWrkTS+qCowDpP_qUK6vr@dDpL{wcJyqY0$v?if-ag+~)nYLwT>QJHBLwKrs(ci52nAbF<~P4R9R zGT4x5UKUrC;_DvN`{o^$Qg2mSJRM6Ajxp1%tRmv zRvEGfJBq5*Ja4@raxTYvTQ!M3Gr7Rq%{sQF3%ossNJ|%Z`wWqmF7OgHiMDiscf=6M zNQ=Cqrj)dFk@uw`($YoVH-<<{7kTRYwYVp4Tl%K=gCWw=CEjgAq@~NehnghXto626 zz*BY-9o6<95ucS_X-%SSU*%QSWDbqTJh%}3waSarB%X?6WxgKd9dETEAD8oJJIFfk zupx8GGI`g#WyllI`4Bqmz4X4SJ)X{B)>CAISI&?@(Af{N(Q9u=S=dqJJul9X1<*MR zvdLR+NLT16ve`Rm$bFD6K(=@{3|WV|6xr&f4!HI@uyYz@n^($^7?e_EyVu%~lhFAV zWQP}L$QtM<@`1P7kkZh(2C~aLX2{Q|OOX$~dxlJc&d(rwyj&5ky#eef^08OjkawYT zA0)vWV903bD3a*SHzXx=oM*9%?(H#TAL>%%Q}3!F9iWpG=K23vO{=th$s=SyGGh+&`A%FBz zYZA?nKYGm#k%-;$W@!@b_gmg_qazvnXYa4S*}3DD$9t^9Yuewu4osYVm>txy-@G_Y zqBF=nPrdzG=9CPO?s>C9MC~8m_ZDgr-9LQbthNP2%=i1Y+`+&^JR3)mS1lGVQxA|pYv`40_=gPn08 z+5Ma;*ysGn##2FZ`h`Md7D%k$+K|y*@Ebaiy#5kR=1>yMo^OK`@P9DmOFV(I9^^?s z1>Ul#qA?YtcQ;5;zmz7?YqS!6Lru6(4?w4c-&K?7c~Q!rqDgermG*ZSBBQRHfAMd2 z%K7*IW~YLmHI?!wdKOgl%NrtR!LxodO`>D0lHV>w)EKMecMTCW#wz>$G>ML}s{V+O zP8O7^?!Trp)O@Egm^84Yf=&a!k0H~S@Dnc$ z{2`iLcGPp8H?Zj#W z{ftf%e~cls>a#yh{N;um#dvOuUT)%_Fk~(6ji!X1rv4M@*v=<^agJ*47c)f7Df&vv zZ>0(MMLLvf;m_9OvTmtrdrN<*CegOH^xxxBF;jNoOIaw@%HPF=l6~w_E={JSSG7;3 zoQQLNkk)?n4D8Qs+~QN~u{Qo1L(=0~MvdOKexe~I3~A?I)MO5oIp9+f*lF+oYDm9C zCeQgvGAhfgQwBPn{OpE^&hvipzv*=IYyC~9r{6{semYdOy|>?4lXz-$&eePPz5Tu+ zqW17#@P`}nX-mGl-Ory8BI*f&{{Gw$sRR!P`8z_S7RV6)tRdgDU^^rHYaycU1&s9X zgh&&V8s%qtT(x~74eH5bV~pQMlW3M0>okd{I@sw__hr`lTSMdxSYGFU93rZh*ZappWGQss^Y0oW z{l3-rV^v-8I?k%!xA{dwMD_doew7eW{l3$0Ziw{zNB)!$QT@Kh-w`6J-}m~7hSVD3 zlhWDepAM0Cpp)QV(oyM=x90YkJ4n8 z9xc0)VvX(3H99g{j{9+WxRi{IlYU%YMJAB)c@sQ5<(J5(NYumAeiuzHJJmnp9Ch07 zX^4#8)BgL06f-*K{auEr+SO<|?^n;SEJx3ROMZP#qIF&Jp@6@{K+b|Fkyc!_I?{?8 zmWYQpEfEi+L_GY(>WGKG_&v2hJX+LKG57pkA)-dhegBvts+7`s;O8lz+7(ZVD4mCX zxe!q!;ZJ``h^W!>mmjZ5G#XCumL}0?IKeyrMdzxqbKo+*Uj{o)P_7`?-Z9RlJs@r{ z#*nX|lL+DkQw({&rc0k{vc!-QhWNn_L+Tn51jh`?VMrvns!7ztBtf|+x%P}dK@UUP;|mqV zK~e|(4XKHhXEl&C!SE1i0g^Tt8zL`&qzhgS5%v6Y`e243JKlF`G;}fqD-CII(5Kfx zG6lN~QO_~H0g^c=QAqh5-KoeDRM3R4fKFgEWeHko@~t!Qs7LLfoi*sH$z^9INK;Ml zKl~*I>Ww$80;MA)Kn&G|d$wTU-|S=$MzWoxmC<&!ua!L*V=NEN<D|e8<#9oc&362{bsVh%#!4Rn{U+|+Qmy<5bgZ+eLxHA&mHDoQwLNqmh@P{VR z$QKNP!m7V6+lUnmVwgB8VyaZZ;IR-{iC%sx$Y_YfuuzcQ5Q$--AWw*FMybL!VydNz z6tQI7b&hjUOH@nuK&O}x8~GAK0@e1Dh@mAZN56pIv4%;|Wjo%_i&jK+4yX3Xe++M` zF?JGy785yQrGlrl&(Z!W9hB6B`>Q`Jmkt_h!u_RMQ6_lK5NSo(ppPNaigLkdCbmy2 z1XGNTw4y?wzL1PDA0w@(5Uesf(uxYfHlri0cse+!NdieLo(|Mk!d0KDk#GpRM$ZK2 z4N)WE?BjSFX>eJS=t!s>+%!6J?ojv30;Xa*+KCK&+*Cf&QPQ9Qe6MI(H3z{1u9@Y<@H$=|ChQRRLhDh%;5B3_;9Z%gqjTp8F4jPijlxi6qHRRs!Y*~4r{t^S}!Pb^YeA)=HJ=ji& z?ZGIypXln^qa*|RRQc21mP%$w2TKYV@|-1`OK~eY248BQqy63~IHw8sdp!ERQ}95O zXzuMCBrU4XLA__CvU-;w6B9c&x(0cTj`VxipoAgPimpLrL!=emf`*1jE4l~m4Uty# z2znbLt>_ty(1d+fecCISuF1E~@mo9(_72|GB)Ud=Ay^k8YA5-Hz%S12l3m$8K{-QY zx2J!gzF(sDKxTP7GxiS(m1Od|E2OU`(H+*QD+t4hs4h@)P#i6d4>GHl$EP8x3@e@W zmMk&k{XwEL2D>FnXSF2{4f%bt=ybyBPU&2;BteroRP+qqTMeH_2RAk09#sAHV(^w>GAM90SZ;Ac%H(8xDAJ{yAH4LOAS)ao4E z5X5v;wNIc~cxFI7iLoIlV95QRcy~Yi*%+kjq;#TrY*Ua`lc00i_ihPJaVfn6Qy4m1 zg3BSIb^^BscS1y+x!VH2vyQwjRSKoH2NN}!93$^S*%8DWa=aYxukHvoF`;kFdsBA= z2}b87{LWYHKko>>H>6Kv{Avstbw?2Cq9YbFX)@p0-w_mGLd#aURI!TE@t@Z^B)>}7 z8KluM$d)*D;^S)qFh_nLPAEfW9T9K%C z$e#x}7~~F-ejuL)B{hkTpCdt&5E%oVBf-cJQ9bxsFg-+64;~GcXflgN<2l#~D0M8z z)Q#iZq8E>!<3TYdbRJi}s&|eDvklqX*Q4nubv)Q@O3i%QquH7qG&-tx=4*1k`@ii1@n>n(f*9y8A6U^Nr%VM>r| z!Hy6~4{|-2+eep*S<&C4tRO!GiwyDK#@#SY))*4&Veeg&ZHDx)WFHfnN%&2=);VU# z4_&inTV7kby5V;AR^pWaAb=zas6yp0$86qtWxsFxSuh7X5dESrIfzCQT~{GZqH|n^$Q#489X)>38X!YtS%|1} zIYVS^h^V`z86#UY;p-jsi=Ir8J(@%+o3vJ-aIBBcxo8oHDVrE)~72*E6zA0%g_l_Bcwd5YwU z^fg4iVXrhuY-EBa(Tx2>2gnPWykbcF8jl8QGTV??kl~sv&?LdR_M4~Q z@K7YO$dF2SSImoAXR#sKcHKF$11T06 zZOA#tr<0mYFy!sCyoM|mSz_#bIT6p3Xq~qWsht(?>(*qYAt@bR{}hX?Go(%td`(pA z%pa#(n&|wFwyQoZ7I|n$He3&?UE*SqzYIxe?9s1U#~IJ1uA%maAjKn|At#n1>mzHG zhy;dY#1B)HcF9N*L%QS1Pp>-Ok`YN^$RfNaFd0bc$VfwadoE=JDWgtt{3QnKTH<*~ zkg}1BusyeAMkFtF=;l`-dU3|M#pJ}=NvRq?LrjY^ROO(sx1kiNRq z5u=j=znd5eQaN(c=;Q!V{Z%<~&giUc!a7wVmyJ#Wh|;MN`N`|0cX+{#Y2tl1?|iY!Ihmq;$xMUg8v5y`%n?fQhs9@%Brh6qJ|ulG6cGxPcC zcmMKudY?0A&YYP!bLM=`Bxqkjn(G6lSnqX_$GwIAF2~&LELZt%p-*H9BZe|ML2s=9A@V>6PbG2$iR$zM5mk&y(eO$&0zcIk(c+a!jbJ+*WIS zixlg{-{d=>HeRa4_wc(~DO8IbIx7#e^?G76>Y1lo-|grgzu2*5Tu(PWa-HA_=+0jExk5N@)o@Hcn+(h=naJUBc%iM z=5a#z$^m*8Az6XkW#u(|AlGVEazDO?57hf{&K;t1TwP=nr|18>H`I>4(?jboCpg?_udYM9F@Z9Nm>1VkzOp98od2O2~YU zvCJN>hL-pG% z@8CKw0sW2pYHkgUKH zJ>|X+({r;JA-P|}^t>#qy_fiGw>BU%P;tor__hEWDmeiqgpuRB3r zqp0`Q8TovuH(<#^|A+d$6ZOU{lhNDhiI|CcQNQQ#CwwT&Gn?<-Y5HCk>eEsmGEG0nav#5&pld>!zG$LM zwF12_>J6spOD0Ka+aAA9LC$GAA;%O%>m4Da2}|J( znBOa;8OyCXiP}d(TCyyujdk#Zv}3uyAMe(MOki0KnImK^%UYC&`iE2X7)vJK2h;UE zEE~}q`~s<_>8FKYt&ewbuN3mz6xy$JZS@ZP&KELWchX4w>--Eon-D+M480tSO7*ec z=z&z9=q-iFE5I`3{E6NvPS!(a=_7>1tX6meqSlh3k7qe_Lf(Zl^eHUE@Jvm|D?|Sz zE}w14CqrK!Cp72wGyRxM6&N=+NnFP=^%E=~L#St-sh?t@Co1?2uanR*(86ByFhOqmk%|j zFZK3uLfiOKe>YC(_uUKi3@O@y`xtG7EY?3s7v(Yk>Mh&zQaw|Ocsog>@k{j^Ec9M) zH}+z=K5!bP8lkmpkM-gptM&6jMr*nGJ=z-Goi1|Lb{4>jN{CsbS7G`2JM?gIty`;i zWoeNhpKjLa-^2-RYrTFjPJV`L)NioVK|RyHZ`M1_5Zm%bGdAn}q-am;@_235N5={6 z`xbpV%Y~#w?XP;8woTt9#GkJo(-S|Yoc&+y?$C<}8LhQ@AHVTId3NYcrFeC?Q|}(5Ft3r|cCeTn0GJr?>s5uNYH_2w*FvBDMIdgbdUAa+xpcLs{Bzc`oX!WGe3)2N(5gLgW$bj(jfa1v30& zL2^ldhNWwj1no7*WxcKt|5#knUlby1z7Jxq=x<9gX*JsSA;0QFSm>P#-HCqHM+r&Q z`eOuUl!%!Ym(MiF?|NpOEQDOsHwck4z3DCOP5nvSk8#z~)Diq!e_o2&fuFYi)*G=z zc(mtly(P;>JlgZO{xZuCJo@yv-d&2e9c|+*w)MB(kA;rTWyn8z_PPGixemFfUlAf} z;~wO`{z#@Mp?N-4wuFFDkYx?tDLjwT2aFOdP4P}4@db>-8jV@1;FwxM21*h0d+E6{ zU`+qq-+DI0Bp8$Ck)&!(C*up=kR)RRO9SLn1fm-!S>DFBN)8!` z0d31~;W>?v5cvyhx+3K?@;(sruu)uytg(j3`C;R8DdM*<^fd8^@fFKq9E;Y7dBj*F z#Vg?>MxHNe>#~Gh5R==;5F&Ga9g@d5EXC|rRQ4S58K+owrOW$rKI5VkFXwzl(n3Gy zw~=BhTX0R;4Sx&v3lExesdUH)9W+jb9QbeuN6`-V1^(((tr=x_Wjln`vwYPD_ zCMj!lTud=?9C9wCyfIFQ+%I~HtYoZ+lO>43uVKHYRIvcf8K7Rl6Gj1+u2PcfTi~61BlSY~lzgC|#X8uPqh4^>#Cyje7>gYUWJi3I+0u9kKmodeb z(!Qr^uaCm&lp>~*6z_go*H|k=?ge&Bt82vKgwoeF4#vq}$fut1hmcsH$5&YOw+_Ay zXCy8Y$KATmPxK89M@Uv+LNzv5!VGE-xiyA*{Y4m1M zY1^fm6WBzdB*pqkGL9LR%T28oz3%M)HqE-Bu5(bp&?B&MmS^}fbbA^w@t*I4o& zDYufg<(IjyQ9Vvdh^;plB3ou9NIzqd5ZMl&hP-Y3%%a-i0OOhvd99^PW_g5jOSRc<83>gDeoFxS)RcAZYgtF4&ywf^zRxQSxS_UPrieVb1WP2UWQT) zHX5v!+e+i>;b3C}OBRIQO$;{jtdTM5x;n%d%c8EULya^RbzL25WU#2~>QKX6OZi}S z-$@=(G$ybt!?x)Eh8i&z%I7)M*f8UGoHT$8H-hUZc7!&44So{}8EHJqvWshMlu<;8 zf4oK;rQ)O$QjIp6ND;jPx@wIzTCu2J!C0fC6t7n>))*;7_BVPWpK-<}8RI=6jyJXo zi3Psvo20#snDIu|dMfkQ!~@;&i*Lw>#zB@NkO`1UhPH)bvI05rCSp2dvQdDg=s^6w z8NjEZCc0iUvrWsXYGSxzknPD_%u`nLB0WlvN87wBo ztVm`Wdsz;Blca5fd}@^XPUiCk#*ru{!)V1q?|?`?GlsI9@1H0n(^&DnOhxZ^C}y5< zoQ2+LlFT=fcFC9}7_Zv{`O@erBuy)UJ_)V%yvR5xM4l->A!e~rX*Z=Bp|u__*OOUh zOqXIlKNoi*#4Iy1SbjV&`#;N!`7F2cDp|s^6Jy=fM_OjAVd=UrNxLla*~F61+ZL9# zrmdUhAv70snRln2Qa3^S1F4o9)ntsGp)+x&c@b{nbI z8cSI6%tc}9V&ufw&+;^CEC~75xWuvx<2H{#HX7yjPO+-~>O14wURlrTT;6Tu+9$Vl4SPzf&F?m5 zvgE3V-|HdOUL$!w#iVNa3SqtkB+1Oah3Bx=<`E5;n_yT4~ zqC6*!J}fP;T3rvwDdT6Bc^JER19IAM4$4%2W9xJk{n;odBo5^$<#>n}tqEnT9%~GTUYo zmfal^w9k-_Z4PDWiBt<9j#>N+rApOOaDFX=gw0Aqrf4CgS_6ri(^_?381| z1xImbERT9J>Vk|pi&VF(eMIoilMM6es zL+fC5Tu2%7Hp}vI30f6MS#$ao%4dZ5CLmoi%bQ1p_;>0G<|P>;u9?puRRuHo7m>>B zHy}}~1F38VSxU5(SFOrs4wip0mzb{MmCeE|=OFa!kIH6AA^x^1n@_Lt0u?)YCr>YAl) zNufLbDo8zZBMaT}DW-vWjD_9`ZGtp3&D$~+&6n5-X>9gjc>_I=1CSTZMM}^Ep_u08 z!8kbuX=zUSTjoP^Dk!FvxrT+_Ns+WMPqW<`@I}=r*C326tAKt>WXNd*6yn^32 zBc_8VFGFrYI(pI^l2jkxg)txhhqf-ycL&ndtSQ958h1AvvZ$+Zce4$Px*B&kd&CJ{ zjeD9a?o!UNfVvvLVP3i?MO}^ino0MisH<^*Gb%*3F?yEiZ&u=%Gq^@&LkS0%bu={4 zxRHUuW|RL&tN%#H|40uZ{)qHovoqelsJ8<&B0a<$%A!W3-!tFAQHkIB`{r;VvJPp4 z;e9h*$Y`z6l?2hp9&TpJ7%gxJD|llsMwka#QdvfsdJ^S4MSSUFcuh?kV>&EUV~-=% zSo1LB!?=+{Hy;!I=Xk;?oye`BarI}{lcHqGp5$b4wkmqysubaM`i8ts{3 zZe*#CeiY^WvH2s5+SVuLc_IF`W}3eVk*$SlW2Twiq-~8h7dDeUv{_~zDc&>4Ec0<8 z@)@KV%JZrD0?XX__<9Ruw%I&RUV(gO4iqA{)ekby{6mU4@=w|CU0~i}F?oDrff=x5 zd1!?q8kJpOx>CIIEHHZsk>weJoWC?T2}#w8ARqcp%|i1GOB(t%Bwv}oaVk~MUz@i5 zzsGBdS)4_^+gfT?5#rb3GP4H9sC<^0?HYpc2VftW0F4aXcoKjA~{MV6U8 zC|kmB?d2CkVrDta4WcXCuKxJu9k%tIS%+mG#{GuZ)U@qpI?LI&Wy}uqYnB$shgO-} zX|7`V$&+tbRwEx7lf^Q@i`m6OwMtvxX=Y-dsXYJkw!SxavZ(smWnN-QUoY!tw|S4{ zVs$A$n30Ig=cx@+_L#+3=t@Ghy4QS~h3b%GpV^gVJLkOL?8UN><$yVi@ zF}s^0{tCS$ZBx&Ncf(;Oj0mbnCS{>w}i67#-Pe#>0WLSM?O zjC^jJn}qnS_qN&ZQCVhMouEAOxou{$94mymsk=p+D!6~St^fA^$W+`HT$qU z#4-2G?P;=goaF*W)udcdF`nFDQ7a-QTX$LZV!voiCfRcGQF%t2 z0gQ@B$-#2xQ591_idK+k)F)d*S!UxcIlT=`wobFO_hPD{e}J5|)v*MvJ@z8m8q6|_ zMYpn8cCr|jTR^7znZ>l~3-OP;WxdKVv}PmaY+LVfjA{vvHC9NvcCKHN))g_1RjZ)P z`K>?Xyyc+PP>BCK-=Nh(NUBDwp!Gtkpw)*Z1-1GnBxG%n;=S#PS{n+9oW)b^JBW!| zg$v7V6}v85W;Uy=6jRND%Vt$!S=kzE3F69@&8oqv)LD?tY9~aV1;dbYc5A2*zczAM z8(7r7=d{jpD%DTOY29a0*XM_=yhW&l{@I<&Dk?-?$Hrk>xva`@k_LIydQM1|rsjU< zwO(XVZzuCwU4+Cm^&6Dwc^{MI88=^!<(9T8v&_Rz(O6Syt0BvUiE{L*wAF)U{KQ0U z2lf;>v$U*-(K$#ND{pa`^DflSvzTpI)~dwvb6x!I7%}Cn?JON2bd9QL?J6Nt?Zr}d zrx8=pD)qROuMtCIj8!dszz(q^H1+&m&FUirD_5Y#=vla$HJzpYhgfYD`P8ubm!edu zTKbIy?Ixs_HB!h3O|7nzVznquF{4dAXQG(Jonp0T`4r=jD{wtbvAVIOVXRw9ZV)E{pVijevGv$1V6st5#x*?y@Qmo1>xlkUt@6}n>-cm>K zIhGszwj#x9$TG{zxdqDxFX#3wYW_3Y56cz2^Obejo8>XIOm06c<9JRo+7Ct>tr;f3K+8_=-i%5lXREvi#(g=Np!0=qJcBZ)0is zh3pNcSi4!?TcdJ5#M00^n@+Hl^UkL8EMM>pQnVi-soHI{vAZ~PQ>;5O#(P8eoK>N$ zXe~mL8)6LBY7i&cA$6=lQcQK8HnfJZs2NiYt#K@B##BQql||J?Lu&?$nlaVTnj=M9 z`K@fv4XqU{bjPPTR1K|2IVz#N;}=JH8e5%&_;;%&)?gv>UPHcs!Y{GuFP6OO^8~E{Vp>}rM9hW2YW%jM8KjNXS%`lZZfo@v zBFobbF>S3hDdszG;`=idF_LV}WC`Hirj$$;=eUwZEXOe7PFJ=L)(R=!UUaZ736VAb z8n)HZ%2Sc{LiWu2LONOR2^sCRjh@zMDc;?or!|v>zVtdCsd`%HIF-7N^|XFvQCFLu z)(sYQZRu&j>o>mg>;&k6pSCXEV%c8CTJ+16gwBDuV?BbqQd5%$MWlyV`kaTU? zM10rmX)57#7S&^X%}S@!741;<7<*YOSkx1JZ)=wjd32}`@VZ66I-^u~8YXC|*!S11 zOF~k$L_DX?fb_9uK0z_58jWXWK;E=A#0jN(%i1nP=1jk0>u2SyBKQ5oCE1?)TSZxR zr7I~d#XF|`tvW*FG5rEL53qU&@%zsMtxOixe;#D5VWG85`(d+#tfZ=<%wC^=khNC` zzSUbEZ()%0AnVsSSp|8=!pCPtZ8Ug1QH){@wuZ(Dr5a*QVo^`x?^&0glBqUelxBEM zjM!U^pO!KS`7G&-xinT=mX(i58D@23c@6gqs@37vKo<2xKf+qTqDG%aTB}$}{EKgv z;`%en+RakBr5y7dZCw!JpOs^*s~odqJm!jETVt$et5FH%@!ARb!0IR@7C2TrNjnVr z(0a5w#rW5;iPqCXWGZ^%m}u3HlT%2QYPA*O&wiX@^=47eAXBVCQmh}bG6UV;r&xo9 z#5DEW|0&kTEb7=|FYGsBr>pK?poRVhk72=mU%{nhcmiaPDm}cD+;?HMGxBg{O zbJfx<;~Cm_|G6pM%EqFeT+*$gLSmZwrCYl76pQ*5+cax1i<;p!&DzeQepxon+AG8_ z^EB&{5dR2Hv#tukH-pdv`4f9F%_>rZN;pL;a2cx{KxSASg`{e4;f`-L!mJ_dYnBCh z@rzx^Z0lQ2^SQ>InErr&;O%skV2L5jI( zg6!?gvl_F6@Xn2{;q$DQg!nnnvpNWoPh|8CdY;ugP70wsUs!2Uv^|*BNxuwQU`1-l zeZLJUj+ig4XIb1wFlq$(%4!@ZRUwP5ek{2WQxmepn#U4AFSQP2nYBDl8bg-<=RUa7 z6LlY4<%wE1W3?x01&lSGs1-1LQ7d5hqRs+e)Cw5BsP!&|`sIh{Lo~X0Z7qwoBFKWFMU(}iFi&{6s7qxE2Mr)fm zIy7!s?0~s5_}2AJ)>|x}c(PE6xpHTc_A*j!vz80lYG16GsC5;xo?~j`8Lg*~EGdEN?Gv;< zLS{ZExAhv!ch*1kg#=zg?~C?ghgGbBlp=}px1~F*<}40+rvs7CPODub8Pg8)6W)h> zZyjkOWd`1#lkB$2y(op|1X8L!*4`FUUc_98F_67h)s|ASmBP3>WWP1`B`NJNhEH<9 z+R9P{B_ug$-DN3<84DzbtfsAGs=Y;J%#T((mIK%p#T>RWSU%#Mk61C56E7!d>4^Es z+RL)zh8(FlYGq&yj^=Ny>zJU;M$Azwi-q1nQ;i+7a-lXU<`(jilE!kfi;@j2y-^++ zvyG(&*ZeWBg{yH1Pl9R%6<^c}D!!-{RD4lw)fcsbiZ5!t6kpWpD88t*P<&Bqq4=WK zLh(hdh2o1^3&oRUwHC^8Z{O8gC?`BoYoVO>q|r32{()mbt2ohLEcxnJN!r(tbDpT* zGF(s+RKI1os3ckamcf&t`c1?|>ykJ+sajfRIXeq6IJ&AQvku$3WI1uN1#-p8DMUUO z?1cPkRS*)>)S4=PSe01Rnks)-)mhY0>m1|Z5j!|o>T(dG+)CwxstbbY5 z3M$vFKnE&wOjGNnT(@i%wK~e5R$dmhg31l6IEz{@<)&4aMXiqVm(@~;Uq63YZwisu zL#m%!*5Ejy`nh9`5#ra+Kh|6!e*OGwEn-pi^RKmzMb*!JYljrCel&X*$Efi%8o8~i7&Z;(&umT$W#gf(UQunBN}CF@2G$ zG-C4GGgww&hQRQenpV)xWciZIT*$t`(jC9HsDf05?Izu1dFCLWXCOuGxh!KKbs>-0 zOIZ@IUriw;?88F*@|3hs3GrK0N&A)%zeSa@1L!fRYgB9GQ`*ic#BXC|?OZIXjg_?v zvZyvz&VF2qcYc+(%W;f4zslP~S=9Me-rmEaT4n`%KZ|OI73^awhTCC9`y7jEhn4KB zEUFz=w%u1~zx*@q3HuQtS%Jqw^7k@N*riyWgwXrtDt3J#evMVNdkFDs>`D8rIH4MQ z${r)cud!*J)cF@Pc1u(MV*z;+WUm~_4BNKjz!f^Z9DN*Dvw`3DRu!Ne*HXWml5LE z#`AU+7F8S1+t0D6+IYck!=m=9j@^qzU8CyQZ?mX9tz(Z+sd!K8+M`%h+o)%M%%b+a zzCBZlca3Ub&*m6a8x8DbLgao?@2i2mE>36-$p-e;IHA=t8``^t$a9eT8jbBELejPO zFn-(#t>s1g8cSd@)((X)UEid0?gPg!0`O3>~=y4#CbYSoshdf4k(K4>82ReL8(B1CV3(L(zu%TDx- zb3l68S6EW;>_9ocZvV|P4$lR}5!1)EUz0WV?DKNp-?ATJ$yGm5tAdz*b_te4*mugO zzx^bO#`QDMuEX*v=8HB&szG)umh31GrFz%y!IA^Vf@*$Toe}nYmM^eoT03lOl)ai|=oVQ&W9)4#CF&(=Zy;u@eTZcowng%ReV!#7-Uk3eSIT5q`*AEOS7K{D(jaZ&*?$F9Tj0giGF$XvS@i;FThfMnW}Sh}NylYDNk zU|Eay+!`{^KEhG~EwdZs3;Qn1KWGVkA@l75ugmfbL(3cmSzxEItV7F8g?wrEV3~`S zNo8JWk7cQgnx73>WG`aT5JUNVW$$G@C(hysxn6K?zZ^*5;LXA=VEU~Mx z)Ir;zn5A|{mN&T$m)Rp(>Tw+|x97(R)!_^{9^_^XnB^UOh7G%3UpXC~Az9D3X zy@6#Tj$jMOPPWA{#Yj0xt3}?YRLbkJff~z{k?6dcQKsQ zmPa5%MJktGb>NP62Ca82WWPO-r4!DbG{^yaC`$&;osS_0?S(8IabD2AAF@}lltpX# z4DzF$^0v(RGWMPFIczs&$;b6`#BRs37H88^#2m55u#`o4DAiB)M=X_bEU4y>+DlnV zpoLS+F?%~p7S4;ckmL4kmRUGDwC^Wut-mZ!U9|9RLJF{ag?va(+QnH)<4oBHIb}Cz z>4xL=6Xdj=#j*|goQIsT!vkc_pJU&tgg@J*SSF*5{Q)^^k7RibXUc8JIeP-jPCj1e z?KGBrICr#Wm_ce^Wx0h^v=;lnCo_5mL6!UB!Ak2S$;)5 zKZ8^^>_sfg&|02{+_Wvs=ce{t46XMSoK1h(*;(F1>urjdTXt=h!f3s;r?>3}EE;ld z1G!^gVA+H{rF{OjgYU|m&ma|*=N~&4%W$;bSCQ(j{Q}EvY>QIevm3MAz*{P+=YQ>P zELX8#6m#Dm%rXwG_f5oT&Jvc+I2N?6fU}0hM5`Num;~n{OINgBl0@e!%WcRwNRm@v zuq<;a9Gz51vNM5Y0`j3Fs5?Khbi}?>2@U5eOI@_ySrF6dK1AkI2<>nl#B#>7Oy%Qc zJ83K@kZLhv9OpjEDx{*la2@kK$|qIp`yJk|V$3|~RAu=SV?67SD&({mBK!073z~@2 zBTneIu2H9NoNPj>Y|eXe@*O0Hvp|U7!_Vbp3Gr8n&+VL&V!hc7cSg*{%;Q{)%ZIj} z$BDd8W%l#Q>y#AY=ab)gUW%7bLF7e$v1GO8gD+~$heBS=6Q$%xvM*}Q2Vc~h4~4x{ zYRv~<)S3^zs5KvaQR|!(QTYVbIw!uUbxu4a8P z8uNTiNwRu^_C>8D?TdP*_C-B=7WeW|&z`=hXHQ?$lV%Aol^Q91+!OWeSyD+*jgOU5 zlB}LReNpSc_@dT%f%tM6Cl;-V?PBOhr%BIxv+zQR~1w;fY!Yriv$O9hfIQ zQR~1w<%wDcrkW>e9he%PsC8g!d!p8XN%2Ij1M`B@NSp=!lXzXH?E~jdU8h@|(9?Q- zr?(LQ&2@d1v-f7ilVr7Ov@dF{=>{s5w~n+gYMonO)H=7msCA@$QL9G#qE?OeMXeg` zi&{0>7qx1%FKX3jU(_1WzT`M8XD#}o)`<2+tr6{uS|hrlR~u@LXkXMC(Jy*2YDMT~ zo~RX}eNihydlFPDLN{|p%44d1@ue(tGv_YLN1pT>>R(0axHoeWhLOnDLZk7`95+tr zjcRjetdLlMMkpy&OJ|jktUxua-zx=-zpS8of7+7wVry8vKUIt2S<|()ABD*(DtCr-JD8er5s!)%iP_m%F+%oRL|X=ek^lP zLYhb2-ANipsbW?Vo`5UkpRYREr37xGcS>tVyz0ypk`)*;QO*YMIaNQDTi?@MZhe6BEXz3vy@eUz^kTV; z(R7+4G|=hKl7RZ5neYRhkwQjj!`sW82RX$i%A614*D&Kj1= z_3(XRlzFh@ek4)_XqG$G#t^4RoY1`YAx_#Pj)}>YZ{Kq=<3#*Y+ZmH8x88==iyQ8w zNePU=Z>cEf;m+4AGf*C?=i$yJmhU)bloOdOx3wSJqL@)mb0Jv)T2`@EMi#>p)XKOaguNrQF%VWc}nl-)11dxYG;@2XR1?; zr8l3aQ=P6Xw>-&W+3txuP3GJL<)M}^)p?WUMa0m`+v(02A^zGd)0{~xt$&wWpXN*! zGQuoyLeB4<=FF5amRbX6nlqn8eNknabC5-SQDvHQUr1J97_JEyP#e=6Yq~7q$X4=L zeC(7KGDSOv^>Y6~%*Re0mimcU^{zR_^qkjO+P)#j6J|LBSzdlGNpleMsguR>J$@gT z1CrrfVhQL8T4Bf>r^yW3I=&bA8orPN`OG=OG7?W)6qD(6{8+|RMvXlK`P^B~vK_NN z>OiNd>58ZIsgUJP z8p|c@*XNK`jyY52+&mG#-+`>fnLvNBp!!YLdQa5KY49$YJMtWy!T(bovu>6_wT!%Jh&rL zKC0LKgF`c2NK_wtk8_(v^|ALl^D|_O>QV1=^2{O03OuwINmU}Jm_p;S&CF7hnzD){966dxh+K2D%~#*JNjp`JnHG@h!bW} zHFm_w%c5%RC#M37dMY{UJjbGH=iN4{*r`V#aliA&C6aopvM#NTn|UUIU8T=3R$z2fX* zQK_yt2jf!FJjN@|Ng@6`#$TK>Li{JPU!2QvLg&{n&hSzApjFl3g`7+eA`olTM^2awx+I^(@!)cW%&*kSa8fLb@xSul+_gXr;Y3-vN zkUyP{i-mZzA8$H6g!r=`Z#sQAM)lrrIs>J6vmb9d<(9}2sy_BDr`jqhsy~0n`CLjs z9k0Kgy)3P81gR(Uk5g$ir5dS?<@twqoezX$1#TCWG54G_mb2)kP^y2O+boAs8zlFg zr`L$B2SObZw1U`E%}rs+%c%nHah8t|L*F7vaFf=`R3mt%Wun`Bos@3K`Elfv><(pF zf>h-px;vL;1Y)SqX1FU@{-qe?Y`AAwreo_AW4c#aVtC%6Ih>}OYrWi-h8VhLT5e&M zzNjBcWxJJG7NCBlfbkbgE}tTQ`9d?Wg#^{FqC81n_=UqPWKXv8XwE)&iz*AtnNDxyNg)VT{xGkZIm(UzVnFNghkzVa=RHq(zKm; z`m2lI3gvb;3z2);9Q&2Wy(&{#&H;QO2vWek&7yi%1zc^D*p_tw_b=-06mU(J0rT;$ zq#~t?2uasM7$K*54h7r-Eaf58KP=?dV#x=gcVva#<}CN{Zi1wU`x;Bz^>S`lQTH8| zUJ#lIR?HpCvhFK653RU6Pl!JktAx9lV_ITcJ+NOT+)XUQAOj#J-E%BGA)_Fr+#;K4 zPt&zpkPjiH-FJofIhS!q2$3zER>3OcP8Jd~OXibjL0NYy%P&dtS1o1T=`4>dQZb)M z(M~;#t4(#R!spHt;(wF9g8L1}sOOZ5?oQ5!zP>>#4^(vbu~g_F*VC)$9$~2gp|iH4 zdy4a+@5|6rWF_}1%l4)6Ik$?Ndy8yQ*HPwzG&{>J$g=4#T-lJ%lWuXATG$JE&V9F(BIxr16g z19`=*A15@c^HsM+oLoZ8Yi_$Zxe9sR?ZT1+ZG%$1=?;h!is|c)VOfHDrkMWjyf~ql zf$k=j4^YpyA@90Z;^aPLh#T52_q_&co??c&jpBr2hPz`~{=%`Km{IPpaY8X;+}b;2 zKELP3eE|K058O_1Vzi{EvJouZ(0(Z9BX>@mP)w@3m8CQGi(=B;GjT#O>2A_axvgi8 zV69Hb47Yfklnp+>h_8gipg*%u;fEN1tD|YC2>*$^0|AIW$sms07K@x zw^?dJDAhvuq3>mRNGRqjw;aoU)CS#mmblg8gx0}Z>W*i50kuIf%iXj%p_mo!S(Ycv86DrcSu6q6GtFw)=w4zm(Ryj##U|JNL2hd} z%0u&8HoKKr^091j+p)~WwkXwBcPPscl$mC;WVv%$Mq#BRnjNvt-N-`UEUSQ;kGXeP z=$mCvLbki^9=Uar8ju}sb{6`I+4GR^-D+_{`RsBVuzY~>P|OeR&^V!(J?;{gm$3B) zkbUlrIB5#m@22dPTQBwyuKJLJZm&3@m_zP#mb9aoy#_h#?qzul>k*Os$ zcOc7qeUh{(kn8S2mK#6edx(%f-5V@hhbL&6kehCK^e<^!ThKNZLTSE#zzla~za=vF0&+i{vHDpa>RWp)at9A$D-2kSQAdn!7N>4KCrB z8hD>V`>>(sw({kW@ zF1R8+9GuLu5@X9v5R)r7J5C0opZQ4e>o{qJnB2jQEJdEcm%MPNel++aOU@~Boq;^T zzgQY!ehaNUkT)3mQP#$b%cbNCKF?AUGhUW-*0lV=p)3zAO%hTd_&3Wb)EM>93I<%FbbgC57)bdWcL z(}hgYCgTVWhP)ND&&%@6c@1yOYHC`)U`>`by)L1iD$W%x1 zt}PAnPOuKkX{=Q98RWg-3MIcLX{#Y4g4#t{!qbo#WOT5fkSTc4l%ySjj1Ts@By-+` zd@eyg3TCjZLdW+eWODGJkW}q1@=0io6_0~`F3X&gKf+ZasA(Suw+oq~B~44v9!AV3 z!4#C9TFYaQVvyOvJikckgLAM7Br{lzB}WO&gN4isW(Y~wCf~x!lX!NRA6&wcZ&ZTT z7%>Zi8(FHS%HK|Y8QjHEtQ4;NsMUqRHGk5!(zVi)WXz&q^BYpWDkfvT3KqF5<$YX5 z+al-1!Hz6xMPg5=E+|!G3Yl3o*-rT}>IYevy2} zUmkpeMZ4GFgxDk(!QtSS=eZU`Lw|4i&QUG!~IDzG}kCL=f zke`DS!jv<{&^F_G2ssyAgexO$s|;FZO~{4dL6!oz7c_ue3f^T|IWJLb1-TMT!8hiq zZEQjbJ3)R4b`+ATUFRPa0HYqeWYy{*+a2E@W z#Z7_S4pz!8V_v|Wb~)tVU<7wf%4ZgS@x2kEg&MH@2KgS65Sqk7Ymoj3NeZoCS&l87 zf|#MxEZH&Aei34al5)zNXCUS(Bou1G^4b^p{SG7&8W<0bk~4HsNUAn%VuF?j@^C2WVcL4C)@Uid0RhP!DlKG+kkXJm zp&l$xB8DViXama|sIey?1wyTI$-SW8^^+6{{mMeWh;Ilf9xCt%#iVPuaNg7G&=R3K zaYFr#$3p{!Owpdi4BM7SRWfvt<@62Q-yvl}m2%56pU3KXVN$8T0(m}^ z!BXI6lJ*zmh0qO_-Z&QbAXwEpugv)gh}8~bO`!-&o*a1p4rvg2oaIv-og$D%p(-q| zKuSXzhf-M1qRl@6X%c#mWn&(kJCLTK#ViXTbd;NgcCqZn^{_5tnuiAGljRAbNBttC zWoQ>m(X=G3738H*LVg)z;!e~V(mGUxB@46DDW*-R4a+y%WK6rzV3xgfUq(#((Cj!F z206~;==DSm7MjU18M$zDATvUn zSPqs+&^kaq4xMJn(*kRrLp}-J7vhgm&kUL90pkkbtV_Wpi-?^b4LgZf1cjPle z+ohPZaYgBe&d9vbUY2oO!g--1QoQoa3*~y8wk6B+Hp=rw=yf4}9WDqB6e71Z6fp}z zXQY@XZzqZ`v@Q%?W-;%{`dJvd_CP)hLwEiofs&$r#7c%^k@Lb(F(ENat%LVT+ zM|@WY`}K9`36_DUR0R)Ni)H=~=)obMHKFE0{1UDUwUaSk3D>C-2Gx8&PmJ*Y~W1}Z+8zgE6 z5aUU3+j#jNZIg-#cB(4Zuk$2%YGGXKkjj^TapuvP>&wTGGyfBlfpeW?v&uQS%A5EO z9dt`*SXnw=ew*Lw#Vq7w>dSG+o?3Y8@5P)ND_cU$lWti1=mNI&ohQY2;eHDF!IRHE zm9yXudUEbT30C+}z9aKv8dpuyl3zx@*GskJ7{-<%hdil_^USP)->i933AI6T*pt%f za(3SlPmKRaxRgxg$8^E9it_o%OEnUA0FtAgEFLN4m?yt>m+$_Ld-5tD9bX#Z%9Im( zal(tKJ~%<-eA1J5TgrS+d6J2inHQ;iS&aK>QOIc(6AYD0)XG54C`q<4v!l{~q#ERB zFXrgnMD01qIZs}$nW!~^T=b+Cq&4J{Cqrx#r2kcu)NibrUs4-#NMyYR57dwK}|}ru`k7 z$r9Zy<)6?!mL$C8rkJ~-s^w)qxAE?mHucOupKxSUa9h8mysITt&qQ2DmaJXP4szX^n^tIfF!{vm;ELtU%zJZ=A zTvdvu-nQooH)m0A+aFci3J$`o4eBT4QIh=il_c?w-WOHo{9eraL3wW~^gr?=_SBQ$ z(Q7j2B3_Kziz4CPaeHwBsfvUL^In{B6SQ-XqT!Jb>_yRV#shm%+{^j>Ci3gXCBkb( z3}yuJo#^rK1{QTEDjD9$qV7bc!&xlqPE;m*lSSQ$Du)v*Q_Wups5{ZqVV6bSiJlJU zkYcGj(bM5vLi{Vq)8PU_{5Jozw-=poR$jrr`;vgO@&=@a7qb$hsWkJdwkOoa?jy#R zJ8#JOHYr|={#=4)bin%9o*YI@6!L;6m)FSiqOOu4%@v`x;Yl*Bz4jQoR^1CF@Z88vVN3kmliMSiXVO z6Y}B%_1r?`?DgwBNv3NQ_2NAFe_~b@leOW+B)_}>zaYl8eCdIF+Co~YZ6&MzMk`NL zf1|Z0s=v|36V=~n>xt@bwDUyuH+)h34PR7$qrI0(^*3JjMD;g1c%u3n9X(O~4PR7$ zqmvh-`Wu}+QT>fBo~ZtYFRH)MRmFJyjc(!A;t0k9s=v`q#fTmV^*6eEqWT*>JW>6P z*E~`Ejb7odoU`h0^!8#@f8%veRDa_QPgH-Sk0+|X@unxLzwwqQs=v|K6V=~%+Y{B_ z=pX(}l+f={4+wuLBu%@5w=iAN#s-9!#l_J4%z@#}Lb3uUapuxBVG!q>C4M{E3#kT$ zkBFF9KwVo@Oe|SlTZV+|JV95sSU_D{hJ`a&)U{<;_y&u*whZ_7LS0*YQP-AH;hI%s z&g$ASCOlM%rLHYw!rO&xO{6`gvvN$>txBonHS;a(#h7r4kP88I4vq^?V^Qbe2jL7M z{&jUixaL!oPr6p1I%XOmp9$eUEG|~M9tQa^T=i)gbGts))_{Bzeo2UbEGC6J3X$a* zhnPv>)z#&Gsk=;ict4A}%S;RZ#iH&qGs5>-)U)u%;oQ&2eAKh>C*i6r>P|E>tfAjb zM@QX>=7wk1lA`WJ3&J~C)SYNS_)jU`ooGS$u8^$2`UI@Xj=fkA?)WU_Bjt0*;_x|^ zz!!$BsIsC$(Vl4FVH$m2f(}bjI-!x0mc0)FJEkX4hzV(EDxo`k6n?0d#Za&); zIeM}I?;l##z^sq|k>g0U(-YNm*c;v;wl0swWyI_aXMbMShB_8MhFh_yV{thA6^l9+ zKZVz@sAF+7e3eBVi(}#9FUXwLu{a*4bsXZ4#o2ILJt^u~Tn^7-QODwPc%BsRSX>S- z5|R}dJv>3XhrPHQZd9N0$qKxhTRt8C9`47o_BQU_9r5H6Ucr(M`KKo4OL+C8YW^=z zsOD*=T`@BkubJBG*OE=^D+!3u9Y>$bFU=ibZes6CzQJKG4}jLNSSvd@O1X zUQ(o7oKQ@1q&kb5gQrIt#|gz4k#;O<4xSl#Jx(abiVSAyn}jD{h#lE1#6M5n$SxtW zZRAIc8`;;8${h20gW<>(7S%Q)5x0?yQElU)NDda&HnK&^u&B0?JyM57wT&E+gDmPi z%@Y~YSmvzSMxn@P7S%QiMLv?^wT(iNG$CnPSIqA&g}o>g*(5~n*AtK;kwQ&G&Q>18 zP!FwGY3L>OtDC{IH?aQ8R_$)EKhH=x>k_#kx6mV9a1%tgs-+xJ6yU@w!@l{ z94vcoCTM@v)3jQVsx0(Qihi^9Ed78If3ct{Pi-a1syx1^t=Ear7h=?Po)R>U3>0!9 zpxSDa$k=8SBZaOBEh1Z3)Rm-dSNpxPQ+87dP%A%h2hDN%wsAs(qk@0at&w3*wQ>9qy zDRNZgGZyvqH!AWqr&7maROG6VVHzD}%4byM&DL@+)K><^M21T7#%aezJ{2POVh8qO zTx64w3zizW{UEYkNlXs5{Y;7!VNq>tN~DsI znAbAXB4gu(TIP(%5|z(T`HVI@vP_69^AT))c4RxpsIAY598szGik%s`%c9PLFCtaj z$`Yz8_M*shLS)XTk@KQR6Cp8Ct8_nI9LW%pu3g1Et4oMk9@)fl{xdvRLRLm@3z4nm zE@V~2Xh++UlH3V*{77CQG3yGRcS_++&ALcYDViFiUKgp#qQh+NoLb9~8ZRB|7`p8WovOLtw-4ID=@0T!)5^jj(7qT_+1fD%}LcWO< z7ZNkoc;>f}a#A#UzM!XxZzIiF)YHVK$l{kN=a{CRCbmY_u&AertjJXs^%SrzQniCj zrJnv`kx4A->F>M96c+XLw>|PHi+cLo5n0cop8j@5zGYEQe>)>tEb8fRXQW+6xpnpQ z_kH9Q7WMSEEAkeLdivWPd51+k{rwOb!lIu3eu&IuQBQw+B70fX)8F2R+evO+J^k&A zgjv+n-@Zrz7WMSEKhi`>pxc@xEgz28fr!>wmU9uetK3$dXoA)Z`CN=NXBmi`J3xMkjAhA-R3yJfHn7~sUi5(c z7CFeW?gPxQh5QjY#?p6;lAl>#_T)0l^sy@DD$B#3++z83w2JwcrDSvb?oVt>?|BPhoF3bEj?$qfh&y7eaAu&xoiT@Sp#iE|XZ$%cds3-9| zkrOQHN&N4~RUz_flYyN7iQJblVm)elKD`?${R(Y87Etg0?nY{{sCR$&B3*@~YI{~B zXkQ?ody&B`&tiVua)=h4AS7MOg?CBoAc1HWOPAV7+D=GvG_eP5OP1#VM2{8~5(`kO z6A&|6gGG&F+tC+T&aO+;E+fW{4rCc~DoMKraiVKj{zS|jh#TFG+H4pRRP3Aqt)V4Jr2nht^Gi% zY|-b1$R2(<5reA0Uskf(R*oo3P;Dz`w24R+)6`gH&Zygy%IxQpOQjO|(AIM+@$$(X z%_UOBG&RDIJ33p4JQhzORi5Z#8RNB$ywT$-fTsaA@Z=|WM?wWD{1$ZZ`*&b6b?8?^NcUe7!wT7X4u>xF1J7S%Ja7i}LW)T6E! z?JdRYnKy{O&!T$f4Wbh`l`2nz=nNsUJZDj!2GRK(qu#JIjP4L3k301k8%2-A$rYq( z68&9B%o^GcYqOxo*fjc=lz^HG(KK4Bk8E{nE=03v@i%>;xe(2x`T9yx{md58axAK! z*&Q^;u+HSeNj^mi6D@8YHCZ5B1} zqE)o&+qCtpK=q%Kv`IK2sNADu2FD{!|OzHkD0Ir=5bIpj?1n7thRhUEw3Ofemz zdxWHFub)Ow4>@;;{w74WvBw}CqvinG)`ft2TiQ9=Rfue3m7=bPf~ReGcdYF zNUCal4a7D zp=b<$RJ4+mz{8m7-vV176+O*Dzeec{86Pe1o=6oqkQ=EWA4QA2FXfj<@Jm}rYBY_d zUMKwe0Wu}JorOj`hCtGzxrS0If5c;2w1^P@?l~TpJMD2uw%ei9uaL>}dl z$a!WoO~{3Sdeb*6x{F1-o;nZ6d`giKEt-Yjt&*#k8doEj+GKnC0rUkB1E1A zi;!w*G--s~mYS=!Ecy(K>XR&sc4bk0lI78JLi~J|NB`s))t6Wi)kadze$0w!DIqbh z|FbgMokeYHWpo0I+SaP5JL-S)SrvU$NGzbXwK`f{$P{xA-ov)AHEngYB+I@kSe0E! z8I}dbRZRK+NTvTsmH)_7|B>qdk(&PzR0aNGVs+f*sQK00hm0k6#qVxtTy(7`G#<4c zG3%nWMLzNocs^@g=4P4!?L$*b;;^ZGlEZXryit*3)?a|INWGc0-?a_X5LfhIA9Umt~SG=i- zro~AF@_qE2kSW>@T={cDc1L@BEViZT8~=~2JCC!u`u_la%nWY!xy#(o=iWP?dq0yT z#P||JmLVbQ$kHffVlZhEA!{*)A?u7mV@a}j9$m$U^x^WKbK;LeMd2eqf=RSvK)!dV>!Zd zEc!T0Hfqnl56E& z#kldu)Y}pE7$rNDs8=BNW=gWenL5!k?LyAG7?6+7(j-`B}?2mmVYmq7X zL1aF~?Ov8h>R&=Ph{Dd6QQ=JguNfj3YL=g43_l}naXC*W$D1Ru-QLl zY0R~F%)X9g6Si5_^fCKhC0U|8rz&MHR1#{nO559ht!km>#$U%CD{b#aBHl!==`N&< zee5?>)4v*F&TTJ@Lf99tBXQuna}DCM`_@az5qmHbzA{9&cTqxTOFYD9?@MC4HiYI{ z$Jz&wh$WaIDc{G%+J~~pr?&N|d#wFSmScEpCf_a@_A4xMoWE~FJoVcvZIGo?S#?p? zvya_MQtViico|a1p1*^nuq~ciAa(8MSq`%#*yZ;~GM}$ddJ1Cd*(b0(iKmJ* z$g}nxKTu2^h|H&f{UAoKWU9Z8MyZ&F_S<_&j5kRd*%S7We2DM$WIm1UsryORpFRi2q|JcT! zbXdg*>j<{B&ttKUU^{z40mT&HY>{g*b+GSKLhZ&Bl-|jna74yr3VDX-L0+WHJ@sc9lV$t;SNe_b(e`wfnwRlR z-5XzE*$1(Fa~-3SN=7Q7x;vFj=a}5Xl+PIZ9F|3wBSb7>#@H7=l4^{79f?h@S0P7t z$J)2BJiJG##@cs|^#5EkFKo;4bIIZ@f+RyJnKDE~;v2|&LUu1?X zwKr8lZ#kzZNoBFd^p@Gvev$cPh(DXreA;F9ca?8chBRZO~)EL*Sn{56w(5X-2+)b@U1 zAE_kMW|j4Y{R0y50Pj;|Szp*^DVZfMSBw%Xlq_QDwFqC0Lsr>WD9H-G(_C#|NBOAj zkw=E(;<|8Qk5-02xNks8__?261;YoYt%Q7ZI6r#g;~RuCh^g+#XPL1a--AGEIub+_&YxT{%8zgVA+;Rc z!sH61j$=WX+=J9}Tnv+vX?PRi2w<%TdB=Yj+atHRi6cLPevB+0+1h1}-n z9iyX2?pB~Ps-@!!%QeK>$$2s2$srov6<)mbYaY!Z4sD6$%ECX5Ib|kZW2$APle@7lm^l*$UA)j{~8(9iV z;|Urv$Z5qXEmzlqfM7GTAYN<=+Pp>bU1P z@|9$WwOm%N<2*|d#_;9wn(1g3BXiCWQe#h_dE5c7(}7 z$VZMVEIRg~%z1$$)=k?y6=$Ywe?D;}vb^KP*acE8b~Ml^MoK441MFF{>O~ydl{MS?loPdlFfTre8&=Gkl#RSxF|IdZ}gG;8@JEX(F{Pn;qvl zRT8J#>X?BqRb*M`c#rLHoKlh@?m}d}b~>uzij^^!A+p{0!LgL(B^-BoquAw04p2;^ z84=ShXIq&#{(eY7yR&Lk>C$SO(i8h1}1F9m1r1WX#`?qmH30+tG^3 z^XItZAj=eP=TADWuv{%oE&pjpg*cf{hFFd^O77J{N4%0uu?)44*YY{X&@hqL`vu1q zPW2tv{ZB`K^d@Di)%qUB0TA<-BRfory@5NLV_%q*fn0Gs*;u9uJ!#%>)KC)Y``vIf zQbK(Ic^|ytNFote-i{C+AXht%48h+msa^I2MTuO#0@ep5< zLW((ewIpf132hIggmZK&N)?6UE^{vBEL4&q%0YTV$~iB#p_mmnFsBj{?d;Z`WX)$$ z;sc20+|q$$NGevegLs^xBS}3Ri}{dPXJTg(S^DP?(>apm6xv=HQ^}d%MaE=^0dA5i z&V;U%sspF0?i|F@7_HxCq^jjC!rEK%JY2J%u9UjYPTfcrp2siyAf~>v!D}RWkU~f! z=g{sX^YPp*&xAzh&=itsi1`aK&pCIojDyItuDP>X4~qGUb8hMM^(0xrIk$1{z+S-i zsNR+?D4ZsLGp8mQzldGZeP`-fk|Q|E^$;`0+45JC?;y<~V!A^nIj{Ui@(*MHB-^>_0?CuOPTzw}cP9Q$;)6_t%yiyo`4R1^ zyc5lKR``Qr#^NjfIf$9-9Qmh|41Aw}-=>6oen`F~TYqGuw-8ptB& z1l-%?Hvhu1+}Z6a$&kHRa}P1Ao#(HS978_GAZwj@*GU>diXiKq_wSGdAon2OIhWog znFlHRCf>q0L8?ObJ4I=d+9uXQffP7X%1Fsj=a`hE&g`-j zBhRSjkmJsx3M4~Nza+@d&Q%VQlzVv74msl-=_c8Pvid=ObxzhvdP2rPes>lcBuguvE50^K2jnbc zY_8h%NQNWTVMqnnpAAXm^(AZSaNTCEcR$ zm*fuzM(kT-jMeqka49tAkb#(2T}PB;2)WYQ1juWyzm;4l)@u~{EiG_XxuQm>R6>fJ zSCHarspP_AJ#JDhdbm1MjM_puL%WA7onnezjK`|=$hoI0kMog|2T66^3QM&R(%V&f zq$*2|EagMeTn;4}LVS<)^zf9O=89)Iy^H23zTujuBs9|XhHD9nHPY4BwNgoFT&u6^ zDy(7fjkjHIy&pOjKd5{%NrIy^16D| zQTo1cPAc8#7)QVZv^lxeO)C0XKGJWbSsOm`(rkug~!xh1|jgv@gFB(d#9k6O-s zn(ca%r3=vh@DNikZqX0?(<>U=(z= zYdVXyI@4^|9F|EK*`}C{ggcdpGOs%=>U+2Z;^NvLJp;=0XZwQO5msd=h& z^`(+*{kFQ^VzFAbZLYyeLM_`i*LzCnSgb%<+gwv9CRq9o*GwhUvVDn|9j@&hW3^+u zU3*!qvv;@aFpJf)?RNdFB-2(2-zjWFKD%A#m4w>5-L7eK)bUbxhixim2}!W6+v{4x zVzqU9U7H_CwbymzkyQI#zp_|u-G0{`PH?Lr4q;d=CUoOR5vh|B2)eDYQ~a+H$J~Z{&0+2h)HIt%P|o#r&y|ROjJz#XOwE~ zR7zDMW{r~2{;|buR1&(rY%yb(%2c5<+!oVqnUwIDipw935i1^z*`*{rpT+qyhK@y5 z)WQ~Xmqe80zQkiO5i3=Uuv+8CVoI?L_>R7{e=Nqy(hVYyMX4AciQ23GL(ZjQ3f53Q zF&JsD4=EGl{8CDWc>HpdXa*@CvyMf+1#S;<#Ax48%=vv0q8r2=Gw)lHNTeDB3B=Ui zK++771*sG>bR&rqG80lI#<_{48)PXYJ|>Cf1I(uT67oz;{$`3ivLrqd(r)#`u(Ei1)W*omW`yG9Bl}X~^A} zL02fB{Kqj`2odgcEP3lNCJZU=7FT6VjyQ)SD1R%eq}!<^Q%vW+pUs`FBtujx!pt;0 z`ImNIWVwcGSw5eZcelJoWwpZGmHX)F+1-UKhhy+Wk5n=4g6kAB9_OkXqPr7ski;cp z{szS3eoINF=q#TdAci}GWgN~`DRJ&>mI)Y-tPQE;o^eyvA~=V$n!Cy^Db%VrK}{;EN#$xr*YPffVI%*^>(Nh-BrxnB*EU-GwzX(#607k z{zyz+_s3x|vKDpSpD78wZLjOz#bUi}ujj6ON0uIX+g{(Dtt1!YeVD<8?P=g%$+7^? zGGh{jNOZ4dS%+3sUMWr7yI5}j6Qx?(rtTXo&GGw0GNze3>aHv+S8T^@F)7cx%dzx0 zj`bl?R&%$;(hf2V@`Ae}OPQ=FA;;QUyIZnct`ec7t$S!#%wpu+-aR8sWIi3->sWlq zXFQ~%yC_ViLAtn|_h<`cjNG2C?iMUPU&C7}NH=#XOL_^c@Bn$uy+BE*SJ2&E;l9i# zBq{EpkI3upQzRk_SGOEn?%_`QSEUlI(HcwX>GnO8k|91v%tDlv>TandQ@CHCF{|G0 z(Q4LerdWYDqH)rylO zcE);okm2q|ELn(=GQypzBvUNeg5EM@q&ttL1xmjKdEXrowvF<+fcsZ@$Yl3AmRFFE3o^xB?JRNoe7DZh zDhUi(Cg&qZ3mZTdx)&=6JufeE7m@_W z1{b?8h2?FeNYuG1qnEpzDWSS|L|Mz-ohT+)dcM1x5^9ZKK}^28 zbU9U)O|G*o``8=Y4wh`pY?LEQ8{A$Nx$-u}JkDZ0L2qzZRg!7@1z+*^L(UuAwUva9 z&Ib3tcQUmnI z+_zauWT9t;m_OYWV<}ZnmuZ;yC6WR z_F#MNLaw_-9EtpPvcy32+1!gCCz0z2Re;=cFMWb!;EQ;Z4td}fl}Nf{4yLR{q?X7M zKwq>nVv1{PS-hCVE%#~(O*~1doOx9s8!J;xg%%NNZS&?{l`dQ3X^^^F1tp=@I6-Se5^Rm@XeWT7tt50ty`*(}MyA3S83!-|gP3G(hLX?-`77F|NLc@4NTE~ zQbPI2|CXYiR6_gk7;;Y0{-hXnM#<89Xb8mL&f?bb>Y-gzF?5cdQ8BgznO~O8dj3k) zDzI4RPpTGB5;|K_wf8utH)cu5w|J@AG$k3f!4TOGPt_JujN0?RqpV)qmr8a9pV9hg z8#te7zfeAXwEZj}L1aFCw8BU7Nz<-Al22dljuP4%S5Q`8%~nqx3(*kc#{WRx)Yh`B z#`Ch2bS=F;#Z0b?Uk!u2rR`venS%8y2Vw4|me`PD7NMV69`d#p-AGD~h`?QVOrj9| zwHPIIbliv;pjA_nDdcw(CS;&in?;Udku+c_kI`?EW-M!uThfL^?L%3wfm*VXo#NHf zc$bYYXa;G%#&UahieY%tlzVKj_N9{0-gsBr$T3!`AzFi`|4lVi%YL5ZUS^bd65BjX z+rskUev;vuuQ|mOv5e4$vV6ocQY%mrD*ZjJkYlXU-`94$@ZZu$X-RGWTl#1%p*@LJ zdWJTG#VUP_c7??%JyUCwBvXY-AFHJ+$rYt=N0YxaG)^1LB9CAcdOPE_@nKROb)TTk zVY!N4Zl}6Je4s5=k}00R_aL&2N!k^b>Ora5LH?IqA@7y7kk6#xHE7j+N>HrhHC0Pg zsj@^HKBm*OHY~mQm}YBBS9!FLfe$hGnMK zhs9+1Ft|O|ai6UX<(P>Wm6d10oM4P~Eaqxaon_9MLP|Zv%nQa?N9PmmnU^R=ii}yP zwNa8Geq;G8n94eW`C39UrII33t<>^frdnv|Z^(bG(Jr#A!4{Id_KHfSt`w4i|4%0T zKlw<>_K3ZYMXGCXjkZL|7?n>Ol)gq=`$)_;+D4X?$LOulH`-nn6UX8iJU4x#om7%3 zcB8jLa)Bhch2LoRl!VIqMl01#ZhL5ZHfnYjtE`QhkHso$qgF{t= zsMTSy-nMVl8nFy1M_ag2YtGU+DD6msdt)OXuk9tRJ+@UFK{4VrE^Dhcf#qaL%6Y3c zokhcpc)2&W2J^A%6%wmnTeU@;N?;U}Qmtee$MXudYTvSC@v+#dZD%PgYn64B`wW} zr<+$H+qE4m^6928WT!Uvbs3W(>S26du1~v5D`1Jj-C+=7c58_}C?*eghY^rH+80Ww zt&=Mz?bW^y6WO!cul-D^f@j^2+GQoPM0NDF#vq>`wVO(^#9w$1GC@h%p0cbgaUL=a za!_l-a*gGXHi%^|_RlQD{G{cF$pXk>?GVfL*a)!k8CD0gACan1TdM@W>x!fE z3*@X;&`Xv+OWZh&Z&)C|YVo~EioHd0PV@C4kt4vD5p!NUA0~Go7qt5(3Ay38k2L?cH2 z>!H?7307st%+{eQCSA!`vHmjFx`zmTqmnE!2eYB%cvOVGgT&SV?{{Qb5&AwQ7m5u& zg)w0zKP$-;wg18_cjObH7jZuM$Y-{a8E>h2iLL2)Ck2Vr3zTGvWPItiT#2)vjKS9^ zyv>pNj{cNNKJTohm;q8UMQO|_#ug)=3hzkSDaPYH8YT*gD19=E)iW=yPZ&rsr90A@ zP+Y&QWUP?C&D|X7i|b;LjJZ&((_XB#h_Z_7Ygy!(xkZT>EMsy+L-bd7K}zT^D510N z1jMHIS29b;<9-%WN-t7ENBJV8v|i?2nNL>mD3{TlEY?vjqx)EvVTScpq$;CVR1&Ib z89kN7s#jTkB8ye`vie#jRIh)KPg%Y45UT0@OPGTPDW~Tt;U}PX@g84ar-W)@huHPN zP??Hq;ej~yu}X5(*;^43qwi3XC7L!A>Ri?JeI&LzK`G#vMQ`F)V311JFDc2i zr`k6Q$4kYCVY2j4-E}>g#j3kUpP}SJF{|z#{R)d5-IZT=dh`mzsqWVMCZFDo#ae+U zRxeP(djqBVu|Agk+gaQ?H$q~a8zHf-i@<+U$vqzuYtNhdDOJ`ip)ba$32GXr7b(Fn zxkH*l9@i_4klRdmqSla#dQT;xub!*wZ?agk4Xf${S@z$iUP@Jc1dBEPTvg9x`T7F2 zEmieNEN|7fB#*^iAM-a7u!^I;KuPF{wW@yTeYu6S$C9yyRrT8>;=rCLkqW7y4;ZCl z#Jr#A>-(De`z)nL(er6deG-X!%I=R;HT7H;YXqjIK37Sm*z;PXcvr>jxk*GFYZedM5YP8%QI>Sdop3z4up)))aQb%8}BrCYhb@d-u3J+5)>goj~!ELUq zpJK7LxvqX*N$BjYtGCRMrSBBh*_)teD+zsFo}eF8g6}xcB2C8jB*4HPJsJhQbs`~m;B~oiesr6kMd;j$9-7g-XytVF$= zl38LH?jYZytVBJZB^&dR_CTK3cZA6iNDJLIPL?%G$gBGoNK3t1nEVN8ttW-aO-MU^ zUzSW2I>VFnUzN}q{tz)qx_`WkA&DM>c{}>cB*9uF>!~E-IrQFTKPp+zQ<7<$!z)fC z>kC-qXpay1Bm?iY$#YmdCFn=IDqF|X?X zW3kqX>8>|X5^7hw>o0_foO;+@e=$ttZziVb-IUP$I{BN4J@np6sLf13y;Ai-N~q0j z4C$qRu7ujmmXO~1VI^5Ms~zjB|ID)H5Veqf^>ZxNY7%|*zgQ;Wxj^>5`sz27gwBn= zdh5xdGvP&))mML=#1@bKST{(z{)UoJKQmn)%3}31-_nnTsG^owDsdZ4U+`U4W%nkIC%^w&#FQCn!6g6)ylX@9+(k{ogM48GAos{VR)iczh^ zV8{SHQ3;hk4l+dFp@e$5vmo#5ou|s2sU`UslA))E$udZ$eo+axEs(K#w`np}rkL_4 zo%7@LeM+)apH}YYaeA3-im`gI` zZ_Y8YPb>R%}@Ok_<6}Wr98i}e`zElYvogYkW7)(G!(~>Yk(L zvsiVXuHWZWKjFJZS=MyD&4;q|P``J&o~JGheLlW3hVXOY{cw)ZS3FkpKEjZ>D6H8cT^qy*|_XvBZxsqeM%{8vSgTbbx%R-%yexR^du#@ zN=8Gz)3ZOOn0YulQy|;)+brGjeEKou2R;51ifIYShwRZ)Stc$+>jycYPY9EpkVATv zMQWRa^B8~9dn%!;-~eKN(npg7d%{QbDJ&V7Yb39@Bl;W?+vm7)WgC7(U&yi+BA?ce z=qp&PGyJIjCCfIv3y?8K^^GLrAyy4KfzprZ-z=6}80zJo)Hk!d!=<0pcPYsgA6>^d z6H=Yj?=O+5a>XpHW>f_EMX&fNNk8oQ>ySb{m1TQDl(-K$r{DgJV$NXw1SuEw`%6iB zV}^-s7`~g((^iml+=F)kkehmgd?}&6%^kg2n8YFGj-ILn>r`Ow?o*KadcKkj@dn-( z)Pg+J^FNpQWQgMlLNtIxdbBS{PV~eV^N_NhY?hbr<1Hw}ouqIlB8Lq`4=1J;_A83)l*2?GYPE4xnGR7t+p?%2FQB z4h4`T&pwvjkY6AjJqep+svMCm=e$8)@=Q_^s(UxjhA@%mYB$eCB^heQf|OT1ZN8(@ z?_!Rm#nk|3Q|i9(iCkB_C<{t`Ckf6K8SGgUmXDk(GT5`0Q(1FG-t~;y9x5vuIS=v7AW=`y zUdS-dJSCxb^us(mC`LGNy_+iLAjeoU_lA2W@1T7s&pH`1!gE_m=m?JVi0@?#^=Y3% z%t%iKC0W7J-}h)rsFzz4G4Ff6 zVEJr3jjl}goMri;wUz1*mWltPF|^5^t1R2PTXKh`3cq2Q?D>~vMNl5wsrIw2eo%ZY z*4%^1o{B7j-|tNJR8~S?=QTs!Cwt_OV1J2{}(ApA`- zNiYnJI*&pZ}uw(42WVis$*>RC@d ziE0lGly%l~`>3o%hUh*3-`+Fkf zZ%@*3N|lE7f7(E9c$TuekJ*#2y56v9`mNNd_6Eq42P8U zu2PbPb(-jV>I&YX3o=#cJal-Q{Vs)$#aN_rcvF>x?iWt)0+QhU!sT5VmP+0)T;7eG z%DP{~c*|Z4PlO(Y@7_ghp3%Z!(KDy5jM!3Cl<3=<%*sLid8X$j9T| zLovaeecqqKQWbA1gwKnl_}gAmu4y3myy3m5V(5Oc2;%p4{X=bYaNbuX@9Qj9+gr)| z28;X-`b(s$3b-DN6yhp`-3LU)-;-VB!En3XT@JC(d2u=Glc5Zj)U`OISJ{0&A* zRnCi8N@GU!W+nMbLPxNYcgSC%{rtU(nL-krqgdHHhs7FUsOfarqgche z=aE!Zyah^V&&yJ)c;k!Y|7VJlFGYw0D7}jJAd3fQ_(@2-H{}w=oPu0{)bS=k>2#KqL|zm$noT^R*nrrl?_Y_O3 zJ@ z=2Z9c&^v<+_9on2zA$iYxgC!5&Dae=%Z?{{NYV|*u&jlIhEo6y? z$e8iojsH-Ld?K3(ndEJ7hh#d-RIlwG$p*BCA0sBmo5b>X8IoDvJeKu%B9r;d^&V&G zgxPdb=6kjKl+SjE%;yvD0+tV0KJ~``OEIr=sukXRmh0Q_T_jR{?k!?D!>QJI2R)!v zWg#07^Nly*A<1CKkC5-YodkxearVyURJ*z%+-h~qB5oVw?o!7>lW%Z60n~V{E>CEXn)v)fl9VFTNC|s?-bZ93q})Ri&R;VPQJA)=kP7L{KolZU&Zp2YWEHF zUJ=vX7gd4e$E6WsF(l2`lBElt=vPA0eYR+dc@9tO8zF;z6IkLQyC5Tc*>;MV@;%mI zhm7}iiy;YMd;W$@_Z6{pcrrpffXwv`lrLIv_Bs+HMVa@priyPY%lY>sg#)t0H$$VC z7m!aJB;R+O<#ps!9kRxkq*IK(1M6ZzHv6i1Nt(?RqATQpZ!F8I`O(+LM=|f< zJWPWe@@2DJN9p|`1-^YO#~|-PPWWyFDAh5v2@@b^d{<18(i@^g9^|5LavaIe*sGsH z{`M`dN|IJjsF;6z6>5w#uJ{4nE zQ%o@DO0nNkjBWTe`UbO7>{b@vDU3YfokFG9-7IrU)6Bg}u>~ZmC5ak^oQ%}t^!dFQad)br4;J_)Q8lMt*<1@CQFySl!mb_SmYLx zbYijQ#y5=ZrXEkLCy_hw~z#%E1Sk%WU=}iO=EAeJd63^a%8e;Y{Uz4 z3$w&im>*73o~3m##z&&cN*w^DmU zY(bwb1@e5XXiYK)*N$wTTg5hODc27)O%rbGz7$EPcL;6ax^`K6Ye#%BLFcAQK_Uu_;L;0i;?A=^1;R zB_3nu@`~#d>+B$7G6a5fSA2<>^jJ5G(@*kNte-_Lt}SEw#XiZhDM0deYz>ySm}M$s z`o|_H$rLYkjS%Y~17fo~()K)$Ej$Am9lPLFlKx0ld^Fxt#ipc?tat8oP_-bHsFltck7ITgK28C+Gici_K@T-ZJcnJ2 z_kqayhuF1YB9G~w*rbot_Nb#nvN%lS@!Au6g9Ph=MvCFcXK$=FpK6hh`>DJ$?u%{4 zV#A$C%7NHEEPXJCN*?8dv7=e$;|?oh4#n1W0Mz?xLbSA9>nG^CixsI`tE`}X84wnT-p*T4nxWr zWj-YdVC^FrqZ{wDR72f=hZsf{OZq;{DTVlrY?hQ|Scwi2Fe-jV`S>x0FU2&Pv#fPT zD2X%LvwVr8b00B}8>uYiQFkd%7{gfB;7(-A!05Lzf#nFE3#3#w7O+J0Cwa>Fie(s% zpvzHr;UGDUPm8ErmAj~SxU9|4NoO9RlMP6*@qS>8dB4U zXPNts5MD?vqrx(oDs+ZFW84^MgU}iN1Y({sq6aBaBOd>QBp5y=8RE=pe9aB1Z)~vy zYgIi5dDa*?Sf;|Nq==EJ8X05XCHXXp*8NU2QpZuw$=E`9r8F_tvMk1!gKQ6*7>QXF z(+}U?%9v)xJeFepDW;j>9xr1;wP;}^D8bh#4>87%(pwnqC#V>kT&eG6NGszdB^kEO zkQ5~;9Ak~;wl>l@W(7uf|v7IRDz-T}#bWqTu^W4c0QKJAUKIOg-RbT>~jHgU`k5E+wX z?Btl;AVkKzXk6r&30zi3;|j;j<+3^&cQ|Gd zm(|HA_JQ2<8Md#vtWHKLC0VxHWoUak8+MKKSbxJf&awyZ9s1ymdeumsL@|{8J;N=Q==5U%}F<^C<*n5`WX!=Ms3enr0QoRE6K36_fRd~HinXj{urT} zgqXLDeN$!58R8qPZZQk;j*&Kv%97(_pFoBfe`Hh4&Oh-BIFMmR@f?x@$XAdNMmd%w ztONZWzI;UwLO2~{m!RGA;qZcqCRq7 zYDDCP>LqKj!l+LYti?*BEsIt9O5q#}pKdbtv-H6>%e&_$~R%@Ga<0DG-kY$IF_c6%_ zXv5`O>79mHKr*5f$u468OVi(It)V@}eU|zxdySM&DAle)`bu=4F@q%79_}*=Ipz-U zjr~T2g_P<|uIYXwQAw`&5qsku)O5eGfMW)u?qeW_jN>frxu!oEwndcBI=lyw-va++ zq%Ps@VL5E1E+vsQl`#dzJQi6~DMyR~B^lxn*W#!VwM?eU70;urRXs2V#;C?p4f60= z%z-gpWRZ23a@;@?{Ds!@JP{OYJ%x%e~5*MiY*S<6h-WqZP-j=6r4$FLKNeoX;)e6^^OP`TS$_w$5=h9 z+r}V{v3gdwjrTal>Y?2+#&L|*L%U;4xTunZ~`- zd&Vk`kv;r5ID79K>o`XC@E0iAK{2Y8_zd!|F?6LmIzry5S3!io@)smt%*NaeDe2#` znneCqrj%&^d6x5+Fj4{W`WJmkF(nXl31a%cB2i`Cg;etIAyN1Ds7yQ$`hQXq8VRiI zzrkYl9G>!bSgUeY`N)>`DSuZb)N?3>e4g^ZK{3IctNI5hq3cxkSF8HRQ!2IMpB<^5 z_Rmuix)W9R-=KU{Szg3c_osdpDm@-j$Di;u)zlgdtLM)mvB|4j-k0n7b2-MkNP|Sd*vaYji2&NL>s4cWD z!0Z}|DXS#ZPpIdQp%_)urr4fm{Rv^x7ShPygCy8bNc0b8vHA&#{xK}p+~!38w6J{S z@k;dPDG7}}CHil(Sffu({X^EtZ4Rw=_?&;0l3A*E@kf31F#LgUWy~xg?_W}y`*$b_ z^(tHV_bCY-(-!`VoJ#i9yP#ez{0ZwRAK6!b9rA*IG>h!>zYS^Qw{4&p+2FCd9 znU6W8b0IJL(>78}zMN$W>F)n(la$c*^z?5cQ9YR>&tZ(w|2@mc=-Ujz*V8@y2g6cr zKt8Gdgzr>7;yCV2+aSIC_m$*`vUoeN8`8(0wwYq?pyih_>Hg7Mq~wSwJoOhq`uP*K zQp{4^-wPpw{6ksf9sf_ryZ%!wpQ4v?12WX_+(xNnZ%4{7e;bz15hIW3aQ`S4tFJZO zKbghqT@3fnV>w$$y_DhpgDhV+k5D-e_dB;!SwCUchwKrJ@Q?kTBpGu&q`c=}u#4n0 zV&wTe%AdBIq!(gjUt*NM%^s4AoX==~{63O*IG-{8EeA-N;{GM~*jRssA4$HyhfzS3 zmE{i{BH0U(IZyE4I80LKW7_5q{LKnT%G5>=1*scJS=&(r-WBIf=S>$n-GRuFQ#X5o?`lC)z&hiM#m=FC4 zEOP%y$@8bP$o(Vt#%%vmmOpXUl|jrL|0x!0|IG7OI7vCjU~j}A<|BV6miCYUOZ+>mRNHCKFY#YtvCfty z{>rB)XIYDy$mcVEHx`|9Ugpncv5v)Z|3((8rpx_TNP^qE!r$gJ<=h1$6>^W|`}0`j zmHRBpTIt`XWR~h1kH=N;h5rUiXZ-5-m_#8~`CY%rd}axGyriu5KdvOyBCYXP;~04a zpGVGX{M}gO5o`_l(qF_PkHw3Sul&9?~`|<@?i;;$<$-FAW8yY#z@T|0@{t>TeVilJ0m$ldDD-1!L~T&@UhT9hBGc-c$ba z$aPD~TC)Ui1;v^rc+3C(MY+vcwqNnACwu0%{8=p5xc_|1KUK+v$3pomV+rMRn#HQ= zE&p#UR=sZduPCA8wWy6OtM;F=^b5tTn%?(of00=G=U@N4OC(mk9{5lFEhR(jz`Q0o z&Q>hY<|;{gA=b-Bz2R18#mNck*&B~na5%u|8;EMB}l z`3Esi2PzAEVv22UgjF2BhtvoZm5`Dl#J3ujS`xxKOE9JuvAHCq_j<7Bga_3$<;P+m12UkG1~_oD50F?*lGJf z1(z)ALU8OfDbPSkhLH0!s)j6<(#TrfR z5~vYFWm)t2y95$gtY@N^0*OjOZSPBgmP+Wl$U#{z1(GQySbEn$iV|v{XCtO-;55fr zqp2x@-&m~gd{Y9KSghHYDS?|TQ*Y4|eM;b8mN$6JAtg}St&WcE-Js|!*38e8fT@H= zWtU)kQUcYKgxcqnKuKNY6P$aQ5^$2JyXOi@6((yTJp!jVRb_lzDtnA+fwCUT$9if@ z3wV@d+T?c%6jO!8`uaXC@GMI)o-dIWc$wuY#x5w;7?z)ToHi}6PzfC!xrJ$gbtLK= zLz${?;CmKpd-?`&g5hsx@o#aZe2-KiIRq(y^be>I#FVRmHB}&kgR*Ep))9gX2?^#| zNf{cHC-II|rWzKMO+Q45BFKoK=ooLg26-F>9Mg z2ex?W?6tNzE0E?Rv9@_qU?_{V%~JxUV<~14Z}ZfElf~NRX@NE@);8w^Mk_%Zj_2I_ z*q+?LDUPvf@nOJcP|j8@@&a{PtXj+tB(hkwm=nz$zB27E1zmSgcxn7KjY| zw-!qSWmv3QEDJPav1+j*FjPsX7Ape<9AnjDb--u-w-##xby=)hd>Keok|EAvmY_Ud zYXj*lwfMMy70}{jJ{dw|`6h5h304fi{GX_C7*`M6WwAlZKsE+SJT6n^ivIXb}=l9W(8Rvj@v z1c*GnJ#8;$xCE$MA7;++z&T`V?lFo?v3{uLx|Fo1GF|--hvWUX3c$mYgldNot zwU`j&G-oKmm^o%gY=dZKb`8oWA1jjWg?LS8O_KF^jvM_d=7E@FS^9UtJUzq&%-bv* zP>WL#)6D-LrHYtAzj*$H8C8p9T>$S%@T;gm(IihZ8|7a>^IZ1Hbuc7%9OCuh2Y-pZg zG4OSsyrVTViROLxYG<{c7sr+xxCH#Ey9sIr1NH#U7r=vc^c{>J8U65QJ&)bFh) znipAG^^H`&jLDLLXX)S^2i&1}h170(54K;AHCu-vj^6(dNx z*)Ngud4;!UfH_)8u2_w)jQik?^FVVk%Mb1-F#@RunT0HCAmboI&4eaY`cLDcL^fok znZhD}&qc1R^u9Sk30?2ibPHYqD5`x+MzQr%J)|OJPncSZ35ig&N5I?9GAIp2#8wVhh&1y={i3t<2sw8BZS&trP`y4f zpJB1;HQ#KegzEJ#YBAq@D@>xsW3s<}7VGWOYV)v?Q0c49D@sE5g4O1( zR-x@_gwj`=SHt9a$QtvZ655{jkhNy%)@qNbD=r!GjTx^bw1peYEfgc_;@!GDe>R%? zS)3IuIZ2{cSxH4c8_i!?tSfh;S)?RXi;ZS#o6t7*RjH#sV5zk@lLT=$U^8rgK-okBW>9(@0Q0d#udP=fvmls5c-p^pf*=)j6>=XPFN?j?f zSUSH!?fiE0MV1tZyz6Z@Um;QZP_}+MO#Bc2b{3DX7A0CFphq1P5rJMM>J<{Xdgm0# zF7r*5Pv}=TcbS8f>?|hNu+3F5Y3*e#GK7?Qklkif2PwJYT@fX|fb2I1DWN?kx93N5 zq>`P*7Je2X_9Es-Gx0^4Dim|bY^j9K=VORDWTu716heM72ZqTtNP#&|$LH%6q;+3Dd+7EBgOM5>#Vs$Nrq}k z+9?rTWlV;60_$CLf&6MVP!hVj&zb2;LTAD`Gx=qiYKJ;EUPG$$=IvLcgpTR&rs(#l z*uunx=-|NLXl2$O7V&mD7%lF%NzZ~m%e zr)W_kLY%{fJTU*IRJM|bsU;C{#Z%SMv2DTk!gxKgqd3l-LZZ z6_+wVN{(2sMT&!vI&njldpHRq7YIqZVSs!$Zbf&xK0Bpre|+l zUlZ{Ql5sm&-uXI0oWY&wxw!pGLT$_QaSxQBZNV1$k?Q$4+aQ?_$&VtZGn$`P~i4b&K<>JS${L`s%8aUAP0 z=E6PQys-!y?4bknVABvOLA|dfWt-6Ug~b#Pp0S43j&MUU5Y%{1T zw}qu7MwWU&-i_-&l2S<-1{oSx_}>4Kb>{(EP5&Rj?=01%#ZZz8d7gXk^USk9&wXgL zsSHJ>p*>M)r6`1u%xIcIsc9LbO;V|(O)@PgrfE=$79lOPO#8^F(ZcWjIp=fc-ml;M z`I^r;=iYPA`P}_JQg)!`agdi?nPY6Bm{(k#vZQ1kz*7d0EY~2GDJ-wKrm%dC-k6V= z99OABHuT4{8Ll4LvYwqNwE{5{T&b@~>E93UNkQIlEoONSF?%5sU5&=dm~I^Nma7ZP zG<*k!uHa2^<**EHh$o9sYO1T8<>E2q7RYqh^c<=uPwS31wdq;w0@p?sy01qv)Abw6 z3%F8|FcDuMbS00IrSfr&nqua<;zF{uEFZo#&eqZ58tTc^p4)cAaMV@;NDAx|Y91rE;}tING6@uUv&J1sqf48a-LY zY(!3=46wzOnkU=J!LeU;Y@K44E+kjmix}(|wXLpsEK4A%h$(SRpGu|dFIaxku(tyQVwD1^LERF2w#C#NJcyaxHv^1i!9+Ponk&Vs^Q{ky80>!@aJ( zLhQV{*LA=lblkGnb=)Cz-14LA4j=wu2%X0rcAb%uklrO(dj)Iyo9hZoI|!}&Z?2qK zV%-zoS%tSmA-}uku&jmT30cc>d3CZj8*;?8gQf1rxU(;$lx5E!N#Y3hs4IE4thp4j z3Nc4r16V$9hPwlhW3Fv1OCehz$6d|m$Wj+pCTTl_bZ1e=y``>ULW&a9c~hyYV6NPP zpYj$w;hM+t9dE%Ct|cskaNI)Inohb_v5bPyY&z*$FC}3B)_pHpKjqrWay{1lh>+bZ zU2aO!&I>urq9I0`M5Ru$Jdb)vPP?kTE7z+rVy=apap^4mP|q!pv#!=G_n_45Hu&y= z>min#Rwip7K+0VGS;}$Lv>Nh2 z68?0pV0k7{o`aOTHnO}4p)FYM`iaHEF@L#^v9#uxzg$W4dbi>}Tr2b&~o9Z{;n)tBWy z98uHxa)oOMOH0&Dzl~Di8pV>yW3|FHj%6T^)e6@%mY-(fs}wjg|JyZ-uA4y5Tc?#82&z-eg%KnAedq8e-cUd8&$G5nW z0cq$Suu4ijJo)f9i)__1J^%>6@f{`vaceM5!+BGM1yeWFK~S_uedH z+Td<7#q@MfU|EPGe3IVo3YMO@FGD@w#~t}nmbwI4iI{%wO)NSu;XwDVEZunthqy1X z-1v%I=ArHzzmoO*^)QZEP|s8D0LvBhA+en)+`)4?RMa&!S0YdDNImunYF;7*) zxhY~Mxr@G$rD(f1h2*&-+ojaPGn^eE`R>y!Rkz^%Z%Bc=)ps&x1D-3OYXvjiMJ#XR zNSW-fQEINc70W_=1>!NtJh%2e#mI5^BxJt3?++w0le`XD>YnY8JjgP4 zxkKhaR=6MCB}%1SU4ie);yTG{_aK(x_u_pBAy2W4#BnC&!`1HRS-Rm~09{>K?H(f} zTl=TKe42N)dkx1NhtRd|?f%-9vZV%e(^heC&SJAs=7~*SIH3N!jzOY-_E% zK!|;uxz_z9i@L`6se7jod99oB*r)D&Lh?2Jxg>2JYW~!H&LQ7GHoBAd(DKNbpCFsv zk2|CkQtZxh$iI;7?s6gaXxZ&<^()nrr)|Sk+1|J=y4&4Hh<%NEuY0f%xxMHP)L!>X z4xyv>AKhaeLf2sTx$_-TZwjvWx|cb`13BQXd4O7%-z6sb&Ha#&T=6UxZLeeQ0W9?O zd&=R*+|M}VJrR@Tkj0SW?&%IGf|R;<3(3~DwnFwpTPNJwL27-P_QLatS`DoGDR*xn zd0M9z618N=X?K@H6odD~ajkMP>N(>c_8W=aKWE${h1mUb#{G&zsDIA7a~(qcbIv{4 zA=E!*?s*Pr4f)Ic;_tHc?bw^s$o$)#%aXNGj^4lB(;dc!#C3hIUs0q31-X;W3buPd&XhQZ2k^9`&Apf{W9iinZN;rjI)Xjq= z=_N;{T!74iRMAfh$<@BO1>fy~RM$tHl54T}WU{srQbV84^1)yDB0bJrYU*FHjJP1B zmVQu5%I1HP#1;G6dbQKCp68AyiBh%oILm#D617iJ^R;>oOD1AIhotH~&d5?zaUGxt za-BZ!td#X=eH-L@z5Y2VfjW_pyR8t$7@JuUQRLh`j=&L?W? zadoAop6QVJh`CGea7DCUd1tM)-bF~BHVn^re2AFVdZ8ocV@Ml)t3xOw-L3DDqW%01 z+Cof*{wvFq=x3Tc8Tv^U^;BnuUcr)sOuI>xnsU`%9;*2seXc{kM$EnXN-3fbcS73g zk$>z`)X(kobcfLRX{Wb#2%W9pr?(ekpRKpoyE4xxHF>Vq6Y^*o@D_)k5V z`b$Fcwc%K=^SC0azuF`kLrCKQif7}^x;xc7Qct9*2tOt^ie|W{a`{b zjKk4`zS2?i%NclbSl{T7gsHeHs~-`Prww`r?=C?G>-(zG5=yB98KT!rAvuLY~YX{AS!wZ@1Urf0GALT~sWPwDSfm-W1jU%Q}~5&8<2VIz~YIOG}qb0PNp z8mSjMgtpg6eTPG6dp)QBB*fnCFX+DuvG>a_>PH==E@PQr)XzDjC0c(;*RB!ULR*O^ z7Vbq`qxHo#NV2u;e?Ow;D$&i(|*czwqCT|_iQUhW^_dQ-a~Gb#(chP%-HMSwqh*S| z(jhcj^7L{?DLRUqp(oX~Yo3pod|j84a%%+N1;x{k1$vO>!u`l>2!BVvgXMQ*e!7-2 zQ*R~29%Hlg(?aZZpQU%c!ETFY_bmNkhtO!5r4Mijjh5N^Q$p;~GFKnvh@pDs=~)h; zdgke`JA~?aPoMUmdfwOP39*-OpPDawiNxdNblj0k0BrE z3xwop8?7XbWU*eqzN}{!zRa*5vP2I!q!{v{KH4EWAs^}cgyd=Wq8^eJdSL@uPu|ESm^icZiW=;{T*UJw&>40 zBn;WAk7rTq{;fWpg?>edeh+AyzS|*^hWkY__%UD_<<_*ZtdZ4*%eI#P0L-y+< zgyd-l7ba_SAqVs+EM+Y)FCd5XU0mup)blaquztiL8zH~xJzL0{51{5Pkl*#e4*3pp zM4!OY3U}>)gdEj>WYKz}9mp~Lh(k_5j_cJ=aXf*^S=7 zA$1^UbT7+#tk-cvnJ!4dysOy zi$gj<{?hvkv9tO`eW*hyk6qM9I)w6Ag+4}zoewYR6NK1#?24Y=Q7+H)^GR9{v~@)v z!m`<$r1gRPqc3JT%c2>jEEj&2F^R^b4^TaL@(M@7lu43}p)8p!Rg6(W^0l5g+Zu$L zs~Qs=G8|IP2y~))^0m1?|rcNXiV(Rth_klTz*7CQUe2yq#+S=`7ZUqal*X%;%O+YZr<@R4nmQr~(8Zv(77H)LCuVxPe8T`9=(#MV+xm zjp;0O7D;E&F=LyMY;9vB+^5D8#*NG#vSxLbeTPxNvfoV9=n6x+u|P<+7DB0Vl)BR> z?@6VywV9C1kfuhvUL<*15{|m7%t+9h8!xgn#6&Gz-69fS9&MfaSLN$Rv<<#uS!u zcsBKJ$o-UUV}y3$f={4`Z7UJ4f|09_UL;n6JHxD+5oW z^ZJ2C}WDn#;W8aff5?Uu~zd=SD zr4Bg@8Dq?SO2*{jc)kLXWqc$gPiycSj&k#H=3;DMx%VRO(m-;IB9@w;$xNGT9FbDl zKjV$Mqp9XR?eI=KX@FAWjoCu%YbIKsYLq!72AN?r8pG?3nC6fIV-L${^x*@L zImS_k^oGnc{&L7r$O7ZqS7kj^>Uqc_Bh4YNL6#VChr9(@W;7fpOSxaiwL!>A!|Raw zkX6PAhb)1tHlCwXER3j8d=2(vV}g)8?fgaj{sm&z7-u=A1x>d1`PyBOO_24*UWa@O*=QVb$R5b&#(A!XN*#o3 zGPKFE_4^?wAzvETJLD2%tD&=~W!`4o;SgHp?M6F?R2zU}Y@@>rdzn+-#&g|9Zal&~#5k$Woe89M# zWr16Mljoq(*dfX+Nzq1?$m^?zjphz%gV}P}nCFlktu^g8;|R-HtQTFuJ7S!d zl0ZV=5A5M~Ol^V@iEcDnJEOV*RS4f_AKbHAEQEIxB z%A9c0DEohs`VO^a_t;6JzC&maa?-fXA+!fMW%z~Idyq56?T#2avN~t9a0u0N&bZeh zR8N_a`JZ~u8@+|td!lk<Rc=g>qD{jQelH=PJETA4FXNDqJne1l zQ%Eitw|^??shqhNjSL}qn%c`$7-Kj_ogw~hEasRoXp8E(WR!Evdux)kCm@%Nc%iJB z~-8O7M^Ue%oXg)H^V+9a(2F;&fZ z4tWn!%`Dg?W7Ix3#at@H-lo;f)efO;QQh3&5ZV^km|KL{+q9;+!x2NZxO1aR}8@$E>oM)bcqMS=8Dcf?RVE_0|us2-O& z(jioj+Z^+sdJJ=dBW5wSsmEL(MKcoR-2tz8NJ`~Qb~Va}u>55EC%FIAlL0WWMH* z6OgDm*CAISx0|1`%t0U4DZqJ(xk(6q>0>V5^?)=pn-|Mpv2NK5m6hctn-F^4&X zO0_k|NlDrAx4f%(zd2EgSg-pKbHBOFA(@c&=I;*a1$n^y$036t51Kb^m1E-#-hy4s zh!pME3AqKkm~B|Pol(+-OKo{GN$Y3fi5GJa$MnK)NemV;#Uan2t*+*;EcCA8%aHEo zQ6a|?#^Y_%Y$3@dv^@4U?O|TeqPA&Iv$2qT?Ur}(Bo9jUGHV-*u zKIBnz#Me}_okjbZxk6+np?TWRd`pO(A^Vy64xyRb&wSS*G;<#_mpJO7o6iHxj~y{9 z(fT0sONY?@eXzOHA#~dlv-=721|hjAYA!!vHj`30m!B|u z2(jn#6J`m=sJZ-vd5}fTWwb6OWs*-mEBl0*vQ4g+n%z&B1uSZI4>cFE+>X0BpJOeC zn(G|01v1Rs$l17%B;Cv)=WZAaSu0dVA+Uur&&3|Y%C;O8-ZTk0eRZ2xkHws zS$P=poLS!?$A#z)DTjMef9J{@#K6Xl6Q#Is(Ztd$at8nOg^K zjWb`7l2VAwmW5Ke=37GSIhbqaJERdx<(dnnXy0MJyCLJvPo<>f@(3Sq774LO_;_=h zqnK87h-UXguo|B?ky!V`7Cj2COUhO?6n02IR#=mm!Il;`}7`6ADU_L0sZheB; zQ^ts1jiB`j<_iv?(fhhN(IK>Ndc)Ly7Oji7GMk{(n`Vj-nKN4m@d&ZE@ZX3$?wbn)O-eEw+0xZl;>Oq@E;!dpPD9$BarFl4g2L< zkkIp5Gt3YRJ-^Wx_2ip(uux{BEtqe1a>!c9+vX4JIb<+L~$pAS6$#c0vC7^DOfhA-H1yN3!-LVrHAAvXtnJk&rp& zt4G9I2%$aNJaeK$x?YdFu4ctCJBE7feN#VfOI^qUbA&_a{B)7I*dd+jYuXZ1E2UC- zTAfdj`Ox}D<^q;4)=ODsW}c8SNr)Mbm`}{!4tWc*)+}OKfl?H+&fMt`idk>obJAYI zr`qBTeY1;0-bTz9W`BpAYlkZy=17OUie*)^|%Y&X9;EkyesSCZ-Y zXQ#PO2%e8Rho>L$9qFBB`WYER-<%2c!5u@hKuES0?T-7)sArei_^gZ>mmyC2kZf&df}G30m}`U_OQ3hncA(TRW~oDVL-w03|Dd+=G>Z8J@~b(5 zWhlO9MRM5OC?s2(hHr)(g&Z{({3+Lqgx2f0*}PneIX6i=irTQEaa>=0+j8DU&~y zJ$A+{XL$rir__gM%5^RLDd^{VXzQ|Bmn9YN98t_wGfPM|4mmX;|Crv(R0>ZcG{d{eDD|(I z!?L_ceiuE_GetQ~8HYe)^x=zpY)xsPlxH9_AZec6)uglUi~2d{+0R1DLs`P|w688>)?$w) zWf;rd7(Y_Bvot~mkg}hp6MA(C+Oj-Duc3PKwYDg=5)$`}5|XX8!fc{zjCXj}2+0#d zz0t(esHSY4VrU8P^i&AR*1mWcZ+oGhrk(+{Ch3=bo1nU?c3Z$3ExRL6~ z*JdDw_U{jShB)L}#Ps&87m};h$(3vRh-aseZ0$D8IQpH?M?CTRR8OwvMhs=PM?JGy z=$&Hf!#CSAUub+EGTYP6BTEgcg=gX*b38j8vJx`Sqj_b_b=z@o0X|1bPjlmQ+m|3;drq@7gCxzyQ*fSm zluBWb2B`tr;i+IrgWLew<;jf6nEjaVw?OuKdRtOri19#v_H1OCizB@VY&mbfgn2giP|wbHcTJ*`+au^jRAVCjLHsjZ`))Z1mL=8#s96P_+CJFz^pUZ*_E zSdtOb7BOc%$xURbYav}B<(^$qD(6>)r%Xt`_|juv#8i0nbSjmvJ^mzqXAN@2Gt40) zA)0p+OA^*}45W%zyHl1Tp;9T{b}Z8&RC7&ljze-GsorfY`_a~P$c^6mO=UeKb07`9 zBUmy~&j*mk-Zc)PQf{x-OqQarQ?7t`ym5yVLVVs44*3ca_O4`^^+A$GF}HimS^nU4 zZ|c>X%bH1O-CKHlvpkHpXx;DjPH+gV`@P;mRSL)0J0SOa%USB8)Gv@u-bf2sGyUe~ zQAlU+FqS)T{PPE-r+2nPE%ogiQdI5m+=d~qajngJ6UMPje|_{ zCbyC`lT3o-dz(9i#>PzV2#3&ipW|K3^7e~KT0Tn6_wIAZ9LNH1&DOH!@$-|kg^&-t zLs*(%U$7jq#9PEt6Z`iUpT_gg-YYC^FUocQ$Qx-R>mi{vUEv+T67M5(!fNmHEU%$v z$`T)YC$Mx!hNN|0<1J*N{V6T;r`}SB(3-CE>UYbwUZB~8n2p{p4*3-Fg*S(#2ae1) zLB8^?WLb&cAlc&G$MQaYM}xN4R&Pp%teJ#H<~QEvEDz$l)>~0(hj)}iet_)qmN?`P z2Sy(mlk<{jb?$`Z%CMJ$2FiP|}oI^#{fSC&e+FHyS+DfebLq}p8MG4E&= zKWZlV$GeH;74$|ONOD@dt*nR6FKE50rww5F1xH^rqH3m1cL`ax?0wlUCsn z59G$QNIO|`18h?&b#vMfhggWYEp4$wT0qRSBMxZ?@u%tc$$AE0By@$u(gv_p$GX!J zHc6Yna+a65X>r}b!W$Ivq0la|s!$_%vC2hu)mwnH9=bV}>c zQO0?(wQsdL&U1bc(yO1~1a#%`{ z$Cg4SrxiNn6Uel*3YLBtKVLy+rnTxO>mk_&nVUA9r2~#Y_CnrI+wBl4wJ5DocUfvW zVyLa9X+0c5Z7olm$MQXDJ^=Y7?X*LVL)N8b^pN#DihBNle4aMJAyjH}+HRI^6oV1I zB`u|=EJZ>ywq4oMcZ5hi@l%n<8o3_s(v|c}@rSy_D=OaV@4f!=K zgN4qCC?_0D8{!bk3BRXJXX%cSkoYdHS)~;^q$cES+98M3hm@x!KP=m#Qf^2^T7;z- z+muGaziB-jLL(v3m%~yQrD(5L&9}@UwAZ`Fw~ys}WIsB~zSdX4atlgD(bjdo`n_fA zbghz(SL^t4Smf!qyg?8|*rmZJT8-+_3N z**DK29T8*tidkrHPQSb4^X+y>cft9!WTE{Rm1^!gp@MTf;(oB02}T*H_w4)=V-1 zrS9`(^q10>w^t`$V1O+%5c7~PFi=VV4DcOc zNq`hVhWOeImNAVuW|(g!OJ|N5;nRl57}}$4N2!s%I16Pq8VRF(`yQ7uBs3CU^35A6 zg|gO(|pOpWhpf;@_qAI)Vz4d*LZ}CA)$FO%eRe1&5L=y zaZk$_H7^$U7CMA_W07wUiyHGweZR7(5x&%Sj75#-rM|k)$ePu7Ug=9`QKNUYZy1Xj zy=#249YUjbt#2ob8old%S6I|o-Qa8dtZYk-%#FV3BWSkySI=3m32#$k%TLyjz^#$gS=HcpBfhqe6$4xw?F>d(xTF=`y%;17(K zqQ>D({)Zew`7L}h3|F0}+ zJRAO$333V5c=r1PEGj<-{h2Ikgoph(ENX;D{c9XTBi!#9#My*_O)B>HbzM zYRup1AHbsWb5s8W7Bvf+`J2Bf>rwf+rQbVAikbx({=RS7LbIT)zi_e?H48fW*G!S3 zWcjE=jdOWPk1g=O&!T3*V*iIMYRoV8uVGOme6jx!iyGm}{mBbtTWUP7 z^ry3^@x0nU#33}E*Z60%sPSCrFJV!mcb)$whFA17qwa^y)7E8kThQb7ROJ|H$PshR$K>cw?V`zzQj9JpbxH z&7#KhVSnaI8KcJYQGX$e%Hb#cr&-i^KI7lGMwU`J{G5Mhp%gWq|MZVuXA6zz3;y2g zrKs_I#UI!pMUCfw{ren3<2fO)c%zI_>@=a3I=)EaWxY-sOy^R8kzm%dz?`?r^zLKIwuMxQ75E{LnK-VG} zqw>5zFx(-O=YxS6ENX;D19MnZo{t6=vZyg14J>C-c|ICg%c5pMG;oAP&4MO@lr3_3 zRGz;x5MWXBqFG=7i<%cL0@EEr^Wv^R35%K+cL&N@RG!ZWG%l8HshM(5AcIBa`FjIH zSk&BU8z?A|rPSQHKal#Zln%IiLdWWvfmYk4sM*vxQ1YEEv`xDOM(vcMW>fD#(f3l+ zZ0a8v`GXWSn+66t?2@8p(~v;#-BQ$SdNOc?Ma`zC1MT+67&V)o3#?>Ov+2da5f(L@ zUJ1>D9o3gHqIN8XFjL$QGJSxq$(PrKs67F_8J26q-$G$O)4Jb$_=d2ALX2 zJ|acUrndvFSk&I3ATXMR&cx|B^PRvs7B%DE4P19r)}zMryg<|;G@j=N`mw0dyD0ED ziyEto0wY<}I9wDcU{T{|aiEArjg}>WQWmw$O9T2bxjbt8d>GixqWXDx;GyIHyUZ&C zSq`CPULDxMqLz7W;71m<%xeROSkyAF4J4P!n$k+37s?zD`d z>l?K0djg-ZsBQXFpv)l@^Gl%f8Cgp8&%r=%7S%ro14CF;{~QcVWKsQdI8ex<`scU6 zeipUvzX$4{m2Fvf;~ics^O3+vmK7}$wTB=_1KWhyPtP6;>=t4_-FGZ-L5Tcn*~2Jx zEKu_t)m&UXx1M}5q%?37%jMScNz2kejO8?1ABa+=fu<~fb(c>ylm^0t4i0LB<*SRbEVW$zs`0xkS$9k?8p66 zia8s2Lx?>#&IP6l$=6=on5d0H%(=k3EC<@)H%%aAfvqg>a?Brr-7MJ{2^4caaEPTD z-g?f3{23_tLypz*hM^%AB09mrtWkKcD#gqW*=XIb2cSq=F&kmZn#ki?+& zr>w_=t9)NWss@L#6nucO0Z9qwu|qLCt}JV^@IB_%9tj+?zaR-RY=L&o~&K-9tX;dlzF@wD?OKFB>rXEWR9=JkcKb;;79utzUy=f$C%}^>B{9B0Jrmcja2>yy| zjmLZ0^sG@R$Wn#wzO_Y+E&JZVmqOpc{oKl!l$ps|CSoF$GW$&_(O}9|TAt#Bhw>A( zo`{JBT~eyA{ZqCT3r09*52Q~EipgZ5twwu-8B#uyulm?kV+sw6B) z)}H=<@(Se6V2-G#C}9b{4L%9dJopjUoN6SAXN{T%*RWhS0`K)BrbRIR53R*lNz+#) zYI7iW1@{RlP9XUJ(mI&+uUu0anQI^!!A&e<>m+HPLGB6ebjTLSy{gA*eU7)Ve}LQ{ zoTTB=F67mj$j_8B?+?ykY0?X4BqC-G%S*WDDPdQrkA^z%tb$KahTcMV7uYwo8W0iP#edJwoINr!3kt7#AY*>h-9(S8$GyVoi{YE|S~ zsPX)0u(^;TO)Ybu-~u6en(r{au!5TV1xr~rNAT`IGTy)nj;u=c|o!A@)|D5zMGT^^C=ns7cxk zwDoo{SX0zo{mfJ(+5FvRQZNa&SDF{vwBDdFK$jsmZhpd6j4Hh}17_uOE#38#O zOM|^@iPptC5v7pT!D1oy7F-{^Dx_FbTX22wn%Y#VNK?6PL$I-s;uN*LHUzy=G_}1p z1V0vHZ?BEP&m2PA>+@g<*P}+l=HN~txhZe%lfM+aIk-=XHVwyh^o8Ng!807w3Eu{% zz0Bs|U#b+o4KC#>%boZ(xRjJ@ML%mQM{QO)vkIMm(a0=PQftq1@;zT$)S6aG6}rAb zThNvpasP|5=oVE^6?%4;Vr-$Orb&t`W9Ur*lC70OPc=|IC6z*FCI2fYK2H9R99pB~ z6}hI>)msuJ!8cgCUO)P3e?GGMhQ6uWtV9o302v=M1KyUzy+Jc9JRqMzYHR_H8qbzFF9Ssg( zQKRl?a3YJ^?ni_BS=4qv9(<;*tXYlB6T#^$YGj@W?ypBN+1jo(au%Ejma){!#Jd&f z=aa$d^(m$(;S%oN()$-@f+Z~5@jVcFLi=3sk`mnACMgRxxJj1!=D2){`;TB#7WF>J z`CvPiCy-aE=0AfySX`KcRL@_*q6V^_Z!vQz=0dPci2b%sMKHA?#mJ+SmRRP$gDs_0 zp8Z`4_TZQmc;=sG?xo;YQmU_m(2TnjEa8|U2<50t!8;pK&GyK=6l^U-?uG9`TbF`Q z2`R382kL6@WfnENuLdWusM&orI75o2=HS)fd=@o&uLf6eDK&bp2G_Ev(R($xNl3o7 zxnHu@3CnXe*!*VE8{$b+%47cqN3*EWtA%E=sL_i}&7wwcLTDe08oi03BP?q4RtXi{ zBHL1!QPyeyOnsghjmmG&r zKbSuYnDZo(uw8iO6krrt5W(%N!S9R zeN%(bXqJ&!_X&`Op^t@(OPR!_8iqEod|D}AvAkMY3cpc?J}63P+ayt=Z!9zn9THNU z&>cek(=b%mO)Yz-dTt4M zq-c3~i%m*Y$XM;|8nWka2{mV#gqQ-$J}s?3LGXbOwkcblObJ+fxC?>5!wtwKWY z9N9v9b2_8fD!(SOg`%^lDwuSN!NuV-@@&=8%V5l)K&$UA| zam^}N88Z%F+@hFJrBK%T2(5=g-MG}MA+qLhXaGx>%SobCB=n>Z`zmN8^pX(y8xSLlkLW(psyE8(KeX@1chZ&)CAu=b>THF)r;Sj31U1*??B2CTi4xtyh9yJF$ zgbG;H_Uag_8J2CSo_`=TIwnQ+^8=yexD?gTokAUMm!h^{r_i=0Bt;1aUXo{9nV~%_ zje5wV?gv9hSW1t`n1@1tvfPEY^JtIOIh2?#>q+=0Nn4Bc>Jn-!q*znqr$;D@V|G>G zT~EaH2%Tm*3!$vlD^&kZS>d!B%%W!Zz)%5;+RB4ME2Sj7z71ay!SW0a z4QMH9PUwIm{5?W$zKg^j2}45N971a`G}K>+Jrah8MhPiSQTcgzs8onO4u^-*TgkOh zV`F$|O@;|PbdaSUL9U}?iP50|%WJrYNbkCi4vk_-#ptD&S3)a=WNR;Q%qyYah2&{H zE+%S=+T$xLp)!tHn2@Ml#PYlv(mTqw)UjVys6t4d7JE5KyNZ~sQ2YUkky3R5-u?)U z6f#z$W?Lk4~?KQXi3}=onxZV*Uv|EX4lp*MCF9 zh2(1VYzB>=e?vJOqmByx4JAKH^~ft-zlfMRLW&dA%+kU=xh|a4hk9e0_Gm{bb;1FMoWt_m5bnm(0;MiN z8ib!`8H`dSH-{HEBoSk}ariryTaV!zNiGAFuczpUPvgM(@&OKjC+57HO2j%aF@rV{0#{sCK4XS(iSySOf-;~e!miOPq>uj;6c1|1Zf+N z45YU5HIm7Y_TdFW^0fF0O}tswF{GO%UG7Q#8)m+Dl@!bkX&Yxm5|=y z#)ECy3>g@n!t&D@?1dpC!n=pa7?NX<7sG)kq;!5GS-S|y4i9w5zmV6$uQ;UIBD~)n z{?H-ULUO|2IpjvjxbPK++zQDJ*BvU`s(~vo^b4Bf!xb#cu)hdHCWLzrlQ9jky_!MZ z2p6*SLFT_3@@9C=lQM>+J7jV=>nU3XL#BrN4wrHVM$`+C8R3i(woHJ$6K?*rlwypi zcOY}ZvmLS!@@}}?A*&$og*!YWOC7?sChD>G!;PMml9_|MZ-`kCp2Bh%XGvQjAB5v0 zWz1yU#oG<}Fuamw6=IG-R)$ABCu2y;As>ggvka)6B-;8kobo)ygQXwrMUx&+HCBavSF%COJc7*F@+0qZPH{9GI&p-}_`#R(` z$no$5maUk%Ga!G2mpNnsq$0fAA!{KCkw)3FEh@DIQY|usWeq;f^dscj$YPdHaYd14 z+;x$1RSM_A6jLYSeNEO=z%e&OhOp4rs}3OM#>hm6oQB*IS>}*S5IwTfA=fO%S!AS) zWfzvP0mO>b9V^=+NrN4rxk3updvvXxVDjPo-@s5{5*?1sg9*T5eQQ5e2WEqRf#$6)c3X!?@ zNtEgy`GI44Zo@Y^gdAd_Z2YW{c@tz?la}K+6w)hF#zNV60_2fM-Pb8bX5(p)zLDla z@-)iEb0GaALs%#qFNO??%;Qoj8xM|bVo}-n@kq@#WX&Y2QR>M^ju1N=505Nnp-j60 zF;7Q&zbThl<=$5!`y4{KH#gF5qKr|wH!qSi$(CKHXJ%y2TT&?Z9)Zk{oN>r$$orAh z$#zURWI<#UO9N!lE0BeeE>mO-Wzpm%cwZ(`$U<2(6|y)|H&4b;&a4kv63Liqix;vi zGGUq&%9-hq6_K*(w%iB#I5I3>3gygRkWVA;I%E)JU1UECWyohB8zO~o%Tkmf$3Q-d z)Ge@O0%TKUI?LwOnl=Mc6xq*0IrCjeab(0hvJ}Zjkgp?uvQUPk9{Vm*eTG2pXKEEB<(lIp~$HDa(O62QuaF*ne7nDerF>^EGqj|L`oe(+3(*->ie=Dim5=& zDbd~zskRiqd=Op6qOxDT=xG+pel)`CN7EN@J)Hd-M2E3Z_M@0a(Pb=@{V22D65Z*L zo6wdSEn`ucEf}r4P}VFm8)D+obcduvnnrUR(iYMxx>Jaq+3tzXT}1Uv)25+UyCLSD z=qD_{c1_eCg|v-sW*Li^L6G~RJ6SvsdfvEwH1L6}$6BC?->-ZiI{QN@ui!(A^oy3A zqT5zUsfn3FUn9wk-tn=NQGIbWV>6FbqL#3GbQOzQ!k*Dh zED5}X4@dPivaL(_y+m4`N2BdNm$HeMuurt^w*OtizR@w;?Ioo5Vf#l{2(g!NQ1lST zTz?Q*4gE7HT6d=`b@`%{VbSF8rKm3$JQHos^2|E_Sr93evNqZTskBjbRX@hki2N@q-xrbu%wPyHjxVIp0M7Imct2~#P z60KkR|qPDMXr zQD?sAqWgv9Ypv1GG+NF_^@DOv2V&Hblt+iL(ABx;u)QusKVW$bTktN_d@;I)g|638 zOhxn*%MTp$ceH|qo)5nVr7lHB9hPm8&=|WKoqbFSU74iTwb&*Wx(7s~E-_Zh(x1m+ zQmp22xu);mit&Ydl4JEtrO>q|8kyB%bx+8e`~EK1t9q>Hv@NteHDZTYwqThbfYgr7 zK7$yHXL`GVYQ8SEi3Bq@m0PbH+sQ(29MBTp5IZZx&PerQH=mWI)O@cOYbwMZ35{X{ zSk!#KCAO4Hsri0etdvE~cXzCuOVNC%_o+Rx?&oA%YQCq%`m(6`o))`OCS%lm563e8 zfbe{$J%|+>LV`7=`A%ozcf_7|NIss4Y!b_1q1jFET;Cb{f`w)`y%FCmwvB~mH`UW3 zmRv4trrAwjFlZTT&O)=B_B(gS2C&fVruPEwiA`Wp?;PD1%VVM0O<(f4FE*QnW;g8} z?vMS*LNm7qwrR&$nL}uQnikXV^R+M>svi1n_JOGsH_ zG-95N?Gu9Q&%8WO$6okbwyv&pJr{eMMa}n7u_6|_u0{2{94i+hGXRaBS7XgD$+jp1 zkYvYlSSSO$j%_+N_5}-N0Qz1*PHY!A!lF}bl+7Mkz5XnlNa*i{)r zLZj}DSi5Sq#HgbTK&?-Tbz`9nK;vg}YzPZw0Fo)Oi9&Ergfl5Bl^5HcLiOZnlmTe0 zPK#w$mo-xcpqLr4*HdkwvGI0n0SjdSIwG1GOTSLWPzInSoD=I^R|;hSYJFa8G>dxY z=)KrXA$A6sA6p}&IDxK*Pexnw@pK;j6;*i$#~UOIVu?3U%|!__ab`z%VHU-%5n?~_ z@&T7BuA=JsAXZnF(o{W5Vkd>zPp5wvJ1+!pk>CmBS!nCS*d>;H$a_MP>QU>()#qo) zrxBLLYOs6+p{JCW#p<%$eNNtC`6$+ir6Yvy3VjqagxIYwkHv-LYaKqr)p)eEJl2b) zF`oTi0a+1yjD@mRA!KDN>p%6ZicJt=_v)(HbVtl)lv))l5mHpy8y{Ep7}fkOVm^u8 zb0e+kSnW`nyx+7o){*5Tq$>WmHr9=$Sy;t9%JL9I#tdf3s*D-VGPyElln}c&*2c1g z*lV#iHc^PZ%xh!2>eCY1r3z#Fg^bk(JuaW6DU2oGB-SD&y;5qkbb`>fD2&}Cq`3Mg zmRyT)u#NKyEwPiRrkA29}_e=Rz_HSaJIpkhE zul{Z9TZhp154XqmJA}TqxFc5cR@pj@-c;25LoC3ub99o{5V9whDI`yuA5PLNti?~U zkq)^V@=L6MrRR`D&5u&Q#?}e3bIZZlW*JlYB>cfxg%EpW9*lKqOv{t41xs+30reb? zO&5}PCQ2QTEo7lF)?LW*|I|~e=5npC zut>YDWXed0HX%IpZK_WBY{I{rpGlkc_G9=ReeX zRiT>cE9ig3PKy}X`c%|&K6bsE+A2;^G37BsN_Ev6f5n0ksmcgNj&Jmf=^FJi?NQx0#lpR7Gr{qn;H)-jp$w%X29qL$}!Y>A^D zTAs_XVvgySBHwWQC-xnSTAqJmds)=-{1ZDM1ixo@2;X)_J^#cs-5zycLNsfDkmBlB z)8%?qv4*i^_E9pDGxqO|~T!_3@K*vbeS&y+y z+?p(on(A6JSmE$ZF1_jx%quhOy8Q z5w(7+^%l!t+`7x!%%YAe4eK-ueN*>WwC=GQdu3ZMj5Ai)RMts4{A;Wud2rXbIz135%~pN)zh@3q7|(^)$6oeN=Ov zwhzzA&~avSYXHkUEHg<9>r)~27;9;5k};LJrKNSPpX!k@^aM^ztD{5cNail9uMqjf z-HpgxcUi-Q6sM?Xq+3~|S-M~gQl@QXy)LEld6HJvOcwRzOe^aHE~TEDZe=ZJQKPq& zwUJBF^DXp5Su5)_3yl_fHmtQ(H$ZDqEWYkeF|Dl(7TWG4ZLEiOrptz|5<-Km}o zYd4E}{yD?C!lIsJ&am{LY)d`KoMGJ*l0wgK&>PJemd7FVM#eqX?NY=O$Y(G%?zK8| zjCumOt@Wf3d+xNga)j8=hPAaOJA|GMyWg5E#C|sH0c)j@V)0ZtohdwEjf~1ARL^~N zvW8ev)N@~*tTim^nZQizCl=}-+7D)0DRC-gkEl%RB_Vd!%CxeD*jX#ndP7QO)_Ty& zlTw+r9<<(JQQQ4NYa!R8vetvvM?&nZ^^ok#VsKGrfJcF*^>HVLtNzQ2`|{=Ypxz-r#~e|vs_HGxI- z{6Om?7S;0ut^Gpmo*!uSZ$|alJwMQTLWte-1FdJJRQCKJ>m@0bJwM2LjYakRAnPrz zNA>(5D_@A+^MkE<4xyfZ+*-z^RL>8y>No%2o*!Nc#G1fj7wZC}P zy27IN7q3|zTgtZ7{$iXpibd@&##_r+)c)diYZHswU%Y8exJ%Zf_7{_^jVx+^G1)rA zqV^YgR)bcul-gfRw=5R5zj)gk$fEWaGp(sY?C~?(nk~fc`PtTKM-1%?W?R12)RtXp zj@3ely)T$!br53j3+7rqS=7E@t~E$X<-TC9^#Y697tFPCxRlx#%(dQRQ6qD%Rludx zzF@9Z!lL#C?^$V|9UbFUP2T!9uIA5PRz^v|bQm zXQYMJYYw4|wAh*~#Lh^|tOY{sjI_)e(pD~^+82Ce^}bJv+82CeEoM;}X}PtXMP;Pr z))gW4*jR3jy`So_GtzSF4Iy?$T5e5~QkjufSTm(mW~3F?d=|CcS6Iur9+i<+SZjsY z8EK`p$sv@HR$JS+l*&k-Sn2KMnyP(4q1A&$Wu$f1C?R&wueW9ku`|+otAs^mq|dB| z9jIn`J)CmOCaWEbI)eSu%3)C%=__j;i^@n_tP&QLk+xbDEGi?FSdBW$wp2#?)_Rgf zWu)({0v45#c3Rt5R7U#2I>e$f(rzpGfUH?%q`lSv7L}3qSrb@PM*7+Ml0{{tU#(wQ zR7N^v)#@Z`RvGDctEmus{2aA1gxEcQ)SB*yp^S9Y`oSS|bb8D>D#Xr6$ECqQEGi?Nva(oIMml9JW>FdGlyzTcS&zy{ zr>yP{p^S9e8Yo46I|3Q$j5U^HR7N^$%@Ja6owL>&A$CSOYklDm%1D1$+l1H|=`ZV$ z5IZCNWi99?mr!M-3syl7DJmmfu#T{(jC9ee-qY?MIy${*r3<5?^!BgNy3SyV=f$9J-*j1-SQ_?WC&Wu$n#k3%RU-5!5ZipWTbAL0IT{7sHg z8L3Hpp%8oPG>LB%VrQf#@vRP_jMOx~TZo;JTET`7^8C<4+E44^1Qx=8P!A_RyUdcdfm3JB5#0X5<~Gv}P0<$3wQhyQP8 zcG{jZyScehbFmghq$aBO28@&3D`DAsW5E+q@)f^OxNH407zsXyc z5vi5h2!$e28#Nt;B2rs*0}4f?_G%UiMWl}EEfk7KFRO{Y;93-sI;(G>P((^mXP{6- zdR5Ivp@@{K=A%$V>aLo7pk<0kJ=N7H6p`Lkb5STF^;S#tg<2Gm`l*#sC?dV3wnCwZ zG)PSoBH!{2QFBn}Enk}Yct75<++**kHH663c}IOGP=-gXchpS*;ZbX-`n?btwT7xc z36W82n0g+CqSi3=CWs%khNs7WvCOd3`MPX)kQ+&>b$FN7b2t9yXxM6@Tm2^nkz&`tq;@- zLS)qXKwU8idPq@gygF|P2t}>&>SYv)S`*aLX>xw}IckFH36VQ$g1T0Sj9L@aEm(`r zQ4`diAb!-EsD20HN3DtKArxBoiRy7|hoaU*^_&nHwI-?80>Y!#6tw_rQPlcS?fDLj zf}++(>NpgNTAAunA#&ztsyRYr)S9W@MWLwmvD$nnZ&^mIPt`OOidu8k^(Yjz=BXD^ zC~7TG@1juDTBz0-1}#(6TBIhUP}Ewgeu_d->kBm-g`(D%Y5@vGtyOBN;m|Tgtu<}6pBbE)I1c5NGH|1LgdV!QY(JI$0Z}uDYX#_MWkQVnJ9c8 z@^?+=)$J%0kuIorQ79r^RBMlimMJ1#Qo|?|k*=t{Q79r^Ri~m*M7p7#MxlswOD#45 zTBe9}TlG*VBHdNnpio5mTg{sYwJ0JLs40^`C?efcN25?gQnas8C?dsa2T>>@J)p%- zhIS|-71nA9k>{wQ+C&sOM-|hq3z2)QxONxH_&+Tzu0216w+v-?)GDrx4G2F+mC!yG zBBNFbZJ7`mwMuFmQ7CGa)OLaRQLChui$YPWq;?u>QPe7_T|lATTT;7)wJ2(p)RLz1 zKJO8q)aCDyN@>X`6tzle!%-+|mDX0E&^fA%b{2)AR#~msG-#QkR#{C&p{P|>>x4p4 ztE`rXLQ$)%c0M3HYL(M&frzN}2#Z<|X=OfyYf;pCSgR#un$n&9Y@L6C^kHo@N_rmq zy&P8S5$zUAgOltxrVBNXbnL8c&KU?h#wDCEs8=rO4V9mI}{I9t*sCl4>hf8 zKzKYfwBA^Y;-RhOp-?=GXm?R49y(g$C-=|1tA&Ngc<5?9Q79fZ)V85eJZ!98K%scp zL`#~(Th3G{9yZlFqEI|+ru9Ujc$losLZNurLfaY;zJFS3IVcn%+h~K=`LeUelVQP|WPEy&I5btj}*~ zU!YJF?W1L(P!#R2-A17(`nLAuTz+-A>jr7Hgvk6gNE;C-!z1J%ZCgNigdD8x7a}9% zVC}dN86k&gzoSru9HQL;@gw9AEnyz-k9^V`qCEuSKWPrpDx%PS9-`I2S`;CNXdO`K zT$iS$qfmrQ(^jKUgnUOkg+dW>sFpY%`a=zM4<>dOq+^A5ptOJ+~?2^ zMaW@Vn}F~LIb3@cL`2B8Y)waK=~#v$8i;P#hbp#VnKa!_ReNwR%G2jvA|N5+dW+SnX@9 zMNgVzwY?yI92=+Y2l3!Kqou5bt5Y0Xr`1{oLUC+^)*>K0j&0IXP$-UV)jkdgk7HkHyHF^OeXAWsp*WVM z-9Vu@wpUAB&95#`gWqYJQRp=Iz1Cq3FC%y64_b;4xu!p8QvzjptouRR7Z4unal?bFJm&}nd=RvpBTb^EkBD75GIX<@8Iv2LH% z8--%ser*N{#k&1k77E3>1KKSViggFITI-;P6zdLYZBQuI9nyxQP^>$oEkvPMcSuWL z5A9H_JETnx2#>f~ydgveNztKAL=k99w4aU1y6 zWvu%}OB5nw-7i|fCg>r>x_m8uD+tB9e61x4#k%9#AQV17Jk}l877CF&>bUmwHn=){ z1N?EVfe?9SJFbO5{8)EFYY5`Ux)WM66k7KaT03lqV%-U?ix3&>PHH^^!eiZOZ6Ma7 zSa(*tfI_kEH?7!q7(2zf3tAl^a^^2;t%S%}cTpRTLb2|$b^wLWY}d5AC=}~%Xy#Y^ zTJp^Hhc*C(V%;q*9fe}uZEZCQ#kxCMZa{df`&&DQLXqvBR_<%K7DYBiZ-zpVEmrS@ zLXjb;ku50kMYiJlxPb7;R!U!kLXoYUz8i%i+avlN z6pC!+^`xEr>N2ub)aRj4WUHiC+{MetefXH3Bt)*@WBTAg86Mdl)7J-tN4CfH?}W(6 z_PCxWL`JsC`fn%{*(&R|LHx*8S${B#_eY-MD(eq}_>rx$UI~SEcxAm7)}qK(Sx-Ts z$o7Oj4uvAy6Z&=(ifmQ%>nIf2p44mYhW=1wOVZn*P-IKehoewrOVSsjP-IKepVjv30Me_ro$2*{bW~g~-*ZuFn@DBU^QSML>9DtEq1hA|qQJ z{h$yT+3M(d-$M^6venhE=YUXTtE-3ifly?tr*}i4$W~9EDn#z6dio>#c`bR0tEWFE zL`JrHdUX&#venn?fcTNEzOJLty4TkmVmlPs>g&yf$jH_}Zxaw6*;G9RYf)q~^js8* zY_@(Eg(6!>FMoiKLe6|x_k_sE7S`vWP#kmh>nIe*qI%Ln-j0l84fPf%6vrCrolq!_ zJ*Q7Zp*Yq=UmXx0$C~NiqEH-rLBEbdajd0o9D=J;9BZXFMWHy>M&FDwvyUQvcc7h~ zco=F?9BZ$K0>a~1M?D#Z;#enrTtIjn>#DCup*Z%MehY=7R(Cz=2*1`0#lqjw=&5%? zc?ExOqL)5Vh}>g+^ch0r>h#gS43yzftB-yuAUtaI)sLrE9sMSw@427ar zKfN}HAGP}F4ho&e`sppO7QLP8r?)|&-P=!3!CDlx`sp)JC~EcBx1&(h>aSl%p{O-L zPdds+A%CXumY#w_QR{7e912CPxAoO16t&*g^H3;iy{$Jq1}#(6dRuQ95FWJ#>RmuY z)OwSx@*sUAmZ7LMSf4FKuFhb6wGbJ#2J71b!lTwZdbSW5wMOXqLS)n$pQ<$b;Iaegg$ zM(fMoy^qt&qwv4A%4NK6qVNbXjMbW`H$vfm(SVnkq)$cRYr$oT?wx>^`6}~kP193R z_!AkI8TvRBo&9DImznwu6n-!Bc0ShQPeMCA*RtPYW@YB+9tvM&Ugk4>AeCYF7ngbZ zER_B1T3ieIx%%zL<8zm78DV7$_Q z4Exp>ee-E3!|!f>t+jfV5V*Vfox5JY7Z85uZqQ4g;kDq-<=-c~L9Z-CeyVe$UK@pe zFKMH0gZOvuM!h)--MJg}4p@us+>Lry6uNUa>Zw?Z?%a*~3>3O^H|bkY=+51wXQ9xY zyIH@5LVu}Yi?002$F)bHztpf@e-(w!B;V-$QRwd8p$|l%yL*Q|8-@N-!w!8R3jL*q zo%&rA`b!O2deT|AI{mGU-TLz&3G}x%cI(MlhW^&ZZoLxp?)H=)qqci63O1@XT{f480;5FVrU=*K`rjGE2% z;a>d~mZ2Dxtrt7T$1Zb9wmw~mJhx=)9}AJ^mTdiV5dW0&o&E(1?UwKKRalEoDc|Xv zgve9M_xiU1;ir@wJqO#Nzh-qnKLApgX6%4|1Z7HL_>JZR`Y{xm!vp%yR14?ufPNf> z=J0@i3dH}5P6za6=lN{Nh<#X36(S?{VZC2Kc*H)c4;LaM_D}j$A@ZBaf6^}qk>5=I zlV0&R*meAF;=iHxlRoew2u1au^>M$0P*nd}Pr3|3G5i;OxDdI5zvyvScp15ZzvxAT z$QArWFAL(YV7^`fg;p?Me;jMk3g+w8g~$~=uGbF;U%``l7~7#0JfpiH{tBMapGBb+ zJflB{LMwPiZ$`Cn1<&X$P-q3u=q*9~6+EMF6(U#goSrL0uHZTSWI*@|UeGTKkt=vv zSFVbA_P<;Hvfe?6T*1rwL@49`j^Snf>~&ZjTEQ#&-5VgZf>-nzH$i9xuj&_t$SizS zFMNxaVc$}SS@?z?MWHNwOCN;7V=vDjcl3!Ul!gD&H=|G%{zuP7p)6dWC;kc7qAYw* z&qkpvtQeP3C=16KrEWtl%EAvA6;UV)7dAGdtgi-HxTq0-2Wn9kE^c%{;W3<#qNFh# zg|cvIV=fA1;j+e36w1O68NZ@X7EUzo3gO?n5U=!PEkA1XyvwgH^KyA3Y8(WTviaoEIW9bv5HA3T5hQhVn1Jx_swZ z%_xmRnYxNI7pqfz?h3?#HMlGyGnYxL^^9&Pl&R|(y-_GrH!yafP^MOm%P5qoHKX)@ z;_7~;){Jr>3T0}|NJ61Ztr^KEl&Lji0EnNdHDe$OnOZYG3<%HEx-kz#Wa@b=Qya$DScWpSY2*r#ao#jKD<#-&k#XKMQiaGkZyJ3- z{5WqJgHULVIPV&BK>RrG8lR(3oOg}IC^Uzz zv5acr9Jh`)lZ zj0z~Uf~}0lP-q2P8Bb6xT)|dG5(=$gE29R8zk;od4~57TY-_9#B3H1ju_+*Y1v?m7 zLgWf|GV+AT73^fxE6hhKSFn?jDnvd(cQUpYh1H=I>}(t?20|;?+2~yYgjTSN@vRV< zsk<1bQ7BV)GafC;Yh@~wsZ)*iD3qyt7=2MFQ@>$+j6#{Zm$4LuGIej`E(&GpzQ*=a za4pKz{f$#7l&Rk~;!8sr%G866QYe(E-!TeMUaHEzyN<=5VMe$N)S^s1!kCIenR=A5 z9)&V>hH(sqGWC1LDHO`o?;GXILd%q?#~HPR$V~l#F$;yi4dYMHP1FkdFUZ!>P1Eb#Lv`=3=f4e^&+DQ3dN{JMl&jdF=~;~ z0)=AKBBLdUpQ#raT>`>0^<$`5KScuF8%Z=v(!gIk2<0Wi|BL6C* z4TvB4R~fIMP~=}_bU~q6U1fBmS~#n#j8qhw)m27M5I^#-G7bxok$-DXsM68b|wx!z{f5+c`ho1uaDE4bZ=pwJ3#HyU9rTEXo`vJkm~Um5KJ z!dLJcqZ_tEE4b501@TvKr_l?AR&b}$7ll@Er!jzP;R^0F2BOdk?ljUs{1x13oDm{d zaJNw)M6Td&WbV!}Y6y`lm}8_1kt>*EtcNoG3bMb+R~=S|R&bwD zxdsTW;67vfQy{d0`;BrnxyTjVZ+s|3-j(}}*+S$B?l%^I_$zq8SdKy~c)(bLwP*zo z7+ZwM6+CF{3J71p!^VDWhgR^YaR|g;!J|eV3a#K#;};ZK!K20rs)Z|f)HsboD|pm6 z3*xWfQR9WD`E1A)%rkllkt>*Iyd4m}g1;E)LgWgbG-e2qD|phlEJUv0NuzQtSPOd7 zb<*fx2ZUDel+m*;2(92L<8nO^TEWvsv_2Pk`Z{g&MxoQ!StARDPG7$nk2HW(^rDI6@^Y;Ma?WB z^7K{A?4a{&O;buYhIcZ>&Fv_39xG{@CX~5dCRY4Jql{V60-^KRBW8Ue^7Qqn**75k z^i|%>5F$@s<;|%=vJbhI(YopNVtD~mkS0MiBtERaF%h2horg;#BPG2?6 zyC`(}s%cgVi~jhhubO6M6gqv?G^?XfjH+oqO=U1f)imp%P>iZ+)(7!VUo}lPApG?8 zwAlhgoW54F(^oCC7nY&ZS8X$0h>Y{K%_0%rXBp>fo27-wIA7Z=58}u9I%X9V+AVd= zr?3{q`8sBOAu`U_HA4a6alXF!9JWJoUNxTw@#DN|wnU*gubQnMT2p<1|t zP0V8`w1Q2{d=P&Ho0!iv=3|#D*vw23B3H1P*()G?1zVVDLgWg*WKI+!Gxba6Ss`)- zUosP)gSDVc{gThlPHv_ zdzmH*W$M0WsTZNv%s!B*`>_HGGfC{qtKr=n1%9&D~fp-i1-ZbzX^J=DC7 zLYaDmneY;CS!U{y<}4J-)T7J-6w1^YW?E~gMVWfEX|@5OO#Ok`NQlhT6U>Z&@Jv0? zoFPPJ>WSv(LS&|%WUfY`Og+i`3dGOUlg#}nl&L3~zhEuO)RW9JD0JseGB0B-%G8t0 zif#EQWTu{MMo}nJPd1ZLC{s@{hoexYo@$Omp-es9{2hfdb*6b2g);R_vjBxM^-S}L zcF-Tn)HBUmD3qyZo9QT&sXsASpirisV;%zWGxZ!Z7t2tlo@1Uxp-er;EY=>bPMLa+ zSs%pD)N@P~g);RVGlW7hYL4kr8H`bL%!Viwqvn{6LHtZT$7~%Co~b`IyMc&Iy@6%w z&&*L+hBEbBbA}KZ=jWP*JMdA+I6v1cB}B&gx#pu-hTfCRGb=+G{|)&(vpUwII6u#< zD@4Zm`KA>R9_K$d8(}+N-+}KuUu-6$P~=~1z6jz+{>5fH6pH+d&5kHEtBcLfR10Ty zv6+HGv%1(!1@R;QVspO`8Tpr)7lg>jzs$TD5FYtgm`X>OLwZlL+AJqTM*h`iZy_@B zuQumF89#TgHnTgyj-trF#?0>wLXm%s*`X^4MgFzsY9Vq>*P3-xpg;7UWUZ+Sk!!lv zbV2;wz0PccLJ?q{`2yCWHC<=66C&4iy_pgazNQ<^Uf2$;;1;tlh`)kc%t0u$f?Lcq z6k5S8=5VToE4alRg+eR1#T*UduizH*st~z?+s%Y-d^Y3?Za2#Ygs z^8yOx?jOv?Z}4_x?%r>{ibA>jpg9MHa`$0#1q$WvBW3{#{ zH?}2M$taY&3tKHwD0dgO2BA>yE^MWvQ0^{j<)TpTE^a9U;aZftOITGw{M=o_s)1!F zcbBkC6w2KttP~W=-6gE|LHyiZ!WxG{xx0il1%+Z%2`iJzV2mnZ%|@XZRl=GB;^*!X z*7AVx++EVz3?g#(c9y$KS;w#pY{4t$|pEa(5YP z7?klNe;MmN6xu&!tnt_mMgB6@heBlJFKf*S2#@>^Sxc}Tiu{Szau7fACt7PzDDo#- z>rrS{6RmAj3uiUa`Wl61HPOlf@gsktRcA1t4H@|>SWSh<$X~%~9S|P*AG1<~$jD#C z8Yo0Y{wh|E5E=QaSjrIChxF7|#i}tBgd+cwR(Kc)MgAwP&BH+`@+VovM{tpmKgnu@ zLXp3^wE~4Ae@(0GNT|i{W&R1S+Lnnzk-x4r0fi!eeQOp9Mg9iX1r&<>s+BVeTBgXa zTh~!2@>^EfbSOiSKWx=Np~&xAJu{$8`@0bNqt;Rsiu})7iSI%giu{eOFbYNfCRPdx zMgHfl-Y69Ln_Ej!DDuBxZ5ARU|BF`t_xQEs`_-1#brgDPYh?`|17+yv-EFN#V?ikL zzif2^@qgak$r>m`=7LVvntxdAU3%XdRQ0O_Xi**IW&jnqqdnl9( zx>!ZW@gDB+Kbh9WDuY7zau=&2h&ZM2Z`AB!C8JO-=xPl>peXOwo;klr%^%0231-n=-=x43NGL#GYTiHTnF6eJXCh)P#T+rWY zBt+(d{#FYtL%Cpp)dtG=xnO|R35E900P8huhjPIHtCtX&3*NE@1%&53h~|(|H-WrtevA3XyC2p7l0}zk;K!VJNhMqpf$b7OmiDYrGJ-g6~^1 z0>W2ttTh+gp%ol&EdcRXaJ;n)g;sF9wE~4!aJ;pOYT*iwx7MQ23XZongZL{r-l{T# zk6o_dBr7aLuHYoANkI4tPPN(ykt;aE>M2C7;0$Yr5V?XgtXojVU%?qxqnWTew1OX5 z;aMQGf*)BsW`ocQW?G3KbCKCR)9Q^v*?hKj7lpF<9IN6dP>ZtpTq_xcviW>#8wzFf z&#i0}%H|8L#5vFoW%I?>T@=dZORb8ZLK({DUsx^*W%HF*8x+dsYpfZcL9M>k;InG$ ztUMIT<{PYWbD<1n^G((Y6w2mXtb-_&&9_)n@pJb<>sb`a-3P6fSc{(H4qEL|=*~T8rC=?3jyq^gMWNh%$l8EHx%-f{ z1BG(;Ve2{y~- zwGM@1)Jba-mBAQw(%OnbG3uoC6^Ng^Pg*|&gy-&4)=wZJcYn`v_i5`6mZ98z#wxaq zk6p(3GggKW8RySf$Dk^53))zkq#6&v7>`ZzTvt z{##a?RUj1kZ&?RdgHYuE)2gwCi(Jz`ttCR_n*M366e8F3Piqs1zk;`|Z%}9jZ(Dn? z7OmiIYrhb=f_JREfbbRk%lZ}Dp%pB!&V%?XSYTa2p%pB!uA|Tj7Ff5a7Or4{bq9r3 zu)rz+@mH|G>b{oGhFrn_taKrA1^=@q1ca|(tUXJJT)_wI6++|+K48bM<1Nb-e84t^ z$j_NSV2|4Xt3xaJpq;)EgjVoDJAN|=tzd%PQHadl3HIwkrioKQWL}I?)K0@Pl+8=q z3$Y9rUZ$L#hh-?gm$%QNP=2pu7vI9Ko~clNf80()q5S@YT?2*kdlfqkh4OonowgO) zq5NLWo`XX9{VDq!6w2?l>|B)68Swk&b?v(-$MHA58`u@MLCb$MidD|Cznrew6H(|? zpxeW@Lz(7T@QiQTJ5cCUVB2lJf--a}2-|5WbSiM{k5TAU;MogN=v2_qK7m4~g2whO zA@a@bb9VAheywRraU9q4cBx$;bOuPae@CG+z)SWW5dVq3wOuNU*OI4#)^>}4@KZq> zJ4J{*6|}MY36ZCQw)SuoIu*3FKLGJh1#Rt*QRr0A)?R|O=v2_wUWr06p{>0cYtgBo zt$hK7P6h4k;=B1M@*ZQ6{Omqg7~L`RC^wlp;JMsy&8p11*vu( z3Y`j4?SDc1Q$eb&>=jq{<6)}(APPmlRJ#b3!RVK27e}G!mui;=@lOS*_TvHJr-Ik* z+92XoaEP4>y4%gM44n#k*r`Hf2I*n{Bt&MA9`-4$Mc+x-!@h`R=v2_tz7A#loYK?2 zi$eRSr~MzcLpi0V{a`kq4VhEkuuBJo=agP{MIn0p(?`zaKf z)xLHus)e)K*RG2~v)b2ILHwN3*Pbs#=9B^UHX$;n46yeEgy)oj_7Nd6r=-~zgvgwd zW_#a>u_vr%-(|u7hHRQWP>9SaX?A`N>{ZGs@7UM(fly9)$L@UqgmTJIJ4=XM)1h{Q zgS?Df)1kH{M4k$U+6_Vc6&z+aL!lKMX1|2BXa$Ga9fimh9B#iB5Wa#V?Y`I!tzd>d z0K{Lx3_A^lRxraJhC(ZtVUMI*xPlpW1`4fUhCL3%U%?Fft`NC`qwVI0_-x1(9BsD? z2w%am_G?1q3Qn-ogvb?~U>_7BPX!a~_`|Rkbgr9V_dg0kD>%_E_9FR*Qxq_4I9zXFias?;ZeTB#soMaCH@xNngvON;SU(?C<7!+F5$@YiX4z1~A`(q(; zO{drk1H#vIn!Orp(RWPEuq*xyqoD7YnrVko=sTun*`0*QwU}+E3xV?w|DL7U_F@!% zlHoGP{t1O=8U79Mb8Y1pel7X;Pv_eq6#9;-1$HtDeRt?Wdo&7t>(pX(m-M4TYauc&&Bzh2zjp+ZOQmvNzc4 zPk`|J#mj86^HBH+g#W$lEq0lcP=-Hc^ZsnNlThfJ@xHM=6u!rJJ3H;3LgY8&W!VX* zcstV+`ewY{_EI5okL|HnLmB@}vd2CdD8s+0W{+LzG;c@N+H2Pn0%sEb_pQfb|VxzZ~R~nN1^jZj=d0t&Ko&)4ho$Ca_n0ubOy+=JD!DW(RWPc z*gXQmGxa`u5Qxasc`Q@!x2Izn%G3w!FNMf^=YX9hM8>fL_JM%#ICj|17b4@>G5fj@ znPraI>wkkDQjGf1UV0IPV$_fJEfo5WsXY79-=PdetvtJlkWA$-whvFTQRLa}gurgO z$mC~xn2?ObK_}vr8%*-;Pe7CjJz|9%w>Ls9C6mcrR_26#35Bone@sr=r7rQSj}cEv z2`kw<5L-pz{VC1lw4IE?*PYioZND54Uh9lK5`{+qUglSOc|drXv-U0&zBhQ8bN0=E z@G|G^Vwa&m_uh>ceg4g^iZX;fqdmgPT(I4MGn*F^1Iy|rBstx zlsO;?Q#Qpa8vA?ezuU`DKHm_lTq(+BBg!B)V^C&?kUdI`)3M4xQS0CpSTEX#zuRT5 zg3unjWY-XqsU$RrQyyb|zGN>ANDU@e?1XE)R;I#xs4=;254;Z2k43G9Om5m2P%`$$ zDs7njWtaH_%6yO$uXJYek6jU^Ka=iE3hXWc>BHn-yH7v{GP!4`p%}l#Deo}(&%TaQ z|3JL*9+TLReiN>hQ4p(4U=knd9gvTh6bfAyGEFI9`>W0c234FhtW zU8{1a1BhaV5ywZtfqS~R55P7Y-p+-VvJ9R^>S>AFXx_Z6)gkQb>eZsHa;Qz@pp|zsrOy$WB zVwDgpqlUJCB>Y=1R%u&AVX|EatVL9mDW1gpJSO3xhhmjaRQBy3p@&dvenC=62#lf> zE3JoWVVNeZ41X>#LaLBG{rXeb5cfnxE>zAh|MguV$#2Ue?bC>LdWu{fniF-2(>Dpw6!=E7?= z3k?j&n-TUK5TW-_TBpY=eAm4gnkod=q6547i=mZJ#vgmj&~~bYV{aL{fI?$$6}p5% zV{aAu3x&qsI`m-m`^VlUlo${`_V%Gv6dHSn(5`^+vA+_^L!q&!gc57qKlYSRT0r>N zUki;zp|QUnnjr+n-j!Yb_0Z=+-cv?)jT70Sd*}wC<^VLo}pq-$=Tpze>2oFAbfB13e7;F+2|Yk6NSb;AXK&H{bL^xGEiuw14EsJ z$Otenlqv+q-kn{2VCb!Y^kFh6v;^Am$38f;9z>y03=UmEq1i|a6`;`A(?X9u4Lzj! zc_&l@h3GSsLRw8Mq(gp5#)+H&mtE*KLU9uR&Pj0s&22;T`G zgwpFkEt;PR6vv9w{q9R48mSZ-=_IOEh{h#_#y;6E!$-=;H6^qZt{y{UpBg%Y!pD{0 zBu1I;*P^k{2we-58OYk18B*$sQN++}eC*evy)ifBVHqyG<+=Bj;qx;u)G|^=p_29G z9^*5%CG-RejeUElo)8$#NW>6dKnzp$RB7u5UtwCG{?pITo505MCxPG#{mBw>afvCi$UdC>bcnLu-VtWoMosF@o`6E7p5Y_Sin;Ycyu#&FsE&{^%FAct zMfN-$3Zrb>7N^W*{W%?KhPB%GWjdp5@XNf3^0yu<^6BZ&B521y*PRZ18IXrqKTd}> z2IP?k73EB5hY+}Tel4shXG7l$$yA(rampgrpYx$Z0r^76@jyF#etrv`3kbi~Z=tIJ z;n%tlx+_Fp>vy`Bi*Lz|7g_HzNueoa;+0R?nN7+fJziPQu6~8e_%ZV;i61kiY{N6s zb-&EM|0ieS;M^k1@Yu`O>!x4pi|jb1@T)P(Enj$Pb(9C#cCoAKN z-{;$Y8H)2#=&ro$m*GA9fwd#$o3~;A6!>Mh@F*%}@g3;TJu2g0-4}nxq|nusFk43U zZ;zj4q)<$V@yk$5h`mpEOo;P^#{`}W9=K2V{r=#6!Xtl?`-I1h;=WLXEOVdmmhTtd za@qUJ@cDW8KH=9&yifSGD)>V2r?M}Jc%qgDM}XJYG4@B#ER@!x?ePulEVxKfg1nN@a?--4w5E8OELmNeazM zh*v&hZy|i~cfwPCnMLf`fxnlK^3cuym1*p~%~sZqEc4@%SY;al{iFK3Xjiq zy2dCk`L#N;pH2MLEk%RrbZ7F<<;O^~Jvnq|lnS^{+*1 z+Rm4EzlNRJ(HDO2@E%IxcgnBNvA?Y0m!Wlk*_SW6z&j8rkxtN#lqyWF#Ie8RLACq{ z`3gy)3X|fMC|h@5{E_ho*9Y^(<3x?l2=W0X`X zz60sI-Sj$rO+>BSc2F34lPrnxJF)94c<+Vv~wK@v{YW&4n|lNRoQ@+6*4)584%wfLDWEj%P3{EkZtrw4@Jaqoo3 z3z2u+u<(pP8Q#u_@SK3~c1DC31%$UVGQ8@4+DQ*@2?%fJ-SD?U_QdcRyVi&OwTtjQ zkir+6!H#-ATvQX!4uy}fGJLPTA5KI$_ywFH-}h&1h|B(R1e>4xWiXSmewhp>UKzz? ze7Le`CsV0dG@fN?_LmmJ4N#gE0+|?Ygz^BEnG|jrkTI;C$>HfjAhPj2KQ+8S$oUvL zvrP*x4+zhd)571Q(3x#|I5*G^e=9OQ{DjWOeqPKEe-k((To0r$y*M_~IWLdbW5V+QECSQcx z3mKy{tpdAlML1Q+bfxE+SS5#*SrJZ0Is8++lFQ`F@HnXDKYy(ZPZg4>Twu>}huC_p z3{Uj<)iagWMPrqpn5+udY6Sf`V#F(@X2vM1!>@q&qgWFjAS6>+axz}jS`$u3naPB= zyf%EdF|@+=VPqQ`f#U!TxPN{+&dt*m~08p3dp}q zwuiFfyF^k)jYdOud?fyi?x7unUh z6p3^qVJ)XIDG^Bzh{JkVGO{8dqga_Tkvx zl_?)lQ8u!xbEy2)|PrMEawwVtal*t7Sxn2ZY}RW@KVOmN2m+n**|%NhESM zAp9<97>R!Yu0D=ko$si|k;(z#_r-G&Cm?)3H;ufCG9UMI^GF)X-?*P&j4TY);ybf- zBs(B{hqsB`3JBlf?IV?6g#L_RcO2iVogzBQU$~#UL|zOCFVi*B7o`dA=WdbFD2s4E zr$#mhYVrNtBXTw%d_TV#Db*6L&aX9@{nWl+q#nvgY<~C-e=Cw45Wd6Tj=UNWzQYGa zhM}y(9iA4Mic<6t?C{}{?SWc+ho?tQ1%&VLj7YJUpg;WTe20&Ym?*1ohkp?16cE0{ z$44?ydf^VA7@2{x7kBuSNOqtW-{BudZUuzz@Q)&uTfx=&wYIS`A4fbP@b+?dWA=?e zkqH6erJ{{oF=K0m_J2c$S$LNomcm zK1P|to{4zwo)?*mGS)A%G$8rxOgk?!x((E7zARpRrec1iZd;JCTi{a`^CQhsdgj9u z$NWeNN?F$Da_lrXKXM4AEo+B=8fro0B+5B4DPd0h0a!;p18o*#_dD3YJFp3z?>*vQI~3vDH}` zxsCGrKk-TqljRYmJ@1b^Ykd*P7m}%%!(x@AtjrgY0+cUUt$ZdcA{9FDTA9i#Hha2Z z@@1qB$~bI!Wu!4m#?V+Lp3TpyNGp_+7K`dkR!6#mBs~9DoRZFDO{71Td3iq7iVR@ ziKL?}y~iRtlUx6p+EFC?095i3YV-H?<*E%*|knZ)}eG^KMQz^$(hIw zA+m>OBS!+ldw4c-Mu?o}bCJs+eh<$@-s&c8Bau0&Sn(qVS=*R|<^Ckpkjf^z|ddRW03(3f9b&S6Do zq!8J|iq2{wavxT5Hi7s(tmNE4p&mZ&{D(q4tn8HR2P;TDeA0Osg?jj;(+q`r_@pxm zh2}8HnJ5H$IEF=%BxfO%@mIN;bDe78KCI@v+8?e?J*?sMN1+}*<)ouf4{JG7QK*Nt zoKq;&!&*+-0BD(dSlfAD2=tI=z1q$ODC76Au9HW#u!nV>W^di!!}?AK6zX9ErzZ;a zP;>gDP!BaH>1}9-dZ;<;QK*Ny^DT(qAKl5OGT0y8X*f`{<3~2rX#?U%Hq*I_!mq`D zc4Ij=2k}}kc7DzbI|TvRnZo`mmg5eFmfw09PK*(!A&A&fQ`w9~oR&i1t=h*-T&G+b zjO+H17;zSD=+u9QOIA#kN{W)tehS>sc?M;71ms!A6q2EgXYa8uvN!*Y99D(>+f(Rx z+gN2WYq_y6-?xiZ)-q}0%SrY&o`2rB8A);Kb8`~$_M7+le&Ky?;g_L4w{-f5{+y4Y zxA85VG$Al!J6Nrj&Z&UpFlpml79#J#_D=jzm<@W5-QMwrami55pJVUPSnTcS3_@AV z&ip)Lzv7G*vL}Y#{C9FXjDU9NzUb^MM4?^R#kqw-yRMrPKN4!uuIuKcq=V3|>*jQN z7ld}*tIiP=`q{**&M!hT6zwGY9v$|R;#Zx&P{K?;YR=yLJLY@5oebrgttdaT+34;h zjpicf@J*+d5SYW$tjwEEdO&!T=;KTfBImH5vk=6OWBr^)@58ld4hJ}kP-qU{cGe4# zb2!j>V=Ro6=5Ua+5QXM&u=6(x&EY#v!Z@f!bNG&P>H`p(!*`tZ6G3PWhdNJA;*z1P z_!NFtGSu;e?1`aS9p*F@GEHecJWjdH#y-qZCi7aE%9b^8%55RFQR?@NRsLl%+-WF8 z-nk>3WaBzz4NYfMo3mnk&Us+!ymJ6Bykd^@cv}Q z>{`#Bch<3=1vnK^4lpUrW9LXf_#HRL`86Ofuv&ASy8(Hb$pWXyR6Yu5rzexe&JzI{!eqHq zHz4mZS?PE};LY}jOx8I+36VQ%Mk= zL7`put#bi|cHOtmj_JJRJu$TFzIE1Qg3zwp>3l6j-lsdAe3VmN;>2B<YKb_Sm*|X!7f0*2H4hWG?*?&2CDD;H*k8?_hjDG(( zjX#GTQuO=R>5f9tPjLqck39 z>k;?$fK+3(9&tbTpIVQ)p9aeCTIJp40ja}km3OnT*84@^j8wt>J|IQd8^wxl4$4*b zj**|ED!D%ggn!QJG52?r)^lMWKH(}$<+%7A_k^1)fsm%W&aWuj>nq ztKNOW$5r25Ev^Oc!<(>{RX0z_9%bQ^&}Y+4`ht%Ogtu(D{ZXi8+f5G$Z#m?aS^>5A zxcD1j$E^$!6PE-nyKcG=dB=I~ACv@(w0d>_iqqo(WzLa6O(7%_%Hdj#whF9 zXFO7wG;&Lzbg2!}*iA%fcrjM##mYSAHbYteYn(EK$@A_=l&{$*xJEN+;jRYp-&(hH zPYIDbs-^qdN`7?^K0hzH15jvwTDj=~;q%klZL^Bkf|;MnuGP-X22lFBOq zBg!az#`sR?>|Oy$*zi)UvWS)G?A{fUp*-9^UOC&0m#Mdw*UC_Od<^$airZBPTTO%NR6g}J-D74CNx=Vz}HSOhY2nb)(Uhdg| z@HOq@{tco$b&7pnlu2JVem%eX81b8cUo+|F_6HGb@dJ~$+=LA<(njplKK!RSY3@Xn zjZ4^f^03($>V`K$nM$k-zvD)@3sL&A_w!svx(89{Q;VbAgiTQE_IEK#Z}wz2%AL9y zImABcc+by=8i_A?Qy2<}fZV8#L@Rs>A z$cJviSG*R;zpUlyu8DGujWm7(`z(!H^lM&5uHY=UoDg}fS?<|DnNqCQEcfO&yq27e zPu=((Lj2kI)a~>wmkfpGd4W3`g@4NvU%>@#zL4|&j4gDB?1WmOOR=KNB6k+bh%0dV zTH@|N(ZX?Jy_UM+UAz{I;&Il)t=!Yqgd;zdw4C`!%c2yA#z-s-O)nijBTdaTby=_FNJ8gNTCS%mHU}!2S!ncjpA!} zc|c4i-@3?zDYBh7zrVz+w^r2~G(b+>y3BxYMS{Df||`zOjnERyh_;_Y@H+{?$6 zsXUw$uQXyU?{;gT{CEjukDDT7jI#81XlJk62c;?#9;5cU!%!ytKxM{(_)&DPJ5|Uu zC6~3_!)D*Z;69Ko*Nd;t_ikc9c(nP!tsx{+dGP}Kq&NEw;{9%mfc&_GJ%hNf1msWF z@)5TON^$n-BVH!gO-EVGW{k@*cQwiylsvb{clY<_XSZ5Fcz=Fzn+k#c@ODnPa|6P! ze#%{na_&yNQfDChoQ#_rkY8DuGj2gZK5^LZ^}9*m^Q+7KbKdoYz)nbJt8?CM2a?c^ zeR8iIlM8MaA=8z%?0#Rz-n?CKhYEqSL?>3}qWeKWQkh(G=lxGRSKXy3CD~6xJF$7b z>aG?df2;SZdkoqUzt!81wR6qAg=Ofsdat{W{~$)8?0q6mdDml~+i^7^Fbe*g<2T%B zKx!{wziHyO2*@~A=C=DNv7i1I_5MYf)+k>2l3l&1m#~jtUCvlh zua1x`@x2-ASeas8Z!AMIR@_@E1mm}}o{?N6`d9{R0Qx>pzn8(VL^Uev8SAWw zR`4o<_`mU8!D}c4u6~x)s_3O)8M=BUZ=jHAO1~Mg%C#5RXO_I`2gQ1cC~=H^cK30w zEr>FQoke+!dcx}glAyCQHlO(?y#7MuJU`)O3W2L%Vpp%?ZO1Zn^(Vbwp&fCpKUkS3 zJ>`(Ny11JPm{j!=guuNMyOB%c|CFijRTd(zUfp{RYKhq>!fMsv?H!w znw5FV`!paGnAG;ZhgyYck2UZPqZDP|T*1fQ!222Hr6cf(-v-_pl=gp<{EkAOh;HEh zAp};L-*F8*<*=OjYV7Ko_cDlo?^xceD0HV--kT_NH(B0WDEuzaSv!_DT!@?>%Ud7> zRwvBL*xqq0L-P~zVvorFJj=?2yeB~XJs6of87H!LJqdisZshpdJ3KS5An$*0Y{11g@3BYBlmYU>UkrW3M-~!A zy~?9RN3S@_+CwA{p)A`6pLFWzH3aeR_l{mmA#&zBdV__)NEfpHyzH&PGBnaxyvx|m zjSt|rQC{)vA4PwZHy5+ton$@i`BU70%a1e3})Zj!LnXgZxzb-zHCBy z*_Rz4VwJyO{psoz&lA@YvW>}WUbBGwz@&%wdO(gcdBYn9lAu2uubgDk%X<%n`qRss zClr`6%+n{~2Y4pzz)B|YQ5#n zz%n$?Z+lCHyr*1xE?&9E%DnC62$`;oN@KqR#Xk2w(EAVNV$*o#!AIsoo|HNx21g`!$t2M_Ph-K*N zpL*k<9dY%N-Porez0CiS&%A8`;pgzV-uD6Fd3m09J0KHTUS8nUJ}&yK@VBzO%;#Q1 z6#CtPMc#`jC)p?SA7Ibai@g3Qmz+3dIqUNxulotHH!6nUJ440 z;zw^Z3SW!6tk#d-(`V&ctX;~U$h<}<)aO&)ND#lzr@iq)pw9`L*)y6q4@8^>%P{%X zTPg&etrCS~34v(*IFqwp?*G&}@8t`T_vv{r_8jja+^5x9t>3)LLS&}C;58Ql{ozkZ z7rb@>sl#gh?o9#lPiR-XnJ9D~yW-77p)=$aZ>bR3pDW&eAEeAC+tl0c`g zKfQxki(3BEyDbD-eu>q(?UnoO{yyLFY6yWo^EW4VJT)L4SgpUkE+7f?=H#B&9ff{N z>z>yaWdQrBEB}V8d){CWW&VHgRC3RI4@8XOHP-SyZ(cz9Fj1l#u^pa;hcJnaevLv| zI5wIsWSYWrI8jwuZPSI%WMSk@${vInCErLQ%0VShlP>h!$GnR~&N8wKai&;A*qe(*K zt}7XBCInV+1uIi3`aYJSy-_;40@@KhT*u0kj>i8k`Xl5kCS{{9gZTUKq3ElDGQ8G9 z(Rl&kwH}F{z*@A&9*v$4lqu9uQ67yJ1f&XkKCKWfa|vdn{1|xtsuV4cGWJpUO@T_$ zDk#t7vd`PHpL$e^)man zRQ_(p{w^>pQ#Bevd8s-3Bs`Pq(WXN7#4KTNzxfk%jc8kt!Z+CWO!EC)Bib3uJUs#4 z*VTx=iSqgYsx?Fi+)ey9QfowKfGDl+vhyr!`RV8!6wjB1APJ?}cL4Gy)~BN@0`1&o z?K~a*2Fv7QnYz)vfigVn)r}ql@t+IoMNb6E@cz_`UJl4vHuh(tBd)-D4Q0Q}zyAfS41XgEg2K#NMXuYfaTA9is_Fm!|yLu=Z zLUF&2Q~qY+M4Jg2lR*9PqU}*evRZsCyl7Vxx|SF1fkJ)uq63A<^RpLC7Xtl>-@<-s z7QG_``csNYYo5UyRnc z&c_8~XK_|(6>TnLjB1fpp5(qR$8!qZGw`m=S#$g?9MpXm5~&=Ipz?_-u@h4o2bU1m2&~ z(UBlx6#Sb#Mn@+GWCgp{`_Zrer`Fi$9u#`-H#WLIP>YXZY_uRCd=wu<^*_aoDU=H) zMVo^7^D`;h5=8Wew=*f)9cvvu2KVxmXy5;-H6=PYP>bI?Q=%UQgx@<;qiX}g@097$ zodMa$;>JhOqaX>>y22aI+0o;HGQ8I8XzXn0=Wl*|ef0jfm z3XxY|677vbS6>>PCuEwkWkH-$b)upyjeY~-KNBsF<_dwU|H!VsJevNOxVo6bn@rY5 zcY!3(Y^;m^fI>fWSr!O#a7QP!?7riA!j%!_X z_20Zd@;POFwBkQ7hvtS@aXJ-m06%*v>sClva@k7RLMcL}+C7)|J?A~=J!h3%au+3+ zkZW?;MUo_&O=v3>MJPo-w6NXWiKNlVwkc9>uPsScu{fG=QIac zo85(p9Ou_&_hxbntX|^!&)Vz(OytPFHhUP8aiPo@LCAL;`PXKD!!mN@Uzfd)i5&UY zW&e^;3g11~W&cYgO^)FoWnc2IXbEX@b@!v}sziiemw{g&WoNRCe7D+|-Hgd}es9{C z-8!KZu7PaKj$A}O&hDQ;aNO9GJvxD4e=p8{D}ms8LP_=yM8egBPqY7EB3BQ#WNYVz z7aFdC)B>BgWH({55#}WI1nDaX^-5fQ+M4|ak+7$1%l?2$3E$DeZP^_VEOQ z7H-dOc!6q{D`q>g?_eS=+?j0)qH5onJ%LDA`>yO~sg$T4W4J4O&P8N*_WA^Z7Ji+* zErFng-(;UjAZX#Y+08V(<9Tuz!|$>!Cep&a+3lEM3>!hj_GafY!5B6bq(~6u#lGyn ziG*J4&%QKC)D^~XfA*CL1TFk2J3E1(g$J`o5eY3U&3=xFwD3@Np&+XEL)l*w32Xl) z8{CAy5^=5sV;GXEeMi!X@57-?rEikOt!|1mT%4-?4R2&eviAwibb}W!$fMn zQlBV@(tMRZErGa@_p9{XRH~x9zpl3a1M87@HrCclnaDdE>*zOE6!{YSk#Gikwcb(? zs;fO{zFNPZWRj$wdU{VLvbEIL2eBS`hhzhNq#(02+@;(d^fb_)N+5lKWauvlLUrL8 zglqJLf|P__T&I7?MBcS{o&GVABzf23b^4bHdP)mmhk*WL0vQB#U9bO>Kt=*-q*tn> z@|6qZM*YeJG8IT;{gwnW6G&72-URX*kY;+j1hNRoZF**9imJ@T?fUIRlH~n|x9f%= z0m@iv@f=FnO ztxsnneRuT*g3umUtjp7v3i1=2fug!R{eOZ`e7=I(eZ4q=`~ak_KK}CZ(Re_Ann+mt z19~A7S$ju)xgb7t($L`AimzP%RKp05S)+`(HZy6I)< zNM>pyMNfppg=)g~GF>k>Wm`GU;Un!$c8yt$&6zM08^*WlYJkcafjbx64UAYHy4Er`xSv) z6ZDLQ7Cua`Q5Stb6xKdmuR|nB);?TsA_&C?OAXiWVi{Tc2;EK~So;XQ0~1;MD7|-r zo-N?}D18*kgtb4R=W;1o`xE*L7nOQKf1PDy?W6T~6A0EmTHnA#);>nxc9EVj`cEVi z);?DMl}lmm=@9v``q_(0jnymF6A??2_f?M5(-R2ZS21lmTLMi;EhNt!2f*h)N!+f|81ZECX z^!-G_Gd@%Fk@YFdawo=AeL9o3zM-}Esd_$i6jmIF^s`K)r*G@Et`+tq z4ZVz3?BCWKGm-OzxAkUBfqBtla`F;eEYp zBO-V+@Et%t)F(0-$9FQX)E6;X1pBCQ#eSu}LJ&2EuhcgPLXp20^sLmgGEuW?Evt2# zNErFm`YM)@t!1r#go$h|YxUz?O174@`d>_BYgwzO-$-7_w(*g^mx*i}oAg(16841m zczmj_5rk~UXThiX<^+P{&!@W67)zc5Dm6GjeyI%7aBC`GL)N^hlBHPa%{gviKWNZ0Oe}{=|QQzq+1j*C- z*P}Bv-|3kxu#`H__?`YLlb#?Gg$zO|CX?aJBa-j+^xLV_(@Rn{AIM(48I#e_RxyVA z^eIe!hx0_W;f-~lzMKi>3(s-v*H1E8@-W;M0HjnmT9W2W_)3NM_a4?KF+ndLfVz(B zU0R`xYIP^{euAj}enKCWK=7@tOy4C)i6(RLyMAeF)Kj9#eEp$UV|;l5Lh zs%Ch16TzNKWWFHq9>@36XvQ*@L4wknasNH|Z%LADbxFoBCbHEf8!s`DzF%T2WFmb} zGxjr)zE?GxWRYgs>Ml3BFp+V-+?dTo#<`lYOAtD@g})9^-B^=Ne(izwv;Q&Jk8Nxh zWTrNuU9#30@_vj z4VQ@=Tk05Hr3}ZpzR_KfnQ-5Eiq;z}tZxhzL|Ith$W0*qK&FAQi}ZviWUn!fGLiON zWBf@ZwD}t2G!tpzHO5X$_#QqTt~1Utk#WAxXdNLLnTv+TU?#F9TyJb=B3r@@#m!$gh%{f&K0WUK3M zlroVmbAZvZGwG3SW1x{GNS>B=Ek$*p@vk8H+N1Cb6pz6A^FSl53zo{$F3n2O@Haai zGm4m;xtB)BA;yNTBs2a?nl%kE+H@l_DviEP3^4{XNlmA^h8U}vVB5e|;vvQ!LGrZ) z@CAGr0jb&)C^gR5&{Jp*S7pZ=>AjGskz~9vO%OGY$~6jzgfoy_!+i)#9SX;m2}Um_ za)g{<^dS?@@JSH-#uNhqg$~CiH!3iW0@e6HGG@EcZYY4 zoPp#i&Oq?X^>Sks6FIN@pV9g;(k!#H#u&;(W~a!=Wg@dvWK3itv$M{a!9>P+y-{@# z)jpzfs`fm@aJ|uj$zR)(wU>ZwG%g>EGHQOf$!I2s%Ecz*89`KUDmDs;guSWQs67Pr zsNPg!G-D!LLW$9wNSK`xBa4Z=d%wi+m|#?KPW!1*D99x3TK?UNtw!eKqOK&_2e%o$ znMmJv7&8RP)2@7sM)e)Wr%YDD9x8n9d}VAEggku(qVbh6ZzyV3bJ1PKN+z=X>@qeG z34PyXv>t(_ij&gdge-pF`PxVsDfEP%eq&T%B6IqUk;z16@EfBy6Pe{bMj;c~R=+j& zFp+KbJEP$!s!O)aAB?_CWXs%d6f==+>}TV*(Ns!C>=)ydAjL_q!dVy`)qgS4o+Q7v z!`=IMGs9t{3zK9R)iH*@8U;*70KuWjrQ`YQ3k7QAENFo-)$MqaM|I&loW#vh|)Z;zYuno-sNyk?rt|Q9oDc37-Z3 z8f^qoz3E@$MUn|uSI!%+FpXl}eAcsT-v8`Td<}msC4Bbt4 zl{s4wvKiZIx;amfnc=S+r<-qHq$k~6!Q>}6eTJHAn?GKpr;d4u^k@$~m8>m=7}hZ_ zAW533orjV3Lm*e1)t^yh9gzCw^-OMq+crK0l3|)iV8uRfADndna;-Uy3I1+7&YBvU zMF|9DZZP*urBa(Og)=TdGR=%1;Nnbcui=?U9 zcN*!Dz3FCia{{RVy|S6v>^UWav$*D_pFn;9(#jl?K(O{U=F9|g7|16w~# z8c4PYp@2X7wvLfELLzf&h6HE$C=&^Z%vaRhFdZ#aZNoOxpC=;qIOYIB^0aHQj&k#Qbwrp_e`Wt@kbU6{x?k1#V{CK(y$C(O^7$T&Y~{vimpI`ra6Gqn&)5y8lh zF+UPSSvbzzDoDO2pSj~q?G-GQul3jpC-2}*GuQM4p?skidFCD_(u)b^ekRh331;h8 z$!6)rMDrvQ>BVI8)z?TydNIW;AriKPDdsjo$P4VtQ_N$s6t~`I%wh9LkF;m1IfjX> zeX6;GiL~%Jvy6$fXS&(z_44-2Fbf6A)BfaD;u&VmH&7-|!y9<;j=LG=LP5wvJTsDS zX1vLI*ut6SNFt$M1?D)Gkv12YQHrrgt1V{Dy&<^LC zdjvU@B>j5L9J8RjU$2|r2|_mC1f^a#4=0dYfxKaAZ z>BRzb0u$-QTV??hY2hMsI}_Ow7MuG8Aq(;OzSwN>HrkV~tv!^g-FiR#;)6L%5Y;l5 zm|YeMn?nninhz04k`^vC`wEh$tr?f1eVGlN!u>BTCu!V)6Vi#28wLGraM&}@O`HRgSSkYD%>dcD~@f#iV9N9HGj zsF~G9b1#vk+NK)XNWZ=_rwBq`;2p@{ znX5@Av}dpRHJ6f__nPOJNX`4qE-Og0{Efn&%oR-JZxoiA6+a{y`5Usom{&28zcc%b zS)YmY^cOReiOljZW*15LiFVjrAxNI~I%EgOgu`aD|6yJET9pp;{&mDO1X1nzsM(uj z!gaP|=Hr5>9(T-~o1ng_M@W@Yyz5zpAml0fm1=cN zARS>Oxzrjahf2!nRS(`jJRjdo`<+iHv+TD|a)N%F`MgO%>lJs$0hdDGA>hudv!} z5t_ph`wA=Lb0X5hnpV|qNL1UXWnG&T#!=%%1l=phpEH2d>&H;&Yqt+`- zZtG4hs-88U$-O}EhQ@l<64o;Y?7?3|t#6rM2;a5Mur~wG6J2Ay#^evM5U$|@@)nZ~ ze&8~y?I=^M;r*-lzb4i= zLGrYsZRC4XYZa5vVBUhKB$`@TJ5V++tskta;V*+XweAyyvV%{?rq*C0Y4Z2GZ?;AW zQld!=1@ndLTCLnboIZLB7&M`rnM zt2Yywn0{LgPpBbTuNrJvvm&> znZeGM#YASXv*ocKnZYjBBqlP0-K~{OWCpui>zT+5_Ow1>A~V>_I?Y7Ju#aW$E}y|i zt$u>24ED3~1W_66Z_N~hGKjkb`&;i5NgMJ(iuMddet@-F5S76J)@~-U_CeMGCNhIV ztuoRhGWa~`8D>@e8ZDfhwwE=Jw60__Y&q36(yAwj8fiybH!+bJ9BDOWA~QJBx|2wl z!I4&siOk?=s{@yk860hOVK55NoA~QJ7`i6^J8v?o7@-wB1cfEiX9lS%`U zwKt)zd~5J`qLj9SZ^JII&M_GV^Fv&3DX`joPconXNMEI2w8rcu@+-VAF9tm?S~Gqi zvJRg39|Dk0F$hJqApC!a~xo_iSHg{?hI+QA8Hd|JI>UHMaaJ#Sd`1W|h4urdWvdfv2hnMghJ zt!0A1yahbPy5?IOm@M9$qE+k#cOO})2g=vAz^Wq1Os(bOWNiz0y1;s!Nye4%>xV$z zwmuX@XXSvl|5NeqijpbG@6KV4bYZsN$D#F?J zzoD)bR^>yeIbSP*vu3x=h2Lnlt`tONd9_uKWWu+b)z*#wk!hJgaK5qHvISAmSYypj zAj#|C6r@%33)Wtegz>@qA=g{G1Sw8h*i&Ot_AAO1Cp`{jj_uOS|U{ZZQoaP3< zc3W4JVO^99Y#U!&4Vg%LzOfuZlwaRiZ7(YIjn(6#QhThYE-JOhdQK4K`yQ*1iLC2e z>-0sXzO}U9(Zb2%>>Ku-Z>@AD*f#LCsJ&K4BI2osqv&3%hajr1z19jQvaTPjL4OF% zVW}Uikwn6}ez0Z;lBfO93BI>L#P(Y`e_^RSt<^cW)dNVWRr55Fx-eR|19I4^_79QQ zU~fh@AV;lg=ZGAB3icI3Z~Dy|CdgzBy})~f%B(zJ#!1V+fHLYybkgcVBzzK`v}$P8!`iW~Q&!!J$Z6|_ z1cEcqGgdo6ls*4gXNXjkbJ4R_QW9!bwV$=>5(#TRYn2L;r*&eR&s#qvD|@i6^H$vy zp+}7B==%lhYa(G?TI5G2@|mkePID>w%+(?rQc;hpDH{N};}An6f1fxHXk z>PW-tB$MeTYpZ}XhzzblUVNLBtkrD;XAC1#t{`&ZK(cmSRk)ogQd|>>viZ8mjs${n zzAmy~5IRMRJ1rYVjuH_u#NC*UBBuq()AA3(xC-yxH%1m-iJD8ak@u!*kHH(zO_8k$ zWCV~Vk@HOQ;1-ebKyHa#ah0+MYi|~5BS@Zh-=YQ8t}Jrk)}kJP?e zd4V}KA~z%utjmbx2m-%Y1oPF8AYW!Aqb`**A+1yOUhPLU;oP*3?DN_C1%xt6?GxHMHm(mi5cN2G3dP0Y;uL{6_Akgh9vQ-9d~Z$6h=xX< z6huXASY)Cg%J*TB0w&V;;gQuBl^Pz|cu}e0k?j|i8WH*HqEaIw{|Z8HTew0#B2wiB z^rA%UUc+62BO`@^6lmGQQ?*fd!*|#S2%t>37j!I>Vi=ZF#NNX3A;Hr+sAp1SgV3Wq zeI~Lkfne>=M9xs{;d%V2krs{6o?>xE4E0Ql>|!D{=SLpTL>Z-dR%CnvLCv!w^8`^R z{a=ct-dJAqoXC78QuC`3^Cpy;rTuxUrd_fgZnKFLF!|w&RIMhEMUgT=^0j>+a~+WP zBJ&$lsbgcP)XKkM$Z0|HwL@Ij?#Pm+ zq~|Qx^+RMAlk2}E3lB!lF-c+aYoyK1RBAQbb0RWEkbLa}&>TtCw7(*&m{dFs_r-x< zry^xcHiAqxkbfdsx1gSUZ6fHwukGg}Gni}#nHb1ih|Ft7GT(xQ-GNky{=;NXtyFCY zkgCy)TS*4VI3QO20E<|L4A(Roac^nm-1fpm{$BeMIym6Y2ZN zXof5FXs6isQBjYHjK&kuDNJNEMo0GwqN4F+v_V_cqoOf3nkk5i#@J{-BH?$U@zEhH zBX`}8kB(v@qcJ{ODu{~4#OUD;<)bkrdbn%(Xgn9q>Q>&K=c4fhg6}8KMLQ>uOMy&} z_7#MlGMK^X(YJ|&HqVH@%QDjD8POF?q|Gy;le%N=1)AQ4&O{VMgPugv*TCHponbXB z+MCJby+mG$j$zVdc(V2jWO+_>*F#k5bJ$^vt6?uk+w~?g5YASVfy}GXXPErkR1?2J z^ICK^lQ|C(nHPPV$*&`cydM1@li`bqyb;~Zr2TDh`wq-x-i#LXp}JmxCq9z-(e4iu zk-H5SMEeP%^8R*om>_C)@pkm;N3c|)OlAVXT)Z7^nLscX3!`QNIR&0BjJEA7^k~H! z==;~AXeTBghNPz;s>dyg4j>|CO_Sz@va)0>h=oBV$5BZvC%3vxbzs#<~B)@mU+)H3aqKX?TtmNm2Ch<3vjLC26=Wx-L3tD3GK)ZQ}xX zdca(LeKd7A$xMLNON{(S(N#=l@ZN)uqG=;Y<{-=l@r2sOXfBgG-zD-%R2xY$#UO*H z0JcOkMiHq2GDyCNmNG$4G4DI0Q=R~sBshiiBHVWT5v&?T3y~zWmfcZpG!iwE?2ZOR z!e04Jv?I&NKKM;^1rym%zmKjIgx(!+t^51vX(n>5dv7%NNwOJtKH$pszG%&{MC9qb z{n0`uI3p6LdZP`;p^R$thojqwX#c{RIX;OFNB1%LgUONTNha~hDcV)9z)p;4;dr4( zi!u2_LEAquDVs#(R{M7*9o{2yo1L1E zn)5V_{6~Xej$|);fe1z(NlQC@CJ`C=JM8Uqh{(v_Wos`Jk&(aG?q7&RMc%OI38MD5 z81~yl!nuTDFJ^*YB5g0rm{HbJh|;FOZD3^NY&cGv1yfh_YwAJx`G0 zv=i&-={eq>Pekj*r>n->JDFU*p3Vx5xA!wan{mx*ynWX?tgBe-1shIqgv_&Z1evJe za}dY*33hxv%1jiyyl{kk+MdBg-h}Y1U4|rSqV^-~9zmJs?KU5k_hP1PCJ=mn&9vi! zD8CBq-hzNV>_vfH$V7TkU~ga|y(q9-ZYb}?EZY-grZx)BR@?+peaTKOMj7>XJlFn$ zNSNh9dpFC-EEn2)n8+SiXutYNdB5h_3k6ZVX`cOjiKr_aMc=Sz6A?FnZR-YmckRQU z3K?-%6_N$^fGtS!w2rr@h<(co?d41c16hl+9s6TJR9jta9}q;f=f(Cvf|MkEaVS;W zZNM&EJM}ZFOO7OqZG%W?;Zi%sGSc^@b`BHi`%=4DkOJ}E)EuJvzCC*@YM!YT!k4VO zfGoEwe~Cm{_&>X*Aj-o3+1_>~gDv5I_B=sU3|HFccA!k2R_`R70DT0${n>4H66tVH ziXdz40lSGjkwLq&*4gs~p|u?xZPwYpd`&WEVAhQ@qK)=hCclQH+BYPVQU%5yoDta> z-x8_GYD{%)v-hyfo^7c@W}ChFXVP;P2>SkoeO8cs zEeYPBQO_54-9spoua)+I-&qCnr5!LyhHu#xJfhe#O1L$=36_Q6ATp&<0Oejn6z z#4cqbcgr8M3;#sTd72Hq5_il0W=}hbWR`~ae&bhT$|0Izwb$NYeiyVtty*GEL7C!L^+S z!HZM&048!&KVxqd}i#=djeFa8Ie`bPz<{(V;mrm<)p_8a}_0oE86KsmYo= z<9w-eK@j!MSi#9SPnzXBW13?KlBc}`dsooY%Fb{m8rWPD-nJ?`OSqJLudLz}vrJDI z6L5d#<&LS<5H`#A${NlZCi1iq`CeJm`Gkplue{RvOptu7@?Xi?ge-V}aDEp= zwXs@GY7*9^T4pV$3K22a!nLnj&b3T%u7xDsxl@ol?P{2hrHzF*N#_A3?LY=eU1u

ILGrcRrjq7soGna7M2KALe8prC{K`qYR}5S)LGQ-) zbG@@a8NGm<@;mJHPW4nIYP7lDNhcDv%Q)ieUd78AasWYZRc?+95(Te3QZ0h6?2`y~uY!pOUc#Ct6 zWu%3-I1g1SZ($3kKND$T3ulBN%EH^7cm6{@kb3yt;dbW;k?@J%%K4pToz}^6mM|H>V9##4c`b5;pLtFp6!badL(pt^8AkEDxJdBpsG2CF#`!)@5fhogN1Q5Gl8nrIUuP+iFgpXC6)Ypykp?=&g3wz(&eaDxIkm{1 z18@%&j!};}lhTRY2IG7!=#`H-f7M2!TFW5k@;XRV#0ELn38F^+K~57vlox}X8?VMv zCE?R^h|`IQe0mOXrZJIE&mm5edQ>V3t43#f!k$uR3X^elVCNp>bhy*HKFQ!MYIt66 zq;pb`eC>``Q?w-z=TS~x1C&u-jCP*+51B8B8YM1M~ zJ1d-#x1o%(=R;?_Aj+N(ooD|e^RghSjeY36K_qNrA36(}NH0EgE^AKq$egZpnhB!p zS?ly@fikMq6*;9$WUJfYJaRkAC<}|7v4SWsik;^KQ5F_E1w=v%i=Ej_q=m&!DHCa7 ziIdin?2+pmpE+$>mACM7rhE!^phVInQu z=@c=M7VdU3?kI2Jw@$k|%Uk%JGv+QL(!w8|x7w7q@PM;L5V8=zq#tnh3!?hPLFc}^ zv6Qm;XJ;^xuwVS_47*3jg#F^MQz;9H(tOmZnLzfy9++cJmLN*=Z%z)8aOUuvv!984 z11fW>>E-MC)0to(QElUIXRaWs_P?DXK~!C*oUKH{x=uM=OwugtI^(pn%GY(yIUGTf zr=3WmC*uWYLsZnRb^MIZq^HC#5DC9&rp9X7RH` zPZH$PSRW=$;1mhUREUjaQUtrakfg3q+)D7le%yMYY)g&ik)PFZyY}YsT|96 z$e#Om(AR;gv0+S3{{ee5LFTg9Dke`qLhEDIVl`t_3g5(WY^fgW=n}~dNmiT)&Qgwo zp6am~IY?A5s1ci&K=56-Moha8WmK!XB9=`g9R040*(@WU7gxsGF_Hb^%Ge%3VC4kf zVezzS?b!DF$({yGu8y^7M+E0KC&9vcv1LrKry!{xYxn@ls20^AR@fei@}fa(Q35#& zr5eOm38LCqMr;F-u#IKJ>UKaqYA$hY>_#SXE^%$FH52p;XI9t7o)n}wsarF+$@v5L z&KjE{NS@ZSBvre)JIwH7=^e{km>Ii1fuMz%u@-_T3vZ0IAre}6V{8r+xmM6P_7)Rq zVdK~eCep&jvD1R&X^(YD7WUj6GdrO@@Rf?mEwNF8s4O>&eaS@DbzAHt6Is`7vAPdZ z#KwWAcfu3)w%8CRUGbe7W>&Yy3Yefh7>$;(uAQm&fj86quvP4FK~x4?#YX>!Je@!= zVy$Ac1W^%d9eagH7_ru|B}}9jcg9vTkzU*x+ss5p?9Q0h1udMZ>Hoq_V{? z%EGKz#{_~sB`dZ}5EaAh*z;XcPjN-L9&W@61W`5{v8_asaM4eB#7;0@*AvY;&4t8x6-W|BiSDr?L6n8vWA?)r%XAcEQ}WAD7rr5OkM$mmGMmKr6r9uc zh|L&H1ji^Oy<^j+mePej`f&MrM57c8r#F-Zw-QSJ+#egpEAOY+GhFIwAh>>b zAU2x`t`Xs`hJ&#~uc79At?k#e-|y#Gm3c&#{7vLgtgav`8o$JzAriKPUt=>^M)vMs zV+BlPOZYW5`*qZ;vU5B(PY{*CX`)Gj8?O??+j(QW+rw9?#MCb)|!2Jz9{ zWlZGmqa?S>dsIs9K1y-zWklreqY7?|_le|!9vn$3yTd*pBKI0qc6%=e0?)xxSm|mD zbyacyVDi9jY8#iksVhjPE41)g(8j8{CK8x$TnT$ckkoL!4~b;h@O=bGO?T`6h&0_y zZ*nsP zq1xBMm%gTM5tDZ=Bx^`+cH4X;^o0HE7WW|{p{KXF1DMGEb&I=85ZXoX5Y*M&ZMy;W zeVwVzW?LH%j+97b4J3|mPPq@o1{0#M|c|sd^^DZLt36kX&mm*R5 zGTp8JAz6n|Mmw=(}=r^Wu#vbxA`wrN{%ftx3wTDgE2SvD9X??1)p)QTY8L$ zv@me(--w*`H0^PS#{F*H--$fhK3P1Q9&kG{dAVVVSTpb7E@yH{b4`$rZV8iZ@LdMi z)jPSROp4%UP9zVyCzwRe66x$F|3P)V4>K8*>Ec#qG7jb~NV>XLF_{Ll6eQi;YnkkT zISG>PZZjrV)gjWuoz3Js*vW%3J>BzysJ7b6z4}kAUG<$_ZbKsB2-(Xm7Nj^y-Z0bK zZFW+W3g_w%yMHi|qgH=6?Jts%bN>Erfgm%rkKyb86ACy zj7ZqyhPo@5$lg8F)lQS|vaJqx`wF7IAUxsjVIs$t(XM$0OR2GCoco?2WMOxBFPPx2 z7eqbLCc3*xCJk?Co(NH$=cf?cf zLMAnU;5|8$-DO+~{lb~dWOutDdD;@VF%ro$Zi{o|#XKg{+++VDQLXNI_nz~DX#Ny9 zw+`zG&%5mfQJ&6l`y`ObAd~M7P9Q$ac4oR`5=dVlv)p}xsQAov|0bC*K672|0!2gS zYp!cEk#Eq2?ieOAK85a5CerspcdH=E_t)GN+7%*$()TyqX30eMcY|9yAUg}(DNIh> zsR^>!Elxq1S!vHTO4c5zq-pQFyO}Hi@*36PABuU<$_nmtm z6M4hlcWy@}^47BN+!ajZonYU)1(yrW;T>f^x+j^)aelwMwHnIgY5LbRW8Cj%R3{>L z%kOtrAxToZ<$rQpUx6~@#a{5@fICeP<;6jF4Uy1`gYJ4J(u;#`F%#*F&Hi*3CXhZblR4=Y*F_n%TjR8Q zx*m~Ue8bFXcSe0A>Tbi+Zo>vdWR}mk?J|hS_?&awT|-3re%{?KNS-#VW2#u$KJQMt z4rIVne1AUzUTEI44Y5?d*0li6>%qI{rQXYesGO#GZx9J{n&!R3MCLTjdtZ>r+Vw~1 z8)BMwkjXXAN|JQFs4MK1Y2Iyus5qy2SqbDMSeWJwxB*L%492;lw>^PioGW>$jg$<= zxw6+o5L(wd3wkd1mL-r18{y5x%gDr1l#5bWN2%qRHxZG!sN?MwMCGE6*S9gssJ42w zH>C*?nTz_~w5CL4E;78DH69&Abm-MvfBAybVlb zFSym)&P2w!g;$}us68D0T6ndX$Z?~km&rtq8+Uk*w7^oZ*9dlt;jF2R_Z*WSVI3Ro zY2(ckM77m6-m(OO5o_Z$xm`qFM704#<8E(}AcvBqr}ubEiG*3c$6LWNa^%0qD#ay2vtdkd zy|Oz<=5x5`{{G{>xD@Fn`IWV$$!{A)4 zS9&+eY=I#H^|be@-9uzI_JZDCKO*5M(c62R ziR?SQy)jH=FX-*0QVU++SMi1ebL*CL9fSbVL(w%X6zEJ&V)vjSYV z8Q?XvQ6^98_7jnTUZEgrSNUUJmP0bQs~k%W@*Wi=Uz5AR2YX{!26wrm%;VmkfJ))s zcqBu;B{@h+l4SoH=B;3YHe(+g=4}^5MSi$vwZ&5E+s$yVQv$gQB0t=FQjkd^7f42U z%LGwRw2@ww`%sU1qK)!42r^l_m+x_X(z_rCtQ+%py(hhf_fsjEohM->2LCoyXbrm` zF{+AO#W%8!4Q23+16%kwCM8wn>7kHZBF95T+*5Ik$P*gMJO&)1TLo~2&h{-UlhhVOZm2O?2p_TPYCmtH|p#1c~x}y*FGCm7Vq87$Taq zaJ`qyL|VAsTgyaR_>uQF6KUbcUW>=c3u$4o=LwRpO<)U)y-6d;!qw2m(85o>Cq@$4 z1K)sdhB?v}ZyFPP65(wvpLs=0@M(qQbMKx}SZb!047<~h0@>>I5`<1gp`I_iVNCEj zh^4-ieK73H+r0;#Ks|@TF>||=xkN@o5g83dWHfe!rDQaAg+xZPYcF-IuqPbVzxFB<2`&8EyPS!%@N2II6KUbsUIr6s;Wu6u6KUahUUVG!E-l>a zbrd8|TM^a7b8xSB3<=C&gyBzVG)QBNF<)-y6k5db;1sV>2)&7;7e~By6VSpu?VsNCi&sazVnHTp3t$F|_p~4LT2Dk7B6xb}xR*MK z$dS&e8vcSnnKwp|Jnf-En)m+Ujd_Y>_QU$&OtA0|FKsf_^%`i#(fBW~Et7+TXdZsb zn88&DqX)lM=|wV9yzE zg&;I%L-LO|Wg6C{=4@xZQYJDN=e)tsp^TcdUGO#tqULNDyt>m-W}+r%XcxSWg1~Rb z^d?V};xDlb&e^W@;Y3(`0~0xCONpB=pq_kf_Rq=M8(?8d+-I`)JdsP|otgX&-^lQ! zWX1SsCSSoSI+9B9IZURoOy&48CfCDvG?b|l-zG9NAC_`V9k<^aQ5adw!#!)9en~7|_b>hnfQLXpt_?LpHXj~mXl0cS$?^nmuUc$O4 z8c6EKhYF&egZ1L0iG*{2dhuLAHYLlJSubA1M7}#Th#%upGCmn`b2iomr_?z<*T&xx zgl1@GvtU&%zKjX3c_O(!zK#j*M?#VrFBW7|vTWgvY4x3ZkOX zBtAWXtOVbi#Fq#{(LmBPUTdDv9L`&Aj@Ks=M&ssqLqSwDZjSe0BBRkPzL`tOXxtjl zc)ffyn#U&yk}qzITo1K3k56NQ(LmBNK8p!P1IZonSD5^>lSrHRLP1n~?u{>5Ts}Vc z##bQeMn zy??p!c1)x_@%Tf6sP+?&k4PZ+3&`>Kqy%!phW)E??OheaPat3I;;#v!V)#J3h)C$g z1Mw|Pq!$mwcQKJ(JP@z;p2%tV6{dZ>ND%shfb*zM@jVFypShjlt(Re`JgxDQw5Hc3 zo-c@cN9!72!$ht_c8`}bk!wx8;;HXb?J_=n;++Ih?Wa$CfFLSjed4b#$5JYTec}s= zgzcwKd>Ip&_dfBJOr-C9;-4^)8GJZi%0#xPN8@Wh6m^B8asT)pLGrZmFe||L^p8KV z3T0+#HP^sR;Eo~`0)5=M8Z*PcznAc zYSbDYud)Vf-;^vzt&#CIf|Q8+Qt;QKM#T%5$h?n^|1C(Kwh4Bo;V#qB@wBz5Cr_Nq z#u0l={BkCEE*r`Ccx@(l2M&@6@oSj$UrOYuc;f^;d%>?MaWg>=lIih>67(P`h)+n+ zgJf>}btaeG1~;GsnHOKf zX!5zUDn5+~#s_DWtKx49qB6KT{&5011e#aJ_X|Q9M6xE{VT;flK6lo}yAcU9xHjHf z5S78T@p(*S2G_;6b1B)z*2i0aMmEa~Zip`sL}hS8d?k}hA!3-FkK^l^$P9iG-^@g2 zaC3Y+6Pdx!$pdN%Eddq>O1a7Vly6B)xD@j*nwXnYkP!7{SPeH9-q zh>G)9@fQ=wafs?y@fCtloRRE|=N!Y@RSb8P&AH#3s2Lw?u z{5F1)iHza*@v}^141bJY@*C>O(_{>PidSVKV^|um#RT?WCpMCKVc#>*wFuyiOgUl{~IPUgE#pl)N7HD3(>gMubY8-C_YGT^Jfa8o(0YQ zxkSQ|q`5y&5EaAb{$3_BhPV4^*N}yBBx&gnW+G$Q+TSgRn!&dA_c4+4)w}#dOyr#Q z9{;2uDjM0o*{FOpvVD(e z;F~y}F9`h6Oz=${Nsj*#lQ&qVt^XdALGUJyGWYpg1X0hX`~6)^WDM{3Pf)4w4Z5A5 zb~9S2qT0^CE`d~p7`F4X1fi%RdBFc(5Y@)o`v-`GQEl%Z7DPq0z2EQ_tV_)|I{G<+ zsD9eXKg2{vwXYt+f zb{_MawL+q79^~JhKya0Bknbjt>p;(7znvf|??e3lM8X;45I zr2mT`s`iooi3D;J)HTvSO{Kz!jq)$B9{B_r<)^l$Xvm0-@(Y>Bh>i9e-BCVbqy1(~ zWW+}Mo*-lo`u?Q9F@f9)_KflOQ>oCNv3`X+Nwc(PtY3|Zv}dee#zfjP-tT=Emdew% z!Cg#P*LeS*Hc0ZdUsu34YSu20e#UCt)dPjTBf1617Uiq5;9uxUS z_L{$riF_lQ=Wk~sy?E2Fo`tomHN6FXeJ1k#bb;Sk5V8l?u^0H!1mb`fZ~HwG^x%8J zLjNfyvKK7!XAlY3`4{)eP8U~$VB@7uHRY^HL@-92kEFMU$bHNV+W{fng2ACYnP^o`&*X#GXzo5_|QL< zK)OSzm43Q`dguv)WR<^2kbJGuv}EmJAglea1W{|AYyG`UWD8&G*Ednm7x?g#9A^7>n%Dc~|FpKN=C5!?nuw{s2MpwE2sZ zHQZmf-p^-}F_rf7Z}4AZvJ~zY#b?t-{{trSl=?>hQ$Z$cG8Y^D{Y+#oKK9Qsk-7N9 zUlk<_vF{9r_*Lacs|gHoUStp%aTBiZUdEr{yf+x+Q7 z!rr~je}Rc?tK0lSCNdY>{0#{NegDGOV_3U-nt$Q<5Jb%Zw)=%l+;;oVFu}JQBzyc>Oq#LG_x>wPDzeOv{z5_2%;6`0 zzON$o6vXEzKRZC8#+C!VLnItq4)|@E$Xsn-9%C6fZlnd{n8>Ga zrC=cw`4p}ie9A<|r&{p6AjV0E1W_YW z?Vw686~nhcrgrd*AgX2731$!pTV|c0fQj_0POyrJw7G83@gdX%m_-E$TpS{?0pn9&(zkyDag_H!7sQ44f_%KgvqtRKqj97Sqc%mE_f<|JOaOV z(J=T>5EbV}!MgvDgMz3yHwumr3FF)-_>GAi0U8B=Gm-7EQIOi7VkqO288l=fv(q?O z$3$kQX>eK)_;pD5?ywxZXc}ZbMp144D3M!(SxiR5Z*tXwvE%mObwN}+Y!wsq9V|+PJL>NV<_iMf z-{EPFdhQ9zn7qjMkl!0r8G@xsl2FgbAaid}B#4T9R!}TRzIO5}IG5cOzM2JJG1>D4 z?6rp4vxA?R^zNf+KLRm=vIO!o5HqOtczFvWL53hI8j)cBFtSJ590~R@kv2zz(@dnz z(ct6Z_;6*G*9Vzr^vXmFxDhQQA znRw9mBAFnVO(e`kPOy`Sw5M%Q#zbmv8|03{y7IMwke$Ck^L;_rC(3);G3b*(u=b8Y zp&%upg%1W>5@hg6)H(P%f#6r6&cR_S6%`@iBu@+OqkEPf3C=TV++5RIgG}Ebb3Dl` zJ_@&31L+^kW>U2W$qWfLFgbM=_Lzgru%KNom8$+G?4yCRoFjutOyunrPXuoY0`EKA z3!V(#CsJ`N&#}h@t5^mj-wf^%8WU_2M74zR!S0Lnj1LZ8q$f8xa*>|g;KD_ECI%Jq z&}OB1VsN=2)K(t>za|DZCyZfY0y2G!K2Zc=DA49ikJr|S-Qk>NJLmDNf z2h}D}e186eZvU7b%oC&}>60!}Mw^H-B}t2boSp+;pn|?kD*c)&Mw{n@wSp*{pAR;% z3`TVlSonN!B!RpHq##J0M4BhnBR#W&Tqe@y*}*PBl+AO3KLyFtq|I}J7Eh6$QnqkT zP{?E|doedS$3)sZHwY$UDK%=%4Z1Q}2&2~9VByQbU_q2$uLPq=CR{&!C8+*1>Y1e# z!)}E4pwuftT_)J(*8`asWHKo|1NW~3nIFtx5`kPC0J139&g8S@$(jT2%S(fkO!~v@ z{Upe|7bH!gy55BEAV^jO)tF!(#M)N}y#>kFYC+ynrYI<7vgFegVb8|EeFpU?zdi}> z{}1Ub2(`M3#jsa6cszkz1!QwDg~?9~Q$(x#EGSCQgX6}wV2>cwzwozuzX<!x~6Zyh#aIRkV#PLui(;ILQlm} zaLxtqBRUgY#-wP!Bv&y>`cjhmf~fX-)qW-@PLO#4>N*qjd5P+}yKb^J3?g_{eqA^3!tv^!QHdV*OioG zF=_Q6d6ATp!{m;iQ-tQEoLoUj&-?WUI)&ge1gR;`cnydG349x8M9#AJ19tx#ymH?zzkR3)B!LMf-;ZN72t9 z=OS9t9IRmujjLaQgtWUADF7*|J;d@ad}H%L_`YA7)|};f2bkr7l++BCdaxB3bBETQ zWh87V#+23iv&<++6ni1(irTj%Sz-mqC6GI{TO9Kw{GwAu=q zS%yKXB3t1NQ0-$D?5ov48fg1jrodHFV~~fne_5U_C8de>$XuConKDVDIm9&8+Oc$o zR&5Q^Oxr*Lza9L0vgiWRM*EdxT0%@;khWUFJj@5~cENpzOpwl6NkyiC=voy;W`UU6 z11uN5NTgI@t%V|(%GRD%1XH=%i!9F7ngWqzr6xPwh0xiPUkho{6JzFYRxZ8E{7X z4Xz7%X*Wo6M4to6Vl(8^OKUSj)XK4RI0C=<3o=T(m*u6+Nn#?%XstO**5G84S2WF!nVKY~L(CYh zuOf3mUe!jiG`W&27J!V^=CXv^B#Ct(8UwOGOZbr5Ili{DP)k*0BE&4z z(iO=9d0%@#ioiO*1F}SGr^sTE54D+!tN>Z6IZLrD_;TQFiQ-d`Wm^AbQa-yS-x6J} zO(2om{w2hGq)k^OA7q8Lm?S^-$j|U~K{%FIX=^y2?VQgl4GMz4{NnyOWp!Nqb4pxN ze&bYu{K4|EpDM*~!!>d7+iuGc1ETOCA}$U-RrbM z7F)=^x=s_zY43<-ajDEw0D`@DowkT17?IX#>tzh>)4ddv{*lb5-GL-=7-YS67Ypve z(;%N|VG{ZN&P9+7+8~k>sjI8O?@d5!Zq&v}Nf=O&B>sjgvW?nOnNPg`Y}9ruas%?& zsNGN`X&c-_)Y4W^eJE8akj>h?B=T9}@*rPpwu}*@VBNSDNWM0R<+kO?B;RORES=z6 zO!q;|Hf=7;eNVwz24uUoj^)^pWYGfTTkR4HZhHri0z2HCB>OR^;?>rt4qf$Y(C zlgK{*Cde<^8AX;fP7uFp39IEEyawZ84#fPX6=V4fe)HvBko{Us7Tm6-AirzNq)4;WH5{yX4;>XUV zWcVdhsNo4M@#CN;;C$nxR!WhnaNT`MtFB1##}mXK+NV;4|1HzgTAq~91Mk5~DvSxI zwE~uhn#gOk)7tNx>br+vMH)&!t)1YQdtZWICm}hZprhW3it#MxKrUS^PGG zLT$)rR2GTYo*-^$OB4x#+|>3;iMM=$eozstPlA3*5$t1$`ejA1Rg?7O4Z(I{A4}G! zDS}&?qR%D?M%onpmJ)+Gr|88$5AwmBZ__I&f;kt_>q!yk7r-wcK}<-`Rb)TB0UFZ9 zM#?AT_smp1BqcrytRf-^<1IEEe3SH6T?9{X<1?50=z7D^drfwEn^uSb9*ybbTy| z95=8H({=GB)lk%dF9u-gW%LjUtdGT|97!HYDT=8rQ&IcimX_5UDS|nd)7vP5d#Aj< zNlK^odEBlF z`W=d3@2#L$CXwUD1JGg>^}?@YYv5jf45W&#eH}~wU+*hBDkfs z^a-5G?@_h&H(C5~qmDkCBqOw;QxaWi-lMOT5+6zK(WmEQ4dvK^BS}3yZJTTj?3vgP z8|W<+!I9)aeIQA&n$buftq9hzk-kW#3YGmiNjw9o9@dw$Gy&;NvR;aCX2E`kmB7aO zF-4BeOb|`<&~{lLjOh=ln(7S|ng1rN8tEEKJBS$yF)j37io6Q)xIUZZA&8j_@`Qe# zWdO(wkSF!>-^#LP!q}S&(ovtzf-!4AbiMj_GG@Xa_|7DVtv4Xqny_pc{00PwqxWa| zr&+St4-(UlkpxF>SAED2n2##EeniO@3uh&Y6X= z_~K}HeI7}cc=bh?XF{qTdhwsAhVlN>TQBqfq>3UqC+V%%QUvEDee?z-!JMS8-c*T6 z{1#SV^uCG|0~w&7C&?4~RyqC*)a&h}`hf8)cjnQi;@&w3Oy@4Vo$T+=)BDkgF z^`{kyLCkCV2u1pUOw^Ysg6Eyrbz`^eQ8=>UD%NDZmlQD%=Bq;?)nvWp9*PmuVP*0@ zNHtkESk8bnCh5hJTO(PtB6*qREaWqa@|ns~4c-fQ9b}4rUP>r;hFpW4q9^W^^_iWU zEV3!4D2soNo1)(?CEk0d=+zWi1jq6e{XUW$ap}G!F$eOQsy9>QeULYFYaf=CBQzLW z)`Dc|y%gCDlC4krMaEQy?fMZUN1v(436ME@F3Y$E$s!qknP$E|{#V+H__(n^e@hV@ z)fec?q=;)DCy9F@)k1x{B3+>k7wcygX$Ub(^`gH~&XnpgkdO5H6lo8#TJNmLFvw?} zK1vZ6Vm9jY6?qQiD}B2nBSF5=&nPk(q(D#GACx{9WT$?=BFjMb>YZ6y!>GOy;=k<+;C?7h%<56%y|AFPr(@El0IG!x z!?K3{adAN}c0`JQTwKtrviQfv1^odQ|G2oIw9_{i~4#_wG@sE zoEu-%n;pfnWbZ8lTLCdF*jH^k*fVeF-%E*K zN&TzuV_6FK0@^`7H}%kQxuqzbKyK*{i{GDx@th*qpOcKyieO(YV$4zm`)Zo8N)hab zw;MYZ!G2iIxS$9g7gda;6S6*7Ivy9*jk1bhpRZ}uR|I?SJ%+9b_TKxAAtbV|Ip{ai@Z!Kn9#Otqpj++_giY*?qIeDNxLNgYHp z){j-}Jn( ziRD!gjCtP3C)twZ_ul@-Hj-@d+VUh(56T)~>{g^9$P30Xl3+$X(5Uy9>@8g{B#CAa zGtg)!C4SfSC1cxVjL8=C?8B0@6^|H~8 zrDCTTj?0Gs-42Jus!;L(aCvu_3K|UjlLY5|hDO zRogB|m18u#C3C(Puj4@8GG?;02DuC}+qlGHffRu!;pP}If%_y|pi}^P+bCd}UIv~m z1es?vNsuupkAl2wEMmDAMv{&o3k;DcW3F#b7B0v_!(u7*m6S!s5|)IiaAyr--Zyrz z)a(m))<6~;rIKVmbzzhk4)TFvk_0_!iP1-qS0QGJF_k1Qp$OzN0pvrYYBJ`NpWv@! zFE#3s1oMHl~uCNWfX#dPud#I8Tx#R>6I=JdjU}v?5r# zEb9l5PYs78Ul`EC@tN$;jPpv&VUP{RkWi57EXe1^S}CCukI1vPuq>Nl?ShM!RB|bCzg(I9c3)n6HhnA}K#6h%H86l5AQlC;^gZv`Y)>Qw`)B zV-QJ(xPB;E)Cc*__?@L2@Zvr`F3@IhI;-TTtdqIvE`$;mz8L0C#kfX-= z(*IlMV@BHTBncDY-d_&H95)(D5hx8{^gCfZ&Qi>jcQ8*HJxGG1;H1${5nK&BX-roH z&+n&lZD6$gdj8XFrSs(1ZJ3#(2dXdPo_K*zb7=O-x-gu40pYxwL-eSQ! z`T`=-Ug8g;T7?_TAmN_4TeEu<} zljNt4f}C*=UNLf|#E-8l#-uWsN^TdfM->{`ir{+Gbz?rsmPCJz=wD-}5`(F38V3}? zR0-zE|D;MXE0?9R=!}L>UL~6~S*GT~ybhkcN;d0Bp)2TfuocN>8^vm4!B!k7P54G zPu@c;VT$syeOBA zQf8?N!G5m}sY;u z^K}xrFSxzp^$a)sqCa}W#OH=hkLHO*W_7J}3=^GGtpw~%TD zNFB3!WvU^Sz5(Q3vtbn!InTuF)BDY0RZ%8{T0?#Cx}d&!2Mf+2@rbK$R$@tpepmy> z!}{i3EdEMheX|kEDDIi{&E_npxo6flHJ0PtGwYi}q=+xzi*2|@UEiF;G5)=+`sRBq z{{5Z$=2{m2{!V>!A4`oSc_*vBnNUq`*RyfCSBfYHCs4N4M6Ays2@BT0sctVk4pkPNCVb6z$!QCtLh#N0@d zBgR0TuacCngE2Yc8MtOlfiFuwYMv*NeIECFbF<|=R3GXar6J}qbB7dhb#0QU0@A{~ zEG6V0xh>74d*xQV1v%G(n3iTKMH-MaB?*q)R_5a*^2lutF|EwbG8L7MGtX9LSCU}9 z`ncJLL_Uf7B&2F>4pBq{X=83;X$AMG@!8b2W*&*$(ip@%Y3@{{CrCSUzaj%bI+z!v z(DS0hKsuV&SjNLsudk4luPa;rN+mdgNNSM;Bgxa|LyAm;n5WHVip&J*WcHULdc(Le z7o@W}N|D7Nx;aac)gY$1kp)M9%^(r8O}$_b=96?%WCw_2_9DqoO@g~rJ6pl3i1{MR z#0By>*r+*%LIAid2xB(kh)Abrfnir|_0S@Q{!{M2XP zfG=V0glC&ggQWvJk6nc18J0R-<#V}x&4E%9aIRIHVuq2(vhE;Rs0fz+ocSZ?$21I$|_ax3m3DN#S@Ef0YVG@D8hXDh-zdXSgQ4lMs{ zgS%89gUu+*r~yf$GsqD0IhIqeCJF~+s5wTFZXhq4Z?Lq5D?7}2nE4LNLQR%6++4x( z9LJ0>zhF7TF(b_%Sl)s?_#EUj%G{^OOCY1o(hX?41jdX88Dn-(WCF-oGn3`HMTufM z$T)K*3!P6vGR+%`yazJg?DYWVoFUdjkHR_jYvyd0`@6!^YY;QREd5~6s_Q@|nH8mk zMhs3CkHP+W-K@uw`z5S4lRU<j}F-rkHbC@Cj{isCGcm= zM>NFJgY~FvbEXt=0M4+*AST_Zw3x z(R8~nAF+H>S&j*F%}-cfdri*6=bD>YZijrzKw#s0d}{nuRQBus(km z$U?JkGt4Io-o1q1|AK9Q-~5n79`D#1@0)8C!PZ!87Hv-3PW!zs&quSgS+rRHoY z;yU#CicrQ!=2@0kSXP?#9;1B3eb5^CM9^xpKTBO`)wYn&YV(N}GNvjB#(ZpQB>AH8 z$MP2hJ~mgi!kC~n)|mT9GDIzYVrs3qNot?S~HKO7CdQy|F+INMH2Lu zPfe>e{!6xaVoZ{F8tU_@IhaJ=TgUcUZ;m6$7yjP)%zU54--Dl-M@c4#@E!6Baie*L zrS@O&RRP$rjpi*y@J#fjnbrnNpCGIq@Qgi_zS$hhk~{)_*@qe8k!OCT z$RJ3SZ=O|T1jsfs){g2-{bxK#fjM50H$Z+g)7xWAhS+;KQ51!C-f8BsEQkIx2V#CU zS9Xvw2LHa*Uh@=7xld$o*=v@6O2)v?BvU)@H49l9aR2$$e6S7#$lZY25f=QsW_r?U7*!9V5~GM|wD&g^CLI~M;<+so!Y7XMA# z%jR*Gmhkp8&U-JL7o@~%c-g#74{C^SPF*o8k_0s@G;5LsH7qnW7QcptW>=X{XdS#G zk7Zpo`%8(Jb=BOi$Ps9bYvw`Kf+Y5JfRe1)EN?<897&R_O(d_yYnWu^lgN=I1Z5>#I~2Jc z9up6~R=+tfnMEs}{G~kOZw-+#1BK zN?5B|%EOuiu3eO{Hj>N~#V;g^%1~AbtGX5JQ>n)Pt$i3alB?;Q_4r{3**cxT5wTfU3%URnL!5Wsg_LBrPtY94@ z32IouDix;Q65sC?tg0jv;{C9qRaZ)UFITi|Met4bN>(OGP*zoIDoIdQRcpOW74na` zs@7H(|ER2L?PT$fxT@Ac&c|=Vs#YlSzk8>eRap_-_UcvxlAt~{tj01XzFjq}HY7m} zYgmI={IY6TV@TxQsR=EAr!|N3xewlY#&dT~>pdx=OHa8XRMT4XpL}Xs(7oX=zqmhY z-EDm%Q^i~LZfg(cgHH+G4`tP|4v_?Xqn0(urfm;lJ~)cjwuZA@hSeVYCEVK9Se6Da z%h_pdYnl|%90b>_YFlqhiI-m6+Deii?+JCR-6TQj_gKkJ(DFEwx!0;IMSQ@kb9Jre zBtbrPElo*~$N#t0EHGJHf z`=3-#Sj&_c>_2U-?-W_y1y=B^Gg3nR=gM)uy;aCk>N#Ihx?z2SF`>PcMiPtw?X4y( z{s_?Ca!6zivGfjBdiS7JarAr2S}!H!m(|JI&9aE=)5$u*GV`E3zjw0!V!6y;_UL5Y zVEG{)liY*K3bi}zr%Gcf{ifWmPF6Zeuw9+3$|S*db+X#A_}kUV8cibi1zzuTw(9o` zwhOP(G|NGiO%ZFelm!2ZK4N{(G5DN0j#?4x zG)reo9;XrOl9c#}6tRZ)!Wsts$F_R)4(dD>>f>1J70CkWVqK6D`i)m1dRo_5Do15m zJ*~t(luvxkp{G?+N~q(lM6nR^>1mZ?sW&oNd`MD_Q<+8NxZ2aI!{Q$WJ*`tD!BNoD zx?Eb9}@u)M!oUeV9DM!X<%_Rkmdtqc~whtIcO zmqK@rnm~Q#TQgbigRfHHId{I*Y#`+${(@`078KKlB?+$arF4`MYMCL=JM%4zB-ros zt-L{)PnKApkSN+hKJQrjSZv6rGf1wL_9Dh)h_0Ipt(O(afP7Y2Z%c`f5^JmlEdB_v##$;x{J~qg#`>6J{PXD= zYa>apzt&johW&31*IMHg!EdmCV$GEzeuTHeu?^Q*ACUz4tg|*Lsc@fuYHd{n%lOp# zK@q&4vfkQ9BCoCRiwYa8V@eF3(KcFl4G(HK4O(N9^`I271A5dfkT0xeBtd`_D)y+AAbHjbDG7dW$+On8_`N018Z-jylOMlFztvjB;`g6? z>i|j6Tk@^5GA03g%O_CQH`bAn!FJ)6eq&u$1pjTDRdQ4igLB&L)?HFWWBxAIx7K4U z`#^B-d~07g={$zq9yXWc|rH$1)DSp@e7QUDkD$UT?`d@3Kma#w|UOfM13=3U@+xS$DGd zD=@pQW+ZSg8=eoxKDOJxa!K*uTHS5+rI=twwA)%GCBD|Q$J!)C_-{Pzu?ko&SCUVe z?y=gx614nQXpKFVrAPtDUTc*UdM^^+#{I<-V<;8LE{OTnijic9l8?!CiT&0kk`oDj zo%dU9Uk%E_?b>g3QUw2Pzhx_e$Ibz(J4tZt9JHP%k$dm}lz!M6NfNvfcEp+}^AXJ+ zlq2L3Yc>nsp~ELAj#$OV2K6}x`5dtxROAfEQLC#W7eS6&(-ip^AN=sgp}~REL*P0mQM+_Gb>o z!{u2H)RSv*#lzJ}WX@Qh;^8_nhH5wr@+lsEOp#2GQsH5W%m7IbPm>bzXFFxXZ?n7! z-*B4;F=fLGSo*;CGZ&C7C6PTGOD`M#m?T?VhjDcY#T2l(Tav|UkaFQ>uj6*XwK=?t z_XSA#@aH61qBY#(I1ExbTtFhnjZ+|1!sitcdtt>RTzN9J4{d1$kOtvgMIHcoB)m(kYxVh4K-1z?No$IN=;cCWE-)<%-M%@xogac@Ly(c!we@K)Qz)O$~DX z0;G3%+cYVsb|;FTK>CIEvAhJbm!!)Z!FFM*_6zq`1plpH_yt9hK5T`mt~<`0vQ<|u1LZ@cyA&+ zlOz}c#)UV^7%CmNeOx#>3-by3YGyc%B$yRshN~-r=lAj922#XgxC@g8WxW<|m@Vs5 z)lC++lf+o+fm9}$O_C*U!F{4yB;S+B(y=}h!iN;84>1$NH#na{82ND&ofIBFQi0paA;PL58m6F8g9ozXBjB{&G0Ky z;v>NH@MISMnrM3XEf#-7njT)n;$QDf53iLHzr!*;yo<%3vrP}5ltO!_Ih39i7CBTy zk|#iBgv(2z5xXNucKA*ff5e^{uE*k!*fYaTNaUSaY>k=WmLxgi>ttxLN8su&+))t| za-J2=W%-F?-U{FS7S>1ZFArj7hZ~XP$7d;ThaV@IAh6H(hM2d*21~E%aIAyO4fkM~ z1!vFxAoIfgNixMHNHrK_ez@BlEGt8pi<87CkaxoiNMz3(50V?+Dhts6QukpVQS7j-HHpH3m*Wr4c>a{=QJYh?C086LSzT~j@>m*yk z3t75fle6wE;pI}q5=e#nYfHG`16jiY*n=&ghFijEOQ?q8$|?9R#KZ7q>F^Mi#6OZm zdx+T@-p{fXzAalelQC0nCW&Pr--U;;%!$i%DIveFeixq2G5-DK@51X@ z{88e&aGoOgTtPv&%}27VDz)Wo`G@cZk{sySaHRmH{}A4;$QF>F!n;Va#Lp{}#ZHi2 z;b&H0J~`qLq&f_;H{4f|KS6#8zpTg=kl(`NNwP$bRmmdpS9rTTd|rtu0dg>WOA)Nk zk#LEXL0MRz=VB`EqzQ$%OdWawYtQ6tNmsLEA&B zE8zkb9P12_!tmI2GM^(bW=27-hKGDc^$}hEg}cch*TP?s$Z-tMME{0&kjNVLhnSn; zLrM&;PZkqE5+gAdzmFwG zMzi>REHSc=BuC7Kva%pmQl#5P%qK@Az?)MjDUo9&*`nphWHBG4NTm8EjL8-~&nAn- zAfZS*k{p41XC+8##8TvQkYbUUUtlVEZrTn~GP0B;XrI!N&lJJdxII#!$ZwG9j>zyY zF`o(X=cv;ouSpRn;f=zR5R)F6p~wZ2`HI{GDHB!E1Bx0y72Am4hf=aq$7*JFRDfQv-ta>TBLv^ zBlIh5MIFeedSpLIj_3}hKSa{(Ys@)EOk%k+GM7a5s1^`YGqP5Z_8_$)L$*?VXm){D z6Zc1ED}q-OjU#!A;MGK{$SD$;b63cxL!^9ukPlv8Sdo^B;Ppk)IC`23X{>A>1{zaM(U7c3lCDcAYVmB z|14wr!hRnB@=atBNtT!kzbrojWP4=NE{w?%bKsZdr-ST>DBhyJ_KG#7mMDq5^I%mN1TDKj5vrJ^%J`~yKOM_gEOec|}UuBSMk;Npz=yxNs zRuLTi{*6@o74w-O{Byz0NJB+%jJg?_rwEQnw<5{E$y9yz!aLVcmaqr0_-BU%dq2zH zF#Bo>F$wl*mZNY6X-QJ3#5_f!?Uy-YDgz|ZUe2-`?ttTYC&}K(;`gW|dq0ccqmt}F z2QZ%;@h_x`LO#j%DHiNweL!xr2mLN%ZbDfjK#JKnSk|!=w<8BJMz-o}5L43bPZG3h zX?v6+bWX9SD?;ZKyV)VyuHc+vx0gcal(!(~bUUKR0+Q~EtN;OitQkkZ0j(UF8uRFWxE@T--cD}u`GTYRzKYl8ciDxCoQIg2cH&XYN5)(Mx!Z0+G9i9dQrmul z#lIG=Z5u59^>S_d87X4qZh7yfw%wnld_>+gt8I^F@z()r+vi#Qb$~i{$zxcbU>)Ed zyB&+a4sfsCi$wmGD9*m_wTCEz>i~7_wJiQ>^ZoW8oX?V5@HLJn;aq9gIWEg;hVPQY z*|UM&mjsSVsB^;a@Z^lWPmy9E57~u^+yTbi9c`C#BR^xUy(Jj zBP{;4UK6`7=i}dXZDJ4Qm@nWC6t0;!v1f7&-aoAgWi_!Ev*7(xDeFnH#OgzEE})og z9OJ*!_lVu_l&rJg4q)Z212E1vgq)k%mq>C1?qz(lthp`z zz?^eLr&91$6Nq`tuFisU$hIIY?HwfYST;Z&w?n68eSU-0uoy@iJ6(}pAZ_hj7QeT& zw>OXky`_VFj>YdSPub!O=A0qQd@NU%p0X>F1pTL@T}Kh@KTq33So%Y&4usM*dn(IV z7TsRN(pFCrFGGxB?_k0AmtO%f?cwL-cCCdgpiGk4icA9u+o8XLm^mP}T~(3yL0r25 zNzn39yFbflc)xoE#CY~BmKvjy#b+R0>~}~qgx}|%vG=q1eZIS0={%MmTy6ER>yXI3 z^9|(F!)~R>k03p5R|>6S?E&d!_hj)`v3lDBSo~G2-u4J3A6%2^ZBJDM+qt*>vlQ_} ze|Wa=b9jr*KFN|hP)c9>I?Jk@L~#I8^|MR2KF$sRqw$ody|bH(~h; zR##A7v^%n7f}95#YFJyJ&43s~-{C1sSop5>8> z@Qx3pdc`ha(bD0IcJOS|tM(z54V4lpCc{3>@-|DRopwQP7seF*64o#5`YeCJwiktb zCfdzder0*xp2KnpQehiTu|HzDKP6Gz4ymTu8(6BcWZ6Hl)PyHh@M)G=_6e3Dup(3u zQq8viX6eZ?*DiKZ*6?LmUBOm;$F9aQcsPtmkSf=HnB~(C;cJH=i|i*9X#}#^?!|H% zVo;XYnJkw^CX41EOYJO{0dNBkzMqXtRWtiI6|(l zE0g4il@Nm?%UW$uV)4uR*cSiDvbw<<(Ev!b z#va1*{yA6|C0V7&NRmR9ru?MkTD#h1nU4-V6G#5Fc3qZ!Ah|HEBKo6I4gQeL$zSO)T>r(=HxQyw@(gyl-Be>hX z)*iyL6nY(gnPIIxk7XB#+=@Jwt9K-eaj@-c?Pi7CV$fS&CmF=@6tv-+B#T%UK;M`} zvX76bUpO{)}Z{bv%e)VNHiR|HrDW6j^6|EKE^DUp)g`5g|_!)>< zXE(edYxo1aM}G;c44w-%*v&|?#OJxN&O|Z$S&j^cx7bKp-o$)z#6Vb!t3k4gWfaTj z_BNKnk?<8_h}md6w`8h2K)&uK#3p+>OTo+XyJ}z9YgvYYJOHV_v^7zd%DN7vKML}d zot7X4zj*vO$k+A^mb>8IaeI(0b}q|ocuLs<*=m1B0>9on55C*83a*arlN|F6NH>Vt zZWprDs1D!K11Ydm6J=SW;rjFikRR=Il5DXRQjG@LX}3$3F*o5!uqhyW?AeOU0{O*0 zMUo}1_JHF85&7I(z%OOhixSCL;bJZew94P&we{(j0@ zNOjDf$MWo&B(VwPxSd=?#i@^ zizJwnoU@lHF*sv9XCILw{2AkU`@AAJW4vGw31NNYOb_oW{$syE63iGc+h4QzGsY|S zE*5{rc*Q=a>(`vZ17*ZM^TIk?&AGx_a}jM+WV5k z8EB1v?R_lo&W9}kxoO{13{&NZ=vCS4ZrOcUnnSO<4l%+Rt;lWgoOGhImgN-u3Lf4g zOLjV@VLtHA>~?sI7GhGI`BLKF8%%XpkOVbMb=GsLQ}Et#6-brpoFd5}DzLd`TkD!Vf{J+nwJ@ zg17wAoqtGVZ^5T<%Q%T8u=GI6J0%ps`A;RMniS!W{8gNKBw29(Cz*a>yo%FANrgFA zb)HZJ=b1H}up&5%yUXb#CA6-Kyw1Jb87xKQ!hEb5Y{lKqIFg`0*K%@{e6Xxq&O$|6 zL8^P4Po;!b81g&P^_;Jy#8)rtIeV2N0Ho&<6=Qf?3HY^L*`7)hlH9zg#$vGs2 z+7Pb-IyvVR!L87of2D-ptstKUH=Uxl%d#rLoCKe=G@bG!vNZ<7-Z7mvGKO--=awx; zS44-caGYM8&t#Z+;+)NM-c@3-bkA9?2p%b2oi8|*KWFRae5b@I)HmUwGlvbYOou)Ur7GA7=~`Z$j&g7taM>B#x`^Qh+? zmt>~EKIXxeKJUDsq{15ZcSb6LS9SxPDV&f0{?3ce`$|68=U;SID5)@?mz)iXU|${T z6i5mAeRY`gs}h5$hB+q{sR?~-q*FAVS|jw-{qk5ISj6vNM!%%0WFs4ysG4bb$$(gHkoQjW#JX{ zBxk;qP|NG`&i)kVBbG0h%Q^oPXERB*2*bGXPgA(kbjp{(e6mGz*gMZbSyP>QEKfpA z+tqOG39l2$7~F$lkT;yZBytSLGwhqr5Jm9Hce-;?5u8=daFWYnK3Q=08&+wcteH-P zBzU(g$LY$ld!O8j9H)-9RqFj0k^oAuh#l zi^$ZVWewkU&Xb&o zzdP`@Bg)~npNKyn4nlwVDbohY--?>+w4j*aDayG{JCYo6?uA4#0$OaYGn*usea&+g zC<1c}G0!=oN<(}iWpiQemx3Oz30532&P)#j8z0v zEpnEyTuy^ESV;B0vsMvIwb&_81XC?>uCX+~8?KH)K6Hv#p!x`u1t80vnk-%5I~B`8 zRyoV0&??p%kk!s17Jn7%W9Jk}@SU!Yo%1B}J6#(f)yIygh-GDpW-!Mt4|V?7X~wb( zzTGEfHc5sU@`3yAwdWt9tk0YRMfQMfa1JT?V9pzzc9nzDo4|H$at4#g85$m^ zUpNzFOsL8sc^>@Ic|(fm&CgYS>Ey^%^mZ`*D%O|IwaX5*AZlJA|W zEO%6t@}tv$<(*KH_y}A}id!0KJ z!8-4A?o^~I$S=-)ir8>>;5VlQiR>GRN8mRpoR>+0+4~V^Hc5u4`C_st4lzfaL3d&4 znPR~%corMvgtLeQelOr)lDHG(w6kB4IwTEhQk}&r9_!9H9q&fT5bqv=>vxDb~#$=06;fqpuE%T?7%Ys)ots&+wN2`r7@WjGDrZXiQJt~rmA$o9cw`G(V)Bwy5ld~jBI!|6aG z`|7KZ>W0&eWBhZ%R{?85)T-{Ga z(oMUc+9&k;?}_3AkWy|sNiZUna%-~qTUy#}ND_=l>27Nle@oL{gCq~W2rS2+vTipP zf7{EtlS$-W{s>Ah=guaPTk#1C+Fdn-3*qRmt_qryHiMlI#+gQv-owc?0zj%#rtX%_ZJem6$P-a zRmD9_5}d!P@?OqQ@oQMsJx4L{WaE-VQ2;fp>RwS~7f3ZXp@H0Xj5z>O!)-{CEvCWq znYe;i(+!izH}CN7vm zd;m+|Qp}$*)^)3pWQZEY<=a(t-RdNn0^k3}IZ|DBG|QZ&Nh0NOA?mrONV3HnW#M-; zAm{tt>JMVh+2SoYqg@AS;5sbL;JX>O9fdp2?s%3lkn^~fLOkex#4;76ZL$y#xnHo{ z30Hn4AXOvxM@7nlG0Nia`n?>@}p zAEzDM)+E6Q(7`oX{1MIhZwx3 zcHAK>|1^hZq#>2-ZdRl>i09t-2&M{-(=Ki^5o&!F0wEP@M^_=?= z3*Jw`*?T{CBS|nq_H%Er_#06r2%#OER6xlKXTANrUECLn(Vf z`n#=3f<5?x+eH!FgD<#K6~VnT(A}@dVUQQy29E_j;SZ3(Zd;OIe+_jzkp%l|sOw2d z_4m$Dw>OKwcV2b}aVmfByzG9&;_sbd?gkQh*1G_u4|DTKg8enjO>TiT3`T%qZg-Mk ze+_qEAjuN-p(o(@JUo8>@>_L;`wGR#Q55IKBiyNq;QV={`;HP_YKxVpsDAEDsE%%Bd7RVg;wkNQLGUsPN=DB4_vcwrU ze_>gA$4G)+x5Ay-7E2F$=1RAK zR)Uou4YjZ;kKc;Nu0bOEYJJ$ApWN0=g!f&(u1+vfmm8D)Mczzz_7xynIA^h%K!f{x0aIcUA_bY#M#Zz+c92)}PGpCp~ z9Z}@*UIJvldrXlsAP3w+64^dD7d`0qdKyzn!L_eL?%RqyH3z>0N@NnU(a?N7HOi$7aF=SE3_ zGCAUE7>Mf!pK>*lg#LysB^a??Ge z$QY1>XmuTH2*2(J`CwVe(U>Aw)@{+*EEVAl@;XQ;dV=MH?eK&WNU`Wek{p4u037iRO1*F%%@bery`h7>F97pO!y^++oK0raKv5( zG3n8BEW1HI2Pqf5!SWWzREU-`WeribLQLgoBbJ?4Svel$-J%=y9SK1EK0G>ZPqa)Zly zBwEyx+l6ukVwy(FD}tpri`G&E%X%!@hy~ARi6`KyHu@yXCXkXKZKF2JQykMFI!KX9 z5Yst2hh+n&3P(RwOo{%<@++71X7rjO6Cq}LG!&8Bg)vzAjA&U!u&kNUyIJr& zI0NLZXk(VSAn$_Ai$2NHfMar_-4s~{G4DrTXPM2ZmPO|%vH@aNMwhYRnR*+@+UO>h z_ds@od=~wlWeCS?iXKzsFvNTvEpE&0dWTbOi&jzOJj4`4>$Bh-;3mjV(Z^WUfTW&; zy93dVETcJQPqdpNcR(X}l1g0uxW85NGK za}1Lw8Hb_bJ z?=P3~wsDNVK3~d9^RTRJG45UXwjtzP%4+ZnGeW!>B*awr?j?~k#`7R|dd*0JJy_El%i{0ByS;oRAKZ#M-d;tpRqydmOA!_M zeq258h9cLXta@IVu2g4=!S80==iNmT^o{$y2T6jyQQvFH;_vtR-gpwZ-^D374|=mn za>Q5glzS>j18+CSB*ALN9Uu>QXE_GjxeCaGUa4oWJ~<)+?+VoddC03xBHJ0)^cs5g z6~PsuhF%jza80j~_qZasrq|eeN)cSaYvNfXa!YX?=@G9hNzn3-dShAqmT%@QWbxO> zT6n8i{PnRG-g*{)eXNDImBn8lYvKJ&60CW)@D7s5EyaD>!aJ`B?$efDQa5a$VDGf{ zZYMbrU$<%FRb%n@P8+WtiQGH&p;g;>kC4cAZUXXzr;!Bh+}3jx!FF!zbyoyi^+~Uv zBG{_!yul>0hPd+9-g|{4Xw|2@cUk;a?dW|?60}BV?+22gH9C6-6~WffybB~jYnWa_ zcWj@aH7u_Li{BcSSBXTnMl0BM%d0~o%jy6U_8wBi0I|Imir{)n)a#%KuD5jYY!X=? zTyN>_^&$zjy|=fS#ozWmUXdPH=Pil;I>2*YNfv({pr2QhMCR;4efoQ?Nb*yM!WHYo zkba=oh9p?G8R&In@vrd*dKO8rZZpv9DP!X6HUqs^Sp2L0L0&ee^4D!%^4=lIPw>|P z274P>{B?k#-gXv$9bl-phs9q980wv3@%PR!uU^lfhxdXl9p<%{67tVGBRq>F_|={f zUV1M~75pyDXm2e^hPZn}lIRcljP@G#4sw2pq#Mar@e_vV4s zNzjJVz1VYd+x_3xneOc;$rQiCstBI-vc1{;sD=Vpzws%-ncmaSqXftDOfNk_h z|G;ZUk}q!Z6CEFT%SrfNKgE=N3G)f|*AlM*iQHeGK+F^D6HG zNzl$~yu=|`pI{wnjh9Z65&u<)PrNQ9vNiHyyFT%HEAj)#I&Tn(Y>ho2pL(I8n6s1v zAnU#IiW~#^%&Viw8ITR$qa?C5u*EidJxJu-55Ls6$r~kOLJQZ+QT+>V0!gs#UwAnz z{i{Hn-_O_7(?VRVGXYt#4t5@u0+^%2^ZL3$EM7GaAuoYXqUL>*& z@!7e2Zzf65f4=dSlgPVxc!%X1{3&4g+nVB!wA;NriaC)mtX~q1z29<7ehR+vjicyy zaq-9A0Gf5$=%I@)+N(ps=`&M{&X|MM@ zNw81%dZSqUeY($^OcLzV-@Lb3{C)bHx11!{rw6<}EdDqt4JM4b=C}Y=MrM6uVU$P-a7^INr;st3C8EdST#lP zE<;kR4oNW1C&wNj33_iz>~R*q_udw>NMw(i31y|m&MGn&q-g9GNlrV{=WWu;;RNP-%cioL?(*RXVKDoJpBrN`#6`28n6wwxq5 zzRJe-u=vMUx!6$>d3?PO+f^=hmLyp5C?ESr5xjD#5KGU%EzJ~V;0a0m`bXv1ERt-J zk076_u{DaU1E~?qBgqmMFC>aDL2AY-kHdUqJLiMcifvTndyv|(A4z08W2@d1`-LQE z)q7(JnV3(|s`X+eNG8NvwO*_eNzkhGVs%;kR=qFQlq6`?2C)t-eycW!xg^0+`CzOs zi+@x;7@I*NTXipN#Y3@qByvj+f;5aZ93O1^36MsyCrN@H)i|aravoxu#NJQ@Tcc@g zHc8MLkH$8W1g+6L_PtCMZ;j@$UrB=2XdXMw;7K_Y@f@pT~EX&vApNPHx^*_)i$<@Q{hu^HzB5N ztlfm5#Zpeg^?S@Cku8Sj%J#8Qir~4jLu{@hc&>aZmZu1|VaM1HBtaWK9ZQ>tWlac; z`UKWs;q1^kR*?mNZBR;WDe<#s=a``ge$h^gy`l)7PxaVx&ZhyNPmNfSNwO^e3~R)i zuylYJ{LX+8dyXX7Uq);=i@(3j*hG?Ge??+BEPn5e#NHN-zw@7bF3jrY@p$3T(ktzX$6e$s@p*KOKOH+z;LT}%h zb7t=he);)6vvbbOY`e3w8-2A9fA4LtpGA@PQ62PuQE0rfL(g~6GlVR-2XE1S4D$(k z_*c9Y)3xbvFVXuWSQn?)5V9EhFb`~H>H~!IE**-i@*I7Mkd`Ci?L64Z)sLabHB_E{ z7Dcw*)2|Eh+s^bSD6;Kc^}s})kK9+g>TjXQeYLw@M2Nqy_Rx!=$h(UkdJPmha^Hbg z^w5tgQUau>ei=m`xxMu}itr=1k6vyPjxxRx#`n4yy&8(#>ps_Q6nW(K*L%}e!6Uc7 zJ`6?nMt^;x5WhDD=yOnHZw%I#3i0=Z!TLHB*&9Rk3qt(f7^dGwnWdG6ca!;%J4~+- zPovC*ud5ucTSEBS%L>pNBlK=4w4${d$QSx3A^cdb1M;O_crvetdbJ_QNPUP9zM{1y z$Y^~RitN>~`U@2L#lcs!#_Ew%cs;bDwIgg5r#mQd!4<9J^-(0j6|Lj-31X`bRp8kl z-0h6lH;@F^V^7exqsac5px+eY_s>`QQxrKCU+G!C=K08{6%+M*C~_<&>m`NwV=-B; ziXzvzeyv9d@z=Oc)suzres-Xx33}929xFlPD_Z&JOxO1b@mI9Y&@UxW8Gl9VOnvM$ z6!~_ZJ_CjNneUmi^lugE0kLN5%M{`B5{ddI6xsGU`hFq)e)x@k8b!AKTm4@le%rs* zpP)>~|H<_^#G0o^PUjiYaWMkq2fZkYoGVzMmlfjAye-g&C|mIr>4j(TSR@lc7V8yJmVeZTDZ+a!MUO?1$MSML1w|gqKk4gf ztKhNxlb((u`|v0Im=M1YQ}w@5WFM~5?+fw!aFrfDlea?dt84UfLi~MotzH9VmUdno z%WL)HD0Ehz0d4<9za+}=ryfZl>+~YCc+Jx_{?ua;$a=jR3O!v)1=*nMLikgU^&lJd z{wUOk{HaHpK3oyL|D@@$itwi%zv@#I;q&vq>9ZB#zuGqI$tcv%{Hezl{YMnphuiec zLi|45uK$4|`*5dzO^DxzJM|Z0EC0#JZawF0-u7*w{*#g2dOjiklabwe86p0Yk=^=7 zDAP55WgJrn-hI>Sp~!2>9=$mV^$&kmvPbt6;rG~k^`R*8sp0{B3W|KHctHPN5&l&1 zpuSp&|5Wjio`ym_#(!-c()XgsrwoVn<3jwS>9Bqcg`Ti%g)u#>ha~dekYoD09-#;y z)8F-6itrIUq8C7s<8@Rop_JicdQ7jNl;P{2|J181!k@yN(CeefQ9h+JA^s?z)<>bp zQT|JxD#Rb%6`h1-@4R!dEGu*Uys#&yW}N8z^$TF6be1WN+~C zx~S(tk>mA`UKT};*FSn~MfiAK)>{ej$LosT5k-#I6}>x(9IvbTAR+#EUDYR`$n^kM zbr>x8OV8xbonFwYR6gj3D`b8oBm}cmAQRHvE z2f8+w=Pb{M5A@PP{PW>My($Ww{Vqbg9_n>as9pRy+9SQGBK$ep6TO`x{5jfl-BpA? zN7IbHDAacT94*`!fg)QPF!l-YTN+^ml6cOtrCE(=A%07<8k11uv$i*k=|cQxZP|?_ zC^TOGLc4Mrb-tA&co!s>(HezX%Ev3xFi_-p>jn^}1MIoaXN`kiFXZY&WzwoA; zF+<3jqtNRhMT~nS!DG3ok#!!=NAA5vjogaxS)*b`Aw~Gw)Z#`-6zU(|WA7OspvbnD zH0lcR+g{3uMxn8I9r7t_TvsF)$os}i6q!$Xtq^~_su?>_Xe)}Y9CePV2-t%7^qC&ob(*@{n$zl8X$sB7Frk*#Q8 zyb$8IqJfcPKF?70SVN<_5WmM784XdWrIn!-jf@s3)Y6(Djg2dc)CXx|JVlXX8f6sx z;ngwy%yjL{^)XQN$=cu~f` zo^~;gqm0)0^UAi+_AZ9AP_}~Kw{$gnD8ip0bTj%Z!k-s(H%2MKpBMBtrlM>M^`95? zGwulS+tts=zKF-7-r&Dw1{(QLid;Xlb0$ zA4RrwyfIu6-qP_#tRlQ!6O1W}@OFJ=B%)Be_}%G5V?K&(SG=)Zh(D&2jms!9!>PtS z6v~I+Z%j3wE5h$LrWt`HaBlA+8PkO*#x3kQ6gGBS!(kwGR5#q0KV z)Gq!;VX6`OBOY-Yd@>f_y!+WGf-+hw#or}|sH=?nD01z}Dr1olf9=Xz+qa^GukQj@O$;&j4mXBKpA>xZL=|eL_1!CzL~b!_zQ(%&4r%dY}`hX zYmK%Uy;5*bz)B%@3-RmOYpg>_ z(0Ww~)l#6Iy~a}%nc+br?+RXzT*dUe(OihXis^{KP-H$wjQIa-^`~+AKU*C)?kHQW zh3LnP96#|`vK6O{jwo{F)oJ4^A$}{)7)dA;{WqxRjIjen=6ue`5aQ>2-gqsQ$C6u} zH;SRitu7h15Pz$`js7Tfm$D0D{cXf5@;k_7<0uO4nI}N57}ZwtSTw)NuV^=ndngIo zz+K_mbtrSw$n-NW6Bif)-wfkVif+r^tv773_dG#D1ca48Y;7e;1>z;8FMb>=Jc%ami z2e!IrgsoF63Ootr@0&k1ZV9>eYZyIAHy)zM z=#P!8t0`8s;i1|e@Wr;rhAm{-({SxH$P;6=kWSU%e=CDLHJ%BX0A==pJTvOAp?XFS zqwnrMHxh;X^--wS7oK*#Fs_pX8b1!xMuEIEny$qW#2({kiI+xeMLvXqzbf@zegWxrlZJ~hMFgodTKz;q2?n+c+Hv2;lE%$fz|`y|A;}E z%;s1jeS1>PnaxQ;t_5W}ij0-n+<}syRa+XaE!)iNIV;3W3)R+u1kCe7GT^>|V;uN-E7A^v#f zG)D>X$19gPnIt$~Z<@)X41apk9Qx-?v+zdBhf7yA{Otkv zifmV9a}<@q=%*I&t-cmp@#pWyp`I#c;(xZPV(wJ7;?Mf4nExok*YZ_0GZdLTAFdGQ z%xyepc~-A!Zb6|v>N2#emU#gsE|3|XwehoGZS$HC|0t+!-bInEsBLE2&TF2f<%VYg zH=&-|W;R8ZEQ5C<%)COn!q+>i&w#fe&AupdEb5tq|4&At$a7RZ)80+7_&MP|#Hwe` zLy_&OZ~lcs$Hg-!)4;q>66oKMR*N+>p9`tn4&FJc2dga1OnZ3EvRw_$yh8jhtu-_& zp~!q1n$alKhna8idcII38%QH_5(?$R&rwaxZxp!+$5>M{6@~79__6$%S!gfj6T$y$ zDj$nxW(gsDEKb2$y_s2#B)ER8nHhyb`Q(A<&CG#{6ar~(jule%efZykAT7W)Tz`)88YmR22W;h-Un%lAh-)UI$bFTW z4^Sx9a46H+%z6N$2Xew&{Nq5nnvp^(jq#-bij3aXtbjt%C*fA3P-Lub=2@x-TRIKO zbT>0d0y7inxaesKn9qxC^Dac=C;Gs_6^PGE_aZ*SI9d_VcKEXYLIzE z2;VpUL^&md@0mR3LFPpvJK!6V{CB}1^IsuX;EZ$zx4I`}LnwUf9_G*nnNNj$I1koX zVLf5LQ$A_%tM+S@tSGXjgUp#!2K)IU)|{dUpW7L1YDcghEOQsi3^AigH2wvQ#~?#Z zS4jIi@MWQ!Fo$OLMM=>1-VWDtpo~DFUVR&6xVcaWKbGGG8Dai{B75}<^IsI&=gUBu zFU_!{GM^7YMw)pP;pgyCW(g8))(3C}fih#vN+`1JW6Typ{4@3#b0G?4_z7;cN|B}@ zvE~gSJm*#*<4o-jo)4|g;%DPHGZ}?)UQ!k2(99G?I>A;G%*bQ172QB4neQmV&svks z(u(whGE+>0L_4&CzTBQ*4j>6E+ze}yVU!ch5kkhqhvD52ygq>ATvycBFAf{c@~A*&fkETWo9VC=elQ`Zyv|CYrl&7 z=R~tKNnrU8@FflC!$h;DkX`fXC`dFLpvYDvnoNk_ibQiS3bi5;GE6j2D>5JC8#8c% zN0&LzHG81M1r~1!*Oo$=B(oohcK;>(S7MZrLZ)_vZ{ecEi>>^#O_Di7h<}wyGLyts z{@EtUTq?@&vjm@^N;209;pc=}FoH?uHX*kbhiY3e)*&G!;G0(@XHaBsB$Wu^JxKU=LdBhSiF=11I0v!EgkAo|Z{SrobQVU@{H$VSZmfmk-f3bOrSD>BJdl9U*p!Bb4aws8|W&$-uxa#_QrZM zjmqG@`dJj;s_{7)i;vevbEF~$=7*jF2sGZJIur%ilsu zigWl*^9G8{aHm=90?$W|*G_Xa%4n^^=kQfYtU3Im%=vqeJ?8t0ECJbT)_462=b5VUBT$shwnp=T{ed*k`KyUF~3%X z?=9ENC5rIl^tx$WrJncSQom_-7UJ(eH_hHCGUuCSybyoixM`NX#_JiabvQv^9lvST z`Ik$Aw#^RLO0|U$?{cF+UYft7&?w)9dq2&(rbs&|6JotU zk^4rdRp=(qCr&&31m0PLGGSI(6xlyvRwE&P|Abk|D6*a~>(VW(2S>0A)Dv#yye)<2 z^O{uvg<9Gl$^@*^ihKc*#cE1h1?ta*@8H3V$Lm&WA%9k)>(1*|A0ht+Wde%K`E~0Y z3dM?rnqRl7-I4k5vqV-a9!0hyn^o(sEHeqVdc$g@$TX1bmPr!)FVZ=!E=n1GX3J^C zDDn+#mCK4#WIjlwl_ctU@EyDZ4U*ehAY=(ViQsqCxvdl-DgEJY5X-DakuA+_6}u;M z=I5W>R&)kRAn8#!*7KI73%Ngq&Y5pnj6~~`6aJeOZ1t8^^*-N9)(mA(s1Mgb^tY_Z zill+$v64_^ALg?PJ&^gNLz(+g0V@hc=2O6m zL7{vOLCpoMQHq=dDP*NkJ-8p92YJUD@sQ^uGc0adkEnk>>_SgnOITfnXq|oOD`Xh_ z@2V=WeF&)D6$nLtVALH(Nw~k`WQ0|UQbI{ zXGyRX{C=a9bp=JQx&h0Y25eypsu359xt&sn{1 z9YvAX_i|Q^=d$LPP)|9lvm%-Az^t`30EKGid))`tbwzSOnGdbV7u2qqu;S%ikV;kz zN?hQ@tuUNDsB8@pay2NUP-Kr)wo*_MwEM75raaVB*{bxC*G$o?gH*BVDUt*Bv1(Q; zMe0MD>Q)Sj+!Jb8M^LB_n?aeHRt8FXM0L0}wE?MRJtGPEy$bwKE|lC_9*8b8tYwu% zp?X{_Gfdf-wp=xbaMv=YI+G?%T!}s3S7E>e@YL2$9 z|7WWb^~Lo}H{yLVjILv%8(FizLB*?qoeu>e&kQbh7kJJRccLw~nL41?JA6d0N9dC!}0j z7|!Jy))f@lV}_M8vuqbXlNeThMfjP-w927O*ZPkP)pkR4%lcSIB*ULf?2;S!h5x|bsa^vw2Kw@8s;2$M_dQHT2q9y8ANv% zU9DLpT8#LrNmnaX$U>0ZM_^{#${*nM$lmB`H4@^V!@F8%P^cBxAm^^uzlvmlbhk=I z$UaPgBc+Fxh$3_DWocO`=N#Q>KB~7B5YlU|FF8MdWb{GS5)_JF0?G`w(okeQL#$I&Ccx*PKY%hrtScx~a}AV7ihK$( z%xamHqVu+Y1~T08P$(9k^BiG~Q)CUCZN9LkE5h6UrL~wOI14(`T8$zz9BFMqnWbfe znbNipYoxVL$aOeJaT#S5$j0+Yz*&gEYrx$XIJSiX5GB)>1`c zpv-t{yCQs6nGitLSeYXSf&qoK?ct13!*M3Hgy z+;gf`TgX>1o56n^9Jl=P6(5d9~R*;X7$ zu;$s;R1~UtEtXlY$gd!Y)-Dto>liM0}&q>yOMGk;`YlY{2b(H5>9p8Gj=aa4OLj0ajwmui)_j9r}REXct z$<}Bg{@i=A^_38R?mgL>K@xmAkZfHQ;y)cowld|ReEgOsTiH-#OOvgc3i5-sP?6gp3#>zm@V9jqT7~msbj|-J{32@tij2Oga z(pDJjA=JFc%9ao7K?%7BGwoIZ6uHm;Xq`u)RzyIVWmaf@zEyg}CH{ZFK~k(NLVklU ziu3#V6e|}=@Q6#XK0u-4l;4x2SWQW^e()rVuVYNHqJMUqyJ2rCMJISp|D0FSF8$Q-qiK*_t6F0gfhK^D66GA*8 z))FDv7lmr|q373Fjo+3%##^z*>PDh#T2u z_-$WrEfV6leS`IrUk2LVA2Qr%trNoA&i9ryYl{%xQZBz*dxW%uzl331$et?l*>Sdo2>(iR0i2<-9n)eyOa*2C zdvbXiEX_XaO%%D$@3R_F z8Qh=w=D4~e$M>e+NOvBWsX=!6zK+X%sQt?e~=T_zIU+g8n0(G$QkRQ5Pt;ESz2K# z^9wxF-|0GUJ)tte{yA?&7NL6l{qVe1P>A1S7pxL~ z8PQ`Gt@1+ne#rahl2ua?-Wz{gp9jtqqW(he{NU}iei~y|J<-X zLy^BdZ&)S@9Vt_w6*sJ2iX?*EvJ#bg=7HR{uA|6({+`vW7{&@*fDHM$CBtef#J{`9 zusk9Dy+no;BgDU=WmsPb@$VcmtcgPWJBJKwwh;f$A;VfA#J_XMuvQB3pLb_i>xKBw zyECjUB*E1+8P)+5c?@P)kA!r9H91!g@pfG*{%Ws2u&yhT0{!#A%3VU1Sqt*WDnk-D z3jf!m3LMK%tcpVTdPu(d`ib>1iB=3^@&7dR#Hx>y9{fMoPpn=-#>0OFtpHEtpIA#! z;Ok8ApQ<)Ntf$r$A)TVaw4ET&tOr7}Ld{1&URdGp@eF5a{nmzRe}QOrE+KsV;lCiE zc40;CgM{1hD0190*{LOEOP@iR%yyb0uV=sv|!ahn8`15L*)(6h&uiNKI zG@kR7%3N*<;W^&{$!dp{#(FU4@fG3!gV=ddJ!}R3W_{-a@e8esOHwA=xv=Gb^#%Q`eE8gxaQ`t zdnqyz==|;ft9cpf`64OpFLX0c95B{T0WmW<3C&Fw@ZG+Yo>F;*$FTk zZ?{yWHAH{gwov36*Mjy0Mfmlipna~o%+P?X3fZ?6`5dIMo%*pXGXbQiy;+e&km7cm z8nO(Zw<&3NQ)C&GDP@mPXF>r z4*zwIe~rx^BBUlff3FQ?8rr*r^o0K@@E4p(8rj+F(^mXFvQME*6T5^WpMf;B8wnW- z6Ck|i&+J7gv07_*gQg9XX=bMh>E1U~>j?ifwS^7o!C!hN|18?l-cDtLdv9xd9}3-% z@lkGVA6LYHnxpMX4S0reT2}GyTU$Fyi2v?eTl?)#c^SE9wzZ3)$m61|ozjq(p*`G# zn%mmh8ez?tPY;lGb^#Q5u4`{EP|ENOJJ>%f@;PkP(cVQ8m{&PW8xEq|hlIR20{*KS z%5f6yhavDqO_Vx~d33qA=yo?0%5VafS%)I?G3;$kWX@kh8Pk455?Bt=e*j^&))dPG zI>8JuUo*h$fRK?-X)c%9IfOVMJVR#Z6|x+D1q^_FgV}|J>}wY;%9KQrIWzlb6v~j_ zuQEFgMfNS_`ZvFeW!Wb&av}p~wtl z?2;%H{TxJ(v4^0@KK$IiLG@rhm!V8QJF+F!oCW^#$Ze2;b^#%aQ)rfWpj}FcCFYF> z+Eq|w^ntdGLeU>!Jx%Ga97F8TR+LXt0eV_7%+4Yt(2t&0 z46}2g$mqlD3Mg_P8)jESk)t!rPDY^&pF@Vj?As``NAYXZa66{89Ix;Pu$tG7Q-tUI zr9D#-z7}Gny?`X}YYII%8fB-DXdgYIwGgB1)hIIOQT8Jg$|pO-8fBZ&GCJQ|M%z;r z;nBy~^GE{4%hKL5*8WMz39*W4ti46Z8n}M(Cw*h>11K{3So;);9G$UtXd51#@_7sL ziM7k3Q2+4J8E4l;Nsr(wrg+Wc?IuF_{VIQEH{NbV5}XwoZ+AnXnhQeB5AcnhBFgwz^SO4UPFq#U z0e9w5bCTT+B|)nTG6dv1I}JsiA(QRxqRbk|XAG1{wren|=Y!E<+GLOgcD#^8k@F%u zQIT0tX0g2;B|+-|WxfMhYPaaj>q*c|xJ6kEl46fWkN61-!dy< zt5kcpBI`g_+Q(67PuL2w%C6s?ZzYeEwRXuKT;c+c;8y^DyWtnRf{+GMX9z18B1KD72R3uwnm<_XcDRL9aY_hZVd^P8- zcCKDr(nHQIqwC-{yPS}n@MTy2PTw}W4$5flm%d@zYw%=boBfL@lck)$)o((M?F`jk zLac4}X(4mlg=(1|a(PM;v1=s!m$zM9!h2)%;5lKN9gRY5&x%`pp~#yc+wDmxG!_Lx z((PnLih=C3x1-3>*=1MgLoLlyIb16XWp>+9LUuF=)heQFCJE-V$KHiP`P6_id+a-k z)C1XTYkgns)%|ua6w2^3D09FbrARc$LHm1@^pM(ccdCONvNw=K_-Bbj_AXI|XXs&> z8Zoc-*kQXq3T4;}${e+ueEw>0oUpr~P%M7EIBAbSNsqV-|1FnaFHYOBLe9br1HWFJ zw&O{H(NEjSC=~r$h;`cDt;hn9Gxl*5YUv7)zwFe0JRgd_5#)lc^_Q{>K#NklrVH9ChfLzKA&`}032c?aTF!JMz#F(~qWGN{&{5YL!o@~LCufs`-&6>d2ELd;jv^sPwhWYD4)_$=9&G*Q0yOE zGs(}rAg2G(Z${t3*6In{+sg{)qHzJ+JU+`AQ??Oa6t`Psb5N|o1h4|lv$m8@E;(r$+uaiU) zyia)Bx%4Hkd6wpX7vgOvd?bngU5ElsQ6c_!AqqNGg!tcuDCE==;(r(79jAp5|GN-{ z9fKtJU5Fx19~2tF%#cSBX9$Wsw-j-PkLCHuyPYD=SQNEP5(@1tGhp0{IN4(PR&+G+ z-#hO*1xU2r@LiF)P^PF;4P~^p6J!ZUF((N{_IwFv0g8-O!g*($ta&YLRl=#E2#@ui zGe{ABbtvh~N1+UVg{?|EN2yHU1$-NTAGu|m(?b0B@yj@uP-H%3oX|KPUFK89se=-y z4R{Fm##m2lMQ*QvZ#Fu;6yf=lb4CdXI~S(CdjVzwoe3m(_r=Gef-_fx83-OP*&zzD%{3EWJQ<)@q#5H$5MUh8SbEgH0+@qR1O{Vc! zYME#hwM@wwln?)X@dQSvxf6pTN4bTw8HGlfzmwk5IYttEC%v_ER*3%wdTZw&6gfJr zoyREj_F~9mSa<4Vo++be1&MY_qR8lNoG~aA{Y@y-*7=r1>k01#ybaRMNkNgZ+Bv6D zXzzU&%LHceShD8!j*CJymx3}KoFORb5wSnevE0cSC1g4L;^0rRIynP{wvzkp%o_f40+6i2v-* zc3iQQ|E{a;^hJ@qVLP)?D8tXN=5@3co+Y9|94ANOtNp{AwJ34H`=fJQR{!{4VI@yd(+k8-1L-b1_4#r`$TeRU<|CRpxVN83}&J zjL&TMbJhv*-;e9(>=2Ue7`-vq&pC}E+tts}l6Wj?*C1$DKc^0gY*&BhTNLWUFQCi- z=Kx8t4+lHvg`6uxzn}*@8A5ok@+Z85or2%;STg!xrvi$MHP{)2Lb3Sof+0>43S~GE zq7QR+P?>-qYq+zYMC%08xRdWY+4e+`5zc!k6rEpBzjP`n z!pC%^QyoPfgQJ}`R3^|jhORqf92Z4KAL9%}p;pX?SYw>kD6;Kio$z^b+zY~(#yamP zvJ|$8b1IVr_}h7W-QWbLrjQTgX_jV!(*Q+ApWw7dkvUIr9NH?luYTq9M4=28E$|wlbt#wf%946e~yDpbsCXqWhT-4!c(1&qRb)qZN>N1sSZPt8BTRp zqfqocSkE~W89l+N@V$)Azs)<%X+aW6sSTmfZv=#Qo zagdqLW)vBHw)5G1j2?)~6RKT+GKo$bA)DdZ))f>3MaD{WVo)g7O)RsSw!&EaSf1mI z{NdGhedEkig#XX_Bxj8x_o1G7&Mrl?C$QSkIYAQajYZBsB*ETT6dCOher!a|@0J9W)Zg7S3K8lRB!l{EIugfc(MkuoFE1cLByk?434q~lvrYTYl zB-L4slAuk8Gkbnr!7PtzPnNaA@~Ws&Ce60$|id8Ro7gmk$?XY4d*gphY(y*fXKr#Z1gs=@xl zzlo9NB#?wWhOPMBPMY(B5Pv0nn)933%3lee=Ij*WuY^x?4v_?}gK18&m9ke|XnUFy zjY2&JkKnamooAbd%dZX7vnpHdLWEV22o-cV(WQHf5T_|#NPCAk6WzIiAJ|~^ZD3mk*HF3&m zrN~++bH<55k)w0Y*+*pp>tGcnzpFg&92GKo4gKOc@0=Dg4(^uu9rbzV5{k_5ypv;t z%x4?KI`4dhLiy|gx!}a0$mmy`rW>g@{Iktfr?rqpFt^NOU3EGMDGkrE`C0v{;|bXY zU+JMTJ%yYs>~GayNPIEQO^8J#r$Zx@3ax(-{IePTohUJePfi9Ci4cEIIFs87MP``EjYXkeZ3fXZ zxyu!41CrU@j*_5thM55iB;c0VhWX&0z^^G0ZXHGH&w=%#Zf8aKcRaGV6BWTPGq^vJ z1pM!KWOIKO!sp)k6R2$N1{9fdHun$;<=hSO$>u&*q%X)DZeY92knfo}+*Fdl33$_= zuOrIot`kxLzAwR_R^)Uy3)v%P$aA{8P-OI+?n@N8ujX_EJ9sR)ujX_k)45QFLm{7> zZcRl-f#hnv%JZ=RP zik^&RIw-OjB(K{Qg`%$n$?wLa$mj*!JiD+D17E>dY=kle-FJl44uor4P~Jn4(F?lG zs0{8Ke4R`|x1A#V+faqvZYY%DPN?S{H$jmDAcfs|%2vlg-gUR5$PA0QO?Fd;+freL zTOzDGbz2FEgEvk1D%|33M--W1aW@u)qF;cTi@X0SauuY6tL?#>u@!ulxTIT15&pJL zDYphnddOfn;`n`dY1bx+@PFr)c1Mr|@6bxSsVEfv9>gl`o>1foNE!Ds3XKl`Hu(E) zfxSE*+4l18FqF7J7<}u4Ur#HzV}z{xmd*eb+=(c%?G@Z)6p9}HjBmADk=H>!aQ~!w zuB3n_@y^bP}xSH-W6lzyB$grkc`~c5~q&7$`w;~GlDt}X{ zj$2ca#!%)Hm!ZghuIE0dGJyi4>8V+LH`76C>BX*LT5H&QRZ z8>E5Tlj^~K=6h5_cc3CYpiCn-2}S19#4UV?qWkY(G<8b~IlU(gf6Fv=E1<}Hn!4># zD4zjPb5nP;BEvzV+^?w~8h7ZQX6`qNjDs>Q+~X)RpVsbx!<3JI^^A5$2=Tv(8tulC z1h1aaZh|P|Up=GUIVdvcXm=M1w~Op>-jOtEJgu8ksdHr&l9lye%^ zd{vQcAg23}>cO1%gV=7hqdY^|tDgJ%A0p>wbZuhpn?gPnZ$>lsZ6WV$r8^Mj78MdB z&VI}-iy|{*?pzdk>@ar;ioBvRH}(I@ohM)PF+e{JuJGZmj z9fkVmB(%M&8;>H}-p#d6Qrnw7qF?LX-L688@^x`=rtR(yK#}e0?xv&2e7d`5Xe;bz zo=$}?w2Hi?u^!Tk8tCJyay}2`McyJ+$lo*XD%b$StzofN4O~{G=gEz z`Br}@k_F@o_Y&2E(IY`dx;6gh`N&ZocC=?kz&P}2+!MUCB?h29spY!B%R^#1`C^FV~_W%mT zDhIK~yU%GWY$=~>n&9TV{Ax?Tax0<41!GNgo1(~?C%RowI{N0nU-J&S8&+`@0Q{Ad4 z=@IXBp!Z4=+}b3;yTJrE3WaK}2Q??Si70a4nC2e3Eqkm9l$q|nL`je62VcAisRyfz z-Pi8$GU*YuLE1x^S#A!LIPG1SmoQO=P?=!#S#Bx{MemGdw7arh{5YNM4n?7M^@1{q z?$;=CZ~4YuO%lxK8#j$4q&Uo44}z_}asN>2;d{$B?rlZ*`(Jb2Cii4Z`5oG~u8l$& zegXA-=cb}i|L|Xt^V~ZqGM{AkG1U{`E8$~dt7JEG2KCQq`0w-lXi9dA3R&`iuAa$m z4I%zpHpy;#6pB6(>sf*#N9TJt?|qqb0+gBWHb6;_;BRg4H$)b?t%Ue*Z7g&xA^uw% z3*BBM!O>ah&Oo8)iBR)G_pBoGKo+^zQDi<#-0}~pr3Dt!Jlj&Ys*tO@={=yOZfzk( zetK7EsoM~RMtLdJywqh>CYVo(n~EZjixl@b3T3z&wn}k}J>t=2KFi%|k7fUCgfc5! zU6FK6<52-B*C0l zy9J)`=#=4Mh_%}7h9Wau<7Ry(GdvDu*1GLcfz^oW_0spmJk1yE$KZgj7_lsW$k`D}FG)A9y; zm7i_W+$t#YZ1bzz1SLH}gLU}4$2PgGNP@kw$+c0aH||2so7_|s*&DyPZ-($#R5O2; zz1eL}5}a+`>b4V7`WoGjZFRe&$R6A3#-dQH#}I3)dtMRk1*|-GAE8imek|{B^-vkT zINSrIyFC@j0$c5LN0Q*t{U*q6H<3h(g?R~nf4JM#!gxJ0pWSX0itOjzZZ8zdFdx=4 z6-D;O9=Be&%%?Dv+3R{J^xTS{N%pxhR7UFqU;8NuW%jvWpd@G!aA#f~C7!klj>Ud= zB??8a4rTVcM^R+-18%WQGWsV_=Ac`fB=8&TKRo(jH%iFLHT0|hup5mc+kV*XiXyLZ zhus(y*&B!5I+=Ma%BL~JI_xqOna}TT)7NA^EuhR1cZ?#PK>lzyqm0&CY=$p5L5;`V z`&0&3*YG*v6K-e#>j`WX?>n4uvyfxH(7}P+u^FaoFw?$;hMV%MfT4% zcNdE6pKES-F3OOv!TSQn;+h+cBFF1rcR323fB0&a>+Uv1VqvQr?g^6MN}gNpc_9ru zhiOxw%q{mSirianxjEjH(Pv;81BGJoS-#tDFGcwM^Bs2>34ZsIKTW>tjzN(%-*w|r zXdjykHQ#klEA{aEsC#ZyB#%z9c+DBEt;l@X>b^UiM0;J#C_Z#2kOUfL3BzmLLw72Q zjQ-F~MWN`+pq_{BX%yM3kKDxEuSS3D{-6jy&p&ZjD8lo3=B`)dXOI`}KP16xL5O#g zB$#uE_f(Yea}M#s-{R3_&LN(OBKM6DF9wBLu?b>@c$-nE$9NxxdKL1>R_uT>VP1U_ z?JRubcOOW&X9y{Lm;QG{xHketMi2LrP$Ps3kk`Bg zid+Q=c*jUEpZg$Lyjw#2e6o0N)V_Y{c+ zDeC1fh#6|b{|VDUJ~Db~FA{~KPscJf75N6Fj2DF>qrdO{fkM$2K$&vhIYpL( zRPb&qvL2+O_e_y(AeFsN@6ZVH|8~Jw8&>l?AvH&aYICw_AOnRQg7t^|o}`*L0Y$c= znztH-T5$+!uI8Ol&`=&8s11nu7cF`7LjPlMc~~WkXqhv zC^C92ZyyRpe}-l5D-x!`dMq!z2#+PBf8wQ}P#rBlSuFF3A{9YC^;)3Fd>VOcQ7E69P^PiBS&{l6O}xKJ z0%zgZL^F^m?}iW!?hm6;?xVvTl!~yxtdxj!& zZtk@yCP%O*lxg8HMFxPh@)Ah`o5ZuNXfIhvrOfoaAlh3Zgg@=#dtJ1*21T|a+S`Rf z8GZ>hM|*b^83)qF(~7@3%I!RcLcRJmlxgoxS7bIw2X6z377lAyegNs{9YK-NJ9_6( zDEg0BCZdEK<&_|vyu2u~e++Ld3Ps-tWlV3fB3nT$?`M*L17BU(2jX}egw%yQd_KyK zw-rU^<9Jt4D4*Z4OwRXYJ|{q2uP}1 zrF8RlpvZi>dDl@WpBqq5H?KrVnNJ2tckg2qiq7xrdU}Q;{I0T>H(ZeyuvKqw4oUDn zs*kskL^}n)rp5l zeia_(-5?3H`kJ0)kMQmbIV7&RBfRHAGQ-z0`89Whm!%BPQ06njD~v+HfI>OYy=EZK1bQkvvdlq4zgQ;2u27E(Eg3yMZEO zE%LH{$aAJx#js2@6zYvKAd9^@D7269``)GAFN%a#h1KcaA1LV&4`DqvU*WdQJ0oN_ zNCcEw=KW0)oF`f4MOKv2D?+SgUQI=+gQR#-C^GsAuTCYt6=hf-%KYRFK%p`%L4NiU z6tO_oc#B8^eBPMPd9L$T2>A@&EhSk?qGib*hO@Hkyepy%|1K79*E;VuitNL6UdhU` z6}=$VInyyimu8LF+GA;h1d+UCWQ1ZSwyy#+O?SN*5k>E3oB{tVSl@1hWYhH97hK!`s> zwc87;MfLbIRC~M}Li`!3y@VmivyE z(LaP(&%9!v%9efv^4x2Hk{&YSWVrST$P3RT3D*3=>xM%0G{Q0oiZlm#=_RAcR)nxX zLm8_BlnG^xQPLy$w@12yWMVCad<5Uc>W$JtNQ0riI3yu?dc&8}vCJrvV1}7k3W|)L ziM>FfoQJ_ynb|vyWIm%oUSs7Ii3iETqEON!#=~zp>E1X?E6+BxEi;@kznxqlNIVj;{jA!6pfL7g~`9l9SE!%fLL+4v<`Io{(zLiUS~T zvZX@!_rrOaNVZBylMCV6pCGx}uR=;d&gVehVmpOI3CY6_3*p}nzY1mYveQDU)~7Me z$Hq66`v$*1%*Vb)kt3LoeTPCLco*u)&(0}ZJqCH3g+$?)hTJ<5j{9l>mY*cpV+Gi| zDDpm`04s|k&sqi8K@^G=5+14*V7F0ZJ_T9yXENvA2`~%FdMFZrt=?gy6!~)}yg$ii zp`=IfclvTcnWF4FAarX*X8k{)s42pvu3*iS+Rh~ujq`$fozEA&5l%CX;sTnm=jDWo0z$8-MN zsvJ8gMm6>Xh4SJ5FZ&}F zXstwtwffar9!0uCJs-0Qio}4_U=~VxM0=gq_ts=xNkZ;@7_JS0GBsHoNif5jY&nWN z25YiiD3tR^-0F@ZaUiwWOBC6PI;>2z%y}x5`GnP0g#SmddaMmfdc?tb^gp#5Fhj^< zvHvt+ok@ZjHef?gD4%bj<_2t$BHx32%Fo4S8v4)`)8!p6OFWQQYB?;!!imgMTe9mA!=M}jG(wf~yk@>V?4cp0lZbF&1%uwVJ zNPE^FB|TyteE*r>k9B0jg)Dgk?rY%JW=A$wh<}Cc$l^(Y8Fpl+QRL`!WUM`pPB{ml zo=$8D3LORfN~E)`ie!f}Iy$ zHAJC&3PH^_i$j6`2h^6@?yxB&!Fn8)qSV9Jr8sPjsApz+s8$^6aoOJ}^7!(Y)={>- zER^wBDMc!RFjf_XdV~K$?7}=n_;-Z5vY{lw?+A5gqlNh25$evqLXp1|y0at{YH3Y~ z)t&87q&`Rwb_9j`hkq}(7rTH$`J|kO?`*RNDDtb4eOUHRyj?O@AC?D&wrU17_hDrf z;q~-oQx)N{VpxVEonWiaSwP3}!f)NXApKY@N?c%mu`sPWNPo7AMEmW{Fs&cR0M^3b zTg3(c=lDQ&h(v3D2WE1h%s_Tdi2nt!fh@_yt+a*kzVLV~lOkkbVVI{xxvt1;kU{Kq zi?&*~9%kJ@hOm+-)Xym(Ls?5jR)Gv-Hf@E!zkUT7!A_&dJu{ZwwW$^UJ5b};6Cnp; z!n8y18*Lm5b*Rj`iuA_rIF?07|KjvE?l_i9i2n}nIF?^X6L>e7e}Qludsj%0Hhw*& zh48PM@GJf}_Mwn*!7?>ahwLiF*hzK|KBUE|qQA+L$AH;iW&gnS9@x(u<#v%-w(83H*|J-vnW zhw~V37ep5_Gg#&}3XR|mtS4V*UJpqI$OKjrh4%0lAQRa-6gh%ZS*flVJ=oF&_JI(8 z1QXatLi`a-V4nzC39X2P=YI*Tu@HZB5?D(i{^%sI4nq9VNni{`j!pvGO=W^(k-$Q` zy_(@PmX#zp7SmWiA^uoQW2r(8K!*7t!)Yu>_gDLQ8jBI)_wzKiK#1SZ(^v)y^=e_P zr(h4+&+mauXJt`jug+u(QK;wn8xXVDW)dxTOM2Rs$o7*2Vn3#pjfw0yitLR<7THtA z`T$}jvYLui1DV63P$*VikhyF%ip=Lb*18wrt3#$kpx!&rm(I;{O_Tq zuox8CiWIg~sfVB0QrMCI)U%wOBGGQo57$EB>x;`-1`6do6f#`F3Vtp}XB5a!tPBc` z#YB*mtdSyoELO8*l3>nj*b*T%%hH*D4NDbrHkp34tzqj>WGmLNM^q*_7He3we%L?3 zn%A;ALj0Q7vPLA@J*b(tYb|S!LhYIjIsd{&EAk!4I`%bfg|U`^Y+!Q~`3YnrJA@*~ z>sQvRKhH;g+h-FSAS6+oAvduRLMn)B(I#I~bQhU+2LCU!%S%^<(Arzq6( zdqB3ZN(1EHauQ?*i$#$c?q)>>Qjhsp>OHKK5dS>ChkYQ#KhN)B9|`eWv4?#k#BaqO z))+;O*B%y+LRZ*pkk1}=o3;v$;C_~4(5v|zV0lS`XY~W@OCkPQ{Q%n_Bn*x(epWxg z9tmk6u8ap*qrtpp>cfXvW|kr^Kn}8HDAb1m_-fK&wn35XAiuMHDAb4ixA_tF;SgSP zdc=ulFsB7&jXZXIUXd z_&4s(u}X^YHy19jXp-Pp?=CW3h(C9FkuemR;YBtCg)*!U`CMe*E7BC?68nkj!QN;C z@((+z2tV6gWuFhnoCC#I&>Jh)*ia!~CesR~YizWT1X#!DKt0#kS12-{Yb+Io^683Y zPAJj`cM=5fZSj?M#vuH`P^nbNrKmlJFLHum&tIi3R~S_!-dQi=fgW}tdQmx z>3n#HO+t|w-eGAdl;I?-=Wj)(f!t*oR1apD1d_qleZe!7qx_6TjHDi`1kd`{L7C?) zr;w`f%Z2|3?dL3?kS+r#pXaQIkm>NhXsJvoAv?bCxB5WHit;oT&)G*p{B}KO^-*NI zp0h0|^6ug}JA^_l-Gmu7AH{Pf*#Yu`F%&wMw?Bekx}Ezg!oPtW(s>L?fPY8mAZ!)d zd7_XG3*ZSS$}|+2PiW_LC=}}ymN~D;1(2}Lx2PWM=j$MuIu{=O>YfnMc{NFJPsq|a zO-RoDVcIj;Dof}8vG?ZjUQKJ@|N0E4&PgfcmJDTxP-#LFNvSAla*Il#J(?mzNSdT5 ziY7!!B{wuG5sgksk~C796D3h@B>bNB414X*-hG_7!|#4y-|snptk-&fp7pF}jeG4i z?Zr$!g?;aO%wNP>&P3`)5om?=Bq*^56E#=8zhut zCjlvDjSmrBdrMfq6A5fS!7AK0v$mXI6=xzAo?w+?BBgwSRThb{@C2&`63W$b5c>(% zgCTMrkQ1%PNe|{~RUjp;*O8doTiUAIPupWOpJMePlJ{d(nzcT~dYVa<`;&M|;uLEn zlVNaDQs0VmiZzDGIk00$zf+%LO+rEz)`!?nu`YYet!_vt?K<|l)<7gC_RFoQNb>WNchTy9 zJ?m{Ib8n-x*R$pm31Y8jtwutze*|&WvwjVcRY0z=j(Y;@7^YqC#=O$16e1fzrlHj? zM0Dviwzd-qdZZ@S9wrZ8NY9-n)&VBcBQ>#pVIny@_>m zi0lV)wRIN~lRr(Z7m!dM9tN3a)-)oydOa?k2m5ZVIZPz>=GFovrnWSa`_M06@TSaXR4<=)Y{@+qu8LAp9xZJ0>S>}V}P(p!13KjAvi+|gQt#H92lYX=fa zX*-a)$tw1=Nogk_ovc%lP)a=@H(NDB`tLx{x zXBA^2qf^g1nMuWI5D)0_tkaPgn?0)`60-Szu*b9R2$4sC_*M@jrmo&@tw%zs&|m4g z!`c=ik3*HR@&k#6+KBBbjC+ zFztAPhR9qdqj~5z@A=Kw-DI~q_;Hy30bInlD^i+5YatJKkJPU(QWWC zYc7$X#r3zAGm)O8zx7WfCdc|)=~2d>T@ZVJt5S%33*>RDE)ufm5Rie^un;L$1ime8 z{e;BS=iye~^Oze!u8y!yV3K|X_CY|X5!NY8KGuo=iM1Q0CUf2&X+8Tz0EUXRojJDnj zk@`Shv_3*Y7V2JNjP*GZlk;P(;xFph^Yi4(P%m4hm`pDXSe1ek)W4&*}4J= z#oiR+dfDP=zQHjdGtGLPNRZNL)>0%C`$UxaK1ALCGTr(e3B^7e$V}_x zmv!tWu31(KB>i(A*+Oq6v#oYaX7OA9Y^yU8Q!{5$MPB3gm5THWCxp zJJye+CvV>;X>}>=PV(KeKt8c-B>8z`r=(R~Agio9ndrT0S0cHe$$NLER1+jUi3B#UvSuP7 zo13G|wh*}<$Y<71NXTZL&ugsq6O7F|pVwLshKSy)w$2(IB6`Qvdg~n|`FWFGrn<4g zTFB%JNQJg}gSCuFk8w0M-e9dE64<=K%AKfVC!0HAsH!1i1Nq##0*T4zjn*I}RDZgH z%qD9klKjM;uTpB=2k>=B>pLc+UV@Ph$ZWQ9U&V3{V&804K|-%!V&83TL_)DQfYSNe+8rWOAk-e~lGiY$!EWB~tjn3m_XoeT8Zqg+G^OT2sPC-i zOm2YncRhao&iXr%z~=9)0Z1svKEP1ZLS#9R{nmUW#^xU^_aE9GvUx4Y9Ize;k8#o!--36;cr&XxXS^TV zE{DYQt3~ZQLqxatV)pZ)P`Y0|-d>0#Kkp`J%X(*QarnNUR|FAENQnV62xB8?t+Bkst=k=+Ji%+36PWR@kl5x-9whL zi%mDV+6rV!+clBo=M95FV4v;y+u7V^#uM_NZcmPN_yCxHT)2!YZTh6|MNDzBD+eJdLKZ2o# zhDbjkXWCq5Tz-{Jayny;rusW74)YeUqZH zeTd1|Wz%W~gsN=+P9(^m%67%KOs$)PGS`R5d>|LWk#8+yEUaqZgM=(x1~S#`0U`1! zkm~kpNb(aUV8plqNDX@-ks$UO_HrbYKU+}d#}N4n$i;SQR%Yy%+9x70vDdUKgouv4 zmVGr66MJp@79v6Hwe8!HQ0(7BsM_}M5cvs69s3m|CheEmpCO^xZ-KYk%k51eqUSv8 z*~!^h6Y_TQ_n_TR+|R@R!@$1zFOrd`ub?UR^DeQs)>N+htbsof9>S$H96Zff5V zBGrI2vwI*h7B;t+BOwd*tn796HYE9ZvKrXZ{)Wlrchb|LrTqhuAoiAa?i`&8ioGtx z)zYpSA`O7Fvg;wCxUL1##(p$JZUu6qJqn4ju(N&UyOclsUxu6f;MsJueLj=r@Fis; zRhbM6NG&Fkhd0~xnMfYqY+ubp(si@lhDeaEo9%og=IL;=y&egr^Z~H>W;-$0q`fDQ zTkH}@Oe(tAE0N^q$!N#2*E8u2&nEpYW7%7XBr3tTHuQ+mvVUiC*?sF&2H6`z zVt>YdBt&WgdDb3;gzBnp1;g#VNGPs3&?-mRYO%>NeFySLyF`dw33^7^ zHA1Axa+vqCyCccZ%Yky&IrhB$7|Gy{@oON|^Y$o`2~zrkJr)Vs+!AD7us;luc0fkk z>yemx_>$c)-&k0E70l_`ZIF=6Ef8v)eK!*5=izjxu7?xs2bsv(iV1daCgq3I4a^hl zflSuJ9aH*y0Tb+@Os4R=%mn*6ChcFO9(#iQGLu>Xd5wwm>J#i~Or%$zV9#M9z4`=u z0h5hOY1A~q{*XxG30U#bBbf>IZX!V*POx8EqI1La;S=onNT^J8{!Fk}A~E?h$^I5e zequMI^e)J;$@U>83m~Q4fxK=fmSW9Jc#y8%K;E!V4v~RC-n7dyX~Z&9?CK$+Wv1Gf zF-eXiJ=5%#A);lb+wGa`foGhKYleMSh-jIa_Jd4bVVSq=!6Bk$X4xZ|{0MpY43OFO z%S@hwJbVGj+xC%KTlWKE0IvT^i%i~`+Fq$iF?J1)1G;UnVEPDN#+n zwg)q518?j7fqY|+A`;~M9=qsDldFS4W{-VwhztX=*KUBs*u2kfg@kfmm-2VE8zL`3 zsQvbU5YeYQzqdz($TgkdW>rlyDX@k@0g0XBm^p{57i* z&MGD=c!zBX=W`~*VKzg5kF12VgGi8y5>DOKrasrixU3MV2jm2&8xmvTNzNoBl!sS? zOi5>Yh_nE5va=LPeqLU=G~NqQ%2~;z9uRF$DQ7*Az@Ac$T4U_F5%iRD&JU5Bfs}S? zA~E)qab81`pLhwrsdO92oa!uOa*C5y_aNEIMBjU<=lxD~)LI>v>5)!#E+&%DC-@%3 zPz^(*50KNGK1@D_HP)wqoZ$>(qMxTDfs}QQUuVi)kDtprHIYzlc@br94w3Od$~ku- zG3h$X=^4^93!aQ;JHtZcO$c?4^A?i)#1?2Nvw)O$RuBntwY;+)3B~>%$|TmCd|nLX zT&Fk^D$^A}DmZnK7<(>sHXwnoOYpj0MQ1A$S^KN#e9c7G2P-AM0NtH?5sv&Qc=yh(N(-#S} zIM{ojzsX+1d6G%nwsfkuhVv|w9(=QF4d(?Wd%C1?F1LpB3XveCHJo)whN_ycrPbv5 zTJwG;a-Y>D&H*NJpVg&KVxP8fsFM4vYC7i;32s%Y<8($cLdktrb(~&Ip_Z~tVr*J9!cZ$&X3xhdm2(2@b#%(^*EVlW-2F*P zwMUurkWea)N14lr1gU7`Tty^EMI*;YVp7q_>4(ImqOmi8iKL>jvx!LF>touE_2IXc&gVoD zlB+G9tt=zC+S1w2GBcoVw8BvJ4v~dYH(EN4k(fMe>0F0|YKyMDEuD5DqVJ7w<=hz} z`gZr$PCwG4(%or~P#b40lOY$-e%dz9Kbgq={B4|4KWTgV2lw;0acUC5@~QwO*v4sr zWLM%KoKl|w`v(8+3=7Fr1DUqY>k*k7oc$z|d-hYXwtSpYH#$EtxfDna+z@-CllYlp zul6I&HMMv0m|O)!r@g&%A`!Gl*Om@Wc_frSL~0OGmY-6CKywGjj)dyy+!qR!bEQ%p zohL~qcix*cZ_~+nmdS@ebnKm+(M$%yb6L;rbauuw8LHoWL8i0w1`@KTF673|&N7x+ z0(WrfP+go=O!Tap&W$e4=S=kYP~Vr^#n~3pqcwMN_7YLoEJ>+`pxJUxJ*;z#;%W-S zb}m4&EAi~ru&&+&?rLx@Lt<*wZBDaLs8$f_Hs|gTxf#e^&WI4vx!TQHOhiqDTs;FU z?Bnd*Drc@G$sxo^v;o*4$@ycLpKpttNdyIo91-Niw;k;fu`Q zLT)_hlsH0~Hv_p7?0L}X!9?^tJmPd?Qsg>VhXvBpd4$Oq9O_Z$ zaVB$rPOH%%)5{skr08KHy`9lamR3ru@gUR3S&yW*8V4(?`a5EModZayl&6EtV@}?0 zIu*O}R-c+uv!Hk$cm75sG3#8~@$$HHJ`xJ`E`)mAX~HtiN5ZXT%@gVg=ONak_tLCw z3}3i&`h-F)g-}mAVbgDNT|Hlfaa&2r+??N zfD;G1fDCiSFj>(aYA)2V;m$NBD_={heIPU3Sx*Fe0Ig?)bDlZ{a-+XG%z9pMYBE`R z4}2j9(ly!{#YF4*Cxm*@fglRr%2Cg~u+v*ee&SXK?(GAaF)WjxxCuz16Jcj#K-vH) z31osZpERSMvu*f_nKL?}?a5cIVGjRW(ENteBuV7qkKmpvka^Qt$>eKTrKt^Mn$s&q zGWYC+6A3_OIm6OK>cIL%TOjW^<#LEr13g`UyzdNS;zOu=fGl!;Leg8!ydr3bMJR*{=ZO%p}lCE7&-6A9->DuFrLPDv~dAQFx7$W5%rQbW& zahW#%=yXS7Y(C;lVj?#G>U1bdaf!{!-N8g`&T;1!%d|PqEqy!@vALMror&0dqPrRi z*<2kgJlQQ<+*qjl)l%+xNX$5@lv_I{b7f5C`j|||m`vB0O!t_~lQEfRV=@zCGH=FY z7Q|#ejLB?_$?S;9970097Uj=HC3KE0N{)jX{@oq$j_+Q_WXcMd555X^ytsEUss1tC z?g=udxz8hcEEp%0bC*W+lyi4S^qlSHod<#oP$cTDENn9O4_ znZYrc(MTxm7v2e9CvhvBsO>T20(USHvPY-=LiZIUyAs{t*`)i-O71Ks3)?|^N3w`X zYkuFU{Z=qNGLa2%!Lzt?h+)s@_u-LM!D79 z)kG5O;OY4;7+u}n!DMdHl=3fwvv_X#k~*ck5+@&^QCv;88j}eNgw$ptk(jjCakr98P@n6# z$&<0XkmSG!wvJmA$)e<-wNMithA*?YFa0f3=5luu5+iekdwyvxL%FKUw7y#pi7C?t zZYw5|iUzL5L{ibf9YK16R5WnMA~C6G;J!^VK`I)!B~HSt=;BGdaGS?VE;4p z&#hgT$nM~vy>*iKv@-&nC z-MUCj><_wKk@QwNt|pK_54x{&D2c0wJC}*X)ze+hMB?h@Zek*F^>z0lF>R;6n?7Bq z!qmC}ZYd^G>jtL<}Cw{$t3Qi`iDgnG_B4~gmLpLc5z zN$4^5j9cM!m3s}7t9qu?$2Y;4-@SuLdnRMtAxKQlk8?*OG3grTjz`j0Ede8*fY`^m zGl^ijejS-muei>cXra0ocKAOFGLzh)NQ~x5?u#T7^mUWm2}t^?Q4sh!3^hF@^T8-M z@#HQgqUOy_t8pMR+1-M~XrAou4TaME*kt!0lD_ITm}k>APj(Y$X$zlK-}D9RfV}P& zVKVQ_lzJ1%NlbRh%_cSCl2Rj>N-f*iS>7V;BgwiXsQ{382u6>=x2~*rFnG}Y3 z2|af@#chj(>gqhuGsV4^Wq#>HYtYl(9!%ErJneM1ACtxJ(LBj?cTh;rhoEP=JB?)2 z`EX;vIv}&$cbOclmQr5=neBeSTqf5^i9AT!5p zk0d|$wD+ja&vm;HQP=lNsk_I*_b=Ssb9AVDwQ&HE_uQ^XdaH@ArqyB4^S(P3i7Bu7 z?p!3Mj?H%$Bk8NEL+ehTlvMNGB}A~c6a})--GRi|^MSjUNMO$g?vV1C_T;-qkdQrp z13gRJXU;V#EeGU7cPf%yiDA=V|H5SW!mnHTJR|b}i~&A&FAI@Ucf;GC+bl%Bfjs=g z?SRDSS?NAmLF?(Qe%_u`=R;hdx?`ApBAfLI@kx5 z*1MgN7@05JOD`}o%|K?O+Xjh|+3c>VXzaNjWVW~`RwAP9=>%kldlnM1#|5&>ZGyz; z`Nr)~IaALb_bDc#=R5Z$B&6ql(DS`J6N%Atz}*;<=?O9i-GdRCf4WUBGI0$6nM3Z) zNJ#TDKz??6A~AL2h&zBtP&baaC8}sWrmp_#c0xjRV>Iab&3yz3X`To~dCya*pcSON z@vKK$LCULAHB)oidlZS$oa4QSgfveD&4s;pC{&=ih_{&ah~^?*i)xvgkMqVLF`A2d z?;#=0?||mxz0WCBp!o!E2kQ~dCwN_}XKFstn}@_`KFQmJgfuS(%_n<@DO8}jw3n_y zdPH++ZwM3V6Hf6qBQcuGc)ubc%_~6jXelDD6UXs+zltC^|!BCjJ7(!3WmSM?r3Vl>zA`Vk2<*YHZz!W;|w-iy6XNQ}*w zc#j|<&Hn_=HNEF4RFKbgyz#6@O0bSsrFN#~y56HmjONR{7m<)=RT6GK@!p|Of#xf{ z#jHm(U+J}}lc~9Z_YWjSb3<<_64G1*G&k~gQm8=l)!u&ABbu-FdezO;e2upjiP7BD zJBWldmjun%dM8|lp#seZz&Ve+|DaiPwO$7Z}2WgLYgap=JsAIBu4X1UI!w9=9|3TOhj`h@6szW zHFx%|M?#vbf#zGhdyp8-me+$wpxN?r>tl`uwa)h1A~7~Q-hD_&b6wEvdCyR&AfIpd zMzbC%!P~tGS7vIy!+QXU(R`;j90_S|1e&{g((b02RV5~I1V_ahS0+yyj0=AGCGLj{@#c&8BwN^pQTiHX#Wf!-k`M)MQi z$&Ixh(tIaq9^_R+V(P}zUR@%A=BK@-Ohoe#uT&GQ$7mkvU5tb@cL&YSdaaNc%_F=H zL;}qtyxmMh^GNT~t1~r^@~%fhn)`s}=e>K77|kzwJ%|LFU-WXX!5jTycyA#g&0|6H6mJ!U3N%mmHnJYkJl%7z&D1=@n~lV1p6RVY zLYn^pnrC@GP^duj9PbG05zTYF0nIZtzvHb#Vl=<&{fLA#&jiiyc_&_np#shGz0-&U zB{<)k#6;@G0`D*qqj{lMx`oz5n%@V_i@l4Hn7XmVyMjodd5O22iD+KxozYV3F`7U0 z>LMY{OF{ET-VI2M<`rHSB7x==-hL*c`4g{RE2CMDMOS(qk&xz9pm~+o1Bn?wuklW4 zZPHa~Ttcn&E+LXz=_`6a-QZo$qzj1ZH`WbaBPQon7t$OFy^-m&(i^<{h~OIH7ZCeK z?@1;VU~y2BP2PAWdNoHLuZAv_EQ}_dAiq=KZwpu+RHjJFR(Z z-q^+T2EETa0|}Msd7x*XSBGUp&-Y$KB7vUoy*nd%zW44&LKf1~O~9QM==Cg=?+L$CD3qRWN%#(uzG@@P zw_FW+68=LGnWR5BM2a+o6Z-zkA<|-DLZ$qvL{zixY1In!r2SGiqJ?V8H?T_?NFo1B zCRN+R{xKkh{fZ%SE0A1&ITQVrE-iDMzmCaPxRFegqW(4}XS9S-6_8^7w@h?>z5~ed z{$V6kUOJ`6`^olLTd*c{LzzlM)EgH;x?x7KxPLJdshP$7MogN2Lu)0){mvnx=lx3f zZU@w?9t9a4*Gc|DB(#U~V+T&w`+(tZQQ<~un9~DtazJV^Ddn%nP*gXx%qjj3B#V*~ z>QujBCmq+Kq=Y)%pLVlJmkw3dpNqtVI@8~4WrjN2ujOlE;wtYqL}Egn=hwI=Gt~M1 z%@1i}b~aw z_S}dhY|kwm%A~7nOwW{c_d+bCL#&j^CpKf_RNjx`6Z^O@S{4e zu$~e~!udRviOJ_(NW%8~5YuxwrYE;oG+m96g!QyW61Jy3hcfAMV|reV>6sDJ^KMMf z(wLsYy|sm5drm?Uw&zqN;ryw<#N>|?)6*@c=i!*10Wm#GVtPJ{>G>k2XLn4`d3~bk zs*WU_t~yA<>1x8nr0emRo@ZlvUWnixG zGWDl(OwX8@p2;yiGh%w?$MpOj({o%uZFAV3laPe-xf~Od&z+Ej({)=+Pq&z!UNJqh zVtN+F^el_%Ss&9=>al43IU7khT^Aw=r>iCtldd~sdLD}D=@Zj4B&H`nre{@5&*w2c zyJLFJ>mN;5btK_*)j<+YR}&^CT|HxZo`~srCZ=a>OwZ<+o;@)=2V#2E0RjGIzXqQf9|Xt8uM55 zZzF=^!#f)%R7Jlx%gBuZmHmNC^!Zc023^@78VaSyewF>nM3f#wJ_O^;%Kpj_(J~kL z2U(BS{4?ypxyUaxknDL9NJ-FqkzbKXu|@QrQpIn`q!bXnzFfuc!Q^q+5v12EtNO#3 z3&c2N>%O;U-R_GBgt2C^FVd~cap)}=nbi;?$>>i zLg{o31ah%IEJU>COZ=Hk^v6Fr2$%mk)seb{IU3qeEd^7)2Cd2QclQ_-%+C&nC?|^bxmQc<7Mj<^bLC>{*HzdZM zR(@Y5V$b#dFeJvFzx&&eP(E)1J#GC`PiqT}o{s)`Ohiv-za|o+=Vm`238m{N&~uBw z5s67x7k>v6NmmztFOeW!UHrF)nEcUtEPn+OqsQ^rG7&wFzlBJk$MGvXWAr5dmQ=1k zIz-L{a<^aPS(M3r7*>Gwjav`+r!hG*l~$cT)Z`o{PCzOWQM2HFze_>S1OAa=CawlR z9`p|mN22r&?G8YC`BO(~Liy|f>Er*3#N_ipzt|`(ldmM72l=NWG5I{$uY#nvI+u4p zKIM;kUWXd0cCAUNM?mva{zM|dX}BT&bR?!V4e{SW(pUWofgguZL;NK~u)RMGWT?Lb zNxoVNYwH)o)Xgxz#tUR&k27HRI`mq@{HaKKtB1ZPnUVgzqqPjx1bt#Bf>CKA^Re#w_mj~W7T>7MoleqYYdsRXxoesPT^^-#u~lzh=|OrbCp zqaYP8`dyIpSCe2CV$76;8tdPVBwvZ1vHo%<%l-~KIzCINasE#sJ(sM7eKLNDF=VrZ zn&8(!(p%jR_Dq1dUiIg`tYwHy0rHyPc^r{jL1sRXH~ku~5V;y|2G;xer}({)wjGt+;U2-dod5Z7D&3MBct z`o<)E4(M%vEfO;#dfWd7$)e=7@U_!TI}#vs2#I+P&hbpv6G^Pc~FM9=&F z#E72v{dXdI-uIVALe2A6M?%f>cXKGQd7i%~5^BDG2nm&`zQ<#}Ut+S(!(EA~UDIkA z+{U)R@5*E_kh0M8FYvn~F{QJ>AB1F4QsP?R4?|+|d7(cR3B{$qy1USS8;OZ)kze|C z9eaL4;#%aFLt^4uQ{F`9b4>oM`E5J`F@o*Fs|SU@}d72 z67vLE<}X5GV*kiri^RnKk-wdZqllCG8hbCFP=`V%6dKK18t zC~2pk`uUMitNc}wP^BkkP@nrZMnZkz z+mTRT_zy)wec?YI3ANFGHWF&1|7s-EMt^1`)Fyv^B-AEZV; zMq=`3livx6$)8QW%c0~=e3L&jrsqIR&+jokxzj0jqo)lMnW@<1KZ_)6&ln_OdnU#7 z9E|CyJ|nZ-H~ICDg!ME*63(AtOpHCB#q?~B>De9AQ)6Z{pSvLmr|VH9VS5H}D3d>{ zV|q?{E1IsdNWyw5APJ|dD-)BhDKS0o#`G+X=_xuZnyxF6gwxd=N!Xrt9Ll6?Y)sGE zn4WDhJ$qw%YR-<@b0?CpJr5xX+tZIj8GDw;^c?qgG@nZ$3F|3`B%IGK6O+#`#q|6m zre|hM&ykp(YICCYT#h7cPZK0zdxkJE_TVqU~&r?Xk_Kb?@*%8xIW^Oc}&qWf}Qw2#lpL;Md z_DqlIc|WFSX-rRv_oDgS2uV0yt&oK6>A;~({!EJL`7);Go0y)1F+EqjAGPOxBw>5{ z#K==f%v;hXe*}`g>Ry;1+XrLOP5w(v%E0X0As}D+6N%uPiz)^8*ZFTDF}KHT@!vzT zD=`G_W+@6XTl`Ou7<;z(Um!8DZ}GPy>8pA{Tz?+`U-NW%Htj)}?VCy|8h85z?vHl}B9Oi$Ux(Q>~KN!XqmNW%8?Vq)x> z5z{j-re|qPPstCW`P>*uI9;uggzf3Tp-etcis|_xrsu1ep8YXBb@QY4+=V1;&%;Q< z_VnXW#-5ciJ*g$pd@hb8tfw@Ra6Y?COg_IF(=#KcXKqYS(WTLJHANCmS6d`udpdI{ zldh>TJ>SLj{1VfX{*YofdfG5C>3SAP*q$*+!uCvx>FKd7ny%?c!g}6E64tYnLm7KY ze-zcz7D-sotw_RpZs$-&&)k@vUt)R+FV}H}^^`yoF83WwO#YPlSceMRb1sswo+?Pf zdU`Q2dgjISd=%5OI;LmRifI0Pi6m^#H%P+v9OO_Ye;R%g)zcSASkKc)!g@w=D5K~0 zl~Fz8kc9QTi6pG&Z4PDhuDrmdtN{iwr6}y&)%4x3qR9wh4oyDB&_EOB;ooqfQhkZNledYF+Ce&dMd1r=Cgw& zY)>~NVS9RTD3d=+V|og&iKgo$Bw;G>(9=hC%Ndm11K z+tUn5*q&ibj6I*l^lXml*&WkUV_h_#yCDhZ^P@<@_6*=qCZAWw^qjOlny#`)!g?wo z38%|tV$$_eOwT`JdS=G-9Es_vvmt6vV1xKrq^l2-usu)3^o)w>*%H%Jc4IW3 zFGLcyrv{R+J>8iYd)|!cc_*f4aZFFqP0@UAiX@z_wlQ)m67#mT)xQl%U)31iw*Ch1 zXj}aUk(g9$_xm9+so3tXW+JKB?icwoGdH&Te?t=1Qw~X3Pgf>J&%~IX=`r#?5|fJU zem)YqgQ6UyV!Qttlcw+vtH}<33ld|`PJcHNW6w^%%I3^m-RXBlVsdq--vdclPd^SN zwRfk#Jf^4UmdyOw>6b95i*dCXODuF`c4!Mt?cL)ahorBX3vI9=#JfXoUau;yFc@VakDERv9Ao9OIBaQs@VxRdbfLaty@gv|;Mp7rxVT8O1BV zi~N-3nrD}n-$i~Q)=Rh(5AL(m@%?=;*)be$gVz3vHMI--(byjiAC3RfAIM(Of8l{l zf9CJd&s3#&HfG4B)Z6+rBgFruN9lRVCA*~DrC)gocEV~oajAcS^5y=3;)feixjm=3 zo!<+03F&ZBZzR09(fS$nOFa}<%3a#i#L-kfVyDNRKIQ(B^uYH-uwJE9 zzghHqHZJMw4SONA9&sgJiAUU$6SN%cfrfj0b$#mWlATBAY1Iwo5-R$8@r$TDq#b3m z@888Gol>8E;QT3t^~R)I(l7a)Egz!wWVycB7xHfkw?}dRUG^WV9;uH}dEo`+L#i&- zgWV5NdcK=RS7}@f8i=Hn@e!irF=Swsnj@3WtGi_H&jYAi{XNvilqeh~?keZBcLW#aVD=+2p zuZE}RDC}g$@=mG0FVp#I>}`*JW2eOXSHp?DqUV3ppHM9^Kg5mKi_G7TRjwdC|LX5z z$DeiKto#mYPn0g>m-2ZF$04R3Wb;cqlJd>wf4!>iPg80w_R~_|VE;UhbKu}$e;B~f7oRm*)jhY?WR@w2<1!k_Z#>y#?u`-*bZS`njEie|fKw=lo|p-u|=hUT&Ai7A~nizLwf8 z$^Th@LLFN?$13+{<2Ujb@_Q_~@U^O2ss82OPxY@H=Zn1m&^Qh1G2F$k>*aAc?;`C7 zPA>Ae>RW!#J+T$lYZ>4Du|K6!tEpaP%4OpTC2~)!$qX0u|HQvbI;0;ydO4W<`LpFq z<4G7tCdi&&;8r3%o_l{cxia3IQG$NY#_4zXIt}JWO1=F0(dCCvqjI8tA;t1$9-+$( zl>ZMGrufBA{ZUG7yjXLXFZ+e+v;02zdh$>Dj`WX3H>F-h7rs5hdPQyzzDFcgZJaMO z<4hUfe)lQaFZF{sw13nC-;FIn@kl?mexa6!Gor97PUl}Uj_-7qm*+3_BPnG)P3h~l zoa7(gN3O^f#r`Ix&rEy7PU;`wxpg_&C;2U0%0c|!@1Xo5 ze?ooGRO>b6C(o^@{8wB~?aFHTlsXx{Kcv&w0PAUvnvQx3sk6{uICxG-yC8ij6^;MT z>Lq=-fqqFxrVHN-;d&?OsKd`s@kjGDGkk*kGsstNhmS^~l^~DM*cd7qh^ZUuXBzuh@?2!`E1EQ>yuBvhSAiRF99x@eQY|7(kZ`_ z-UN?#|9$pIdZqo5YxGMyr1l5%)ZDL0ddW5N;+K9m({J)a`a}6$`q^ka(*Miziq?-3 z>ehDDK9ALv`c5v~xmSbw>D)$C|BmhVXt=b>`B{h0QT-p(^|X-sJI)suRzJ@U>-|^5 zNxKz233dOC)GnT%uj5Ino^`2xN`1-JZm1o>xao>xlmBnUL-r=r09+q~Z#kT)?Mta6 zyv}khjvL^HQ|_P0pHTlPcOiTk9{U}BFZ@5vAFa<+Poy6G_v(+-N9p&(eGI;Lqx=2L zdK&ec@wDindI2{IA2qy@m-J_=znSrxa5C;a*7q%Wuc3I%dk*VZp`#cxCDInf$BHqV=5+}$zdM?_A?SL>YDNpz0_}uT=w~tY~`@2 z5ZRZ5&wo>2Nj}Bnfi(9AIYB14JJ64(*BF2D)I`o6Y*bjDLuD@OSn7vy-LzG6Tbbb+n3liZy@F8*k<%x68J)rjAlC7jqrE__QA_dBH2!dt;Cu-vF zM>)558OKX|l5`1|^h>%PDyr+JNq;tdB$o>E=~(qqdX4=J@qNemGxIMSPpiyu+2jnD z{FVI9jK`E`rhW179l2j7*VJQq&-(wXUP=10*%d7xsh`pEK32J7%SV$x5}(*9;l+Qf z@gA#OW;vSY>7V8I^rHTD|1Rox#GlPA|2?(8(%ZC~Qq_N>aJSt`&jFghF!Mk%UnuiJ z;uksTU+DLQnvC;6W*is}mxTCa-Xj`*2;6(E`;Ae&?oIZ=y2UwUU(}`bmZU0$^Dzk} z;YBX`yX@~F*X-wu`X#)`i(g#fN7Jv@A>_Fya#6d4A8UA;_l573V>*p~u}8T0#g%a4 zkH#llJGr;~#NlK1t?D z#ij8r+${Jpji)}vb-Of=19H^+_&hJ9X4KL3yRa$`-<-1nQ}aPZ|r)MMl+9GnTCmpL92J>tqdk<7=Wrqu^u zX}vjWK{-lq>pS)DN!1+Pl)47pgnGOdVBp)jXg7O)NApkeYXq)$B^0@^F7qSBS8St> zFQH`rZ#68Rgc6sY3kfC9=W{P0`zrsS)00xN-|jiqGp|4Od(nMjTX^0l8<+j1>37MV zD{5)`VBQq{DRo0jvNtN1={IsO^dURMm2`-E^*oA4_W!PfdlYqi?an2h=})QJoW3Z3 z5uZyCrXq#=ek<8|qyf1UkL+(xseQfvh^G~W&rvy*NZ%1(yKtW*>VtI$*8gxfN?!%| zO_!hS!;|!6_UG_b#~E5rO0B{Dy=k?X_xX0g_ZiRuUwhTRSH^UvRS({WO8tJe@HwhC zj@t^UM|u1fm9JG>+m~j!oIq|NFl~1+UT6I1O!A+%htf6t6!Kq=^N|Vl^(IQ^&fjPs zz@N=m$xj+zz?Typis$7Gq*vU-oUh0FzPhL@>5=^Z1@|QG%@s4Sc5-c|x83GubEW;OkqEe@Yv2cfl>Fx*V##LE*dS z5GOrwCuWNHHxE;JP`vQ98~Eae4j1+J8cX5Hg|8TKJTC9cy{Z-czP%;6QcriSBYhQ4 z&^)QgUU~masBFAQFKrjx7<+*ZmtwgzhtJ{X7VM)(Ipdz+h2oQZ`Q~&=-^vNZrJq=S z2JyKQiOYI}{4RRPh40qBM(GlNwa+M=`2YC#go^4boe^GsPpV7~^VUCWJ5#avi;tsx zAv@BMa7N$hhber`i2eH-Q2M35HR?*~>DrFc*$UTtlWKrP@lbtEtCnL)F7tP(F9~(r zoBH>pn$VHry}N^UVIB5lijRJWeP_sxyAzpBc%|Nd&-Gr)oze&M@R-iH+Z^)`zOA*F^kn)|YGwtpL-@VCpCRi1 zf#Ox7r*TLNFAd1jy|6f-)AN7(=v}o<0c~~>xnXN;ooV|<|(CL`GosHaqnk;)O~Y4<)gUi<=PMP zV!S@Q^C_yY#8Uy6_YiTZKY{Z!xLyl$e7v4Hrz+`_er?VN+7I`+^Y0Q)p40RkPY3(> zuDX!wPt>LT)yZHSR=5JiBlYyWU#Wc%PpKSk50XCNpHC-yqQ6u4lp?vbA~_hJWayE0 zXZ9URdZeE$%IEm<_?(yYqXo%{{AHh0e^LvdU-0dCevXJ<8PAE{XuR_KpOvR_hjTl4 z?hp2*b3KuGqV`)CQGUxgp=@%}zkQ7L3cgp(?l+jOB=46@aX*$;Z7iLB*yoVc|()pL-a|CJi#Q9Y3cYI6f{J9_b8}f6Y-z5}I{PKH2`({N> z+~K83&uwgP^f~;;&+USq`|_L>H~Rd}#{YZIb;?J$5ue8!1?3;rAK7=2;PIDnv(+0K zry04+@cl`~ZwV#kEcN(U+tV4tsr;UrsojJ+XR@v@u>W`o)hFo>Gr5sl@@+Vr@Ti+q zzxShhNP6UVSbyIX4*%cxZ|DB-A+!U&XN>(tLfwh`|DfH!O8HRo3~hf}?Z*5u>tKQ0 z3jKThJSU|usV?I47nH9__30tX59v?nc?9!2dx_`rdx`wM+a~=amk#9U{X@o)@ZQ1w zt&C4H{VCo@p5}8wIqH=@I{yp7IGEySg0qQADQEJ$tN?e&laE9y<2}e0`l3vhRU1dO?lCCq)-9&XHS>Jv#k@A zH$8{I{w(!Z>i4NnQGPvw^I@it4$$xN z-a+yy^*Ya!MfJ}ZOZjpDzR08NP1OGw=1W3F-34t(KI%R&l(@J{&ZYV&<+dEh;mKgW zDr!$BZr4$Fe0kCnb!8kOdg(m~@|(x||10k2oR8VunmB)xR3&;)dynSJU|e^A{Uto# z6#ZT1tH@2Np1iL~>L<aZ`hOC-aBl^U2Vs37 zgQt|7tCIP&Pi~-ko-Ms!x2E64mHBQ859e8U9#6QGkN9c+HGaNK-miA>`YE|dHEjTu z*Nf9AJydR~U>-=unKHh-7rwQw+xzf|)UL`^CjS^7=lsCqb8+Rp>=N9El~Q-}`fGdc zH~)^~)|6_6^Iy^Nn!NAB`|dJrXG&d%`J7g-@i}1W=Vc#IFmAzdne1DU`9#?tX~qjO zPfzo=aPIR>ogTB!NcAB;KSFXj!91AstI=|$b_-t|Pf|Wbf8WjPf>EBvedI|c?OxUi zWc<=^BRwAn6sQt+G`;6v=70u7* zs(!e>lc%10gz8Cco>$K1(sK*WMMdOV@qHT6-{rX*oxhU)L;8UZt117Y;YF{^+lc>P zb)`MgeIF_H^EOI9`C*-SJ@tP@N|0UDK4pFh*3)@?L*~y#ALT3D=fmr*!W-ba3EXGF z{j`i9Uj2aE8P5+#=g(5Tv|Vto;I~w+sUOH5+7Dp%=}Ugzdltz{{a86j%O`_-4!*|s z3t0c;`KkBVFD{J(Vc!ML@4=Tf`F9x~%J0HSE*Z#?Kf&P*kJfK$Pw>r9ey^4F6j^8a zbL)rjK6EDKqx46T{szOSTxHxt{fU`>6~C-Ye)vAgyANo6N%hhn`px^~bUrUC`u?gb z^G5Q%6kW%>^=+yLGH#M}%qXY!o=`FmO8u{y5BqELktrqXj|Hu7O8(3HlGM-tR=g6Q z%*STN3*&*)shp)=%DQYeyQTk^aiipe_+|cGTsi+s{Wm=49;5mse$hwsz^TZ2KUhD< z@s?TtpnCy~U&3dLpLkNq{tc0re4+JTqfg`|{n7j)znN!_=AY;-C?7@tRj*P0|GVOm z_zTh>wYwmDqw^!#?gPl&hXMP-`MFOVZUcRQ`fc&|;`J`^)4EuK#~UF3R9N0{+An4N z-Nw;)K=jD`68VkXKAaCS{>slDO~3iQNi*tCMellC_cOmsy_4S$XXrEYxbpkMt*N}l z{|UcWlHcg9%kLK=XW@9o{67D?OuKS;d@J&QoR4thYDT#jy|f;g-QV}UO#8=ohQ|Yp zlO^8z7ia#ypb|sW z?<1M(U}n8+)%ltAgvK2yzDFbez9RjJ+$$^d*i=vB>x~c3ApP{a%$t~dX|nBml6fDh z4@niB=gEApl5?QuzMX7(<^4w7Y<5Zf7xBE7)XQx4WYZJ1L*Acc{hh+e`<#@ES;vz7 zhtkf2aUsq_$vu_HVEtL{sg(D*D{D~x%lb9V1Hc#VaGt=-SBV`n`)GODw+i@V zKf^p4ukUB>Ta^An+`qby#e}1I)HLsJg>@}_-zpUBXN|rmNd8HG8SN*9A>DQzEuG}l~XYWT5eL=sA`yR62 z&nM?C>3mR1HRJX8D5v@6bTFSS`;Y%uUFip89qX_9DSc-CC!3$zpFG#){zbDdhRV@! z>HkDuw($R}a%8W`7uokE>7aEe87KVvjn9vhc%$L?NwCe3$IJ|tA_r8Eyi_QC!`OgEh8c#8OI&B>MaLUzeLXL4VO z#Q)#pH2zEm^S)8P*eU&XrVHl^IiHD(ANJ$(K9xU~mvl=w_)f~}`gvmP$g*Yr^Nz9~NuHbJH{r=7mguB;_Go^kn*ty_r10=QGSYZyB7wg?n;vJ<^z1PWaCDwlopC>Aj;g`?F(n;R-r~P_hpzuA z)tC1f7smZrNmU2O@hL^Wr-R=mzM4I09470${c?0VQtAv0pH@3@9X=-(PWFjK*MC=| zJlx;P`+X!HvMZ%VAy2Do@myn$s*i3Vb^mEpKSjP8rXv;OUKW1cQMsJM5jUxZd_eI? zzRCUy8b>9Ra2Z#MOXp0C9@>YWQu(;foDS~s7W;{teXG(>yz>$DQ&X`$rqt)0&l}L6 z6YRSbJ=AVxACp1sm}4xdjp)W|5nEWVZe4L+$h{JhP{$05A z11BVPI8)!qpHlm=KZ1R-_`Eg0OFl_>NuRurQ@I&=X>TGgr=eHGp!^(v1h+W`Al}E)%$X+x4q+jZ-xbnM%kCwCiF72EA zF#hJ}M`kKT!6goBJJPJWKP)rd~>ZmcaWg zO@7L~L9{Pe+J7`1aQ+h8F^tEszcjz!$@`Hdf2F^m{7tF0I6n^OlW=}fp6`W%c}7W3 zbl|3M#@=1076#6|F2jO|}gt{qX zU%inN`T1pZxTIRRoBDAHFXbuiW z)B8BNu>QxtSLE|);>x{9vj0iK`@GM!=_jN|_K8;EeGKQ^P4>uhTlRf+u9eBX(#5w(wLBpg+U*XwJ3E`eHUt`+2k9MA@|B96g{WpTsW*N;`e*Z{Xe3A(M445axPrTh5QLs^B(G#XuU<&SyD>&eNSxv z$G?BPf`0#!&qjpm4CMKX)~6}kNO)p z2hV^B{cACYwC{o>QRSOu0fz_Q}b(RqhL*d-q_y4d=OF-u+db|Im(G>ikOa z{9-z||Dnh2fZ=z^zij;>g->#R!}(=Ck8>rbPx{MCFx@FdT<*ikQCA{|`49{z^WZ5( z@@baO;d4`k0{Lv^Kj?TW|69;4r1mYM=gEJn{AnKkua8r*J>TfwOsU6k{{w!1FTwX| zn*1KKPN&bD+oEvhcd2LD%I~(1b$x{Xm7jAHBhR^l%8klF-jC(n!=Lv{dCK!!{8XOs zT@@@(b3a42@ujq1aix6~)QzxgYh59W+i%DY~~H&G#0-{fj(L9+ji^oM!nP zmM@f5{^)uOsVz94URZ4@Ozq*%#RK6md~Oh4+PCyKGLI3}OXJZrkI&7x@Yv)EsoU_} zRbh2bn(W?!^SEZ*S5W=@v;B&sD_g&otsNb`9h-SWSvMK7i{9VloRGXnM}L?4Bj<<2 zziFoqpHkoOxxvivCO(QMe*co_p>Qcha%MhU<}>B~)@^6C-(ij>&LO$_pkP=(RhwEJ<<9g@1w$_eyVo~CGYh+I#B&g zwxaeX<|LO6ztlP?XR_ZVL<$WZf>8WuBm*`GetCP`=1KP&K^QTHec3 z{GD7lCx<-V-^|{o>rXm(kC*ulsej_9ekZ9&9@5G80*XufWju>%2V_fBo}?p zjE0N8A4c;*z3-#vk&N3?JnxaM-FJ{TvieN}Z1H8EI7s-!pP}{-{uJFGfM}(tJ}gxW~5E z*}9yJU&0eN^G-4@mvjd02J5|<7m{```dNAo$~@F5PilXB9_p$ERR3f?YVSJoXPdX8 zdodHdpE#*x+#_~nmKWS-fb&MF|Iem z_hFV$eUk5JujxnSC-*6@;`K6d+dM(?_1V3KT^ZL@<91E!;_%!#qV>UkCFD8k`!gwg zpN3SQNgn2-@%OY^#CBcOhU}(#ZhjZL<^I6_?X;ehTFG`dU#|V~o+++bXOR1FqR*|G zFZ{J|qKBR%DYc*)<=-E3dEZK?!MI)s_YPsW6n}3z9q6IoMK0*Rt{m=v$MB{eN<9<35+8+&%Sk*1 z)oamL6YHbcpH^gdjv~7Ysf7nI(@LOh3mCEKU=;*^-I==>APAf zMf+&rdp1pVxEytBU+orBRWH-^qj2!NqWY3lQ+XfK^LUOcsaoLobzpuO*M;Dm$j6kw zO&3u;p!iJvlz1rKLW=AttXhj6EI-hPE}SF7a)JGawZR-{pGW7gPZPiABlE@ywga9k zTpuc8`k{RI9?z(rjTh_m$~?7+N6MG%NVDD?bwBPeGx7X3ONWPYWBpfE)&6AA&&%^y z_AgyOnaWA}?NnXz(|u}iKjIJ_KB0)4=fz*m*Z+1nN*CPkc_!5d@k@SEJu&Tt%16>E z{ZmTaltcL-;S*XYA(b(|A@-bnqRc5TZ4xWm+M(D@p3FBr+^1pS4yTS*uFZk|V? zr#aRa(+;Sgm+wjzQa<;C(ymCa=?AF(nffj1kpA|0zIRmi#g!UF`Lv@X)hlU-6tB70 zZcZ=VPR;kOBz*KcSj0`Z%-`XAx{>%rFUh49eYY=1eT4B9Do}m_%H?plLL9De5Khu9 z=RRfrKwPm`{MqCsy|N!q{QtZhvIW|ogu5B*A$)H*Bb|~T;=*}`GL-M4C+g44HxurU zxc-je>QAZv9`Y;I%lH458IOdQ_iKs2AWr#g*7Kv`qQBGk)sjl`UF457ol-9)oY*Jv zMD-lK9u<^t@O?(kC($S6NbfHx^&!?TQ@(|;out%#ygn)RQ9h^C$zydtlvX$O)A^pG ze!}OZiRYr@G>7llVY#GPK8NLvy<(5(m-G`i?I^R}8at!;o>?E0it4S{8;w_Z_H>XP zQ902gE__!kQvWjjaeI%R|7kTH?lKOyGs=JXem9nf{GOvGb|yKwcl7%m^jtW4Ih**S z`4i25sb9y&P5Lv-EgLuGDdjEkQFzlYM(r#}f424_;R^aanqCP<+}JJU7Oi)g@}?a` z>j9-(@<-gHlJN3gMdfMEFA<0DUTvdtmi#08qWy4^$C2^pSTvo~j^lPj<;1>%xYWPQ z_HXjzZR}^@yU^G_z<&FTc4OvMi_W9|oaW=;{;avwpL~a|k;~+1^-um?#sfQWpD2tw zx9aBve8*@oIM8p&cXY4pK+g}7gZo*xYd_pq$l+)l4Er`v4)z0MyMy~#BXXku$t^m3 zT0M1G_&W@bEXE}bdJ4ZFae1`ArpuL4T9mbw) z>3G+odKR@y^ppKYkJz1y=PA-^B<6EYaF0w>Po`X4&n2vfWawt&pyn8>-`yzeMwXbW>HFM9bS+i!% znl&?P*3=Dr;=}r`eKGLJh1b6p-x-qs-{Ss$H#R=edYnJ5x(R&Fbu8Bno7+A}DL3v4 zx-gFK`tIhTNM||ZtK_*40p7Uv-oG>QA7tZ+evtKj*d$)u7Wl9fjzjQs?fe|a*YbAn zx)jJwwTHVh;90xA+LH|q4Dr4^ux|(la`%3ypLzw$zjkr`to;eSf%U}x0^fK0>jm!# zmMi3&-^BN~y(xs1Y-S<*yC+XIywrZw#!cz`py#Z{Z?%8#HE{oQ3+Mtq zQU4pp=h}YLiz~U{LG%;(h~x9_`$M~H{f_n{u6FvFILiaR&L^(}pXJg?N8rcvQqocP zElp49JDh>s^fbtssg`5AAHFBFNBzW}h<2s$kWb&GwfcKmLi6`Y1>M6Y*eb>a`>wQZLdjdT59qYG(uK}kJwkH>Vw%2=@*L#%P8>3$J z(`5Xl`>;O={iAxB_dCC@xzgl@+w|aTj%Y1C-Wa&#ha(J%c z{rS0lfamTz1^PxlMESmK@_nD1vYT3=;_MDD=AsxZ*alBxk zvzE^-jPZ_f6y^0DHczR3wF`tG!adf&_SF0T8y9y|XL)Z&5DCPq}pHX{Fq{Z@`QD)wRFF zp?(@{KQOLOQ$D0#3A@kA^}fQ#Klc+Of2Q=%vbA(xU1%51LCD&VyUm^6Pz60oVT=sm7Bk*tGe8>4o<=dwr z&!6*pjEngbFXU7HuEuRk1$@-+&3nLK97oI8e8_R6=Rvmbu5!iB)ha)a)ONc4JkLvD zpQ7)mo4rCj=lipr>zDE?+t07$f$g+frNf`M4(;l>f4{yw_0xeK16j_-@r1*fA@BEd z6qGN$yxxB_zX!GFkM~#sH@-jjf^|bZx<0e|YcF5OO?IAH$}V4@BPK>eU%;_huqO|>+b@A-VsiY zu4z5!k5~`*tduS91OKFP^mAP2a{i?}Z1;D>hwXZf6P&u65~Mk zmt4+p{9WvFhIRocUkS?B&3nO1pAz_^biUX9BZD(~d@vu&b6k{W{}bgSY?_+CgY7;nAizIoa%*#GIig~^eg{Ql6uH)`i%@?#+Hc6miU z#C(_Xq3#=-o~L$mBlz_C3j+S?Pr~dp^j*{MVw^2|Aoc?e_IDEzujG@qA6Cl|w}gCZ zPh#&NMSetmQSQ->VS&w259y0{$5ih5`w_$g_3QnT%`vYGuV3GpG`|+ykBo7`hkaHd z#qUus=%@QrcCSC)ivpkZolb?*S@lXAuS!?z=i0|xPxvl+sy~f7FU3Au;p?2z?`3t* z4>q**qRKzx`<~;|Z}O9`{?q7I`FFM_+B;#M=l4PYSMBBvZY7WOA&)pOk^U(kJYe)H zy`DH8SdRSwKCa}QX1A5nt1noOs-GgWpP}AXIo(w*+PtB5y>9n%WgYM<<;;D+2hi{0 zJP!FD*KeE`sQ*%a;C@x%dUTYlOyAwk4b`uQg}VQ@VBnXyz5^Wk>8k|x>6}L+JunaJ zd#*MQ*YZ7IFHzr0zkkf+MJ$hgtpxH!*AdD0@#GKJlU;kEbe%{&o9jsG&$AM{7~08< zYZt@&Y;C`Qb+c~xp09hkf6RX4`h)2V;dil=zRe@*Z)hR#Lpz;*8JRD}?{oXm1J}p= z066jc@OZ4JU9R%|@U(o)&-PGXVYsg8!PJLi2zgb=du*PL{O~f@zfn%#jeAPq%SvAB z_lmagd(ZSMoyO1qA>`f<{yVfg&Oea5aUQ?O_froy_@!L+BjjJn^u9t;ILloSzUzJ3 zw+|27`Mc#Y9#rl-oOZp1wBzc3%=6{s)5_}vJW7|(h3uAT59nOWrACiJE_Ay<(618G zf%f-|Q@;Jo?f;tj`ImvdJulan7kRxoy1@P`_Iqyqe1^?yaX*Ob=Uvwh^%cVL%8=vw zQ~sUf8vcOP570r7^Ix!YtDet4eOcYkYX@ zXUp2NLwo^wOFx8j%DZIz`5Jx_w(2ffE)KwDBrp7!8pft8pr-r z9Ou~Iq`#(^j(yb_r(GH0aetKYG=B=0-zisd?o+y5a;)dKZU@fu6xa_&eYIU*TwgOC z`&3=}thp%U3h+?7AzP3421N_Xge`R}Lx9eOk2rRCH5xF1RRw3q4O5kB&h&T7Y!(2M1?w7`W z!wa_3VX5P?t+&Q} z{@-mK#wYnH+VfF<9RCpX9N?AD$MzEcQF!nk$1-jQ^$DBbYdL66;K!b9Z=t@o^4=hC z^}VF|l#l#DdCzf0dll~Eb6*kThU>V(?E<*pT*!eh0e&Yu74rhhbDc^2yW!hD06t(y zyHN7W*1nH~Q1_Wn4|GvGCqI{O`-P9+S)TCzpYPis&ie_4Y-;EHX~=zQwb%Pqm`~!n zQfwFQTgH1LJG{Z=hlSBDi1+9yZ+LF?vJapg*yq%JaPtqT_CB8kobmGYzJ{fV-g8Tgvvo!1tG(f^?j77ec?*^fP$dTH1VV|ChH7Vd4HN zOF8dGluPTmeNWVPr{h658=9W0>qFlU<9$_>*K<6hbEG%%CjId~TqO^=T%kX-ao5Gf zgY`1bdpWERan(<4yv2JS(C2&d_veOo_x;>rAZx4%I&!|?JrnBv^c%b9^Fp~&UU~Q2 z^Hc9i>B@c~{tTb@*#Iw|FYwe!RmwzC~9_{1)Xw=X9oq$`& zD0#vDqI{s<%#h=p{PbJ9C#-gsX4g^+_(|n`B8MRF3SoKjG5AUMCv88Q_f^%O`VoP@ z^}NsMRX6ulQn-Z2c^7(loaa$b^jpI5G_L&M{7X5R#x1uuFF0SI z|J9$k@kt@L$6Ypl5_qqh>qW{J+}l-u1ZF>|@5$M^Th}QY13u?#$|dwi9Pi{S$|dBl zeEjj;wI|0c19`=9OM6VNQ_^vY@m0$iUT*mJ*w4VLl+XG8PD39*2ch2mJ(EwWPwXDr z7vH<#IuHKnD>>w8;d?FjeFAhiyJd%4$li7?t^ONqf46Guo&}?E;qjQRs(S-sJM{ zn!`1!a{qO|3v#=X(#{ifzt8Mr;{6Mb1KO|P-X7QKabNgeMyTC zy!=GAJnC1wRWFZr^}YUq_v3{8un2g}koBxy${ErlPoJFJKsxo-QZ}-7RPrG|XTW^j zbAR7``K-}N&t=V?c|z_q``H*>d6C;L~)!jCwim=|OK*Ie2C0 z?|9D}<@#>dHE8x>b9QklJkDoW=k%Qa`m(FFBkqH6{zLh=o}#`%e#g37?VtSKdON-F z`{B`^3;4(NB;dvU7TOb}c+)Nu`@y}&?w|Yp46ph+_>p=7_TRZ58r#QvyTq6AV=oT< zqxa-)2yiNu&w;;cdDzzz=qJ@DJf4!g7!o ztWWhGzwe5CdJdnALwv8~q$>iwYKO=AqI}!R_s8zEc|`qB7@VHBEAstjW)~38r%H?W zC3}@FRjhT)T`otP5OOa*e#RJ5UNa;f z@N3C-MtRPA5i8rh0^Q%;G1S*{J*_WqT?2eay_fqWw3{WrVLm8?egcrrc_OC&&c>Cl zV;6xOdB5FX=}GZ))Wh;fk9&j8*FS+{JK8`=y`R==wl1t?x z?0xlKq{UPIsG9qqxp|3lko&xBPr9Fb!hc}Cq#R7;9rbdocM92cvoK#4lFC8OACMz@ z4r2Y@_kJHV{Z9Qk)u*A?7P6nMZ%R4A-+6hB?N8M54NLF&K6PK-?&l>)kNek1SAP|r z{}_YQm&JXbV{-5Z@}Vt2pIklou6AEmUtdt);7L+n;dC9Jely%|8}x|xy4W9m@4vU$ z{*>~m#r3@{+wbZ%{dWBJVSJYUUF|^ad)<8J`3>dPj8)JNx%75E-On_?HF_V(?G$c2 zJjfxHf9B_|l3Oia%NN|=Ex%3r0Kjd^J zD>>ac9$9{>`EuoR>5o_a;D^XJArx)aw?mIiZK);^TukY)!LBp46x#xJfy)MOm z-}5Zq7o6YfJ!^|k`1)7ZQ5K)+`r)hw?zni5o9|qk=S#8bbUwMo;OW@55RdKqlb5^1(kuCqm$U0aS`Kjg4tL;iCmik!hda~Z#`az2aIxOkb(h1%{(!!J z;B*7OM|@I#2OPck==UuFN8dxReTG<$=MuI1`8nmTN6Qa=cj*A=2jHI=XF5ah1NE}1 zDL1)veFx3XySPr$_a;2vu0Pn%Ll3=u^UyEdc^`NYUi3e(h0(8+4VMY+Qh(K$b9&b2!& zm+}?lSnciVIsCq`A5!|H@(ufueOdTf;H&MtKe*2Suhw~QqbxFtcdyej3vh~pj?^shG&3X=ROT!o^~IS2 zexK#-nccL9{DAV2j_lvu_ms}SPvwH)H{tlraQtS<8x5aXzVAG{;XmvzbnWzaIwkN= zOlLmeRmsLW?n~w8u>Vr(?{>9w2!8$Uwy>7px!uDT?EFjZb^IPA40T0!$-WeZ~SR;UxWC4^8S#Y_AOmIDC+$?8~m7$ep}Bn_`F}i zbCa&U6VE|EXZh7nt?kc6KfdQ}2fc#*xu)lXpJ}~^=KAzcTrY-#-q-g`6#YiO@6Nv| z9O(BoitD(?x}op-{lNA63D@suc>j0jp%m^$wqFz1X$Lv}*#A?%WfoU?W9QY>SBr+f z*zR0-;ogBhrToU|p!ey_9=w*>2Dc|y+kS+ex7qiBw{ODRH^bXE)7v-8+c#T)H|2g; z9<$!-z25b`-luxK@DJ4&&|@IzuL;L0aN= zKS54!Yw>}+-{!{&r{fHVH*+LB$dR7QA(i_M5AWwr_&htq<-|;v6Dk*MUw^is{sp}L zIbQ#QUjIT({jWE;rQb8v`$jgu)iTxgQu@3?{iBdeY+ObEIO%>O z%ISWK>D|;DyK*jt54@uO3H-bIO1fUbe64u8ef}som`^8soaE&t+NJl;Z6B2FuYJ7q zn(>mh1N4dhr}{RY`hxw}f$y(QINW^w>9|2ZckAcA3h)24`IP*|F!J5p`Ih+^rsbH< zF!w!8FJ;BsF>i9bUAuOO>v;PT=Jlg&zA5DpJ16A+48yoDPB>|P-miGS%drprHpt&N zZy(|5dnM^S_k!OJ)d&4tlJ+@IRETpewf}Q}&Ou)b`dOHlgJ$+TeaATHXJP$Ssvh+& z)7x~v!t|;_j{Zq#zrKh4QP6?s)QFe$Pzj$fj zfq3)>!1mHUi2FZCSAU}`wA+n#R&GyU_G9R#rnIl{tw^L|GZD$H$Hi^-pu!^px=Ry9{nBZRkj@an|y(O zCjT%jh5de_>A`B3;r{W?w0PgoRR^-Ck2mrY`cwC1osZKv`HK5g!~=2e)24V7@+Kb- zz^nV6?c#Ud-vHml`<27%*L&T5zC}Dpmu;+G)`#^~pf~fSAM7d|egg7vvHBHJ~4Ze8)O5=3_l+ z`egkGa9ncS0iOCr^zq9$<_Yy*>gj=R0$zcif?g5$Dac#&TerO|hwu7cyr0AKd>#DL zbN|c%pow+@O3t%j|cfyym?B=y^KG ztErXK_j6pnV;(Kt{!Z<|UEiY~%l=5^IprOGwAnV<7wJ09)yp?^^yjP`daJo@i|os#-{Tm=0=K4bcN zmmz&#;m&J))N2{?{ZzcS7w_RCj(Ioq8})0FuD{QKka|NQ!}CS|Jp4|5DQ#Dd9;^p=R$fmW&m3RmC-igF!)RxO z`+@Y2UC3rT270Jp>W9NRK<^_Nedw1dw;qq{aIU*iufErFWq?=vd2`S21x7qr5A3Du z<~#f*soy-i*BSG}F07L4KOW|P^+#rS>N`|cPv7PKz~M|p|4X`V4ecVmt}y)~mS;Ob z7w+4~dfW6W`b)GoV0qeq!tYoqPq%p^`hT3T`#gFc zcmT#H{b=R#Lw@#}{uVn2)^~c{E|Bt-^9SJTeyqVSB#kqF8s_SMqVZkC=j&%#Sjl@# z57hlU3psA%IA*&ZulArnD4%rv*!sM5|DRp@PqjU47ee~gWV>Vh>vrCz^Mc=FBVXYB zE~YQ4`$a*YAUx`El&dVydWWa$eu}+&*Yo=;U3hKyaU8(zzSpd;slR;euAr}Qy-$6& z^7AzHM`w0PYKQf`fWPi@n;uO4TJ5>bF0l0X1a$r3?>KV4e2#sO{->FacMmErS2zBt zZ}a~AxZnT7J&W4+>2)39>p8%UexRXG#`%!@k$_hT;Hf;Z{aUq0^nD$^w}5yl$RGW5 zA7k^F-t(|9rsG|m%KgLCf_y#Cr|ToTS1^!kES!*y{65_IX7@Bxp8wV$kLDd-4$g+g zZ?)s6_XZ9BzT-D={3aZ~LN3@J^ELH(##dQ7^t;|G@b%-pYlDw(vv$S%WAtm7rW3zI z-WtrCB41pu zx9g=}DD@|LDfBMl!TRWbpYj*-w35{45}gwK2ax%TGH1*qSVF2nOh{Brf69r65t?aAe5eaqQ-jr!^HcbJE# ztDoANK*v#Vmo#{4Z~e`7e(rlQy!(vsUH$r+pAwxX?+W}{%dYMpb3=c3fcXwMGvwEI zTr9nor&~GQJGS*?DNFl%4S*Nyy-`2IP2Pg~UTkm*X~%W_X8071U*!MX2IVPVH{A~L z1qy`AE)IS~SPJ=)^k)1AULVtm586XK^_Y<;Z3ERJp{)*B#@Y45946mxei}-;4tiSaBh;h>C zM0<}b`U?U-FkcL@{x9T}=C`4cPrp6jOZvuq>#UCcyWGz$K6N+R^-PPaKTC`2{*lLV zo?m*pzH|D~kiV9FY+c=J=$FffdUU=z4tl^5&qRHogWB7@5BPw;s3#PH{)yv*bpNE) zTgu`~gmS#^81Cr<4!{4!-q+FhT)v8N#qWrho?rb~{1W;x@KXO;4xjW#y|Mk@UIXwB zae0}3zvJcj{%QZt{(g&}vo84JfM@CcM+*7-jp!%xP-V) zDlhz=EyEMcK2iNo-wXO(uu8}m<1e*!eT?%v;1|V_5eP=wEb1} zALw!B-}Zq(mqPCIcW$1xwMMnSANo)mPnIJdPxtrY2#0aj%Q)+0ocRwkzAEGpe{W(5 ze{X{IruC%xZhJNGX1=}5{xH&kapJ=`@nM|!-0t@qH+6jeZghxrXa4j%(o6Z8`zuMm z?_u}u)jyfRt>hZ_v(d9q_p|&v(kCBh`^hJ4594e<<7_|U%zvC+FpKTwcf#Mt#(AVK z8!i=o`x{QL92g>>=s^Opl%FWMOCUn>#5 z!@~z&j(A!h<8ORrfM5DO1)Vnx?^=qZL%E*3)aKQ`Z2P+qAIQVzr$^7{)U{W0*fG;rvAX1@lfNZ@m=d+v$btm7Gs2InD9KeBAT-yf60{zJ=uK zL%pScFB}i`(_(sEB_|r4)z0vC&;k02zQg1CBg?}NqrMljWXQ+!nOHB%>3i@7SNB_v z3v|kAXIlF79hO+6?d@k04pHZc2geQ)80P;bPmO|Q!7Q48s_-nHmA z>I*4;?2l33(|Q13@2NW7`JM35uWFZO`1a)2HZS(Q9o>FS;gBzgKjAZdIKNa4AGGs# z>pS>f(50QPlHuvy^7p(j^n>~#Tq}g#?~`p0`4{Eb|Kr029<>0kSdRU%oaxiiZz%L* z^*3Ys@xaGbA)hckQtd@8gYrhEX`OdN-)OVDv{@Ux;_i?TMzVD9@8ayUjj`dyO{yKK`cOrK6 zdrI8j;5o&bhL76kn%-UcK1eN~TTfnp8|b&fUqhJ7r{@VHzH@(Nna_b94}TpY{Yf6S z6zVzG@K(Rq$6(zyuXxYBeR`i!+Z5y!L-Gs#Me^N;hgU@Z9q;}33+qq4FLg~Q-;?!J z<^?!Ewf@%kyKBV1--=hV-})iHzB9T9+RyVIq^n<3*P}*-m`Bt6`o7`cQ7_7ua=67S zxx?nkZvIWoA4wtb3tT#W(Qbl%H2J>BF2<*|=hORl)(*9saJ#EH+ksA3TR*1l?KSyu z?@jOh?Fl-xeY4AuaW*5ALk6WaXvrF(-|&geu;FvjOj;r+Gz~pS~f1dA*n{R(z zSBHBCZ*u+&>y$^wM|~fcZXf&%=z>uFnRQk8*L^kLZ+q8i z0j}=1EEK{>pO8PuksaI~3-SQv<2+T!73TrJap}l+Y!I?v;yy6fKMcpE+qn5CM;U@I zbX@s6->+UBW`pKcSZe ze}?V&Y943E_T92Dl>GE0jHGb+y1KV``Gv_EF)Z{m$}O7goac2Fs=CrydvNWwhUAKI$=`Q~Zu}J(vFn=#|Df zp3iarf8Z~^!xfy$N0*AF3vdZFNiPFqnso?xi2wpz7LGRm+N@)HSL0!-ro!3Y2)Xi z-RRHw9s=+n-PEqj^X+gQ`W;~@-=B&8hJ9$X8^k>bJ-0pw@ZWYe>SG-GjpjG~i1%lB zFIV+Xt4IC7--Ghw%BgYl<>V6O7TZs~3iP7hN4k)Y)Ze;~ucdz%@Kt}(w+6aIdqwtZ z?t5A<DQb9`AhqOc3kg+ybyeurmuT)=$|_KeU5)e zzFw2>EQiN^EX?PX9H;l)!+ycFzE6gJ=YC2pfS3P{^3m>f5j%%3U0;as$*+*ddSle~ zaVxp-5RAuX`~Fj!{!xdQrXyZR`@8OAoCJ8J`<=5xyp%JhSkJh8`I3WBFGI>@l7WjQ}cZ3|@ zFZ6XS+s%D4_BYRs+5WVDw)6d&h0V^T5QL>{u|4QU{AiDL#~jp8c-(JI8sFB+=oY4${$t-o{ZB@p7Y7@^m?x4 z!u}r4{q#PK?W06H{Q$T7eR#ewu2G(08&3F+dbnSc#;XR;cD}CuIrs0q1`g;R^-8?I zq5C!u1p3B(S=#^eei!#|UcF?yfB4Qg>%ZIlgQ~r<^(XyK6wQ7q`I>zAd&?jFbo{~M z@%^Y#-)Wyqe*wgYVOk&Z$Ned^TjyhIe<_EpjP@~(@v8d#(DYa?%}07Mgq?HYehuRN z5T-MHG~L6#-qsg^?}<0s*)5Oz1~DD|RtoPS5I*UJxSpfgyNY^G@n@*#3;ymB%2!@K z`nCIqov#;?<|BOcdnvzo2jDGwNC@@3Vim-3|Bn3~?b4}V(k?v3^AoqDUq~N(r@eT@ z5Bd7??DwL+G(YgH1^IeSKGsJ(+W870Kizr&ipZREiy~*DIhy0%l?{WK7;*;iMdM>>A&QK569hKnyd9^b+3F#b{ zcdrC`BYo6&_3v~R@JGG6AN*0o(Le0JQWiWQq^o_u&0~5lZs$}z_qP+zk1!uqa=V|C z@?44c2XLRDmaAR@Joz2#ph~zO#{1#4=ORAj8{iTBzU9)t^APB>sGYAx{JF0)>U$3E z+|xPU(@^>Pq0sKCxp%Ruy`NUfQ!WL4UtsqtYI&vU%RO1t(g7#T$XEGzLj5WHxyM8K zU0;cD{o2J94UPX@{SgHD%k&&v&^wMZ%CB5H`jvbU{VxH2yyufE-%d}l*V-+W`BW51yT;kMy0 zKi}0(+r|4u-S6NVeJ9Gtf7*V2$GxQ9haEbR5)h^WEr3c%C@A1A@ivRF@ed+tUlKJUxeLbTC?E}<4>tBp6uoI2%>!9C* zKPk*RY)_hR)OXN{aQaz($c=B(nfQRe(tbn%2C3bd;iWac!a}H?c4@oKAAk7 zNk`B@?SYMd2Hrmt9lECd!uF@>i0i&mB43z}@tc=Fxqi!~lm5Byx%759%9%)4&cnm| zoAUzYZa0qdrEK8$Dcf+;depC+kKc6OX_woc`_T-j6@bVK)`Lte?n|Hhj7kYafuUP-acP-GL`FW1` z4ck>BfdzVcY4oncx+dj?p=DY zeL1=juQr_d-+yiR|9W_m4x{8QzN=kdB4=;0c|Ph#fTw1Xe9!9}{0@E@U!D=KcKI&e|E}#qySwdWKjC}6ze%@LPaICy`Q0yF zydSH6nk-zBoA-A{+xVyz>Vb?ryM~$y7DELo|dN^LVE4%nqIT6 zrax7zr|HLB-|b7}+<0)g|HpmC7$1&D_dEF|r@yu763e4pjC0+Q%TIjT^|Ab@F!~GP zyuo^g!|l1A-S5mF?8m9XX`l0O9W{FYi1==4{=X~P#`>Fjes`WDo^3uNogzJ+sP8%Y zA-&GhJ;&E=zMx(d<@7+TX}GJzj_NYozB?zvuel$ueI5UB+qJ4vwo_zw&!JUXXwExJEgS z&-p#h3728y(;UC0?U)|-I3LdSKj=k$l=@_(caF|amT`^wX8d`0YU6?Qi*%bT9Q04s z=<%E<38&qE)AM~wpC`+>M?XeBX^+2dI@V2GAIJR%w!dAzJANPQ?@xC5Fm8Wv9HxAR z^0|5P-%l>2^deo5-sL}jpB&xNaS-EAmVJOT?Y)w!IsZ$?Yi``6bV9zkZkgBpfwceH z@*&ch>C^Hj=uhL1Z_s!0^Emi}^k*2)9a6qPJrS?8U(-0pA=10!6!2MZF3juSDL>EO z`p2!8c#oTZeEM?yj-n&KkLs^+;muz<7IOHOuva?L->JL*wXkP>?o8Nm((a%67;o~s z-$(evD%`((d|!p{uJ9a|@qS4SKpY@EwYt;AWzckRf6y%Hkbl9#RIbBY*Q12O7Sj(zL zpPnpaVP9risQwQUy0+t$ENt}^!g}#t-(B#2*m7a?%fl}XVI_ZgD*6ZQjqgAlYWKwW zZbf`wCzV@m|MB{6;YxwO(s4Q2xZ2PAeZMQ-0C@0s5$(%iXRhz}72xCcceTg$boS%t z-Jco4ly7QT(fuC{|E~7J?r&r6aJu*UF5U>^ZG%;mEJ_#p(Yo;iNZ-%z?SkHgAk=$D z=68*BCr`T`^dB#u`c?CG zF+cFo_bSXjx%T@2J%`_K;FAy9{71P@Ix!ykhwY<1IK~0(m!e(x&hDRgj{E0bYj5-y z%jLdPu0#G457Ik^X@Bi#?dA7)ABpu*4z}qM@g4rXOEbh%TKkRbK{nLJok`xkhJ6DeY|e^@ct&>>HR0aU%`2l_xIZO@8i6ajsx=d{KA)T zM&V7pgYy~YU(Q$R2i@MCEammvV_Xo=T$szBrl)iw|D@v-d|Ebq%61J8yZwfIxpa=( z*pDf_a{Zd>MVFk1`3hmTzTxS-uM_pFwSSIz`Hl9@k^0ehe}W&k`Bh&wJ{EEp_sa^u zXVwi<`f`45hpGQ8@_!6*KQhKi_u=i)_uGvQY5QY4+x1g#=;A>=Z8*Hq^~@`t>|erR zNP6V>pXqJBt>iyn82F=><18%X0e@fkFptmhdwx@G?2vAg8e2xWy&HdPn7FycD*FY&4%*K1kb-^JU*KcJQa*N5CozwmGmz-amcuyMh(A4v7av#P!>eYAcd>xk31N)rg*L(W`fxmlx zpH|0AiN62 zv0msuT{^Hl^)k|fcu+1;ezAVYqm*8SFg?wOxZcsE?7203Q zYUV$;@_pu7cDMf5cdsq%J0EoA2l%pfzSMVdZ9Mm#uLsR|Nyl?K4gsg{aK<0ckeg9| zfIQImTnvwa<1sNd9)LG|yotvG(}Q>bj<&<#jFxYm`c1y{ zv-qa{(5ctTNBSYZuFIX@CL2%XU0VNeKFG^Su1dJ*N`=DKh26_$g=XfoBA0)4@%w6B1 z{;&QPO%73?i~0%L(@m$oLH&gE&gm`C(|8{!;!im9s<&{S96okXJ+o;-TP^n4!n z2jaRB`a&Vt*HFKr{(d{hALC!V8~XoC%%56Z@6)c2>jvg$NI2`8KeS4gvVGL(Ple^C zTL16AtK{ zS=-`$*}~2T3t@iLJ1QQ}TUqY0!i(!&tOKL`M!&1Qi1$0`$#o;{*VcmjwLM?|^_z8O zy6=~kXFsL;Pia2L^SXgceA(}5{!w_P`E&3=kH{aa4`DoqAwJB1hxzwZ|6R5p6UPhb z+s+^FW5ADIJQu`yO4r_<`UCMHJf6QGo##00&q8*xcQZ=2Yp9y@;^E~Z-gB=f=F8PT z9Dc;F%_pRH?t82cctwAYtS7}cmv5^0vL2QP{`JWHXqFp}AKNu5X(#H|DN(C z_6Ow&={PMvf}T=H$`|c8`7+8E@+au{_{$UU>G()JWf8!g?V|Bj^h+Q z>t#6n`{OTPxQ>RLiR;C7JeMBN$5Q{&jML70gPmZffcl~x4(*RqJO(#|Z_{+b9XDis zY!CHz#t*#(dOY@_C@%|1^D#f+u|2S#jr#kiY+VrJ=RFPc2kmST5AT|{+qbX9)xWRX zL$lrG#{%6d+5M7`ziz(M4ubD7jEd9Vx!sOZKKf9wtB>}yNBRB<>&JW({rabLXFkHE z9XaWQbUha{I>-GJ=BGW;yEg`%n0}?*BZ&Ad?C){s^20v1n;-LTv;)VwL*GZ*PMfXm z-eQdq*7D1(L)dHV5azdCDfeF(a{Q-qV+%h=r=OO&-iXYl%bA!jtuMtRS0C_> z^`vp)-L5z0XFcinR9|Ag6i>n*MHj+n2zYFN*B+7fXbe-k5!eewc?EcJ9ZG&|r$@U* z!e#ha^VM*Ca`-=Sc+P7-xBGPzpRxZ(&)-jW{%_Oe-=E*7`6$er+^3`-pXc{+<>)jY zK_8hKy{6^S|JC_-S{^+%@`(Jza9Tc1%crofHGjw3w7xWIebpX!)AJnVQ5v@CGaUW-Ty@~mb{m=eFIC{N_a3g|@-xHsxS!_NCrXb%e{}nq{gm!or*XA3{qw$tYy|IHmQ;`!L|9ov`U!S<&5NjpxuJ@N98?I7N{_As4t zU_3d-eu(X5eT0{bkHV)-&#qobJXsIIt{on~=kQ4DN#kujuiH+xgXL)Vg!*`1*0tLs zJ}ifLgwJ!c;UVoM2%mhGOD9~?u^XqKi`+Tj{64??SJhAF7hddN@<+;tX*&6NdfY*8 z&yBnGcp*PWf3nl#FVHXgb<6o_m|uMUuPPU&$KTxaKYfWld4DI3Pme#??a>bAUzN|N z@xuNv#yj<)ynWBKTzGusLQdbIoSB}dr|0Rponz+4>9n5Q9>3$x)6?UzO*gE+(sl5( z-kckEx%BDr*ygM0@d$c6+4hlg{GCgm=KE>B5BA5LN2d2rv0k2E`%1ZSmrI`>kJICE zdOW7@wdC$U<kcZLVH`X2WtKHSN({;(XaGosVo$~6r?k_WKFVnd%g8YTB zeaw&Wi9g>=+e5sk_j7{$onAlA@BL`lDbdelUVd=AjCXz-;c$GBuiNpMFFmhiex`FA zFr9ExJF7HKyqP~OpNkJ)XHp-H_u&YC6kS+9L)6Rm)4m`d=GJ@jOCS98LP5VQoA+Dr z4n~Z>^=a@cKzoVfw`<3lKG_g-*7u66{ur{o@EgGU_v)9@{0Ni+c~}YV;TN)p*{$ol zEiVgp>y_uD--Xisu2pih`H8CCFH`hePJa@F5B~-9Pr>Gxd+acpL9hj=Cb$S2?{R{pg%ifFX$%E#%f$gUMs=~{c4ZZ*La`gY1$Dq9kOWE=wq@Vk(Ag{Ilv$1Y@nZ5HE z<7a&>l#6k|E9EImL(g7wS(IC2O~C8#h48*(QEu1&M*QFpBh2OJ_lv&={(jE)0r#`F zq8{=Y&mmVh3E$^zhy3&(L4Ozwzjra(x5p(R)b~QnFL@zIXIRRw-7jZauAN>B;PetW zvHTCsf41_u)mJw8o~-zi9t=}@r*s@n=SUyIVZXHLtM8_Gzn=US@Jm`B%QGaL;q^X# z{hQ*sv%fdQa6CGV#&>u-Q~KukG!H-J*SXVmbbaf7dU*el9t_8$OIlyP9ps<+(;ws3 zo1=HP{IZ)vUajyR&}X>6?^^hMviVt#;Q$o%k^DThjdi?YI)s z$IajF&qC6ExcIVwPoxXwCGldo#C4GSqssrr;MGyi+PyWd|GV{*?-;i6EnRL;ex7); zj7zpV4Uc~=`uXA|KzH^-TP{@6mb=|?$o_1XD`e^4q2CEN^W%jdD^d>XL9(Z_~ga|@jvG#;DPZQ-&accb@=!u-3ob| zzhjruXB7V5JreC@nAV#skN)e*t2g`mvQPE*hxy(f`ut z&*#ZpI{o0@?Q(3~ei}F5e(R$>YkwO2^vqL3SjaXnh8(1x+>Yygiwi)1eg~XFhKHqm zcn|P1`@78-h2->|$@XJgzpn)JE96bPqg@Q!aHE{O+W5MX#g4;z0qu_TAUyU9+rQqM zKo9cKQ9GeNj+gE1y_`arkMCOFupaWA{8i8g>rDE6q@P~K<2YOKC81nCy)Zq$l0}Wb zYWIJczHe3t;4st|5nAQ)elz(l8_L&Sj` z8})sD(>K?i)DJ1~8#kT($NaWUZh%fX1-=y35WO+g>%3)i3A2ZVep1%ADC zAJ_b)@?K$lclTuXr~CDPpuH=;7y3e5UypJe@XKajupO@&Jl<;Sk(iJ80FQXyaQ-9M z|E#|c{iYqS*oEEDkKdyIvo z^-LD;$>!(&MOu#M9>e*s5U!`@wfqxR@4VuTeLKC+?0lQHqrGm3>prflNxu!wLjTVz zoV0%OEkeH2755ETANic?6~=#T>%FSkUpw2*qYC-9`B5qwze&u`amx7c@f7ufQT1Ve zlJu?Q%l_W?cMpV|82-Jxe?ouXC^Le^ab?}8u+Pv;5e8n|BvGLQQyfIqvQbl3t=fdKV|&tXJS6S zzNJT0O};dp`42k?^Evy2=_#GYrO(ZG0uSQL@r00atUJCbmrzfBo}e8F-tj7A*%Pqu zL^`B#+7Xb>oDUc$evFTj*XelYdtx(IfjlN0=4+3yI6g>k%D5 z*Jq+$#Faji#Orf!2LGbG%E!-!`f54O>5+q%)8~lKT4o1O2=OGm6%BqNKez#OxX$;P zck}ZG^5Le2R~7NFbLvv=eK+We?-9@Q7C<{XSHgzA<#kJtT#GG`+`^byu7E+ z%WT*AkA(cR3yJTV(SC{+^HT_TckOxr? zpdE$dqxMHXL_fUS`)N6|f34*@e~%jaXd!q{mE*FMQ_T*il8x>Oe8lsb_#Jd2Kh#dA zUPG60+X4ENvXkLi2-`(^0Zu%xVmkFd^7$xy(4NBERmvaE!FU@NPvSM%{NRgHb~bx1 zJ-4xO%>GH!Yq{)yAiw&TBK*f65E2gi@fI6zgu}3u;orOGm4t_LOqOTJ{Nu)BzPZ=W ze9?X}mG|67;yyCZd3cV)@U5pnz73C8vhf<|-_&mF4nJq69UR^Tigs*#C+N_FFmBuV zDnWWJSf}L5)jpnjBlE5G&wyu7ZhZUP>*IUJsJAaDH)zVG?S#Id{NV2zwc~mEz>c<* z3Ae9foN(IlJe+iWNIChIAE2EnzG*+F{hQ{0^RKWjggmWen+L);shji0xqt7;7FS|i zHT`S-t^I57L_MlsTj_N}&lI0FeG5syx9L{+eYMy>Y5tU6?YO=h<@l%NE?yP$%O&Sw ze2|}^N76nuzK4bMT2i~xb~%3UHTqOqFXs`C8`z&}JwA>;>F=D@9|?4?( zkWt_Fcx}jEHvJ1YG0yXG@J-e9Q<{!)-TvWviRaDC$NgsD$Nl9}NN0Wry~#gy^KSFp zzxNutkC(6C&>!@U{KIx~e~IHYO=r8({zE@?;UT?CPp0SKk)8~pAFG~{*hgVL@1_%P zyboT<$822Ia=7U&J=xdnT>FCe&IfXc>oe4&+Vyqev)9suT zIOref@7PYpIc^xH?Pi?c88+?rc?t7ucfR6%0O}QKds2EZ&U$k56~DjM-cc``dj|DY zn2%p<^Kz807rGtOd-o3We&zdTwZP7^5PrwLTGdH!=PTsScLX@4pI=q7nzpWYAUD^IWG(DR|s5N`W>S}w+SxD53p zKkMmr;0@20gHy;sFGTxLj{M0``Ow#aTt9Ff)s{DLpQe!?t$l;;xjoKz>k$9qm(j22 z&q~mrwd?6oKHywj?Q`sWyb#Jg#4Gt9gA?=L_yO?ue!hO^xjO2rY7KZwTRp+-c*lko}EMBYet1=wp@qVX;T^MH7*zWS+GqrYJftNUQBc(haHIHmk#$ojEQl-lvfa|q<;zFNG`#r-AT6Pwp|&1?Cb zJumI|X}AB46EH4dzg-BwGo&5PVyl4Pxv$N1>bE~Q6Y`32$}Pqx`yKsM$*6M7$NFf` z&w81jiw`ef%D%gW_QZPvEKmMP9s7gI{1H7Jv=W&ST^O_ za0~go-}^k*&l4}OuoT|Q{NnDY?}b)xDU6dYw7Y7@qg^@EA$OvkIO7a?e*k>PeeAA$ zoVgJ2VVKG{*p(E*aMX9&qhOs{`Z~3eJzcNZ&h*(r7CRN=Cl}{F|L}VjQLaFK#QR8; zN4$Ts=j_0b+|{~4a% zm9x-mOWD};Jk_79KWf>vgnS~M|Mm{ZBhC*;yZs`+a~!Aj&hLJ>;B@Ua)uYpTNe_m= zW8V7(@J9E`?ccz7VF>$AJ=Zu2a_@jof_}6MP5VFP$H{)r@z1Jr0$!zj{Y3O9`Iz@y zIA1el|IM#?oAssr1-zo(lD4;by)A@`!5_t*bw(=TmLdY>}A z4@o}++z;kCV+x0OF&vkVNA(xrb;m!-Mft*h8P%T0`aA81+Vcw6hX9?@#^3=QD=eej9S(gdWD>3Hw4Gw7-|~ zJok(6T)R(M$ngBN=j#dcp7&Y&5TugS} z{f^($gxy56t6+K#-?W_^=byBGC}lgJhhOgVFy>>*$=to@sCRH40R5sKF}&XHI9lW< z;OFBU58{`WPx~j8BP_>o!u-f|3PP7-F{E?r4)Ypop_}6q~%h) zNGJGH<@hV)4_DxPIegdKs($~a5S|D8{guj2y54R3uWG+fR%*#de^29qA94Ob_-UN! z2t)aH{`U7gyh^sUeN*Nq9H!TDkMG}dUohtvk>i5-+V}vj?l!mD-Nj4&TA6>P;q(sr zf39CPaQpp7_fN<_Do*|&zjMEf@e9npkn*V%#H(gsY&<#D9j}Bx?s_Wa>t=oKa+B~< z{GV_BpDM>YwxgY1&!_f8_@rAZXE5%#zt8cVU$3FvQBQr~UC=|?bdU2O`H=Oba+K-M zwSJBE8EtsPD+ib9g`fYGvV+f?fM2^mtDZc?{Y#{HpuZx$hPNw^M+)zCceLrU(aVDz zqFrVoci)Qn7Wh?;Uw8f_eGsypQLj$xrJjC(w;z02%Mmv3$9*R5cOHK)=BIJ#o!0k; zC7@5EpR2~sB&BCM4#D}|f-hiSJr>M@)zXrI;hart7GyOeXRX9HhX zO^wcxE?X~%@v!{@7&j^Y)X%uDhJF50K4bg*mE3P(As^lh^gZfFfe$MA_*&3od5^b! zF8rwb?Y^$MY;EM{K1?qE@btP<-;uw^_~JhA@N(+!#MXyAuO?nycx?CH=L9-c@_V~4 z^{?A*^2_k{O}^cIyGKyTNf!R=wwwF$^Q+x=emn4GB?q1f{`^*pjk1_wi{x_Zr<PtA z9^;>Jp3mH|5z^sD>{0f5K7Ntj9oM7Et!VRd+4v`;UD-m(e4WClKeTpwAvZ0FcJTa{ ze!$xCuAem0kM|Sa=6XB#&8|2jz@_~d$4&HCJTClpz7$R#FX+*$$`u-DjYGT#QPXm{#7j&ly;SIEbI3%z&i9XvEYE>S+R-Iu$bo1Xj7eh24W zaliQPhMPc(hOSJ99C|C-*_ zeNTY-o^ARs_hahDj|=)e@(ph*_?N=J2~1JSCn-54XSS>f7l#4CN9-+P^Wp;3uFz##!8V!#P13=ej-eHTSW2 zuF&Rd?sv!YiE-^8a`~F`^GC3*8cyF>Uy2X=bJV`lS$3a7*Ee>5xst8U3jJBj9N*U% z^&R!_ogSWt#`vb+4Dd?7=T*t4{9ep!?OlPMe8$gvpQwEB?^DV*-4Drmjt|pMPSV*A zT&Izbxj&!w!>I3s*B&>c`ZdxwrT-H}$8qb$yr}+Kj4#M9-H`Nuvh>Sj`;q)JYJ83Q z&Us*6yGL2cH!LhxcSoaHjLXs()B*&bGomP&Qqy9#HjCSJ*k~8 z@A2jE#CQ6g<$N`HeJ#BolFBQd1Lo4n#|ZU&!q3?!J09-x{Yb*Q-2Li)dV|1!>KD_( zLSQG+Z71yszWaXYBaHve{q>~yPgYKkDvxzy;p@H_Qr}=mJ**u@y^8XjdKcR}ul+Qt zKiJQU+k0)L@Sf_Zaw#2|pY|U)yAObXp(-oSFm7wwCf&XE4hsNc22XfH?p zAwR+XEwy*+&b!0mb;sLq{JZoey~r0iJqPWZdij(O+xQi7;QE-a()C80K6$&Ols`Y{ z=W%U%som=B?e!4H!QF31{Wtt1gq6@PV0gOj3!T(nFP^l1fLqCjUKzq#mU;u~dFi5n zOFrU!#r|*8LGKf|JvYDKX#Tg#|Le>3m6fw8Yw=!$xar zvsFL2ed`eFd8*sJ|Maf#y_8=(*rr#G&*si2oJZrjFQs2vp7S2>>8I@)RX*jvG=HjB zjQT#W^90+I`Wbk}vjV^MH=G__zTx=|=o{rN`DQqr$fv{mrCZNr z>G^-HUfv@iA921-$0y}NE}d`?mcsoa!p)6O!cF;{>4ZB94&_G*CzlTR`fkL1*k3#4 zr;xAQuN^nsss}lg?pt1c0N^tHHGaOy5c`#de9`R;C(C#A>&t!NG)}x2!e4Zh>+I)r zT|&C2?=^s~s-KxZ>ha;gZ&)EOHv64ej{K0TpMHgN^^PjP-Tjy+mp%paZ{O~}6~cAa zaC+zVFT4AZss0IhMEKox8udBaJ#Zb3bwcguPowFRt{cfmclf*7`>qk@OZrEO^Doy~ z>G(+H=&0{pw~_v%@{elo^mp)Sjvt=v^qF6LH+;P2^#Q!g9_`dqKAo&ykiuKU^!x7p z0;UsR%GtDj%Gt@LANS8NuSEHRbpY{DznJ!piTWwGd@-HtNbHvtja>!aGl+7B{YATr z^!vEue)Y96pOOykaydC0^*8iSTtBCJ2iK3t7xhQP^XqAz54F#C=KVZ~=Qn5jzURte zSgRafeu)r9IgI(Jl$%xoeI^@E>6y|cKgaoiza*DjIiDcm%k;xUR}NcU0sMtx8BUD)r-zZ2s#ZU5ZY z6H$J(_h*Vmzhiy0r=dM`JFfS8o`?F=^h<9Kf~9W0kD?prnUoHr(y?DuO8V{->{EMk zsqG8&?CKw1@ttY41PkK0um}e9G~a!xQ5)&WEX8#8lz5;f}sPnAS@> z@H6dxF!z@jma?k#OSFGq{+*y3*X4ZwjraVxz9L>P^z%vPdzHNxUC1@QueiZBVP3A} zCGWtw;@Z1}P~Yu3Fob>i=YNHe@;07d6YmW-$M~ciLRiWNXNUAI-bm+t*z|j^IZtK2 zP3@j)JZI%S3DBWx-WPei=>hx-ZztQ6#?$su4~_3mjLWa=Pn_S3=CdQ6pT6Snd>pql z`iXSfb91b>=x>JdhW>2m-{$O#Lb=lY-Bhxr`yt4svp&M#%lR#h&vJSB>t6sq_)qC& z{HK0@jQ@HK|IvR9%9V1_Y>WfG%gXyDu*0wXK0qP#1DT^A`IzCtuHWV43FT`WUMe>W zfgMK-c@D~S=yUP>lj#i4@pBN^SH|}kN8#1xr$RuN^gdC9*YkTrtYp5=@5lE1etXQH;y0X*h4)`6fAjm?2zk##{mwdlPV;@s6b`@V+Cw>cpxb4(?I#L> zJz?3rM}&7NO21!1yVSz%^Yxt)KmSkRP#=bUS;Tv)_@?EsuSh=~ah>=Udq1$0Uu~`zDgWTx}{jPN;+@9g>*^KpNh`xfMHj$4Mgc9E|5 z&i*gtS@(zey_AJ5jPv~PbMxFeJIi%Lwu^QL#4`=ke52Yy|9uSSPLJ4L_ETElpUmEY z_bk*;xZfk5Z2G?n9yz>;-=Xe5AV=riIH3NJuA`Vg-FM==%=>xVPaB?I%W@ZB{BWGI zzVv*A_0j&H{*9SVI5;PWdVf0}^{8BWjy^fLLp_4wWXq@H0_8{ZFZWGoU&Z+3#&K?Z zchl#-AK!C(`F^7ZP3I4M&GW5!rw_;RlZ9V${P<*R$21?$Up}5JJ)dm+@>ubGZhoI^ z`uw8jKo(MX{_0hb_wW;Eh(j(Oq`JMCfYPP>t$*#5zi1vT4vGl&|pR5n4e^S3m z;kV=6bq49u&KK!M__=)i&ix1YGmZY1KWO_g^b1x>?mOu~cnrsF*ZX`QsojpKkEQA9 zdMo`7c(t#~dd<2FdTwgx*`2_p4*duC_@8Kgu_z}?$;C;xl)j`pzQ=vDcD!oVTX*_?>ZtYB{9aeR zdw~6iH~dxEQ_l;QxhRC{x8Sr8_TP+(;si|dDqQ7Fpl>-XQ5u4`$T^-OI!zf9qspAZ*>1T>tBv@9>fb_I^VG*T$WGc z^#23A3SqundRiXwQcg2}6nd`Xeks0fVNdR{urGhNFr`DRkMj9Jv+pnDJ4R0(XGwhF z{|)rfdpMqcfzeC-kNG~((Pn2~$!fDhzFKyE1nvY^Zk8T!bf^6PrD}4HTVA6sP8yG;d^mA zYtF55`95at&F7>4+q69Hqv^Ye#&oF5{qEFnAk%Ak&NCs$iQnJc-~CG4hkmVPE%P_l zlbP>9KQkZJ+m&o|3-Dn2A(sQs)bC^(CtaAHi#P2_{8ciY`2J~s;JdNED@C}kw{}#` z{vpfZdnI4mEXc)Lw)1wR-#HGyX7gO!Z$ocWjXe5^={=?Vc&k9KLN;_h-Nr(`8(PYd z+qC<25BpvT=v@o=L;Z{y-TU62f%D-+(@yXu<$UD#sll5en>&9|-kfke@MN6l_>AKo zN0ig!=Ewa)eUHcCGfw!76F%2NH@m-RhPzyg@wJ+-lXf?Ii9$Bo2I(`7#dt~c?fPf5 zYrP*LAMK)cT?+Z%>i0jM@=fH!x=h#aUe7Y0Lps7zUj6wHkMi_>`(DbWe&2GjX94b- zA3(YLcSZQiA0a&YL%`$0Ll9?p&KE+tQvTySq{FUC*9j9LUN!5)T>5(#MY-~?fY19a zTxT-=-)929kKGgUMZcSAe1T&CkKr!+pxl{XLHM=f!+0y@^*bZq<|iS2udgBe*6|2` z^?rP(-b=fub+$q}-tjJFN!Pz`xC-UIawWp^&JOuXS;*~d+wn@i@EO2a<{0Fs9otd6 zAsy@Scu$l3g;4K{;N!dc!#Op?nXmTpg?#xw)Js38^rOrAI8SpvV95N)7yF;_+x(n$ ztuF@r_+9mKrzg@2=g02%Z=VnR-{X8w`@tRT{b%Nn?__d(GaQ#s*v^wn(1GDz9|HYJ zkC`U~eDoaoWg%ZBH+VlW{cCHW{PmhA$Z>`&5C8RrJn|8=t8ad+y6~8OlC2Z9p7lch zTCRO*yBzCde#F&|>mt<8dVgT=$rr+q?OM8UJbgcc>3mO-?fQVXi}@L{J%>F9bV67P z^D{ns%}`I(q;q`Gk3Q+dka!S(;=%Ui=+1nLeF69|&T^y+>tV=x*dF!|@n?C~%k*9dt<3 z@A(GO`!|E1m-PNU?L{bm?kyp`k~uem?>=&72=$#6`(8*cpKLc?z6-t<=+=`1ZVLJP z4cup54ZJ|7Qm(e|l^nKIcn7L(-T~zI_2z`~g^vrpFZ6!UhjzpaAGUF$c3kG4t7`I% zD<2^5%Er$;~utb7eYOR^CsiJHoMD0ik*QkLT&F45a)O9M`2w>dpz1- zt6o^5-t&$NbSRtndVfJbrr+P|@{J+OG0t*~GhAUu(3$$qvL@ej-`)7FkaoP1b9}yM z`XOILyV~(?z8kj=^;B}jlBkdQ+v&AjuTrtk^l8!5mAMC^H``k7jD#5(1{%vi4zLep7-fQxG?_9K( z{LK04J9i+RA?d+>2*_%bY#fE!o6`Z)cT>&3iRc7e;$yU)P9@B(`eMD<LK+rhU^c*|Nd!!!}T=wIrP2v?T}77-ev0sy>Ipk;JJmj5B$P&j~JeM9M;90 zPq_}{ytS~sH?02SJpAEqfQzt{SNgm?uAZ~(;_Y?}k5`SJLj5WYskbn^e^tOGJcOli zox%QMKe2z>$m6I-%ssu3O}7Mn7_$FX{T}cmKdinz_>b#ChQrfK zIb;2He)35g9%+92;(J5XGegE z&UT*rE%fVAwojw?sAh(GN@1MsXFP2u;$1o}V04Z8Gwb6y@%=7ekFomoeAM(%hGnCV zBAs!4@6+_qy3sT5-3fF_`$_I2KQo|w&RuDbOgp3)6f1+ptqg_ znO<7UGhILB{^6S|z`=K2H~DvzD~0(`Un!L1{Em7eKY?!Q=g#K4T8^^!`U*ME_a!;b zZr>Ap!1WUMz45)0?;eSE@Eo0T=)Y|}Qp!>v#`pJa3jROB+rjw3YoT2iI=^wgP3gNt z5BVs+xSx$s<%qA-xDQT#Kv>Gl{5#{!zxh#s%kNCz(eC}0!hLJBOZPo&f430q=a#bO zvgkLa1CHLudLQ5dUW}7I?ecoR`8}W$!cubkz{`IizV;sIFQivZ`U56Ub-n)+TUg12LD+pjNVF3yn*Kn zJa<@rHRR_!w)peWuMhk%w4;_CE<^oDFXSQj_i&TlPp)JaJFnLH#m=jvzhmkR2z5NX z3~=brg!icUo*4HLzxjL6kNPV18RNaIb~^Xv80tRkVQ3HYy}|2k$EzkE@3-^(KaKxv zdBAJrzvIVzwCkW=e7~PdoU{bW<=Q*<^g>9F!+oEL>&$jo%6snyUAcce^9sAb4_LRP#4LJJV<5$s+@3|e;!Dg4Ec9EL}IF&4E>t5}D|DJ0X={M>-@5iJe z&T$I?UZrpxwc}OOzSVX?JJ~;^C;6Rpr9EBSUa%16%bg$7-ivUUpY}&-I@cBJejRiJ zU)6Go@4t=uK6iM0m$8&<-42QPwBdI1rEpnpymoZqjVj;J)9nt%r6c>9@N;lb5W5@y7D3AKy!XT&)_vuxUQZUCblV zU&2)L(QcgY@1=0~4k-G8?}kRYP<{a~u4C0M+2;Y`i@3^vJCBUz+5RQ=418Y7L(2sD zQOS<~40Pgr(v$oAJS)w&+-As!{5`L?-{eo*la`;~_1t6U?WHVuZ@??|Bh%mcHjHQT z#q!^P+(EjY>l(kPe=SdExg5MLwgNQN-pY440jF{}wd1SyXEyKk<KFV_7L*_&OR*qlH-%hW6ej4??*T5floN#=l>!i2aK71vg-7Uz2 zT4ve27{@2SV_vBR<66(fM&!fyUbD{P_$NHt7Z5M5D`|(Y%3c^x{C= z`+8})58Lk-!<+0JwQJw?b% z-RFKb`i*hWuaK*2(2a1?`S_QR{t0wU`5OIE2*(5FNj>NBcUTzGPvoUP4s@vG-lu~e z+^@WB2f*j}#`-b#JNs?9UxspJbAKz1Qw}g(*Z8`W-Cl?K+VM)#_Fd@ba*G^?{wIFK zi|fjs^BclaPML-Lxc9_(C2QFDE_{6vL-I32@+;ftObLOlq4Hg!ED2j@Ve$K>^p9K6ZuA*0}cPNU~{ z>RUN@)E6eJ*NlP(`b}Q183lJ-y=L5Y6CUU}zW$Zs3B9M+=tH^s+xhy9e#HEcQN1UflwDDWXY|~?E`ID_jdkx-`&G$uz_n?J& zcxgWHmEIGw`uk1&bF5#ZeZkhQhk<^T%R{w8xB76Ovkdx+>P32L+wAkD_Mql!4A*0gKuigAiM|Z*x6h4WAzmv~wx-Lok9r zSN3wR@_XiK{O6C;{Ll7sd~tqXexq$1GHT{I*V*`nrebr#B7&7ko?o zIru=j9EId*FmZ@T_U<5(BPbu`Z< zs9(0@ao>yUqq&E19r*cIK##+GqUXz||LVKFe$K)66MyL4vHdxEF#nAPPvz6$=x4@P z_H}HoA9Cx^rQAO(_tjtMc20~Rup{bW{Gun+E#Eo2-M&%Z z({l^fn>+u=rBg1j{FZ)x!ujW8e*b~rG2a!!IPD$MIQffmg#5=4e5mVa=Xa*3A?OqH zrTKaOu&tlZ{G|XLDeoDgJ@mWJb5!ns#Bt8`Wjj>A)C+>&kRI(3F>jV~@{+;sG@jq# zTuklIeE)^zMx}H9OY%FzmUX15Aq~CaP`ag;x?`zV|h4vIUkL#X~j!LIoraT^% z|Lu=J9y1@~jHm4+o{)?2K6-i{n|?p?u6F;Wa9JPcCB}34zH}AjAN>Z5%1^z3 z`@*C0bAF#}KaPT%o6km-Ei?dyY9JLe56AtAxLfwD&@xnOHm-zlgDY^LnXYc(3^PKMg|8w8>b*D)i5*k#S zQCnEiQDhVuWidri#1t7dp*2Af6hRSI6SNjvOJ+qjHDN_IWihpPP{b6ml?6pMMNk%$ zWqh8m*Xum*`{Zixh5h*M`^Ww`_jz2e>vdk|b$(puI@h_b`?}~KuVYvq#yk)3d5d+& z+5d^~{A2$c!8)zK-dMLha(DRWH<{0FKah50xwGpugX<&ZZrV}mH*#hEzBJ>_{QpN7 zvp&pjwm#_JE&h2=OP^GKp^h8&LmroWKI@e_{E#pZ$pa#GV(=9v8Q4Y{$n&8`ycs&ZF!{ z31{~3{iN9ci{k0n-ZdXuJ#l-#7Y1)Lo$dep&$+XHa@F72cpreQ)bamXvMuKUOP6%q zzT3|uS+AnUha?(%RpLA_l2h94B$oR3hzP}I854Kyn_;#*xF7@+; zT;u=XcEbB$ykD=cG(R|Q|8f({m-Ubn~<6yZzZtVlUfIa^*w+I3BY8 z2aj`{UvqrSjvpM4);+GRJ^nwV=eLAgcg*oAyZ?c$tzHJ(U)wJ0%VLfL+4ot+`%!#6 z;WqVn{)@})IEfthGhgaa&hxhE}QKU6~Ka_)7cn{AGLX{dmtU%Y0nY5RvRXD&PcC_gFZAJ(_=C0@U| z@?|;0{ZdZa;W%LV^7tCue`FrTae?|~mvZV4-VXaK(+w`)V0~E+6T4x54f75AEA7So zu=LsEgnEPXUAwB0t|td;Pqe`c@vZEA{a0t7IOPTYp{lr5!BYuuMMn-?wpO z+9ozHo9>wboUcXQ;bKU0OGd9%xi0{$($tC75 z*M0c^S@sv3Z}U1J8*_fa_;~+bL6(1jo;gjmz&q0 z%#ZV0USINhF}`j|ed@6sygvO`{f_&|eCSU+|M>OlzdL{H4|s2#zYeqGf%BTRug}o~6W z)%gE?=hFMU`&q7Be(*g${2vnE=QH^K(Aodr^LSzVS&XvkIy>L1%khfajI;9i_{HPD z_;Zd^gX<-G9m;Wq{&C)B=UuLoB^>V?F=js*sz1f^Wq!27e0hI`aoi97@0k0)XukB9 z`7?doKlUfa>^Jl?mtXXcG4ruFyWYnCi8H1j7RTo`JdQX%#M>=vHv*j{>xW0`0_L$d*zxUo`=W*KQyoKwDJZ^a& zj<1WbJ(%9&wd*@qJ+i;jPP|=*vK#Mb@%}i}`Pb$LeBL(ue4NQsnfI=8-a$FvKSnvf z7nG}<|Ecoj%8&JGG42=n|3&?~{a`+{o68@LFY%b`a~5ZhJ08dJ_+RZ0w8#8*`=#{% zlzZe+I}ZM_Jo_Hrf7Xv&^}~An|GK@{Zj5vJC-+z6I{fqA@7YhE2hWbHU$nl4y6-di zeo*#4PrMu~&lk>L=RJ5&;O}Kz&wlri&!yh%zXxUif5-bJ{QYC*7eAlp=g1AHUeoR=IB$vP!+Bo39K4?&zm8wGUgX|*&gRu|{ay6Cj7hg;jXf{EkAD9#awTWl zbLZ=}hwtO%SdTKlviarG`{woQ_9vbn^@g&;{WiX3*Pl4v#ose$-=)KM_w@Y__}-lV zzYf=>`CMu)dwfq_uKnAV**e5057@dn?b`c5*8ZOd+nF)-_&v~GUty_-&n+{0547ES) z=nwnN^%E@JX#YNA)+@K|xzU8%U9VFT?x!!?@zbO4Nl&`d_5G*`H+UQ0o0to8OnRcx`=_oAsV6-&6E^p9%MWw}jY#Z)opNci48S!t62qQ02I`*8DHK+S1$q8WWCj zE<60r1K%@BKlneGsa-V|rvHs{M zx2gY+?XBIu96$LTSboo$e&?z`wkP%O@$bcEe2I?3+4IVqdVQG9-;QT|kNtdWZz~7B z2jt&}%=KoLFTQ>LMOJ^GohNb9P1FAwq};`4S-aW(tlZvTjqkh;8>~+`?b-gU-|_NW zxs&djyP4k{|L7;TSuc!#bhfqsYvqzZ-cPAFXuNpPr`Fn+etq7Zo&PQZ`@AY|5yF|itGPF z>2p8FPm7aor-_5}>-7JpVExX~|C1-(_KU22JIV*^4=!J}-E90yIL5Y~!{=$U$IT9F z#9q>USNnZpt(bmWKG|`B`EGZTrN8cmb^8^sH}(_S<-GiK|J{?W*!hWi{9Z#YKWLZ# zb7#!+HovRNm~xgQZkOjB{_lOLZTd-j90ziZ|F0iu^%akqK9@Z1C-Y^@^ttr7Ki)3< z&cfF&vg45d(|rFi*-pCU{`(Bv<~mnAU(SCxALaKEj=IV0GXCvC+h)w~Ch&W?^tdtAHJLG<;;)!f4R4v2Us8UH(uZLpMG=v9K4-! zCq8KQ;^i5Ee}5F`uavVr*^Vp^SuS1h=vA$m>dtRsBLtHnx z)$042ze%|xcZSAk7yU%!d2Xftk8ADp7k_5!NcMgY{7-b1rSJ0B!|UFkwR81GR?pKn zwwUFif1Bucd+=oM9a3*8x6IEY;(BeRmfm92^BGp}alPmCyFKU+pV)rePGq*J|AGHs zxA8CK^`eX(pLfgU*Qfsa^&RgI^(oJlj(Qz?nm58UpjN0I+UGVP8a>8d#J+NZz%oOb$r5o1kIo8f{}Y?wv-d!xmADioSlD%dB5`A zpFfrHwIJgc*W0-+ZrlI4`wgtOTR)Qe!h07rPP&(WXWO5?Z{??6(a-FB!1o}rUgGIF zKG7caf2r#~3D>OeAy2p)d`vscXW~cJU->@3!P}|KI(uAh*K67LH1U2b|Ie4JetG}F z*4h5^^&MY7&)0SG?0J**kW2o;E!GYX`0=0h_l~X~WxtQa>k-k{bO!2zsmf0lv%g*-=(^J%;5eMPfz=g>31~vJ<064#vgUP68B|Z z7(9N^PW=2(qxFRQtiHb34_O|j<2L7^3!BW(_%`)yeP-?ZG~Bu$-+cL)Y=XZz*4J}U9w zjUBw5a`pP%#(4ds&SLAogV(|E{SBSR;{VF}{?W9XSPM5W&z`IAV;bg?Iv>k-W!J2` z-r|1TpyOfW7Jt*)D_#y>H?ZCZZ>QXkqjl%=hQ7ZI>pObBPr387eIr+x5dE}!d<%*5 z-0l8*wefuUy{l3^UQ%wke!nzwucs{i8m-qn_u##0*EA^sLuU-jI4sxs%?OILh3|iYw8>&o4aRaXi1v?!V}FaFcE!-V>qo_|N3H#rxF9n_ui_rkC=1n8&MLI-@sKK091& z^=dKtk$x9$=zha>Ox|xvJhATcSiFAid%k#&ufOj-cpLN6&)IKzzPIlH<9P>NPmG+c zE2iC9zMh6E|H-<3gY%Nk?^5pBSi7IT#_GpntedU0ZGN|z_CK#4&!1f1ual;tk0aiPUtMed(XU+kzf`^WxRGslme0-`xS!_Fo3uN4e>NTOb1~+1IAi*Au)mMa zm~z^`!rzBEOk@0S(vP>f^uFQGx6S@NYSYNwfwx;vavb2jw2#|y8nd;Wr#|8R%*yBZ<1n7y>LcmS-_pi~y>#6* za&1?My|nx3sdgMP-3k6X&5S9h-yHwg~_gkDT=Xw79B**&y8F4-6_@mcd&I|ng zoOAtpHtjOsXVqUkAL++|`2N4&F6BtN@4sb!4c<<%#*{NXuk$TVx}yf&*Ulbyo9cNR?~(V{1KhSa zn{M#_RK{LB-%fv>5SR1&>Kvyn#`=Q4E@pkl$0wF&J)K7-9OI$%kJIuboW)7^hOXOY zkK=egGk;XVe9*+N$X`FP`eta1!-QWBCIHYm*c*>RTcY0qS;TYfaIrHIo z{H7llOAH%csOYFT?Yo{ZBUK_`JEv|5NLH*?vcVf0A#0+4n;5 zo!rZ$-6Cht^WnMqKT193W&C4*we)M-y&%u9BU8GpER)i*dfic>?w5e|$a~xA*oYgZpK! zbar1L=?*$q+9Tz5JH^h2aeJHkepP>))D!Mgjy3za+KVo(#-f!`bcN06=>mc4= zW;^luq-2HZF`nq}BgO5fP99ud?tlC&%g^Fu=KUPpzWE%p&-#5CV~O~+zkjbR<+RtY z^V_7mq+9gRPgY63k^4xu({8#xr<~{BzQ^n^9qruPEONY8qQm^6ob&BJ`scvj-rLI4 z_}041b%H(@i09>fJgUy@C1Tf2W*>RRu3yHMiCx^^@qY68E^ZI@E1RD3cAxG7UDwW@ z_jBd9gI=d4owYmG2NziV&|dtwV!f@W_ve$2{UN{D>W|y3H`+OQnAHQ1pS%04{%9|w z=kGtqkFQ*Q#P3(oPF$b&4cMP~ACud>e@!{Z7mibmX@~h9aIe*uZRf7<{+Uw#q`G{|0@{ghHHK1yH@`{1J=|2ImQ7iceX#o^LtaDOThQM z)n3}$&s)nr^T}0@+;8_U@%<|Q-Mw7;yw7dzj_*ACc^~i7GTn~nS^aRG?nGUm#r!Vz z+%fZKI*YUA=6+u14cFs7d!FF);Y=UzCro$Dc=0FY3T_(QPicQBJA3%!-eSBzI@XTN zpZ>Gl@pc>I*S(p3sQvUWuE+G;AJ3oT&|3#te}2-(@#8pNU$pmm`vdhkKj8Cz@$F&P zn;&sMSx@}`iaigSa(upu_H(5x@b>}l_U$|CwZZMnbn$Y&zq|O8bXSyFzc`~_(nW5i zK8Koii~D7No_l6{*+0zHUMO+C%Vpp4K^i~)b3K9gpXmqnSnjb;iru8oC*`ahk=wuS ziTSF)VDaRzi6fPmnU_6 z&(0^PA8)s~z4(64gIDW)PQIrc|G)C}IYm@HL5;?yPT_3#e9r&J|4+n4Kx&CF6 zf8?g?eEf6R)Ag&QYt{8od>{26Zl~P7`g~qokMhXX>GkN)`R;y%^*7!}-1YZT{-iT` z%2A%HzW-hQYrft`%k3}w!;N~MEa`YZI+tIZ_iuQpm52X38A_k!x9g(Z{;=QJbCLvWf*?cqU?EPpM z2lYBJ;pd@i%ZIXO^Vy_(Md!1*>vbc4KVU$guf=oK8b{ub?DZt=d{KSY`%wPo+Ryb4 zuCLJ^$J1Qt{$0mOF8lO@&$(GY#&=oUq<=-3_8V${F8e(GSPowAa+~vvT#?|>s_Vsg?%rSDGUj_thq5zC*Ef^S){|0h{&jZT{k!D+{v+2lf9BUWbH$(N z{3+oW+jVWiKNr1r%>Vbl?$>2D_4C1|b~|4!^72A2AF4k1J?xwG|0gL|q0bFO?tA|K z&+Fcg^~P)LylUxkAIEGz9v3`ba{0ycC}Zkbj55a7FR5p7(!KYJ)x+TJlzYn83+?3^ zSM9u;bYJo7vV*s?|3_qhi^oIRxA_6a&s8=LjBh`y_g@o^@lf{u)%~Bh9a*3D{1yK1 zsn4fH?v;gu>wDe%)9&Z~Jjnhql;2~<+Hq+y{>S6Dhe{Xk4^Qj$b;1obub^M$KA-nr zu>5o7H)gr)Pr4oSzC_A>_LS+?U_r??*N>x&xIx3eSed3r`!d)9l5{kDR$D1-^DxaO0yS_ zna^^+o?-na;TWH>tJq08K99oZ)pNU&&@xS{dk|7 zj)Qn^;$hQ2^--&r>mRiERQ*nP?)S(3Y>f4buf8k#Dfdl%PAYQ49x{72Uf}-*!)(9l z;r~PzS^wdC-T2;VZol@CrRRI#xPOj+u4u%awtv9|X6Ngzw*574cZ-9~|J7dp zrB5t=>1!5`J681Yf41M4{;_)hIGMRm^Ne3Nx9hP~=6QA69cuqJ{(jRBbzKzqB~F%n zB6pX@X}6DmFB9KC_M*N&F5$jty4UnQba>ChnNqH#yL(@&55DJYTYZi$;RbJ~+>QzJ z$I>PIcUBVafd4Z)_CBzbd-rMCj@-ish~2cmKb+^z*LEG2c`w+uU$J~GPP!*|mUJoi zf`5P4y5*5OQ2)1>cI?OYKLqqUwU=_^@38zc-w9TE*1o+LEXt&3K0EsK<_G`xkalPL z=MkCDr@xkZNIBX!eY{^^@1rICd&jcdR-TmiJ1aLo{`>V;^qT!2=f72dtbevY>m@F? zdSv_m_v?-Q;lEXHR$tlv#_#dkIGA#KUn0jr!uRu}Erb>K1coXeU$O%iJ0EfrQG*o^=;|+f7EQbSRU4g*-82Kj9lCgdyfFt zz5RKb`I;S!k2(D2`1M8owsg7uX8v)%m>;j}SZ}$ubJ@uyXMM%%_1~ROyq|FX#`)j` z|GYh)6XtybUiWcc%lDw#{g$MoKDW;sCjC3*`2TRe57*{Rn4jr7M#Ay9+V)mCt`qJ= z|6UT#6XVBGuH%bxmcyS2FPmS}+oU{r|ES--rqQ-n>3hB~ z5Ag4EwfSYzJ-xq`f2*Bryn1V~*)e&-nH=jNUe0ujPP2489^%)f*58wv=jzxmIS>A4 z?J(Vi6DRIoaJ*K<=WlQ&%&o_C& z;Cw9~{I5dOrJU&{9Me}nD)y4@I=_FFKR-~9ethW1-+gYgaozI4_arra^e@|`pM3s@ z{#iTYy`!3M*6;0go}A3|1J?f+ZU6tJv-5NOxcFba{I6c*{PTIo(f{?A|Mi#u^_T3t z;-5VpxZZhvm-MHUtM%`L;`wmz*X8_<`!-)fg#GsZv4pdAB-}68^J2=cBPBBLuixE2 z|1|e6VmIlg{?YoYJ-5Z}>~qMbhvy<*HGMvx{CU%#Ho^Q{=-&tRdZ*1>&d~V~{?{Fo zbN+7E11b00m78N1Gn!>q&j@QOaHF=iz)Fj_>{Ebxxwi>ixvKtz6sOWPaXqvHAI) zpJ!d_*VT{Ket`QRk?D=>vwY**pI5)^cFXrat-puXJNl2VCujf1anEO#KkG9d@9Cc_ zvUP-{Tlt{r|7^fwvxo1C>htQDAM5okznhv}=dhPg%hd}qG2+N;o8IzI`~35~y_CI?cL&}Z4 z4rXy(ea@>^Pw|+~J5io%+uA$({yJ_m9gkakkKyM({+T|WFXfDf+E2ZIHRkz^@!5M= z|Ka+LooAAc>r#|6=DJn2-_ema3{Ni5Vk|u24R6a1y(P1D^nM{>gW>JpSsjl zMvZQAbQ!iUcl8L>(G_kQLQV7oR~udF=0~@0UiP_N>h|3a9QB|TmJteUwIw~;` z9i6C5*CcAw$6))IL{Bsg@ifH8BR(E+ZK64SVxl2EJ+US=9rkA=22wK+e=jj1{XN(@ zIk76L!~Rnejp@@9ebE_-lJq%B4ZV-BZw>YhVB00TiW14?Md{?? zqWt6l!aP?1*>K1TlXHvKPuAzHk9}p}Et9SJTO}9ee>FMZjZ4nT+Ya%BKO`?rY}7k@3vM~I5IL%bd02~k_|4v2TezMT-4BYYjQoe}R6ttgs^uxr#` zylXVi?S^;~LS-~|gKr`1i?Cm`xM+WbgAu-sa1uft!f6O+Mjb`;c5Th3vPG zJ&5fGvAq=WQp68I{t)E919u{R81jd~kDwh95YI1q6tYJlTZZjr*nSN0V~8I|{5aw# zqKPBBqKc8r5kHOi8H8sMx})Br=b-Z(bp8PTBf<)V=Mi2&_!B}8!ixwmX(%gx3H|70 z@JfVN5MD)iGg?ygPBgb@HNsyJK0x>o;Uk1K2%jJfKg#Hx+FS-Uhnc zri+WmBYX|p+asQUeLH}6MA!+s<&b?HyfeZs2ooXO6=64o-LYK({cj-bkzP}@C*n!a z-wX0egnbb9MVO3y_CvfsLKVV+(3yhx5a=I@a2Uej>7_;APL~yb8}ShcQxT5D_K}E> z($M0L!gdX~Cf(w`gZMj$k3oD4;$smXi?|l?2?!@@XmQgKPltR4;u(lfMqGz*3T&N% z_*8_`5YET;`G^}3F3`~8E`B*Zso1VH!H{x!@ z&mn#eaZh@3(TnNIqSw;>8}}moIlX+7*Ad=8cq@I0dk69R(EV%ra`$2S2M7z?N9n8G z-_qA2EN}xzvtiz>Q6mvHM%V;lQ-sk7V-Ut8d=257d9z1V=0(N(=9O%^Kf>V%--GP( zyg6gOk1!|i=Wb5k0(WKJJU2J5a`art<{?}M`E`*02>fI4kHPc7H{{J6eFNeagx0)K zqgxR#Lb^qWZ_k@L`gX*3=1m)YC*sA>U5xmiyrwbj2=_w&UTohFzCZ65ZW+=o!}gO% z_ax%wNVgpE(|L2pya3%7z;8jf58)kzcM(3yYa9~|YaFu?!X^l#5y}yELpT6o3c}$C zM`)NkrWx@K2=^lV7U3m?KO_7d!R0rONg?DRd*b8A_g#8hY zL^wNtLw9!m0)+Juu0xpTX627YSb(rG!gUDq-1+%$xCVp^5H8H$24MlhQ3%%|%yXCI z_iubD!VmISB%ez3lNS$xDH{SyFdR#_elQO;%D+J*ISA33c{-huOajz{2Add2zdpS>kTWI zyxw{R-9_so6cwy4E=Je@;lP5LO{O3mf^c-f_3r3`1qeSvxDH{St0}k{VFAL82-hLZ zbJGi^j-FmHXUs_j(?*|!_(uhIx*tL3Tf^^0xDEmH9L&bgdI|4eL%f0KJj9!N9*%fx z&&7y$2v}ZCDrq-DyjQ^c1$;=rM+SUcz$XQKdcfxfe6i=zh#w2&1#nz^*dDS?J&XTa zfDikrTtUEp@QKUtA6RfH!q|n8`YZCoW=9zvk$(&O%Mj*1m2!t7-T^woM}v0-OI~7g zJOb^MgQA{viG^$%jFp933O_}^r03^A5;fvk4QV$3wiqXNc&P>px++m|e z;#rx8JfDk7dIT(XE<(Ot;QgRK4&&`};Bz4tpQY`{QWmnbJNY%}%j-rPFphl)?m;~q zk9G9|oa~Q-{2mO>#bEJG`oNdKr$hcY*7&yp&qH3~+x7uZ40x}A4**Nr5%{0%;a>iA zq^17p&*9zMUjCBjE8zFEYw=A%$fXYNJjce0b7AM*>yvH<}J#5>RPr#O%N*z@E3|{1$cG2=qJT>R-V}V3FYjhxDetDR$@8fk7wQk>JK{u1-V-eLMStms zSQQ4phsJxDhl|0|Kg&>`-w$lccrNxY z#(2^s`hjm`u}_xcmn=T_1#L~1@vT3QZ|Yg>--UhqJ)Cs6f;WJieO3D5&D-O=44K;T z<2;Jt*l!2C9T{7cy^b))Tj86_@ca+#OIqnCPlLsu1JVEG+#yVT;Uh0j>A67sk>f?? zcrsSZ$K2x&-VXi5)GCu!Zh3JGmpK{7aONxZCVW2Zge?A3K5{*4mxEuzyIwr+0sabD z>f|1rXQfWq2NFp83c~%flWqZY*au{8De|pH<6F2mUWBD@+z1`vogn|YXOXvgUX63& zePFqr%(9^RGG!SL=KQ}r(5VP?D#5&7s1k;K=>yeZ&H-uy#&DU@k$ITtAGJQ}-t%eT zM$e7lCeJ?ti=8F+**uEJl+=jF87>TSLYc|CM!vkrVQ?m^3ZPvo>I%)A=F^i%X@j`Ruah@Y$z zk+TkkyNV+h6=AL$(~&+eeqxAJpQZl`)4ni$ZudTK20KlV%QNN#`N&sO z?ldsZeKP0o@%qxfya1fO;|KILX?0Q*uNKbIEH%%7ppx|LVy4q`vPl?C$g0jnH3 z;-|_zqlz`Pkq)iyBmY6r6(I>5BkDf+%o3R7R@-nXtm-VLUm9x(0nf@w$D zrxm8nKGE@VWuGruZ1*G3{s5TvBaDNzuk7tmE@=y)!?Yz}rY#N9mVq;U7|ii-JeXCLF?t--K^-;at;%w5*5bz-CKevlYxbRQ7e)7Nl(txC6}k>;$tcU0{}_ zJCOGT@?J3WQucW@qD?993v`sdjxg5~WX>$(#$jWyW(MZ^;T0G^qZ{x)LCA$)dD_-g z`;nG@4uI)rgnq_+m3>}M%elrMM}^Q~+m!_R%3i+|a@KPhn0bu{Gq3VMUJ=ME!O!7& zr@f?4f@KY4^k(RLVA`w((`F5rHfsZUT_9(FkoCQK$k|>EV3tdn^&iL^!8{&?*~gj! z9c7=kIgl%RxtyCCaeexY&9mpJy}`T=mAN-r&SjNoN9HRm$B~Q!ErESyZ(qg)wo5B? zSU+uG)=xW_bt}xe?FjTc1N|;A%i9fRUG>D-`%vZm)U~gGD}#9p)5@6B3p@0)4@^Hr zpKUEZuv`N^UrDR%^OfrcL()YUtM zxSm2>1$~xFm_7)z4yyy5nt*Eqt_!$6;D&%318xeqIpCInTLW$jxIN&GfI9>33b;Gq zo`8D;?hCj-;DLav!K|N}fNKM;3%EYuhJYIbZVI?1;O2l^18xhrGvKa(+XLqQ^1l|uJyLt#jc^`zDYG$*5x+9od5e1F*f>qWj#c@fuhu5dgH{jnP zm*e^xa$#9tJ;t-hPXJ4KH*SyIBVhU|V?vGhN6J_ma2=TT>jQ4^I`yz8*A7!oi`-W# zY%O#eo`04#H`>hD_vNB}kt=(7BW$valCK;uGC!1j$zn5P`X+WLZvt~nZU%EqZVBYA zf&2~l&pAvRKIy8=7i0rvzp zg_%~_r|k{oeL>p(fCmB={r!=4-#sxG!+D+l7Y2MEDxPcL()XTzJ>@Q$gnMMr>BR*9 zslALPZF+C(pDWR4q<@w|=fgJ>Zm0c?Cj|WWKV{`7{4uF(+M>V1>a6@z+}vZ@-Gj7; zbS5+MCu*Zi+BqL)^)GlQ%Nyf(-`V@H#amhVp3i3a@h9+2K(8|#3l)2Lz85DiS#KBL zw*5a_+Z0aYD|a(Rbt_ZuPo6t*Bdgc* zV}HOs9nbsT8My(^zX{~4@eZ7l1I)Lwec`|7!~T$P-}1Z%bPnYCoGG| z>A#GD${Yis&ue6n(>Gz-X@ZXUBjZvjY*Bs%Y`G@8AKL77#OIcPRqpe`@{79&@BWhY z*jDJUTy0>MtI&^i;?g7YRgd$m*erxT`+&0dvjlRc zEe&*(y-peA^nW~<{+CPI_v|?$(Py8j05h-3fU5!)9p)=6_T|{A2GhPU%Tg2Q)CM}5 zm-kcT)R(+yvo6pV4t1pdrM>8z))o2;eUtt}JN1DbVcL;0QeT+*4S~LJsLy;wpYBG^-z`V8<=C!Tp z&`-HX#j>>cw0p_%7}H_dMW5r+L(mi7&;_+_snd$I^tmnI_JBJA?hLpqV9KSvlzn-H znb!j3&F3q+A(yhqb9X&p_OVv6iL|RA7iLU&mw&pMv?F`*B6vEG!ujxg(7`fA8rR}?w>ST)86_PzeVrn0v=5R-G9Q8{$v zcoAk@3A3L>BeHcX%=#$}bP59nGbMnLGGd7h2vNBg=?VC_1jvXR-WCe3%EY8*$^;w zxh-iht`>8~U+T=mQ3hTjXMM4CEh!>5njNCQydGwy|e^gm2v?>Anj60IDBdb_k2veC8oS@@sA-jIv^KjQs(`vzPEmbnVs zTI|StSe{#`fDa;!;eM6+1P;KCLj@SD1Ec!L%bx zJDL{$h!3~HmiQ*exQthPCgDBEWlVef1dL0N3x5FK6>_Nuc_vNjLG;UJU<`nLu_HE> zy>G(w?IG-2hW9#39SZaL-g@Zpe9-`A8#M+tMb3779zKMA%5%T>*Wnt$>r4GKgI~K9 z&&xqB%(fOj1HQfK<@8znk>~k`ooT)ae|jnA)zGI-8<;)_)6e!mrz2pMBQKe2sXSn& zEk!-jrs$KeK;3Tj?PTV;TzM9^`Z7DeJp4lBW`a9|yt)GJ2D7}v9Itvj%REY$?IJq# zrx(n&kUl&D$AUcTE^RGq+S4!3aviujIF4itWWFaMtvrMJ%)QK>k zW0bVvF(~&1wfYag7g`Aj$*SWs}os^-V!X&4Di@2fgRRospJLU5=d9(^Od#`W?S5c z?e0yI?)z6^?DG23pXD6%)JS}P^lFSJfqor)qkWmf(2g*l@em#6d!!$uQgh8FpSPEC z9gG8g{drc#v+*#stT|VDAEZ8&eO(E&euO#ZSAlu%s0MSK7v{M`bT}4CSy)%~@RQH} z)rcH*mEId;9k>e5QvM&-e}!e8{Bh4mPmSDi@Nwv$vaT-AaYnFq)kQ6GK!q-BFy#@=A5$u z%)Arj~W)*a~gczwCf?G5z%z^u3afF&*a)j%MRa12wPz;Vj|P+Wof z5c1%2r$aEa*qKA`N&QVKcye_5cT3>~@F7!NtGcSJ7bm36{$Fv}~<@>YPE zmoV2qD+76zu=k-F%sQ_Di+!n|S}^U49oBgrSnTVyJeYl6PiygU!tp|%8S#xUu z^Y|6!ab1bJWm?f^Swzn9Tx`noT{kbpdLUTrF9A1!#lAfABh0kTVAg}OFK-#fE|x`@ z_1WU>?1i!m(@rax&y@+Yyu$Rk0xWi9-qi+Xy$RD!yVsGr?Fe+F?}c{QE)m8Vawqgz z7P$r_s~qK$@kf|_DGGFyec8JLJKbRMS^Vh%(`VK3`n`dEADC<1D)&15fsV454+Q%1 zY&Y{N1T(J^Fw0U3ru{0M+^DZQ-e#HTgLz!A9mhjXeR+mEV_)hJeN~>lF9Ead5nkSXSoKE>k*l&R0nBGf-=@XPM>SR%$H*hRGrA_ z1Ld-gS_e7vilokOw|RAmZx=~h4;}j45ZIxP*bzDNZ47KS`LsG8_AFy~OTeuGHw*hZ zQ60~!<5_g*r!f5&eX{72+rZ+#_|WcId{Fi_g=tfm=ahaKlfBMENOKw%A*4<^phNpx zV4TgzJ>ue9aa|9-?w44*0G|uJFP~*=4kv+MIv76S*y;>?=mN9vb%WWCJz$P?!W`=~ zE&S1A7II#Li=6c@>t-C2g{AD`v*@r72vbK`e30?B7c4%=c-sf255n|8Y>K{&SN&dp zFW496cqMkoH+dhLFt>UP^(pHZvhFbe9ri0_U(dp4+T|BHN| zmpKgoGbr-QXWRd?#DBI+b6{WD+YzQtOQ56db>#YDP@SNk%UD$5^@Uj{!ctz=C+kO! zi#Etv=j~vYu>;I9b_VjUK;8|e56a%R9>{649L#xCHJCp1f|*wzn0fVsnRX!H2>+R3 zT4A=Sq$Nwa$fD1&q6%{HS?WZ5V_w2CKFBjy!py58&=C&h^g(UQ1((V?UaF8pX0%_(*QdxuQ2t6X&BDG8g;`g^tP^3@i7@MgcBD>( zSr5W2FVAb@voNo}gsIaCeep-;BqA^T7M|O=+qf2MEK|X4UPtCC%Dzs7IW~599l4%T z_T}n?oaO2YcnkO>>!=Ivv2&=br`-tN0ex5e*$8>vCUYLJr$1m>Uy^52{|x;E4zkU! zN$NXlB;V_BaV+b-!q?)R46}ZXXHB91b^PC7!oszCn@a~zIF zX2%w>nb83=Uy+MVnI{)QUu?=exdhC)u`uV%qC@-QPv|p`O|i)_M2=JTjZ&oLF{te8 zLHehxhs(Vok#js0`yq1-ls=qc=#$F=`^w(Fu)I4(+C`XU7pDF3KCK+%78POTeuGw*}lDa7VzM0e1!59dJ*; zy#e&UvFlOr+!}CO!0iEd1l$?$>8Q``@odZsUHBh88hkhW zCuRL7@tg_xf)30*z^{NiceLj>Z!hFA<+~;~6_qk4lP8a+Xe=Y!bgT?+e;2tpj5#~Dzx-s_2`|ahw48r@PT;vk` z_d@$BSaaRInxk_P|`psb0LupJ$j$gS35&N_$IxO!z_`Gwoy^|_m-?q5?8LYOs zB`CvlN*$mVl$L^CR_X-xDlG%Op@dH7-cec(`m0hmXpPchc95lA0y4c0kk12T z_LhOngD#MzSOGE*T4q_E^{~CgAlobJ`?hx!Xso7{`}^iS22QUh_byCt9LTJU&yc(s((ILI^d^GL zUIobNU=qmER)Q?WWRO{|0$JInfVNa$szKW*O$BYQR0Aqkng-fcsTQ<{(hSfymFhr~ zmGI&=UqkgE^I#UpJZJ!!2eUJK8#8<7WcD_JthMH5s2Oyi<}n|1s8S2aO1=)^2T}<5ad7bfQu_=wziOpqWY?ptF>gg3eRw%M^=dWf z3Z;I~)k0ntK<524ka>^4eR=O^fXw?kka>>_Pw#y_ z$hSGjw>ikSImoOaz(F6(FmPNuVEUOIL#CD@_LdM5zjNvr;|C z`tB@{dDQ?iuV#bHt45Hu{v42%w+UqBoeQ$^HiN9Z^FdbL7La+h5M<@(07+}D?^b|r zRbP5QzO_KUwLq43705j3%j{hZGXMHPzO^zmkkONqyXi$BtKEE%)vo*--_jO?EZ-84 zr5FXWd`m%A^06Q*c^Sw`J`Q9h9}lvUPXJk;EC<>6G7)5BP6f!umq{QSb1FeL(o6OfX6GePE6 zJ;=P81+t^D0rZHbm<_TLHG-Z{*&LABTLH4P@}FTV(MpijhWr=U(vHQ-+;UA@26|3u z9LV~?c#!!r0c4iTL3YF!Vs>VG<-MbpTM5YbXOQHU=f>mdxfhfsfL>H82dz|^2zpJa z0`wQ9NuUqlW1jwNf2=lxjeKQcuN_C)NN;5&jmFhw3Da`^ED>Z;dD$NFMtkekFRA~;VRH+HHxzb$FmzA19TPn>5 zjZ%;>d(te-9d*@)f-vSVQ}$VQrWke!*9fb8h*09ij>3bHd(C&H>We^_TCKgRI|ogKWH70kY$=2V~>bN|23?y&yX(R)Hp~l|ImcN~=MKD)oboP+9}3 zQ5papr{vayJ*5bAvQj>1rcxp3ETv+Q9s4Dq^Heqpbdgdis8MMw=nADW(A7%gKtEI( z51Owu0rV54a?s666G68sRe;)*CV}o!lINy=tuz_*8>K4HQl%-Nhn1>9k1I_DJ*89w z>QI3nfU1<1 zf(}vY1btg+8R%%GF3>ck<)9Okxn^G0X{XtrP z3Mj2q4H~XA6;z~D11eFP2HI4q7POht4A57U>OkX^W`e$^R1exwX%=V~r3O%i(rnOP zN{t{p+s^@gOJz-<1C-{1rYJRo4p*8FI!dVpbga@sP_0rc=p>~@pi`CFKxZl~2F+4x z2VJ1F1ayf~2k84sOF>OaouGM2%RoO;>H^)Mv>dchsT=e&r4^uGDD{ATsk9Q*uG9;< zPiYnCw@Q7W|5I8GTBg(wdQxc(=ozH}&>xi?R*ia;BG4;J`Jlfj6@uPUDh91qDgk|< zGz#>IQYq*&rLmyskJ2N`Km|(UK!8$KEI??2YO9uCTNvXJ?L#v(wzmGg-4w8-7L`iy0-!Jk?y@9vv)RVK=(F+5-&)P zm;=fKCEXPny(Z8I-8&altb6BW_BMky(!KLRqm^1fo2%YWGI|R^WxBT&w6*U2S!VAd z(001F4P@6xi$QjM)DDvCqdd0+WLH@oAiD-z3bL!L&WvnXM%D$|36}HRa?s97-Jo5S zR)8v$dO&uyy%Myi%6dV2E3E?UqtpkQth5?*fYR6v=~Wp>yvlduKvs_NASp+ln*g#r z%0YHTKM`bC^cA3k)ZQe}Axf2?Y7pk_AiK_62J+Wspd+w%xLXdYQIcmyPS6w;BY7n^ z31ru5m7r5quO4)!(kxJerkD>hpIbmy_X|N*_w697@g*Ru@eYvH?oyDoOlL-Knb*sA zT_Cf(9AuWeL1H=ItpM3oZx6^yxe{bov%Mg@nq395tJyw~UCpir+0|@6$gXDBfb42^ z0AyFQu7tG~f$VBFA7odvg&?b;Vvt?UmVgd@R{R?UI#H<z z9S^dr*$E)Knk@&})$Bx&UCma2F4MZ71oG>SqNlk{2K@kg*LPK*Ym}yd?o_G zHd+d@HtGaf8!ZD_8+C!Kjh2HB?UwR(gRG5KfUJ#rKqukR5~LKn1Ed9#p6_0aT<^4%$#@B4{I}3eYH}Ngx}MDnT|PO$Pl1Ugfze zkR9DqKsGj2gKQL?3bIkO24th?G|(8eTnn->YzAmEmDPb{G|O`{K~|!Akd!TU6R?`0g`<4-BOSp3!NZ4 z7M6kRSm*-Tv9KIu$3i#Aj)fH z8pv9(7G(Wk21xorzN-USZZkoaTRq4u&jOj{29UMKY>>4_Bgoog4#?W031sat7i8_x z46^o^53=@X0a<%21X+8uf~-9jfoyDQ16g}42HDuu4zl)G0Ju%-+16lv81zCz2AWKmPvK}!LWIduDWIbXQ z$ofG8NcsWpqKsl6pA51-UIVf`rhzPvT98?u0Wy2_Aeo2dx$aGAWd+Fka}UV+^GcBQ z=U&iMlsC_<0@)bW2ePqlHOR)gevplIYd}Y9iUH7zS{w2KP#gIp&@s97fngN=vR0q0DX(s3gO7);?lxBgN zl^Q?`lxBl&RB8m>qBIAzNT~^Qhtgco-Ac`%dzI#cI+R*K4=F7KJ)*Q6WG&MTvesV# zvX>S?SS%1lQlR;LyRUoU~ zDIhxnszH7n2U(5RfUL%+fvm=BK{ndX0GYiyklC9FGJEwPvo{N5_8LHDZ#Ky6HG<6E z9FUFPO&}Y+=YnkXZU)(iIUi&rW(&wh%!MFZiEahmtY@`FBGZ1b6wkJrr=1}4bQ#Dz z?E;yn75FZM=}iKeUM0x%CTI3m`Mty46wnLEW4P<}ly+msvL}{-q$h6Z9@~PW&xCRG zvF1zByZ2AQY*pu~$Z=BxqbDGh+^yz0ua&Y-dgWLEM))<%UH zS#d^I0xH%Nqd*%e)$hy{i+5o^Xb0I@ynG_-bRtOFZGE=}G+NV^?~1WbX(C9@)9bsH zpxKbE?|MPkD6InBsMH6#U8!<6-0uP9yBQ!mYt(_PZ_ET)ZPbITZ_EPuJ`VDI9As%5 zL6&w7$kH}}EbUy7rELaD+TpGf^c*}G;g)&IcLN}^;&vxRAe)WkgKX9^0c1y1ImqgM zB1ro92v-AoNi9zUy{=RXT5aV8eXO!NkaaeK-(zI23gq}K~lC6ZVqUB>>c5nK$gc`&_qqq0{Vv1LeSnyt)P9C z7J;gi+CYaWEe3sCsU38*(h|@#r4G;uN=rfCQ|bhrrnC%Huha!PS7|xuLZxodrAjM6 zbCh~O);CsWs26mV?p+1CR;dqUWAMQOVI$5Bc;GKXY}T0s0H+( zrf36Mip3yHu`Hw4gVAfMv0aVP?rFU1*IiINB$08Cj#3jc{{81+X&0HG`x?`K~3Sw-97w zY+FXQ*vm$^4$y|0b}49#`m!ve*X8xF!km%yc-aWI3RI?P`#@jQwEY>qH5t8VZ_Mvi zFCS#}TmrIs9tE-yxGWYIcQH!I}v35P0Hw1f^5v5l95$=*$6icWGU)0duM`d zzT1$I&CbZ?gRIS4K-T{jgQV9LxOPwYZXjr~mUq=RaX!*9XW~A5rlkFT^14u#lBL8^&QgL(yz^+fQwh{OcZ*hr9_6PzwfKpm zJ8m4c(ybem-0x@A=4XqH3o3J|O_ZTLqISBa)M5SHIg%*Zj?R>`ZH6+AlCO0qQ1Z3z z1xmK9d)@H+lAhFhcfa(Z2A_2Hy20(zjBNE&8>UXpKd*( zoUi!&tO#HKy5n@}9K}y`DA}j6l+0U7rSEy|67Tuua z_oZ%8Io)yhs61{xqGYcr&BLWf%BEzGXQE_pWToUP%SOqu%t6U_YjaZt+$G0Qa(^jE z$!kg?C1*quCG&QsoFkoC*+#{>^QKbrj<$M>@cCSel7C$|j*>?wQ1aFLajJ_uZ#v~X zM?lG-dN3}tqPOIA!F|-GO;v^4xnT^hETGm$&`GrU{qwfag^+X36yN*6iT*ZMr68Kkw@oJvb+>Zwrw#b zTNK|WT;n3On3C7pG)lJU1|?^4!M5_KUrC_ma_0Dz>eK?aYEetvsz=V>cS&XPn*)+#CTXlF{+-uZ-mr!p%S+Vk74tU+ew z3YGe|Q@5x+ZrzKFQ#$ZFkmuaH04399q7Gbi#$}~`xb9RA>abh6BjXBEzua{mjg5>e zMIF;r#+9Xhd*1mysN5aH$0LT4tt?2%R>o4YZN(^O+cI;~^Im{?xi?V(_qVC`Q_jD4 z<~>(ue#4n@1~)an$RP%Yh>K_$9%n363&M#+{Rr(`?QDcOz;O19%HCEIaPC0kUAk}WDr$ri;?a`Y=u@@(;xJX-=K&sLq1J6tVFeiB@dlAi=O zic~Yod6#P|t*DOf-c6*sx|Kv#b=SEwCBH-8os!?7?@7t;(D$KQxYG@w@g$$=-NG$-GMU@J||a%K6@*p%kS0@N9+> zOZ9WB6y^NRqM?+fXPZFDT1}zkY??vI*))riEt*To z*_1-bIkcFPb7&bQ=g>+@u7$Of9LtTA9IY*s9Ib7X9IaGJer}&e$^CRcB|o=6M9EK~ z4pZ_|sAH789v!FT=l1E&bnYuo1|?q~ou%Y1e}R&p+h3vNr}Z}|`5FB!N`6LvkCLC! z>mP)Vf=$U0%tXmv%Sy>!%SOpw%R$Lr%T38%i=kw%6{O@EjHP7%6{AA0E|l!QvXtz< zI7+U;&Xlad;Yb~$WL=I^vIgmtJVyp4`~EEDypz$C+&ws(IQp6rLzO?~j4MbDV;rvm z)Cl*{QjzJ(My894OjjW^9lw=G$$5I1lJz@Ah1QCaHAtuAO3t99b1HP5Q=#je3SH-voTnQpd9Bz&$v)i{sZ>hNgS1HPr{s({M9CR( zn3B82F-qb7Z1qd08oEd6|{!)C=sj%*sZp zEM@aWa`<@YlpG_Qk|UUjk~20dCC`zKlC{b~$#dkUWZPmW`2?S`lx%ri46 ziK^?aK{6$0#3)MEU>qfDFoBXam_o@K%%EfqW>K;Rb17Ma6v|nH%u3Hed^f;-j@ZHB zeOio?=}J)p-A5BCS<|G@qnVW<)KFPJO4cQ2NO<0Yl*}6&8CNVcF0+z6jPE)#Z)Rmb zwbHG_)DgFCQFq+Zlliw$FS@TYRGeE0RBN|JQ6t=%LalP^7?iDcNh?DLH~YDLI0DC^>=yD0#Lal&rz6u`&l|?5Dg}cGoo1#PG9a zrDVEnl+0T;@@QP-(F&1A<0;vW1WKN_GbP8oJ0;8ONy+m1P%?);DSSuIG&x+=r-kRO zMajJND4DkrCG$3;Ysy?oUQ<#ic}-bN$!p3oN?ub|Qu3Oz zmXg<$R7#G18YM@6KP5;15GCt!n3AnLM#+9TPF?%Oc@0RXWKU*LatAp}IllwR@0wB# z-SglIC9eTDsAlfCTa|l=rAE88k&^Rj3ng2$jgqZQrR3a5qhufKr{vr?M9I04 zvMRik#gr^%86`_uNy$>yQnHkdlq_WnB}>^x$r+YP$-YmctQgS@DP;xv{DOr~^O4emRCF^pCl65&u$+{e)WL=I^vM%YAtV;$Z>vEQo zb-6&vx?G`TU2agaF1ILImwS|)8;>YCH=(+^{J*H!@LjZe*q8+{i}BxsijC zb0aq;=SB=A=SD$F&W%_~&W&P}oExPmIXB8ua&E*q)4AtH1xn71cuLNV1WL}0>Xe)t zwJ13^8d0*Jn^Cf#TT!x~6DirxNtEp8&Xnxu?v(83o|Nq8K9ua|0hH|LA(ZUrWJ>n) zC`$J8I7;^O1WNYv6iW8<3`(xSS(NPOxs>eZ6iW8HY)UrL&<(lqhvqtr({1La;9_l^I=N%^D#>H^KnY{b2=saIfIh@e3p`H@B$^* z;1x=)!5frZgSRO8y89j_|5o7*|PhQD8@ zQ_lD6_!^Ot?I=dcT9u+?t>Pk&R){QlssEBdE-n-H093MFebgOatHMaf#trDUyAC|RqO zROMsN>&#k8uDp$ud?wKrN`BLG8zom%DkW!Z8YR!MpOSO@5G7aHVM?x#W0ZW>%W+E1 zrgW!{InSFx$yIxnl56Wiq^?jh?+r?>s9TiW1@2MuypJe(Hszb}HLX*!Z8jxWR3=L9 zDp@J{myX#eS#l0auFKq%{Cmk5O0JKBl>F<*SZew)XYGqoGDj&&?iS8pMUeYJ9M#mF zt^y@@y?AQxQD@!+Ds;V3vR1Vy*?;vYxeqm>$$p+d$vtceB}ZWfCEGEJn&i$gmy*4k zLdo5Gu~Y7&%P2X+R#I}@)>3lrZlvVjn{J_Gowrf4Pg5zmPo`1w`9Aw8S?5EPoRfzs zd4)Je$#*o5Q!;NlC0m|B$({c!C0D=&N{+%6O6Ish$$4;#lD%<{lGlYtl$_bhrtp$= zO3ophl5-;yCC`zSk}b+c$#Kg;$(HA)WN*Y!^0{CIDLIQ{DOsyxl&nE1N}jDOCEFH9 zJ^a-<*D6qQwBjjQN&+QwRHtNnYf-Y5^(fhEjVL+8no)AxT2XQ=6De7PBue&6XG-?{ z6-w@UHz>L5-HVKS6dD&)GHqrleBBdNvQpFC%0|s~zZQz4WGNLWSxQ1=T=mGfvRlGu zY#g=Ty~-+3U$GSa9TnyL^oGxC-Wp!s1xl9J^IPr~?i_t6*#|=+k^vD7j~EqvW2Q7MX5;WV%C<=?+Jx zJ06)XJu+QJWV*AF>8?=nscSbVdG~cMGVW1iocJ~5Yh+xu$hh3pR`<~u zs`qcsSr<#~aK{y+XGT{QM=vcHKO*r)haSBF*2?* z^`rY}cS?>?PfFIH4<+}jA(82lBh!tFOgAnv-7HGZzqyp0FDaBPc^M_&VO|-TZf$6~ z5{mPi8(jT)eTvi1qid*h(Vv9606i_#Md?|gevO_N>bL0gp{`74ijig3q;rP4A>Aa@ zE$Nw|?m#aNbvOEWsC(1bLOqzypU6ka7(@H$uajPKy-WY07sJ>ye7;}s5EcsVJaUQAzR zKREj*u|jkj=U;4ur;o4b>B`Yg_Oy}^9Ut%MWKXAhTB#D9-ubQP@N=bjI;LuLyz{%y zk!d`g;prmPWFKBE!Kd?hx=W4Fu`bE@P`^YwztdQ3Fx&4W!zxMP2Pha%3zd>}~BA!0eSjIQz z8rj`EdW|F}MYkoz)2W`$@U+q~I=%C|K;ix4{Ju}PV>})2=|oQ_dpgC_mAgcjlhT#* zj(uB*YcP?Y$~)bic7C$$^h!FdPjtVMWdNyLUwQbB6=buKYu^&m4-jb|l)1xZc9|v@@R8c&Fr? zZ=G0vhfsg+JXff{cRD?~Ef-HqFX0^Xb0ItP9-wO|s@v!34l3_yLif*+M5HsT%5rlYc z9Xx%8SJB8cIiuad)4OPA`G;bC%4tulFGa^|ba*-WJmV{Qx}B#h<%-VR&eOv^y};AE zJbl{JyK+aDbK2AD%hB=qJYC7t?L1vMe{}u*u`+L2_Ggz+JNvUhk?1s)J>9|6i(ZXR z)1hdzM|ygZr+0gLWSQvnXFToaMb_E>4OpKuanWfm($4svZR6t6id< z&(oDW-Okgy21chp?dg2u<+-kM?o|qPmLfi-U8rB9hljc`y&%*B>0P0oOIP|Ly8L#Y z9`5M{p5Eo@)1FpmN0*b&)0I5k&eOv^y};AEJbl{J>KxDVJ-utL><9N6^tAe=XM9R@ zUX?$!6uBNfUCGn!JU!gg3p~Bc)2BVHF7zzl)0I5k&eOv^y};AEJbl{J>LSnbJ$;%E z-;44sj&?h5FY(O#i%heNb0CF3+2hRXoVO`Yq*rrpr_}XD_Ek*FXvcdx(bLJEPVsaG zeS|6WN1r#t)5`GZ_!v(sYdzyV9q;Kx zPbYgi#nY*t&hWId&a-?^$9p=_)5)Gr@pP)EGd!)V_blJj@t#ifbh4*YJe}(43{NW? zJj?fVyr&aAo$TopPp5i1!_z6L(dDOlI>Xb-uIMx|o{sl)qNkHRo#N?KPiJ^q`QEd9 zPse*Y(bLJEPVscAr!zdQ?Dj0*)A628^mMYPQ#_sO=?qURX`bbKI^NTXo=*03il;q^O_av?62}(E7Wh%>SmcHk6g>P~WE)ggS3gmLKW_`gEwf(=q&cD(Ajbh4qiw8SO+*EBuM50!-t4ji2G^ zcCYI0yi-_Cr54gF=zJ}u(`dDg^m%8zrx(xHBVQw*^>o3M==da0w_730F^jQ{p)O1> z2z7OOSE#$ur$ar7R#(cr>*;)<{*A5_>PK`M9sZhKUlr{I8)V-6e4Vn()2BVHHa8;2 zJ)ftOzc<|d;~e*xz0r<8Z@S}qvd`oHm7Y&0YnD6yAgvU)BFl{NHh(6^IbMG;O$r@; ztbD13Gz~jCZy@^_ytNZ;E#6pV5vvE#1#q=HJqbon@YnPLty4#D6^FJ)Pm{m<@Lv(z+rxQJ$?CA_oD;uNp#&|m3(}|u|Hu)p>lNe9Odpgn6sXs@TpW*44!_o2a zo=)_1vZqr#eU?976}}J0JRgW0uXsrnr@a$h=Yk(cJ850C6V63@)WvA)Pd%M27}@qwp4MORj4u{#{rza4^)`P(E4&Z& znbAJ$>7?17@vEbqwkF#8I!~uZJ1tM9$nw+LMLQuW+Iq)mkLna{y<4=?dPZCC745X% z(Ox_(+DRj$y?9czllDft;J#>|{Uh3=_>)lKeZtSjxC_kV2I36o`<#ErKN#{u66FNL%nbizn3Z8V!)FkL~lyumP$;lB)fGQ7!dkmETd)4`mq0Z39kZ(3B8pC*oI>Q8p z2E%#`O@;#)S`3pJ+6<>K^fR2nFu-sn!yv;{hM5>p?Cg2 z2fr)8FgX{0(}rP2UOw%Bq4PP5MHnWP;X4KlodaH!;W7?%G3WnV@CK2g^X~{sFzh*j z&)Q;`IG>-?JCCRE`3}x-8UL!w8NY#_*g60IE&r;6;eHPA>(2jQ<6j0aOfgjD4Tf3y zPHG&(SR@Ui(!0PWrtmH~-uZvN zqh5*O+IBwWUFZKZX-Z{h$c{>Ih8en2C2)*5i8Hv4EY49uF1(2vXaeWxb%t~FM&UAU z!s4j#rI?ZnMer7?VmQ`;FLRXbaE?|sIDaX-0|vtR3$pXD6sxfn+wndA!aul*r?5Dx z`A`^dqBVM91SVl0eu4AXNFTuF=s$x(cpVMU5?^CCe!(?70dM)0EXac*C;{hhUA9FM zc$=<_#OGLu72qwjvIkF)pEIlr7GWK>Vi)$}5RTw?+{0t|{A>?0;d$glUKB!cyoNZ` zKvVFxP3en|F&dMw0)OEuUg6B{gTYvjE!c@P{Dj|-jx+cd55P;Fl8;+#EXv_MG(bo6 z$EWxbtFQ?>@i+d(V-(87&#loCy!iw@|D!T18}@hzNBoBtasFLTeM0xF{+I-oZuVL8_005pDApctIb z)vkp1P!H4bCBDXbe2ZN;f-|^^+i>oXL1af>#G)D6;{y!EXsp0@*o6c58NVX~SMV6Q za&q3`ZB#=8bisTa#vk|_&ZlGhdFjrATzCbw&=k(6RgcC_+(B0E++|S_&CwR!(H|o* z9?s`CJD<(`4V+I%zKO>$bF&|j69rKY%`pV4a2A(fzszZb+{ljvya(r#kei`BKEMz- zpI7`Dmf%O^U7mx7*uSB^~2+rpT*GCJyj}I^eBQXWD z@fB9$8|=Vg9LE`4z-_28yqY5`^5Heq#3xva)!2$%_yxK0@f)eAfo|x9QJ938n1?0U zgftw)b=<*I82LGOkR7k00h*ybK0-34Vm20H1vcXl&f+TW;0dx6;5Z>aN}wtlq8++o zAf{m{)?x>K#XnFAaz5cjdn2jYkh`(?H_uwnUwjd`8<4x2@Ukt@^ zti>j5!}mCV!^psST*n?G`r}hf!AyLKMOcGx@jdqAIL_h* z?!#A->kQcui!yivb2X6+!}Pj zhZu;Fn2v?miraYMbzXn)9-5;AK0t2_z)+0BR2;z1_zf3v6aI1>BQ!xL48;UY$9^2g z-?)H^Z*YE~1>Q$L498eZ#cV9VYHY#x_z}P0I4NI+dQMjs5tD2&H4Y{f6QjC=6C!}Wz{ zkrz$jd=l~d7>#L|jSbj^efSYaaS6APDW3g{s%VCO7>h}mkL6g4A8-cGRAOCG1~t(b ztpKP27W8nR$^N1yBNSA|BOI7tQbi zhF}tw-~evGNZ@{re0UEH&>Np(3T9#<)*%gt@C*LHdHjn9$X|u?9_3IKEzt=RF%K&d zsLJuj0DOvN*o1>Pk6U;QyBg;;I$;>TKnj*%3%27ZPU9vX!K}``AK8%y1yBZWqb`!r z4TCTS)36rDkbz5h0JR3|iEMZo1yBT~P!Y{B3Jb9bJFpKw;TSS-4*%jl^!L~n zz)XCDowx>nP1YIBFaXoA4&UJ~n6-HCj`z?8A7BuYF$Pnx8QT>Tx3k<>tEX6PA zT#xO;PxuulaSIQTzdqL`reO)T;s>NdYryq@8c0MxjKNfF#q$l>HoT3V7=wA(i6gj* z$9S$0#}(yK2kp=e{V@ukU^ZeJ^IC>@G)7B&gkczi=~#sG_z(Uj-2YJ?HP9UM@Cz=W zLsRZo7=d5$FZ|6ouh0Uwk+V71FKVI_#$h_X#B!{`F8qO-Ex3=OJ0@Wle!wwY!W|TA z$+F>mu4G>f#&E2`J;b)+`S2Zf;|^-J=A1w;%tzTa>{E2b7g&kS*oz~GZOi$DH_!>g zFcx26A(mkcHem<$!b;>d3&l_x)lnCX(F*O+1!FM@Gq4Xo<4@ecV?5K2{|9rod8oW%1TSq5H3ZH&is%*8^i#a8UZ9vs1A*za>cLLL;sYbcK@Xo_y=jZv70 znb?5uaUAzybmFxgB~bxYP#aCq6~i$O)A1!1VLi4Y6+humoX0iXgVvdQEZ#*s^u;iY z!B<#?Z}2US;1V99m{b5*WrAE(u*jG zw^0X;(F21q65}xkhv6H*_aG393P?a5bjBYzjk*JQEx-hPg^f6lGx!%z@a!PY9~4Fj zyooAkgB}=-y|{wngIOk~U=xnwKFSZ_o{Fwmfg`wq(jRkdkciIcg<(j=d00caCeZ^U zum@Lh7v+a>zr$Df5r5(guHi8fl6hUiXzavUyfmDBfqH0*FR&he;35i);CjUv%*0}x z!aulyN2ofI^+7Ur;U8RuF^X#qaj1y~Xp0^gk7<~VB}jugnr~I29NtAuw8b>c!7_Y{ zRGdc47^X#Iv_w1f!YC}oPVC1~oWvFAW4Yg>1nQy*+T&w;$zDK=prF5m|4An*z2E@Dv#QW%uk1zm3F$&}HIp$*-)?yRB$0=OHZK$8} zx``qvg9Ow>10>>oe1s90ffTI97VN`uoWpHAgf)TVkD`c24b(#mv`1(3LVtXU_1K2p z_yH$z0r#O!mW=!O3H1DA0J!6_U!l*W5#fiakfx!8s@9K>P# zfirlDY*V>55Qlmgh)*yVTaboBIE@RqhFj35ac@Ho#GnM;Kuxs32S~=}NWpS!$FDez zJFus74@U)bL{EH<#n_D>@h8sU3hu)aSOqc9$GkcNXejNfny50QH=`x|8t zhsvmd#z?|8?8h;jz!Mbtl4(&1jnD=kU;xJB7o5T~^H@F_pc&er6MCW_lJPk<;sAa{ zI?Va36WU`ecH@;4&Ie4zPdI^paTlc)@EDq)A4Xs@zC$VwA{}>;bs_s1HPHy|@hMhe zA8w=2BK9SkpglU{6MTl%*o0l!hd*%*`4@BDpcdMoFNWe1e1>`0f&=&&=b(JWb%#7? zjStZm!?6Hoa0?G$En(e}6J^j0A7V9r#3@|DO=wFwC-6GDp*OZ;H-5o!oWglrhx3~n zCNd!#V(==$1Oa7@-^2Wp2bTjfFgJcl`s@Lkb$`6yyr(NOu%QD zgT?q6XK@qvV6R}A$b+|07fD!*gSd?+$hVUF4_e|we1bhVjk_>baX#XC6vHB{z&_kZ z`PIB{KsOA)B+S49{D!=1crC=cXn=0$gV|UP=M$c^wd_N@g16y(&QSxjMptaZ&p3m} zcxD~jh1XF7P0$__@g>$`6L#VdPU0@K^{fltKm&9}Zw$voEX68p#$Fu4pSXk@&^K_b zkQWuv6dlnQ!|@5GV-eQiTcqJI&fp>f8+jjwJa`2q@g{1Z5!&KI48|nPKnlLbHhhm? zaSH#y{|#SbAU|SJ9to&}cIbss*nqwG87FWbflVA&yo>@UhbpLv#%PHi7>ZGtiWKa? z&-fK5aS_*X8_uVQ7@N7~@Dhr{`J9j{Xof_*j~*C~v6zh%EXB7-!$F+H8C=E_=v(-{ zE?!0{ltT^FMPqbEUkt=3%)}<_#&1Z+b=<{cWZlZKKoOM0yQq#vXoqg-je!`8iI|DS zNW)1y`z`B)mr()lq6G$HI#RG2&gY3-#yvdy9rGYBUdOwrk9O#V5mPO9fUVexqqv59cmjPF#}@@q1aF}ds-Zc$VK~NOGUj6yHXs#yaR|TQI8Na#uHrGY z@7XrwL}iS|2KA3c~B6g@eXRBH$KHw z%)}b(!;d(J!h5*x@EU5OKANH@`e6u0U>x@16mH=GvhC$sKsD4x5_)3@M&MH{!q?b| zy*PwlaSCT~33m~@k82d|@hR3|J9gtJ&f*eo;4!rQ>`!FD3&@3hcng(K1C8(jdSf7x zF$O8vi9PrMNAM>eKsms2kp(Xx7xJMnilY`fqX+t65I)7{n2#Mu!!g{z9Xx`0kZTZm zQ3yp*4QYJx9|W7KQaw=;R0^r9+aQh z7QBcU6h|Ctq7B|hcZ|p9Scuiwf;1e&d0dD8XI`Vx1Z~k3eK8ahFcb5!3R{qhpYR7x z;~oNsSr1f14b(+bw894%i{Fsx2(P#3kB_km-{BB`!JjydOSl2`7nY5@;9sdJe#KA% zipg)Ezo2ARvMX7X7^(ojRbG()E6%UNl~8gjCHbxLQc5o6b$+WnPI+0W#&4B3;5Ww` z@_&t#@=8;sBEKH|j?z}CtRyK3N*ASy(v4pr?yl5QK2T~aAM)GcA1U>f-bzQMkMh3K zPwAxeS32`M;a!wrN>^nBQ;%fo(L6eaZ-aiqqf?ZD$~0xL@;Ot@RFaj2Otpxq)-u(4 zrr4-VP&O+Ql`YC7WveokUo)Q0i^B|Mr}7z}I`p}+kGT#ivy~r}Im*w>eMDKo?~<=n zPAIFCKa~y2N#z^mFXleS+~=A5BENxti6vZ9eo(IS+vhi!<3HxOt^A?fWv=_o@klwP zXzCfoP|qrsdQP#`^U7Q5Km3mQ1?64!l2Tc{tW;C4Ds|NxN`3W~(m;KnG*TZbjn&5r zzuv6!2_9;a>Qg(an)<$KsGU?>?X3FMF8tzpSM?dSo0?heu4Yv~P@h#lR9{egsV}O% z)$HmpHJ3U<&7+P~^QxoOeCk*=zdBAWpnj?rR41yj>SVQuIz=s}&QMFLU#O+jxoR18 zp8C3)qQSnc$x<#$0 zZdL26->Oa3@6=}MHnoMiU2UoEP}``fYNEP}E%=@-*v%HCvG#jd`@O9BKGysIYkW}c zss6|s|D^U&e^&ddht+=S5p|IIi#k|6%2pgxhpWG__J61o)Dx`vpR9R0YkgAvO#Ms! zT+L8tsi)b3zt!3589tuiEL(C;U8bIA&HrJoFQ{wPi>&z-b-j9xHNU}H|I1q6WUc>W zt?#hbcUkLuti^rS-~rG6kY|6S{;obzkE@w|C)6yybTzB*r24GylKO)0iu$7Os+!$* zSIyzOujceURA2HvQgiv9sxdyrSJ3D474qr6SA2%Au+Q%+<_r3Y`=0TY@MZRu^kwmt z_GR^z@xAPO%~!w|$CPg}Wd){u$5+(%E>qWF>e@_Q-&e}lh$&k!WumW~ubuA=Uk9e{ z=qvB*;;ZQE=1cH>=&S1M>8tMR#atix-t#5iS0b>iNd_8u-StgmJzmzE6B@ zebZRNbYFYl43;vJxn?uha^_m$>*HJL>*rhL>+f6b8|+)}8|K@HceZEn?{mlI{ za~@{SW4_OPCz$(B-xt1gmT=NH$9K-Rz<0^F(0AFl*muRZ#CO%V)OU>~-)E^0S*lN4 z?bEb1K0{mQv$PF9zqZ*I)VBCCY2W!WYdd^dw4J`JTAJ^9ZI3Uvw%1osJK%doJLoH_ z{p2gA{p>5D{q8HJo${5|F8a!9w|wQaN4__-$G(c%6W=?Us#Vf_T2)Qcs%fTHU9+?r znytO31+>~)P^+h9)*5PgwH8{8)>g}>C2IM#_F4fgNh_#z)Cy_uYp-aXwOFl-_Nw-g zR#fY&71R1@#kC)<$b(w8`3Q+7#_|ZH^YF&DGw~=4o}b`C4Obq1H_M zT5GN?=NVRNt+mx!8*MGmw1H>(Mr)^S;n{ZbY~S-tdwHe?S{tC9(*|i5c=jvWQ0=;wto^5r(C)An5417bV{M$K>f<$CpP)aPl9_4!(BJwfw6Xfv+GKr&HdSA#&CyqBi}cmnSNa-lmA+0} zqp#Pt>Kn9O`ZwBceUtX1zFGTK-=h7lZ`J&S8jbmD@@vHW#@taoE_+2Yz9M_5)e`qC) z6Iw~*Ppy=Zu9Y@UX=RMRwbzU@T3O?~_PTLFD`#BP-Y_m{amHosP2-wY-ngN?W&EpE zFm7rUjsLWG#4>7eUC9vKVS^je=>&XKN}zGhmE27QDd0?tC6h#ZVcB?7$fv_W2Ao47^VMZjMh&Z zWAwj`vHBTfoPO5$L_cSY*Z(m-)h`+o^vlL1{a<6Ue#e-i-!rD_4~$v*BjXGGu`yeJ zV$9W_8ei&)IZs#3`MS?c(KT~{ZkP*o(_Ez6=3?D%F4Y6(*Lu)gsb?}*>6y*ddKPny zp4D8ZKWlE#pEJMFpEoz_*~~5ai{@56r}?d(%luByV{X^;nmhD-=1#q!nX12H?$Tq; z@AabQZoQb9rWZH&=q1g)dTDc?Ue?^NmopFOZ<+`73g#jG9rKtTZ~m%RGJn$(%-{7Y z=5f8M`G;Q3JfT-N|I}-k>H2%-Nxi0dO0Q-9rPnqy^g8Bgy{`GUUe7$E*Ei4V&CPRq z3-i3*-uy@JU|!I>nHTl$<|Vz4`AF|;KGpk~iZQ@cjX|c*7-Bwed~9YjhML)pVdhIl zvYE>mZss;dm@gY6&Ai4aGsYNg<}=2a`HgXA0pk<1pfTPoWPEDAVoWd#8xze~W0G0K zm~6gkOficZQ_W(=G_$xd-7IO$FiRPqnWc@-%`(PJ^EG3ZS=RW%eBGFBmNVvz!4Hme)U%^JoE z^F3pwS<_f$)-qO`wT(4q9b>Io*H~xPGd7s@jg4jlW0Tp?*kU#^zB3ye+sr1$cC)Fm z!)#{kG@Bc#W(#AN+0yvlY-Q{=TN`O+8)J{z*4S$%8vD$4#(uNCalq_g95j=RAIy%% zA@hCXN3)ahliAt$+3aE*HoF=}%x=anW_RPL`GIlF>|y+BerWt=_B4Jsdl|>gkBmRe z-o^>DkMXD3*GM<}87IyD#wl}v@s~N!$S?;Pr_I5}-{uhGjQO!~)*Nb_Glv=fn90Tk zbGUKQ9AR8CM;e#S(Z&^Xs&UnvW?VC88Q0A(j2q@`R_j~iS!;*!yp?LaXzej_S_h4ntV71j z){jPv^^=j$I%4FvjvB96zZ-?E<3Lyk%W8 zDp=QzcdVO6y!D^)u65g}Y~3-cSr3dF)Ig1OGBYOc3xnj5S- z<|eD2xy5Q^erGi|cUUdV@2%G69;=PH-)d_fw%VCTtoG(nE6F@*bu|C7I+^FKF6Kq6 zw|U9xV_vrUnpdoT=2fe|dCeMNUbhCCH>^SCzt+d*O)J^_&l+ytvPPPBtkLE@YmE88 z8fQMV#+#3=Pt7ORWb>&t#Z>HRrfSbHefDRjX3sQDd$#Gf=a@lzuKA2T&&*=aH?!Ic z%xCSz=JWPiGn>86%x-TobJ&~Boc2~TxBZ>@vc1jBYwtD-+I!6+_5t%%`-EB4PB)9$ z8D??&AG3sg!7ORtFiYA0ny=e8&2sjCW}JP?eAB*dzGdGrE82I>ckFxSyY_vvvi-n( z&wgmuvLBgs?8jzZTe0ffs@2f;S&ePYYGUhFQ`@kb*{0RPwyajRZMC(7R%bht)zyB+ z>TYMYdfU%geeG;kKl??izn$G0WaqFxwsTs^_Dj}qJGV8`e%Tsh=ds4yd94X{j5XOV zV0~s6v}W3ctXcLe)?B-=HP3$4O0kPt3+)otV!NcZ#4cqmvrAjc?boaoc3EqU{f4#9 zj0iqIJN2*E(b;Sijj-tmAf7>kqq{b;7P`{b|>-PTF;>Gj=`eAG^MF z$!=g>wHsO2?Z(y(yNUIm-ORdex3ccqt*r-kTkE0S&U$3Gx1QJ?tfzL8rT9Bqs{eh< z@^`ZQ{?69({tv8d{$AFL{ytWAKWC@Ezm>~BzmGO_V%KArJasDw@dH*=;E&nH0f`7bK#s8^Q-9N#q=bvab z@=vmw`X^hh{Zp(Y|1|4;|8%RXe}>h=|C!a(Khx^%pJny+&$jye=U4;%bFD%CFRdZ| zdDd|Md~2jX#TxBjV2$%Hv?lt$vZnf%SkwJWtHon>^&hf!`Hxt;{l8dy{J&cJ{l8m3_>WsZ`A=Af{pr?G|4Hjt|0(Nt|6kT| ze}?sk|Fo6vKV$vnKWk<9&sl%_&s%5w|5)ez7px2ZE7oQIRqKlXx^>-u!@A-B*Sh8Z z&${csW!?ARu^#>2K7;c8*-yPez0zNx;K(q4%bUP+s z*aZTn{Yt>HV*`G>NFZnz4P>&52cEIZ1v1-j1hUu_16l2N0?*p9YE zKz94RKn}ZRAg5g?@RD6Gkjriu$Za zX!i&dvU>+!vHJ%K+k*qK_OL(^dwAegdt{)fJvva#9vdibe-bERPYjf_CkIN|Qv;># z8G$nP%)o2*7lE?&{J`sWN}!y*Fz|-`bs)}O9(dDU87ObB4OFl<1m3p43B=o50+sCV z0+sC@fdo4>P{mFQRJHd9s@XpT-m`xW)U=NTYS~8vwe4R6_3Yz;`u2%H1N&s4q5W5& zk$o=E)cz;X%)S(8ZeI(uvTp=h+cyJk?Z<)kwi4`MtHC5&4|cYVU>Dm8cC|AFd)S$S zz3gX$AK5Pi``RxC``Oup{q3B=0rpG5p?2=za63xeR3Er}+25;Nd zf_Lod!Mk>i;63}j;C;Jh@PS<`_|UE$d}P-NKDO%ypV;++Pwo0a#or*P`Wps){zgH~ z-#DoIn*^{Er7AT=9-QD_no*Jr+6Evl#J1()jFElP; zwomEjjw^LUSBAKi+CU2}B|67*f2c~b`)KP(t(@Ug#<=4u^667jMV9cnTkg0={ld@D zFD_h#Vl`!|`)G^#sxs59$&uRJN8{g;I%}U=-KQ*a>(u;k%}dplW%6ib{i187E(@YRW$M(Os#U za>%X!X%9vBb98S+pW}b3s%v%rJ*~4}%Doldw*TqbqU%z0zoz`=&Qbcbru^yFZ;^^! zqbV8gxIG>8(2>5zag#mXJn|fCTlkc7?sW4``a=84*)};3?(*n$_t8h`dT6?Jk$I!* zGNZR1nq%?m@bmK4KXjyrrl`s-cPST6gx4T-Sa>VXHPn>{?xU?IhR@v+kt*`1FSK8- z6%Mao|JZQlS*C}!@?$=iP0sAQkt-njY+0J>ik`#SyZIW0ubztKG{ttueU+w%>VJQ< zWS;PIT-U?r)ypfxc+hFj5j|EEWtE-yOo|GriwFNg1SU*-(&p{ixW6}=Pw8W-LhlR4Yv z&RV~|FLXcsYE1Z;UoD|4)!em;-a#_DhL^Xtf~M4VAN}@2jlXl~oOSjhRcY>46HQmz zx-~jdjrg3wj_$Zse7c>S#Xi=cn>+4oa`=Ar@K2q;U+65)xf*5tqSxT&C#urleKfkx z+g=Udi=*4NxVNT!>`wPbgYdPmsC4*wzvc>+H8?PcPZ)Hkt8qkAK5>h0!6=j6nztvs zH|mtsmCxL9uXE3y>(;*d;W}6^T(cW$%0lmQo?IZ5@?zj;h!q40IYWNI`u0feA z;b$xHAbfWIepy#my7PX;D~?aGv9Tf7H2FW$piDc{klTl#<~sAO3|ebSJ#e6))R%Th*0E?sPRf zh98Y?TdC6FJN1bLe434W2X?LnIfBmK;M@AFRS!O{#d523>+m@~K1aA(N3JMmU1YlG zx|mBffy`b`e{YD2J*h-d3U;YpN8+9>n4Y{_j-x&xpDSfc#acw!fU$X zqEE@`&U=1xX(IYeEo~FdP;~v%xFZmKziX69x zk&524kL3%mv%yE`$Qk=MS9o0Xs@U2*{3^7V*8o}GAKSy*7Kpq;1YXpXcilBu$j9r* zbu??Q7OKaq!gG8Rxn>qeu7#(i!e`Ek2bxmdT}q)B!dFF?$o290>*4b+;1AD{Hziy< zhK8T__hI4sD>AP7{qWlN8xnqWLf`QA-gzc`M%4T`Jg&^|;c+dv)5spG*eqNH_`o6= zxAv9re$Kx*e2&ky!bdevWL>u34WAp&aE#=tI2D;AH8Mx^?sJVRMfP~nTjBk;lkJc* zv2J8f{u0@LSNNzSxvRVr*=zM9dyO9_h31W3p;;pLzuu8|$SszJpJT+9@Er9cN40F^ zsIK@oyjIcU`~UZT@Kg;i`N+iZe%TW_`lWmGxmNBSq~Xuu8kHWd$2Y_Gm+13Gk6ZL8 zMECvww8x`+GP(~wz8YQ^=bj^X#}n1V$9&I(@b+Gd9Ifc<){BR9K2^rK!(EQolxA)X z&+#*--#L$S9{qmb@8kUQt$CaF&mMiz-Wu6ArQh&2P2`|xwl*D&C?~e^GPrKO$`eC2FF# zoQfQrTBP2m@m9_vf6_e#rFH-7k+&R(dotd(D@sG;)LXPG$#CTwdv~tSm1q@L>dJNZ zzZ1=vq+XYKi$V^oXP2B){7(t_xNSJA*F7Ug3wEh&=zT z3VE)Gq!yr@&x@C~ z(*7z}^r8$zKP2|!c$>QCqFi48LY-FQ_SJ>U>Ag{`raYHv%ATLprhTAIdw-!p*GXPO zl#5tS+9Rbmlk%(_EpcSNr2LfQCe{^Y`beVM|B2cum2SMeH4%xUDwVF1-s48KUWp^! ztX*U8@Tha=bXRMR^u-Tl-3&jqp0;RFA1x9sz&B7`PUf>fbmu=7Df=N^?I=<0?S?D&hc3S5tLr7N5t8Sl>!WR>^s4AVK8EX- zr^R!2QKFV0b$_L4O3Ko5N+j|podf#G>x$zc`tfxEYATOt(H*_ivfmM`&ONI25ua&g z-&LDt*Uk{7+8QE%R&8#mwniUqxdENjZTx+SU$2#c5{W$$`yqN@sqAl(*LcO`d!=-x z`!q^vh&4pd)$X!dYBRZ}QXnQ{Lsz}-orC*D^PrW0Omd!i-a&H@aiyA!;twy`Gxi8S(l;xxn zQbzisEv(hfW!~DtQLV21r0tQn<-Dc$_eb?kZI6^l997ZliPrOqHjWst9i#iTNVI+F z=-(r!EcG;3N<-_%rK=NV+BuG(5{bJDq7MCiPA6$ORU&D`sML~GtJQk7Op8RnCh{!m zn)5C73?}ND*mJQTEsq&=56asWR|L)5)Y>VYIsEH;DfRb{$SF(tQ_4W3C(V8Qs=1-uu$Y?7mEKU4SUvw;TPa(UXh z?LN9@Ig*|Lu9l;2De5?OUAh{TWUJA+?rL;XmKsgfa`maU?v_xs?2l{H#5l*^$49qL z-r6yvdIzXWFYe^a({9q9n3S-bGGd%^hG)3)++P}fujF3eUA;5WndmcluAXPJ%5&3C z9r`QR-IFG$*CaO`Q1|l0aCNLA-K|jeTzZ1_rJTy+cQXEyqocp6Pn*RPEa@6i*@n2z zo9Hp-z(4UQ!e}H3{2-&Q&#l})-mk~ zepN4ZkHquoIodRFWhk~$bHCxr6DM)ETck3#t9mR&dZLCM;E8*noR`Nl)Vzq4MV>`Y zrT?s_ynpcPDD?;?->%jOZ|?@&s_o?{t^960ueSJk>(pr2-D>HsSgYPW@5eM{gk1cF zy4>TD>a)Ph#p-r{uo-k=@_t-%qq^Kz-0kjo)iiI*c}ve*D&Ce8y~;xE?*3caGmN(L zzPdO$2W?vZN(S>kEaYj@_(h3^H>vlfj0e@oYEk!18apa?bwzq&YiBO1Ek#Td%ZYQq z0Bt1Lpp66zr>bY-@3nR$QW3}K#u;iIdUdjT%)PB4a=NixUE^O*<;EIy&;Qxni=(jS zZG*0>T)Mx|;+3<@LvF*Ba(-Ehp3Y#&{fJS|~`ngegkNmnOIT^rAtU5WfHzPf?`FlA|k_m9`m z=c%b&KU9tO=d015Z&9NdYNwLQ_0&!!+Avh@4@A8ZHCxnEQ71(`{r%eMEtNlF8~>S{ z{x=nVU##`4|LJ=0e`4v1dM|SCtp)s#Uin|{W4)EQIQNL2dLCz8rO)~OGmf_&^N-lZ z|1D)D>b)qn|McY7KmLeq{I{&U*VF&6`jS85nEw&m_|IHNiMvALUeNMU>J_t|`;*H1 z45{4RSMKl)Y*r%=kum}m&jb*c6Vv_S${u0@m|K-&g%Qc(!Izb@>R5WMkP`a z%ccCRZcWS;_lLwC^?T>2_apXc&)G#PA`+=c@8S%Xb0FqU->hy;OcQH}{SecfJ4AyVJECD3M4-Jfo`KrQY|c;VoO`9+{|N zBBvsMBF|D@>i>znh;4{FhEgiZ-C8LxO75k%36yBnPTwohnfrXNyfg8!7Tv5xBW=FA z*>VoVa-tqcWpMRe>A98iyDaJ1wz4&8-;}&aYs}M1O^HMqh&>l2>Fp?p8X?--gqzhX zb!i)muFA8wX=1yg?4|qJS4xPoZ<15-mILXH59L_S;#sN^RpHZuNQ3i&`X}Es8ZB<{#TCEmX`Esfgu%`bbUh_tzM5LG3%8 z+FJrIeZvzhxeRiSsL}kde09z8G(JvqB@)v_9e<4ePbn91R3CDv+eoTa>tBIZJMY@e z^DQ}L>1Ziumzp!`8e*D=L^~4GL^NN!<2pn8t(Nqxa)Z2$B?r`J6OZzoPC4cx_ac9r zw7FtAZ#CzQ{c6qmPJ09PU9FWputPmsqD&nxs`su%=`Pf!c`IE}lA=wEwy^55T8HkO zs6KW3cOr3a6lZ&J4i$67IbKZrXCvt;p7O*_tRbH4iLL#9#3!Pz{5n{A+M>Ky)%67* z-Ddeni~j26J!)(8HrLxUv4%)RL}CqZ%Za((*7*IoVhxe<@2?@26YGjKL?q_^J8S%Y zdcQwcjCVy|L?qVrmOruF9~(WqjNfR>wdsTw{p)Yt-^JKR`PG6LHQ%#NeaCC}n`-p$ zq%6k8zqVIL$j=6=(Z7>&=~{lbD3`%kuc^_bHEQ(lq%2B9L}EYw^=V>_KcbeL)kdX% z#Mb^#+{XW(O6d3VBK1DIt~zUoX)BY}D|K-c#N6M%;uCB9H{$EolhhH)9~;GKZ|93^ z&VMHd;*NueMDHX0dRe&^x>38UA?Awc`D_$KpE#+7C%2mg5B4vEP zm9vX7?V+V=baa>D%81Ncdfui5G^l0Js#8-DV?u9h<;dIpA*miHUsmYP^`H`oJ(A`s z-?mwFk9vPiytVeipVZ^A>Um$?Z&&4BEVtr2^}R!}#ajQ#Eqy40(^1hYvdqm}$coN^YxJFL-R_z$QrX8b?wWA=t4H+pfC*FK?E>!D~*oOFC zhA6dZTFR%il#gpE|NhbU_SFrMgZy!PH$cwS!ln8vwd4qUq+Dr6H~8o*^4w?m)oPj? zS+zSD-sXP5U1Ft`eWaDH_(G2Oy3XIfQ<){FEY+|aISS@?>IHIiO1t|e%`KLvNiAM! z)1sb=-p5Ie2{fSiihOIxhE zuBdBbnpEP-8e*=P)_b&?1L?YZkesr(_aN>}h&+pFFKKrx7HW4Pe&M%$N}fN}rj;L3 zr$scVWiQf``X;3e#2!g^yOq)yp#2{EG;eh($1?sVb&b*7g%~R5x%7Tt-3U3VxPkA^ z$dTAfvFD;BMG1K;(gw&zq(@hJkn6rvBXK-NXwN!C%3^DxguaXL)h&?o7jsNaMWlCvd)&%48oiX7 zw^|}f(s*2bgZ%?vU)|EH%0SxgDtVgNOHpc~Bt_|p+>6q9?8zF zTgu+1No#x|uhC44SN7v`?S0^Xc3L^VwLBoFBK6PT$dTBOyie7X#hy>@u9ma6ZHVRO z{PlmAns_!V<^DT)Yb)sGm0B$Nd+|R1(64=TKgi2?<#j)vz%N0lJ^VkN(dx9{v`DPm zQS=NDboSEe9x`;Z+(PKp6awJlbw$VwR)^4SG z=lxWsniuKFD8FBjzKEgho5LIw-}W4I#7p{^rT)uIUdrt9J|ZQN!#cp zN78ncHKdWCvfTINzK=XNj9(I#%8~evrug#QN>Xvlb01}0rN*varj8FjTB@dZgeNuq z<>f^F#Qut2N{mXSU((zluOWS9Q;DQ+b>0^x)feeook|owN8RoWZDjt@{RZ7&d0p}A zsZuSc7dgi(J&p9_Q7LCJei36r@r+N54n;37dMT00*+%u3Nc(!IxjL;yVlRK;JE^zI z`Fn*NDCPGm*P*w|)1Lp?_sX;jbJUzl<)SQiOFOkpZ_ihcZ%^&2&3A;_lEpVi*K5;e z^PNhiT>hG+{;JII1HYS)w|nxm8uc0Mqq|F3gqAeVP3hXLk8` z?9=8-twJf&4_B$JLi+uivW@$=VpCE{d`Ye2(w9q>nk{|XRf!U{G8n{2TKS%!7_*A8 zt!OjT=p^7hlV z93R~*c|W2a;9J0Qw3=yiqASbM6Dp&YPX_v4nKtG!UkaAjl}3k3 z9sllIH81a6GUyh|b8ph#bs0>gr1B_lVkx~!2d>YT$;(}49H#V%;+gQDIrAwk;M#lC z7O?phwWXw5)w9d~C)D)T=c;RnJ3yk9ebPf+L-bA3mqwKuD}8BHiKH)$DpATq>K!k! z#!rl(lw5uNhSq)=jjX(?H2OEIV=1W~DCv#i+}I*7Cw;F}Nk#hhr!rUiUg=tSuBd;~ z?=O`*9nx1ymAh}=(v!X~s-!3C+TVYXROu}a>^EH5hS;w74yo9#7*QDbl8%zHIAe>s z-p;UX+=)@vSiR10W#r+l7K{Dx*7l{oNjYPSyG-IvmWeZ%vW;l%D2VfsI15Sdx+wKZ zq_UQ)C#A+p=_##8Olx55sEn4Rv(-a#pWRbC+e=T*Hps_Fx@K2W7SZD@y;+|77h2i2 ztCo}A0#VlJx>K!**+fcS>IU&;3VDqx+SV>P`lg$@4N+F2T%J9op24 zs!>Pnj)=FdJ$YVj3(NQwtkNppZc>l*Tt;cik(tRgtWq~bxxACAPLu9GDFL5Q4+bnaGCmPWox zBz{9Kaw>lJ-u*qb?8PtOZ~IwoA75}quiPK^Hcj+;yKhs^Z)ul^07py7RnUiqE2Hq)cd1vOBBzTO^(pK69#Y2) zqGo4&rGHbbD2N{2Gr~ugEzcF> zArGT}<;p?iMNDhcM&YfDeUuto{jxf?c+XEgvrA)drO%PBij=cK@ig`R-xzYQpM^q$7NxY$Em!q#K1CJc>Z|Zbaa`c|odV2AGqEdIoR}@CP;-ecVFXuR+u6y!( zH4^(F_Dw`LG^nliGi^WGjaScVZQ9%)w7Ch|-0QF9?Lc|Et5>O|kwlL)T%LBGHdrf1 z7Y%Ct+exJS{{^WIDK)3+yc$U(Mx}HIYu7WIwC(z7r7MoA=ugE}rFgz7mJ?5Xe}4_} zZmqX>aJ`KarM^k2_o4@u#%5#XJs&ikH`wLK#TO2gNIZoavC^QMBG28(bKU83Bx<`z z&+?gi-WNS#0i{+iuOY@m(ivOnUs9>D%GFq<7K!nibl#sWuiHY+QSSS^en1^%NOxV7 zvqtXg>NGJf7SEF>X}NmnTlL!Sls2tK8!dgkM!g=Cu1J+DFX@U@iKH@B+O*il;KAxS z>IMF{gfgOdLrcZm8eQJ!ZDx6o#2bjBKfS~?iE@nIW<<6~UQU#iNUxTft?c=?H>hiP z8?#DxT$|)n#7JM9Kfbw}Hy`D>A8VtB1w5Zo_Ct&m`f;72jM1l5tK$f1#HjRZ3$EnHo8Hp}YJVznC2}gxLSnm5Xnky%_I%&lT+!Q#QMi~UqGD}~^b^n2 zl|A=1S8T(d?J9d&=B0$ZmCNt1vA>@|_qtqan~2_$BT+75bR$}$^juuYsq_^ir7vF1 zdEs4oU6E5Ct~!(__|g*?<>=QQR`>FHdd&CbHA<;X%G^8dRp)-lodji?{Xuo^Chlk| z+ZCxuW6_V~REE5u*L`|b4kC8)6hNMKiu0SYuEvM5UBHETHq1B8O{_!=5$cwj}O7C0zB=6h5e#Iiyapn1? zxTny_)x=BkIQCO%L@Qqr6max^mA4_TlkcHL89Ga2{(rQb^2|&6)q+wZ-qPy$kF7jQ zkk>W$=6RkRb@)V`_CgEK7vyQuxh!0ceq_|wNsgqiZ7FLUWI1K7cq8h6oOQzuSH>IS zoGk94)PKNTQ#rjM^ytb_?Vw$6i8aLgCAVs4zwD3I_S=Oqjk50X*VVrLR&u4Bb#Eh5 z?zcb96E@{Mc$BLwrS2|dOs(8kmd^Xied@sL_-3Y@D{(JUY)v|UDE&sV_SVbT0qWK9 zN!|;x%IijG&pHh2)Yc>VoS!)_DA$N0J<&^*+@Ej=Suu9GJ|tA1(k$YS;VIx+VtrrG7(i@A2za9=e| zT+fLA_~SaKww^#*)RlHrs!qGMULEI1HA4Bey7ab}lB?%w0m|Df;&|3*69 zzf%VJ$JC>6JHHxK+MD!!RweS+egl1n_O`IMX^kOj9TGi_d=(=fb7`9L1>nLhYJdM_ zd-X_*agKP`^q-w3-j$Tzd{o|couNgaP!lV!YBSwwTh&)h6Yr7TP9J-9Pbg}!nUQ%7 z>l$?aI^HYL1?c=RkiQ092!Htooh}Fqb-~z&X$D=1E);D#6PD^C(5{O@hb|hOx)?0e z#iB>o8OwDE*iV;;l{yRd*Ck_(E*0&V}ET3xn>Yt^!Z7ef}$=t_pw94Zt(H zYCNkOi05^KFjQZIiTWX!ryq(%`e8b~t}hyN#romIB}fVAM-Z1HC7>Tg>_AFDUrStu z23@s&H1Q2+&<)g&A-)j}xG$-LiEGiIyF)*Pcr@}%O+S@*3>tKI>8BBoMM_vdop>DbeO`SX@p$AJwSETi1mwK{ zeLe9+H0UPjXA)0FgYJI)EaE9hf2N;JJQWSPnfeCe$C3Md`nkkUpg}iVKaY408gvc% z`NU5mM^nFm_)o~u)Gs1_3JtpX`bOfXk^2bx#l#DdW36u@eg+M?M*R}vXOZ!!ei`v| zXwW^cUygRe3Vh43lIa`JpnJ#AjGGLr@KZwz(|4gk_nBce?l!DJz0X=S_^d-8pY`bP zvjGEqHnO}C4Z03Kn=r&@GhXAf1x-F%G2CYx%S0eG#AiF^`|Q91pPksprxlBQ+E}J9 z(h7WbVY$z4tn%4|Lwxq)Lq7X(y3c-m%;zA^^f`o&`y9r(K1Z<0=NN|g9>;Ls6WGc3 zB*yxlCjB_%yzhGkQ+>~3n(sMG_dSn2d@o>6-;3DG_Y&s&US_QVq(%Gc^^~5k4|;t4 z@H*cByxBJpYkh-oy>BqS6eMYep%STFB?Pra_}0zTnzQg!!W-B zH2D=`xL*-Q_!VPEKO0*8?0A!(6KnijIM~mFL;Nc6X1_`t>Q{xg_zl2Y{i^XczkxW? zZxD|1tHIm-hG4DVQ1W&M@^*vYFr4l;92fYFz*qf7;rD*Ec+zh)p7I-mzxj>DYy8Jy znE!aR_)owj|B0CFKN++Ar(h5NshICS4Ga9IV{iXDEcBm&CI0nj^Ph>O{l{oBt9V=f4c^@n4SP{a4_<{wr~U ze>2|azX~V%w_t<+YMkr82A}a?i%b01;Zpzg___ZE+~dCyfAHUgXZ$zgkN#Wmtp8U0 zoBuXE=f9mY{0X^E4A_CjfSni=(2DH?+L#-R)Z2hvXbaelr2%`;948aOxDE2d&u+kWT z{f$vrWsJt_j4?QX*XKB1Tn=%(j0t#yF%btEEqJ3b83!3t@g`#$))>=qurU*d7_;zZ zV>S*o=HM;HTpVW1!&{97INVr>w;79Ygs~V$8f`erXvf=)POLS$@D8H~M;j~fPGcpG zF;?MS#sN6iSdDiZ2jV#6AiT#|gX4`u@LuCkoM0S=_Zf%dMB@mYWE_Q)jkS2caWqab zj==|vV{xi+96o3qkJF43@DbxgoMD`dj~b_7y>Ti&W}Jp|jMK5fScmhBGqBNEkIx!s z;$q`0e9kxR%n#gv1%Vr}ci<*04BU); z0=HmM;8ufP*B2QZ1a3oD;C8GE+=0UacjAP=R-7ByhD!r?;kv-xII7(q9Nlg&PH49e z?`yXor?fkWGuj=(XWAXc)$NYp`gX_gt#-$8L%S3Bal4bayWMH>@Hx_Q+npi)0u8z^ z+nvQj?atwacIWYzb{FtsyNmd1yGs}vbQ!~f^gi^xK|a_e$PfDm1z=TBAPxu$!s~;A z@rIxfyd@|UhXtAN)}RQyKPUEax>WuS)67cDuL|hPL!G%G|xFRSO zUkXaYl|kvaDku~G9F*mw*R`NQ_g(vJJkdS}Pqoj*bM5o+r}hPSv3()_+P(;N!NsT# zwxJ=|jy}OoY#;2x;9w7i1y|s;{3Aoov%yuE7CZn8gR8MBcpzREJO~E_*WmTRL$Eq{ zDBch}3~vh_jw6Cc;K<-nSQ}i6_XLl|3BhCV{@}4VHFz987Cav31y8`|f+ynh!IN=q z@D%(Ycq+C9Ps1a@)A3?(9Y%MUf!B7ZM{|dn*uBFn%;_*2`*vu+;tq4s*hsF3thbCO#VF_;Runa%#upD=HSb;}6ti+=on(_M%tMJDTEqJ!W zYU~uU1~Wp|VphmH%nezO_K*$e2-%3vkWE+?vKd_=ThJY{6+I!_usmctR)p-pWg$Cp zWk@T&7t)5GhU~&!A-nOjkUjW$$X?tNvJdx$?8h%d4&srJLwF+OFn%9$1b+xQhG#;K z=EjN z5utt<85)35p@G;jGzgu<92$-Bp)r^h8jCrhow0Xl z0`>__#G+6O_6<$O;?PulH#7~mhNk1T&`b;r%fjHWYzz;}!N{;&j0(%cj$s8D9ae~) z!iummT5IUVOoQ;O>42iv=coVF%hYFZA2QzMxjm63;_}?x_eD@qA?8BRs?lkn>tZ1@R)JG$JaA82L3q_#v1AZ|iR zAfg)AMGV9Z5rgo(h#LG$#1PyXF%;jA7>3&-hU3AA5$G2=iWK~jtJ=s~Vk6RHM2;qI zht!kEF~seWJ|uE1aR=neD{>t1HAwFlIi5HS>HQ)n5Qig2A#x&dB+}bNPA2Y%wA{!k z#GR0%6FHUmTIA?NP9u&(dcVl&#PP`cOp$fOU65lJIfK}Y9J|PR;w0p{DRL%p3UZwr zIg7X}a=we4jk%Ex*e7x>7Dvv*lF0dJi(G)Ek&DnC*@%wF#W*;!i4=w)XV}OkxGr)T zz8$$7H%6|&cOqBfrpRV|H*ytjj%>mAB3I*<$TfH*axH!rxem`nuE(>H8}M(D8}VG^ zCj2RKGoFv!f25(5T~hOVkM*7IhMDjXI6Pqt4*usIz!~)H$3J zbsig{F5r_<7jbUXCHzy=WtKfw(Rz2;Ynf#`mH^a7$Du zeiUW$*Xwp7iDx5w)G?E|0okLDS;TXZKBr?g@$<;_Y{wkprN|z2 z%*Ayb^YG1%1-QOrA#>kCT3p8>e7j>YZtQ5ocRJdcy9ucq9i7CRk-hHd!q$!+G(=aR zPjn^vMpvO<^Z*QquEwC~f!Hm25N1W!V1D!vv_%iavC+dw^Xcf}xFC82E{q<<^uSKF z7}RMr)^{3%uXP%WYdVd?*E@~JwVfv58=WTNo1G@(`c6}DXQ!$7X{TwpyVG?1u2UVJ z=rjXQcB;qIoo3?SI?X~|%xv_JX~3|UxfmHU52It|V@%8fjEz}@sWFY%HD)oU#WZ1N z%o6MtvkbFhmSauK3LFx%67Py>#<4N0a6(KA-WRhPm&UBYiLq;OQtUdM9J?OxkKKS% zVmIQn*iBd$yBQye-GVb>x8kF*+ps=%JI;*VfwN+F;uEp0_+)Gwu8Q4-Yhrifd$D_P zOYC0!OYA<}8oM9gk3ERnVh`a5v4`=FDn_81KgjM*p}oXpB3H zfpO=sUEFyLj=O*z;x1yxxJwuvcNsgy>3JhNJNpdQh>O6HaZxxbE*fu-i^1BsSR51A8Sjfrz)5k5I62OO_s1pUl(+mU`X-bU<1 z`l5I{@erghig)7Bco$P{LHeS25Am%?UldgV~@s-3Qk^PFVBEB6t*6{;~A3=_C zd^Og`55&jf2Qhsn(htPf5YIxcBIAeP)A2)bVf-*$6h9oFi64QB<456h@wNDT{AgSf zKL(e^kHzKj0=^VK5m&}f#+T!#U~~LbY>A(Suf|Wu)$w(>D}Dxk7GIBh z;%DL)@w4#9_}O?iz5)LhKNkZM=AkiRJ_aT%z|@3A*fpUM(-RhBMnV&oB`iT#!ZLIx zEXO+%R^aG_m3UV|GmcGIg)3FuTic?9pWp_Up12E4%E& z{$2LtJzWms_%4U=zAlGxVwWTMbeChepv!Sw*yRLn=yDR@?s6J8bvc9Yb~%e*b~%Up zx}3+qcDaE1#EWQ1yo7#oo@&zle8&*nq;oB1$aHXp&xmSY%iIgW{z6KJ-a#O{{U*wbYa zt0fADTcYtcOAL;%#Nud6XS~yrfKONwakj;RPg;_3t|b+lENQsRl8!G}GI6;j3tzNk z;|fa-zG}(E*DZOt)>43PSPF5Sr3l}&6yth}4d1fZ@m-4(H(Ok|-QvLyEfx5&r4m~$ zRrrZz0Jd4G@l(q{++`Vrdo4BirDX{IVi}4TEyM6P%W%AG8G*q`qcAk77Q>Q8qbX?& zh9`~1h@^2Cmoy$bCr!Y_q={%wnv9mDDVUTr6_b;uVM@|;%t)%k%%mCEKdBz8l4j!d zNwcszX*Lc|YQRxRbMf}1d03k?AMZ$7fTNQZ;k`+XI3Z~U%p+JlFa_TsUmefVwC zemtIZ5Wh=0geQ^?@>%prK8L=^=g}|u z0tO{t#P-RTFe~{oc2Cx~<0>iH2YV*_VQF#zmL&(GD>(?=$-(GJ4#D!|P^?Zi;SI?V zcvEr|)+9&c;N%z_k{pXSCwIo7$q6_jIT1%DTkx*rWE`8EisO^h@ZRKfoRFM}_a$fH z#N=$8l$?VPCgB_;Jc)Y)zShZ7Eanvy^GLCuKVBO{v3uDKl_? zNQkP&- z>M~4DU5+WKE3jwkN-Rok#_g%AaCd48ewDf!zfN6)$5YqhiPUv?Hg!FoOWlCKq;AAt zQ#YZ$>t<}%bqkuhZpCZ6ZbM7g?U>PZ2j+I&iA7yovAAm+I=b${{#|$Dz^;3+w(DM; z({&#{({(?-*!3W;>3Rr1>UtR4r5(Zev}2f=b{zB5PGC{mNi0n}jrO!NcuU$@ye;h< zjz~L?cc)#zacLLvp0rCiKJ7Bzo2CzSX+hYK7L0S#LU2J^ zC@xGh;i9w%d?qal8`GlkxwIH;N{hwk(>mjlv;=%HEfHTyv*4PvWPCj>72i%v!;NX_ z_)c0TZc59-chj=*gR~sno|cOrrsZL4S^<8NR*1XOitw|vV%(i(!~JP?JeuajlW8uD zvwASWT7k*dN=&g~8++d2^Qtb;J$T7w1FAy{M`ihZrau+%yn?bZ?KvW`Nx zwH7_r(O7OBgV$NdVzqS~-e?_IDu>s)-uIuEB?=i|fH1z2ZYgpXJoagKE{&bKzqdOT zx(U}=H{%BD7JSFL6}MQo;a2N*+-BW@A6R$dht^iyVQs@U>n{A%x*PXe_u!Y-y|~Z1 z5C3Z2kNd3$@qqOZp0FOq@2yAhr1cn{vL46N))RQfdJ=!Mp2ojf&)_-hSv+q&hd*1- zV_f*FlhZF@PWolcPuI8SSf~4-E!_`Y=>b@t9*F(ZgYf$FV609L!J+A)cw4#& z?@W)t@##@mmmZBX(qr(k^jLf%y)({9Pr&){czLK7Xe@;)wSJN|b zb$S-QnVyYrrRU(z^jvIB&%;mB3-F8dLOh>dgg>VjV^D?-+h^FZLxvM0GF%v$;lb`1 z6_}GziB%a@I3QyHUY}8ogE9uu16j1gFuF$!mA z)Z(0s(YPXG46e)=i!W!4!U#pcX)_)6w_ zT$Q;2|D3rIS7&a**D^P=?iyqtGq>P7nOkv7<~ICG=62kgxdY$N+=<&VTk*rpHr$c9 zi*-Lj`mxO2xF>TD{xx$i?$6wZ2Qv5L!OVmBRpuc)oOu|($vnckN01hgc?{2F9>;#& zPT=+3PGWVp(|A+2Gg#B@EZ)-X91iPt9!GY&fG4_L#9z8yLf@>*cx{$Gm@`+F4_0LP z;jpX#tj!9I+k|N#ER}&SlvAvhjh=uk==7~Z1+5z+`RxF>|ThE zbT7hZyBFh&-EG*?-Hz{ecjET$F8r{&2lsZbz;C-(q9MBqJ7*8T$hvKB{VK^mwI6ja)0w2mAg^y*|;>_&PxG;MRF3KK@OS8uX>vhYJJ3`sx zab@-dd^dX{zLz~2cV%*DnY^YDcp^YOJF3vfe^MYz33BmU51F<$7=gvOjD7?raOlX8}0dd>>$m9r8Z zInCHVXBFO*(}Lr3R^!7tYp^kAExwww4mahj$4_%M;NhH&cq(TT{w-%S{+hD|1AA`8 zu%6p6w&!-t=(z(kd+x-(JzKGV&o->-xeM>^xf>tqxd)%>xfeg_xeq_n3j79 zt+|&mBUjJwWioSpa6qmfUY{F)cjX4+J-I zU}CRawDiiuZoLXHt5+d*?^T3(y^67KFIxw_t{7=Mz3g~%FDDM|<-%Kfd2nQ}3cROR zC64b^g%9=`fHQhkVtubcxUyFbzT9gFuI@Dyzw0%O^iLq?=w8E#Pa?H9Zv-0i zMq!7%TBfJvjYezU7);L_i#_tjVNTw7?3p)#C7pQ_iEHvE({IU}hPUTU z$J)F)9FsSLxpyIB*Svb1m^TwA<;}v$d9!g^UIR|gn~Mwc=Ha5e`S?uU0&L7%gwN(R z;^Mr;_*`BSKA*P)m*g$OrFqM7S>6hKA#WwVk=Kl^d8_b~ycTTBTaACsTZ8-a*5ZM@ zb@+AOdOV!B0gvQu#G`qeP?x_M_4!-SkiQk%=WoN{{O#Bwe+P!-@5Gw?Rvez+hNJR# z;T`$AadiG3yfc3+GLcjxcN@%aaFV*VkVlz$i}=O4kk{A2iN{&B3&KY_FIPvV^X z)A&^W8C;Nm7N5yKhtKAp$Hn;<@VWepxHSI~F3Z1+EAsUr)PZ~-{Byn^w&VxktNDTW zR(=p}$`8hO^FwfReki_|Z^CW)5%@uV6mHLt#*g!3ur)sxKgsWmpXMjvm-&gfFW-WH z%}>Td`KkDIei|OmPseZaGx11%79P#d#$)+8_-%eJp3cw1AMy+E$NWM(n_q-K=NIFJ zd>dZOx8tw*P7Ek;p|QY&rh*C#FQ~+bf-3A-FaVa6^y_~3P$0Kf?9mEU^LDt7=sN3WAUkiaX7zVJU(480b2?t;;RLd@wI{} zxU*m?eq1mOTMMS+Ck1u5yI=->UQmyF3TEOr1+(x-!E8KQ(11S{%*C?>^YDDZeEg+g z0bVRvgufOv;-!Mc7}UE7yYyay`MsB+qxW(g*n0)u*n1@o>fMYt^ScT!l{M3@!`Tf_-Nr?oK?6F8w&U1yuyRHpzshbE}$tCeVw?ZuM3y<_27$rD{w{MN_?qr74GRf0MGWV#-`$d zxUzTS_~-}jS(edFtTJU#+8i2E+ykJ zv19@!mrTTzlF67_G6j2IY1pS^Iu@1GVQI+>EHA0YijtYwuVfZhmdwVgk_No4 zWG)UUnTORS^YMm~1$cAGA{<)Mh_{w3#@do5yrX0ZPAFN1_mwQiX(cQ0p^}yOSV=QB zl&r$}B`w%kvKpT)S%XVT*5a~~b-1!*J-%GB0h>!U;_8x3_*%(kTvM_IUoY8;YfHA_ z8ztLuUC9o7vt%c(FKNZkO4@LD$u2xtvKzlD*@H(*_Tur9efV9;e*C`VAf7Bagr`al zKMqHww`8XvaBV4W=% zXWBaBh)+H!EUEf?3>^6*Vt z0dBGt;=8sY++i!mk8C#l#Ae4fn-lljTzJ6d!DF@x{MJ^9KiI19jBNm(w^d_c=|F5( zItbgB)?jey5DY6Fil)+Gm{>X-&7~vIQaTEgN^3E>bTp=vj=|K@vDmeA9Hy0yM{DT> zOfQ{?8Ksjkt8@x>FP)0nrPHui>2%C1t;77%8CXzSkG)H0Vqxhl>{~h;`;|6eW$9cT zTRIQ#E}f6#N*Caw(na`8X(Kk4F2-j|n{aXI5`3<788($J$LC8|;F8jnxU{qxSC+2A zmrGml&C=Dlt#l23RJt~l_jZt;vveK)R=OU2?Hka~z7hTHn=rt>8IATW7--*$q4sSU zX5Wq``wk4Z@5C5;D_(1F!&v(+>|)=IiS|8cw(rHR_I;RU-;X`)2QlA%2n+0ovA6vQ z7Tb?uiTybCx1Yc&`$@dhej3Nv&){A5v-p7h96n(`kBjUV@EQ9>TxP$7&GyUqnq42p zXvgk@8|;4gwmkqh+5_<&dk}822jf@nD8kHt^z zopF~v0Y9@R;%>VIzqTji8G9=JXir0vBOPNLnRu-u3zHq$nBvI6Oh+zubL3%9M*-$K z3bB`?2=g4pSmdx_Uxyv-4ktPsE_6FQ=y6nFxuX)R994LoV*n0tRO5|~fq08!5RP=z z;3&rsyvs2Z?{*Bs`y9h@iem&m;24E79JTnUV>H$~#^7U)u{g&u4jUZf@hQgyeBLn; zmpCTlQpXfr=9r2vIHuup$8>zrQHLuXGw@|cJ-+IgiElb);d;kxe9O^*n;moUJ;yxU z;+T(raV)^Cjz##sqY<|`7UKtwCj8j31X~@;@Ds;!Y;&x@FB~gzucH~ibgaUIju!mN zu^JCK*5KEUwRqUE4!?1%$K#F-_?=@To^Wi!?;V@*q+<)7a%{!Zj%|3>u^s>B*nz(~ zcH$*REB@waL!EOM`a5@HfO8KToqI9Rxevpf`_be)h#j4WFxq(-J2{VF7w0idbRNf4 z=LziUJc-?%r!m`k2J@X~v9I$S7CX;lnezg=oEOpUyo4U-Wh{5eX!c;hl8B~ zIK&xg7wgfkdNIYV%aGZgP~nsBT$0`GQ4;W%eBPISiLBxfv6c6P@5oe4O@nTU@% zEjZhmjB}i+xX77?&pXp`i8B+IIu*(mH#R*Mm3qw(6ZF_>637R_bjFuiO%W|U39ZeTpEa41A=l9%q!z#3#yT;q0>6IH#-umzT}O7t7}1in96mdf5V8 zTeb+_C~L%xWsC8hvL@VCwgf*YTZY@qmgAnX75GKjN<3KBj9-k>Zgx{M24dcF_S=<>m5U4FRO6@bsV z0t~@;BD!`vzg?PbLgqK~#sB_y; z@3y1S?ZiO03){Ip80xOTFn1+Jx~s6WdjQ6}t1;a@5HsC_FyCE+1@0l(+dUKu-NUet zdpH)kM_^y~C@gl@Vu^b+I^1J$fO{-n?;eNM?(ukodjbx0PsAJDlW~xH3J!Kp#oOJ} z@Gkds9P6&byWKNzin|^kaL>f4?pgSddp1sYH(i-v=eZm4 zY4>7W;BLa_+)J>@y$qjsFUJ+`75I{SC9ZTg7WZmg?_Ps%x!2-m_d49} zUXLHTH{d7kjo9YigrB-M;}`BNxYxZEzjSZIeeUge$h`xIuL!PatM_g0Qd$RF#4#QJOi-VQ;n~92I6a;LAb_KgRgsr;9Ad6T<;l%Z+V8}2G0omz%vTB zdus7x&uDD*jKNPlWAU(O9Dd^&k4HQc@T6xVp7Kn_)1E2#t7j@+@=QbD^6BVTUWfkW zGccgM9^tFXMh1&5Wd##_tRU~TzYyrX;_jxJx1?s5 za``5_zkD-JDc^z*lyAkU<=gPV^6mIY`3{^>z7rRgx8kDmHhiXh7cME^jZ4e-;Ii_) z_(J(UY%brAuaqCejpc{%o$|xDwfqRaUw#a?l^@3s%1_|-@{{;s`Dxryeg;1(KZ`rd z&*8`A=drc?0-h?rh^Nah;Sc4P@wak4U!f1E@IhmR9|l$g;58M2m{}2o-711Hw;}|4 zRfJ+*g$eU3BCwz$3QH=Y(N+npPHiHaPYQ;~~J6?wR%q5zvK3h~v7qDZ}NHBx&kiiy`CHMYV={1#GU zE9}H?BV}0O#7z}0e7C}bA6Hc1!HP;eTv3HbD+b`Pifa6}Vj$Z34Z`XDYOt=~5PYKF zP+ZV&7{1bPIIik90{`4^6u#Q87FYKhjj#0^gX{Z^#m)W3;lY06@tb}V@OZz8c&guI zJlk&y{@iaWUg|du4VBX|pt26zSI)rD%6g2foQW}&voOALHd-nhuxsU9%&eS;Ju2s8 zUgZMpQ@IFjm5o?dxfm-doAA2IB{;Bh84j*oj>9Tf;K<6AIJ&YK@2*^h6DnKqfy&i5 zwQ>zUSh*ITtXzkGs$7pxRc^qiD>q_6|4mrfe>3*!zXePBZ{;Z1kUL%dw-MWs^Jf3; z#7^Y?RsS7$OaGlXx_>L)*}o0P_TPnf_uq~8_TPgO`tQa2`tQT%`tQf*`ya%Y`X9oV z`ya;F`X9l;*B!$l*B!^v*PXySuR9ret1jG_ts9}c7Dws&U@gDd&(@9R7xa=Z8tu9< zy048+-Ale--SpW_FZ=$0ZGLawU9J1c&p19?chT=2e9ZsLDcQRD{#~YK>lXN5him*l zp7wIx8vjqGy;AqO|7XOn`+rV+!vCAPm+@%bD|O%de@Fbi|M$eF{C_4s<^K!uY5(7d zPy6d0d8O_?<8RL-^GyL=V=5XN)6lmu9sL_K(b$-U?HaSmcX(qmMzZ82W5>o^?8K6j zjMuW{Bx77-9>%le6l2%MBD6LZ^Cf}|;s=b~h^HF66HhhvAb!x;i_{)8=9Aik#y*WM z>`Q778cRs+L1Ss72OXsLrt!CDlTo)g6%C6k(RXn=`Y+BzVoD92np9T2*jh zm!{Xz-1J6OhrpEohp_X2kDI*z|C88p?wDb;xQx~Xs&S+s=}vdL5R%-G?Cir%hzY~- zxY#BhhwTIhNC;)rWtwfa!w7~l%r@I>m(r472Q^Sy(=t-ZaG5Ui|9<3=gtqzf;`6yj zkEG{x((`<`9C~&OhUnQXIGvtjf{~4?I3^g~sKYZiE`w)n{3$$V<6&@eV^|y; zEaAvwgY!7@*kESkF>onI9vfW3k;ev)+ISpX&XJb~2X6=qZ>N`UI!qMtw;5G11Iz=J zfPco8zqlksJJNKk;CF#?Pzf#uSA*BV z5coTI8~g*j2R;NJflt9LK_R{c|F^RcUAyqxz_Z{V;3M!Qcy(943w#W|1bMp&u@l%6 zOb17RVlWSsf-E={TmXIv9tCfK-FD|X!E{gz7J=iz+hG1QA(nubz{_CS9zv`D_kbyT z3b8ZT3(N)^z#HIQ@G&@XFToqk{Ox{lHTV!j_7#n*0vXz4s5V$Um@DS zI&dA>1nvS4f|tQx!8_nXFnvGn0eBv42JeC!_ZQ;)0|f7UaSy?>U>JxW^31_>V1rp; z1*iw7gS)}YVAmh9AEb(SCZH8;0GEU7!S8>}90Biu#DP3Nunu$|B*XxC348(i4;JE2 zpy?<4UT_0=4P2}XabSr131$Efl!D{I31AKQ8Mpvk1vY_uz~kTr@G|%t_yBwX3JzgT zfgge?(}kFPs1Vm2#`zH;_OOL`5i7T(y8T2)Y(B=NGYV!7@+- zt^kjL7r@KlLtrlEo@AKkU>R5eP6y|J+rfk2NiYBcM{?a@UvN4&2Ydv+1c#LhF$mrQ z{{sI8lgpTIAOemDwcs-F0@!H@&j)-4_E^eX2R)z{JPHQDAHeJ2E0A{-zY$yxo>_)0 z16#nPa@GWR8|-6Nw5x_4Hg~490$*U&EQq=CKv`+9m}%@e*^DJogZ+24{nlD)|JLgKn@-6>AXGfF^J)*bJ_(5#oN<+zJN4?iX+k7c##t z5#mg6K6vCZWWp8PTkt#ZX_pY+0Q*WIKDkN=*25xDz}B{s9iy$Tff@$bu$t z4`{mqIStMP*MghD6W}%QCU_t0a-$G?g9CsKW`WbdP2g={-^~373qdV#HW7LRYr*N@ za&R5k1Re%2gRg;p3%?aC1J&Rz4U&IfOR&w=u5<}A1d^ntg*fiDUX1?8XvTm|j{YktEvxDpJ4A<*($ zY(enMX71hpaSm7swseQZkCmRA!{R_?1U^V{Hidb!s{{Ut(giC@FRUu!mN0L2Wnf+D zg+q$b6BdUk8F;$V4gXZ>fe%%-z=tU%w}yqGbik(44_nHLyCNd2h~9{ZC@WxF>4Kxm zARJQ)?~VvZ(crjJ2hUIj;KP-|dm>_{l7Wvnh_uoUyUHN!DTNP2M6ps1mnhxvT&4RaTg+1i;rYto&9+#eOxt9Og-Qlqq-1Wh z#bU*{-R8YDqu1uWHREnulqxy6OzDM}C|lsAO3^*GI7%tH*A~l^IdHkseZMV^R)*l^ ze6KBzQAYk~i({3*%eFX9DTj|&`rryB@F!cWP&(jBr5mnNhT&?Z|5aPmC`GT?BC8ni zN+k!^D!p)>G7Q%%xxd)FBewxwrF6m#N;ll76#dl}P0Ad&S!suJO8YyuI8o_%-bu&b_5?DV&Y*gmJ zHz*nSMx_(JNg0N3R>WyD#3qG8Lvf2T4en72;ae3AzD=0}|5BNA<_vMWVw^og+@a*) zJC)7wU5fGZ8KPGyhwoOp;CqxU@V$!DF+<#^4krJL?rh%A?DuAi$*L%x&HI@v;Cyvx{%qdQ6bojH0(CPyRo%QKAp&aA(u4@A zId~_v2i{rT0`H=Zz`Lr`j!KB#)Dn1iwF91}_QQLqMavRmPqh=?OYPjp6ML(@@b}bV z`1`6>=!tz)ZC_6ms@j44wQ02u-d7!f_ftpU{nhq^JaK^9cd#dZsP-wI_>tNd@VI9O8)s)k63nRf7*!x4=J93#WTRQ44?S302L&nwqgap{u=cNZkS-qLxHGFh4#3B#gYdEH7Wg=I1U_C3+~V;L ztp=}94Y*Rx!BuJxT&?z07K<9SqpFxUZ2RGr>Iht`7FHLFIyD2=tDW!(>Hxe--3&LV zMoqD3R5!p)>JZ$l7OgB6In{wrR6F1nwWGF}w{yGTHgynQt&YHJRHLq#w{wm9V&2Zp z!E4nG@X2aFe2O{(pQ;)s6pM9gC%j%Af=^S8RmEb1S^}T0mgI`X&(se140Q{9raA(j zrIwsnEY4QD;Ge6T;SM#>Qp{VvzuO1#eV0!#Aj#e^D%MR5xE#EN)T*7Z;10)pB@~+5_LB z7F|LFSe*mks`kUTsnaek=3V6?_;yu;?@&A7JJnwJE_Dmst2&nzi@Vi!_#U+nzE>TA z?^8=IFBbQ!C07-T2h@S9i^YSgc1^M9Q|sV|)GhGCYRR?5;t{nAepKBIKc)`DkE?U8 zD;7_v8{mGmAAVAubA7RRO6`Q7R(s)R)M5Bpb@=9D@ti6)6^rN974U%C1^-Gdyro#Y zzzi%FzgElP7u7!aH)>%|vG}c8czdyUNzK5U)lT^T)Xnhk)M1?5u5uchQPE=kqrE26#8E=<$WT z)1HH;X}$0s+AzGQHvGgwv6oiZzfkP0mBZiDI^pkY-S9rz5L~DkPc9Tc&^qCLwQhJn ztry;3>xU1}M&KW6&QlA;kF<8UNE=Bk5nP!0B+$BGMH`yC zSg2ajyv0J(y5}zzy4DMaw0`&yZQ6pxV!AeM(PHsatsFj7>w^!|Hdikeh8Cz* zmV;}wUO1~wOJu}Ktq88w%HcY#1FqLP;S;nWc$GE`H)wN`8PTY1fSa_AR7NywgK$oB z(iw50=9FYaiIHiR%@N`8f^eRNej%&h<2?IUaOVECu^PXDcS&hsy0xS z5$iNjoe}FbQJWE`@mw-ugVq6`u64mb(?;MkwD$T8e^sIVgp4>#8-UN&0;@9O=h`&5 zLo0!Qp_RkuXzlR1+5mi>7HG_f^R;sL0<8|dP#c6hwZf*1xQM5j5f^Jlb4Fakv&)D} zwGFw9xQr)~;cqt>S7yW&+6K5w8-cIXO0LR?tF$ioYOU*bek)&uuweekVX;HC_JAz}{v zOKk&uyS5p=LmP(g)P`@)h`Y4Rri|#-GCdh_x7G>YqjkggYW?tinsIAJ+^_ZCmJttV z!|;Qe_REau)7s&OwD!9);$dwNencCAAJw$pjCf3|gCEzr;U~0SxL-5w&4?$p9Q>4) zyDuZ2);7S;Xx;F$S}**ZHV8kjY4>NufYuKGO6!AP&<5dOYttUch!-`Dpz&`s2mYfk?U-S8i^A^2r&#U7>n&5M3`P}>Z@q6PLW6|ZU~ z@M~HR{AX&{=w#1y>)F3>yPF5@qH48Q@s@SQUL zu15ymNnZi)thd9v=pA`W#jbikyqhlamx|qW2cD*Pzw5QYOGQW*yDt@o=oxsrJ`Dd< z4@_Gs4%H3#FufBt^dZ>Phe#J#`Y;^UGkY!-5xpI@^=>$-55X~g7?}lgS zJ@DcB5Ij>a+Iy)uLNAAB>0R(_eGs0bcYS}UNa!PQQXko8sYvNg;Zl*-ogXX}uI>~s z6Q14<7wdy?i7rZ(iMje5c%I$~&(}K_EE5a#Zg`CVnai*mgVKAL-bv{2r2HS{$#Jz!iEAyh0y>EA?TxN;h^pT2$+E;2J#xXSvTuiz0dJy&taAhq{-G zdfnK#T%4c}!>jbb4a-G?UJf_v?QoMm2si74o0f~5UULm{^7why5@Or)Ik>%nv zeFeNh?}bm-#iPr`&-4I%h8}okxj0jw1D~aL!)NQg@Xz&rxI-U=f1wW!EEnhKg}+)Z z&ehA|^Yl*me7zgKKp%oH)Qe6yR&?qHe39M>U#$1Sm*~BF9w#oW!5DqVxG);r*9^eynUddUIDiR<)E_q^nB55XJtqHik2 z4f+Q7M!g@tNuTy@rMOw&aBh{@q<6!&=sj?cJ_6sW7oJxoZqqgRmwFw1yFLKlp^NjY z#GU#a_%6L2?$vwYyY=47s>D6|Fnq7BU0x;b(;fJJy$gOo-vU3VPrIT@^ywM+A$V;iZ;xXNUAJM%M@Y8x9{ES|HRh4*F?}DGx zN8smm=jtjkpqInH(lggoi5K)0@UQhQ_(i=3{*4|W74}=b5PnIoyRJ%X)_dUp(}&>S z>BjX{;`e$d{0F@o{-Zt&zpT&ct`dLJH^77Xv>U3#D|!xoRqusg(+A)`>wz1q#Orzq z{1?3o{;R$P9@0nPH+18sD*irEC;WFkx2a0JsrPy{;w^m`ep@$+Ys5SH9C(Yq0se>H z2fwQi!~fJr;P-T=q(;22_rV|N1Mr9XAUv!)b8E!E^mh0oy$}9a@0(X6KGBEaPjzE{ zjrdH@!Jq3J;1RtO{T-*YrPBpMjwH{)t%Kfq+4?Eq|jz~ zawxE-M&yO+;QUbCg)7CBP&Zr<8iJ>WjLwxJ5Hbc=ieRV?-YL`r?;PrbcM0{wyM{Ky zyM?B`vQq3G(%@;K74RORE_lyS*BdMOdrV#bSSj`njlkavmAt!Bd_PnU?-S~T3q#Z1 zTPc1J%E9}Fdg1*-!|?v0;rCaH1470JE5#2(8Tdz`Zn!Aa3;#IO4<8uX@Zm~vP-w%) zE5*T~A^0buqEA)|CDaY8p&?ict!S$idZ-Q#g?ivaLPPNM(45t^;-{ev@S)s|T5(ut z^V(Wrgff@a3NzFWTcJTX9MUeY6_HQ}Y=^qLYeh6P1jjhQy{?aYSeiJS)@*&kl9Nb3%bvYDFRxc(qm}L*;NPwE4AKkq!;PZfNtLYpEVE zUau9!p$%|Js2`pi5`U=`^Fnjr`Jr}rL8t>>7#f5Zg>pl+VsXg$TP=Uhst!If)B~4> z2H>(#;oob;l29GIG}Hqh71{zX3kBY+73HBi_~_68yga16RV$7O)xpPxy5ZwOTj1kE z&fB%3BGdt|2o1uOA?=-7Q5EWdt3!iuO=#MdT9FOq;FX~*aBZmPAGM+`)bnmFe^+Y@ zd_qY3XRTNja^Qwg4sHzfz)hiHxH%-=s};FW5qx5(=>1yJ5-NvVLnClosQtrQu{tyW zuL%u&UMo%t1x9K`d#Dax8ybL54$b*@tvDsr4WAks`l42>3l)A@E7pg~;nPCSSG8h8 zs0%(l)C2!4Gz6a!GQO@AXNEfAvqHnYb>i$$=I%Q2^H2`%2o1x(2xac66X%5TH&AaN z@=29RoAq{{=>Pr3qh1#Xyh zW#GO^SAna+HG$tvx)xjqt_R&^@b%p&py#lY@)zX87uXn8Mz%>g~{{{qwVeisPm{XUS$`vdqRcp3Z&41!m{ ztKhXjGVjmeb?_JPS1=S{zk@fyTY+VHZwHRadnZtpwymtc) zdH)RL^4<%y<-H$h&-);7YTk!|4SB6QAI+N_d^|5N z_*7ng@R__R!RPV{g1^q23If5s^Mk*p8y4@Km$4mfkVJ_@KbOoI1CuT1QrN`2(UpE#DD|hUrlf-Fr=)=kJWw3mFr_4T)|9!yo2JYQ-aKV~aMP3p!6&CI3_d+& zQE*_&;^1$lWP&eEITDlxUzt)Cd~M1Sur!!ga8z*Tf@Q(E1?9nc1xJJB;Fw@#!Lh-b zg5!d91;>MmU~|EW;8_Kg!E*|#g69`h2QMtB33e7_gO?Vp3|>}H3+jSb6x4$gf*T7~ z1#c;62tHWQ80;%(3O-cO9DJl87ksqf#NZPJEx{)XT7yp&v<06oSRH(}U`_D3f|G(T z6to9_SFkqthk}!XFBhB={8PcH!NG!c!OsfTgVVqUa60%II0Kvs&H`tHpMwtY3vdoN z7n}#q2N!?~K_|EfTnsJ&mj=HmxGWf%dU-HD^@?E4)UIIN)GNVN;A(IUxE5Rot_R&< zBe((F2yOy5gH6E`r``g3z^&jm@JnzzxC7h??gG8wZg3B{7u*N#2M>S;K_7SsJPaNI zkAla*TiPYP5mu+ z32X-c2Yv^B5B?DJ0)Gr<11|?#0)Gm&1qQ(@;8pM%_%nDN{000K41qVm-@xC&o8T?* zHh2eY0sjE+f`5Yd!293>@F5rm{{kO@kHIJ4Q}7x19E^Z}gD=3B;4APo_y&9n#7=F2 zNnkR_1NmSIC;(GI00hBKU}vxk*cI#sb_dhI9$-(f7uXwo4}2f&0}8m=1ml4h4q+1DL=9VGsc}h=Le!Kpe~fhl82m z2rvuG26I3HBtZ(KfeXByE)PoW`i!{!GpT!dk7TShNeqm&(8d$37bjZ36<_>*lCQ6S zE6yH~=Tf;g(YJY`?{^bt{9&T?@-tIipEs)gH%_#Id85}y1=&RF{fXAc z6Rl6jT7^Q*|5g@Di$eb8&+*nFW37C#F8|znc~6Y}>++Siq)kyMv?*8ZBhR4nr(pcs zKA$??`uA8XUwk>y_w_{U+o_{>BcFdOar|yf3XGqdH_@6B7{6;%0;9+AZ_otBfA7?Z z_541;Sc5BgAb(2 zb87p+ydO|_$xmcEbcp1z&og~?eO~1AGM`uYT<>#>&!_l&rqAd5e7Vop``qL6-9A6; z^V2@R=<^?a{)^A=_`J)Z)O_a8h*1?eWt8PLI|T>Kq&89co6lQdr{I($N`~$9yw&v@QED zwPgEZ|Co#Yb9V|<9Y;-YK=xm5$tS*3p!#@umh9K^r>&6ueK_Q|Lw@_4sR_S-wa=&c ze7b*(>Dv^1G(5yPBTq>+XOL z*<~31^LqIdhJ5CDo;b(Dr{@2X`qn+BJO*dhKTdt?sreuKjEhH46#iyvBRqZG`9A;N z=eOX@`kfzsLQP z4d}Y_;pywL{gT)De3{R8_$=@0^mXzs?c)E&SJzK}N}hWJ{ObCnd_KYF^*(p`e7n!j z!I|}c_uJE+mdDS8r?2~&&+^-+uakEpvwojvWcwJO8+|^@=UaV#(&xXzudW~Qd6#Ep zy9l1X&V@7UPw?AU`FuP4>iQ@A_8^>D|AF7L>vQrxDDd=k37?Pm`C2%${vmkzI{B+# zT^|{cJ+<(w>#y|N5BdCw&)%=(-YTE_e16yG#0zrdqv6c@X21P&pRe?LWIMBd(y!$) zmd~fdnf04|e#-Cp$mhdel*h>VKYd+|-@er6O+G&kpRub#z3dsg+CCrWbGOfZaDLz= zDrmni?gtNvefZpm&q6*6`P@&m?X(}C332QX(qjL8)8fqig#53fqO7g5t-dB`#?LwasPX_+`F*8rKP#$&}8%Krs}n+%GS1q z?4gO)CiBp8o4r=rO~&j<%8Nu|X(R21?L;aW^CDr>^x}y`(oC~2Wkkb{YZ_)Ml}I=V zBN2&5oJ2Al{r`Nqv%~$S6=#1cW~LlB9Wx?kBIz1@ciN1a>6jID>{!ZlVuo!wb|UVX z$w-Xv=2A=}Zp4$3L^Lf2NJd=KvaDz_Y$UnrWXepYlXg52H6qDWDjtufBA#hRW3~}b zr<2K)Gk~486R~tWo{Vu<;!ezpB+RIlbmNhvZKV@VGHoW4u45+RF(a9F zBJrfb6?vwWw2ZLJ0GSG?byf4ql@(RAYC&1H&23(}xQS*(cC67V`;&{y%?i^r4dJ?8 z*oegAv1rP0!|7Bkl8Cw77}GH=Bf-FOhs8s7(|jF=a${oYnCB+bUPKO%G-FXGV%p(w z$~2u+*mL>C$%JWIJb2GYc%EygtwbylcH(Z_40~4EbE8huibfNz93aBIHR9oLG@Oda zhhl}(Q8&SDb`oaXj7L2ynlQMWNF?l7(YPJ6xUsfjr}#=P#}H;L;>HuH+@|W5wYj$X<|a2)+}zq0 z-oA69D--45*7{Z1oNRdMaKht~!m&hxZ?e2t(ulfg)3YN??s&wCINS+08I2_@(~dK} zVm6m-+D?s?ddY&aHGH;I*3`H2 z8A~LYpmsW9TDE1Stu#;4Fyl;f?yD1aS!1cFXQ$1C%~WC@S$4!StcaWNQend~gb`1e zM%r{EY&(&3(nTzoF*9yNZDx>{=7t$w+DpY8K3K3$+>9}!xjsxspP9QTJ&^3@;q#!eY@l7iR5n_#N*x zjqnc7mU~O*X558o*;-WDw&TNNR&0mE?Ram*Ml7Y= zO|-6Us!laG<{Gkcxy-7mkoy`_b(Pi3!T+3%Tf4TIjaxe>X5-ebvE$@yq|=V3V+im# zGSfx?AWM*c5j&Z%)9F;oi+MI{Dv@TX8%YOYj(m2wg)FzU$;w1C(HV9OJMJO15$>^A z%u2CxQ%sgr-nC3!|selqS zHE=px-CUEUfwUWKq!wjsIghT?qUM&y$_9)EHX5=`a7j8{zoxYp`e=j@kab!pP z(a45C!-peQI2?)d<7}j{6-_y)R}X3KAj#vAv=dI-$m(P&X?eDdX0TE$`!vFQ$K#s_ zV7?($AvtEmrQ+~xgDGQ0(@FFfcP?tiU6d$y5v3UpyO<_%&qJ@rO*7^?X^&g)+7Sj| zibPRiQi-4wqXsHDiFh|+ZiG1$O*j_fC?4f9-LQkYOUF`?l*L6yy6qC*v{8OC{Wt8%=uYcsdb}8(e3^H6m`*L@RSk4Q{aM zGU?rf8+9$mG7ZaNfH->JPH+bL6w?E3YT60r8nO_hBWz-qp=_-%%Pwj~q^eDM=xitK zCE^J=!2kZKa`bdO62=xxq~a`Aq<9pi;9yWiVlg*?L599GF$R&dhM91aX2gpoT`Ost zi3GwJKEoZx9Zg3b>yCEIy-P|Ha~HU!6{9Lx_GGJ9RaRtc zE6U^1@G6$&wtH$?D=TshmGw>A^p-SMR+rZ2vJ3G$m|m@wW%ad9m2InAvdik*>bCB2 zQ*y`led+qzY-`)rW0DIJ^H~cOa-`9AncuFjsi|AB5zlE(KJOB4;wsyFe^=5N`izypUM3EKJb-~Yz!j5e_ zVfh5Dl*#i$s30_%$24yUcOfXY+1oI)4PGb!u(+Mvb#TgJRCCo}znt^1o-ALN?EC;Qh_L7MR zlgdP5;u%Uo#cEBmCb`;JIvU47jpH?lg&^+WwpmfnO+-ALGAA5Oq_OfbB2j0UxZxBV zZp=uS2C)}88dfYb;ryFT);gv~ohj<|XfXqtr zfD9MEEpEr-$n_Y`3^#R$-@wjrTDlIAc*@{m7!iao_5#i?10&lBAwJ zJjP$)N~6qECbTqTam7bf_NkmLA zUz1iEvyGW$#SJmDM67yi<+hl`T-5F)r~NvU_S+oGHlH)3%Y69>mT zjIK;#FW>=~IHd_5JCOGg=hI2>t6B3lPm%jBmjP!utXnh2l4cG?ZJsxJ8S~n6U2F>{ zoUoHv%V-zW8^Xm6CvCz?$q2z9!b3(nh4#YUj>e_F^qh1Qm7c_gahV|4maHwbv|s_@ zNn&?m7hu3*W~Sqq4YrJKV9>UDGIM2EcQ0a1<<}YjG2S!Ag!N zF&LPrBKV~!K~I_iE{_HYDJKRG z$HHWiPtJC2W)6y7TJT{r!2n(y0ZmW_i(J}vX~T{gQ4D6>G8?18l~EleZy0Bv?_plb zI8ofjrgFJ!{s2i#4y3Q+xVDpEA`><9FtRWx415K7fwnXYFcyf|Swy4o6fGRCIFgY8 zN`Y&KEtal@bLBB>BPr>bxd>4Yn>`&(5HyUs3@K$OPZfEba+w#1bMB3iW=iu&kh>_E z1T&f&Z>0$iAVVT1YR~1$iF?U-lQe2DeIi_&G?0k|8R#Bg$Z}-Zv57p{2y~3Rl;xs! znAf-xm@-}@&5UHqnuPtZapY5%ZXQ!b4uI{JvT&2R$1xTIhuHWdxP44%^npWQ6?@Q1 zdib>{B;qTs4BMdsQ>hrSSb+o2Rrx16?5@q(K4Fm)W8;xTk0%Mp4L|an?WL@;RWhVJ9oN_xwFA>IQ+C^O8AKTF|>&3$C zO{2yVQSK6P-6-}TAtb_hE=QUOGoFhte=vY~HevJ{KT0}Fgz7Mo(f^odTs0HKB)ZBZ zV$=kyKb9aAgjs;zbetH$1&-%J`9&!?0CUJ;L{@E#b%HC7o|IEInL=$R@eXlt5U|Jy zTwUT}She9~N=kZZ>bnRPB8^FO2|v`dZJvBOl}^fEgP7%tnJ+HWF`P7`c(AN&#GoM; z1b(PxIShdKW0`Oowg}5HfsU8Yk(-Jnlc9$s@{tF}4D}3jb=->~%G0EXY+F}tg%Zbq9yI_PP%hDE}AtC3ug}lUzV2ZP(<>3~wXDgiKVIhYc z*ChZ$I3GoaIN@<8lO|6T)nlW0h-PzQGR!24B6tWCdmhVx00uJM3@2nzhR{obE0$9^ zjvhd$ald#_NuCGdmhZ&Ix1*>J2H@G-apW>Gl=)%c5BOFJqLpVM-B^x{^SnG8*U<2o zb?G!FDN_-}6O}&!6c@85N)mGy&0R#RINMfYh4PlZfXf9FJ8Kc5821Z+@o;YmyK?g6D6A!uOL2KL5rbFvnQ^`vR_c&+BU0dtiN>e zlEjj^ix*Y+2l*_O8YmG#v` zaz@XSJG?Bh;B8L#50#})`i*D)=C^mdCd5pi9xqsncTGiEMB>?HQTn31t7nmG~3v`CcC($hU`L9 ztsG+Xs>@baYFNC z)>hPHb8YNcRSy_rbt9HutK|A7auf2jR!*y{U)jcIV?~<`VXlN*>TB!xkU3F8p!G6f zUD+_VDYu$V;%uw=b}rQr?TnjUYlxGP2TcD`(m!T#&$F?G%;RDtC5t$_Qmovqschq#28>vTg&Q_mey9;ZlYZ`*2eY^M==jEwunYz zI^l{&abl79_-#o7d!DrAEG{{T4T6ss_J}bhrNKdr$rMRMrOf@MP>`7P1fgOUkxt?; zhRm%d!enZQM`L58uu-eHqK~nFOL@Jclp|ZDIzf ze0((ga?}*b;IwqOWlRizjf|fY!^Oj%#1Cgbjyj1ka|f=e&0b>4#23)v4*#QmxpI6M zEP}K}9)f@v(JmZ?NGwj06F)AF6K-N3VWi?M?QnQ@l2K%mrlkEwgb;h$F|ZGL5X1!{ zX>J?VHc>pTpT`^Fiy0IP2J?^vSOOyz6NY;T%0RmLq^4SOe_1~_X^tl;&9NkO0WY;8aPH>**k&el~e9+ektbM8q7+X1&M&f=&7(C2tAiTGFM$9knb9PDH{YZcSpD zHH=@2jIqel6S0UVNk5FnCoGb64!$e19wJ`AIwXA0FCkiu9AeH{LOJj*T$IaG)@~k++~sqBRTC8$~e^DMji>Nt@sod!#XOSPVQTxJ;ehldxFaRnI~L1+@?l`DZ* z17C(bBhyXV!-%k^aI)Np!E={-3dag%g)cf$o8!52SGZq<^+@aTSmZDXG&=W;`Ilf}5H~PMS2I`m4rU(y zXoL(csVf&xhs>Ewh7t1e(32d(5=27#50Ax);zkTJKt_^ZVImu-V-|9tPmP8a(w74BD(~mxfE6ip$;iO z6A?_6Fp7Xky47e7I$Q}i|A&Y z05-P>i7KThQB+nZbwH$Ph#9eD38)h>PccAgwq|v8mIex+$OI7BQgdI#AV$Q;;O%bjk_?bkV;#f*yj5$Ic zji@=nbIL;;20(n^Yok`lY{-fh1Lv9KSBxn|6plPzoEbtm)+OT|A!--K7&376abyu0 z48TEzn6V0pf~5!wVH%QQli5?OBN?8hsEA@3(y%? zFCiXHGLSkdf<|esEW)B8+=8%=xRwmPIZ|fFi20z?n5t+NPC%Z8rEn$o%94noy>ao$ zEVCHUnYi#Wb3)Y=wR3U+Hc7Rbs8TPUa!`N-Ybm2c7cgsNvJw$TS%pI)iTjk6<}x-X z%7OWersXNb(>!yB;yrFPstR3&eM7xmnmo1)*&tW>9EMiR@wLQr<>N6J0(a$D94`Q^K5`++B+MMJyac6>K6xtCf^!Q1HQHUF+k_l2TNCE~xo25y@m}q+x zFL$5VF@ZjG1*Rh*b_|I)wl8zS;~Fr+5>&&*iKp_M>y4o_S3ah-eV$WE3P5i2w}gX9CgG&@dHcWK4#tm|a*4L{@QXJCXxohgH6R{^{vSdMv2KR3sRJ4PE?r5Xo`uP z(?nieCZPT!B|g@8%3Qk6mA!wP|WcLZSqBl~^2a;9N&*g#2 zoQfxBy3|J$=a?)9CJ$D03Tr573hG*LEeZc)`=ADJVv*D25vWEX?uVtvNG$!nMnY7DE0A^{vQWHLq$1`ZX+Qahu@$WfvVh zL^@Nei;g0LhDU@NAn%c)@*9zfY?kG={O{mcGZk$L5AgH&UW3{m)Qm~^fP@mkDnSJt zMG;|QALKRD+&n4z$>q9eUzD!Q?h>yeElD+_EP=ukr9=mJ0Artdf>RSG-GlPLZb!Rg zVPi~_WTr%nY=SgilT1e!y-lhPJJ6zUBgrs6Qb`nAQYNI)^-LOR z{9q>_sIjMI7J#Qn=0l!_olP|eP9paJg`bc`B$)lIXxwJB4CXQkNz#SfSR@g07?&R- zo%w{@OHd52oh%Ioiqw)!U}K zu^l-;swv!6~*)vz3>Zaz8p2tVo0#ht#KBM1oOkDUv0&5NR6ryU6*a8#WREF9z|b$bxU(=^GaWq zFUeAM&8(q+Zq1R^t(jHz?MJ4{*78a~JAKv5_~C@?`S9QFV3?+C3&)p_e$o7<=93z- zHMLpvMe|CpxrP2!|E2F}#O2l$)ixWvFCt3X+H$0zw&~?%g{|{q$w}GDRnZ6&mp2#W zG#zdDlXt9VbhgS3dbe&&e5Kq&+Gv|^*lxpS&)l-b7z@#DkJ)Y`#vV+NC5ejCvP8ui zv%;1IG!vbKN9o?SM;?|bD;w)uz>#HO=B(AkEJY=#0*sietf8@}t#wtF&+TKGB2nF# ztsqd!p0ZrcT0UmZ`c674Li%Y11FUXnkXa9!b7d(#8LF`@w`n!1ePZjx)s=oVFbA>> zSJu~3O<1#}vUOGBq{{k#F2Fw` zE5{vgx6wrgsgC!Nw)vjXt+_Hix=ruc=GJ?`gl%8D%>i34@4I^#@Vi}dwDG>tj1^ac zaVl$rSx5fIQkg&VTdP|~KmBW!M=)7hD}6(D^!V+!o)w7Y4 zs3ebFU7thQ%&kE-u`Jp+zOtgShRxP&7EYIxtgULPr`6n6mu*=@YL{z46iUI5_>`OS z2V~^CQVcoz(T3!dxl(#sdBs6K_l31935H6lS}j`@wf+^c(CVoCmE)Pnr{)@R7jkbM zd;fb{k_)XzjWj}K+3E~hWR<+w>fC6Hz9qz|M*G@ETmErzYy3FBC66P8H+I};OZE{? z80#Bt`NuiqeR%uqmE)m}#>N|M`F(bDyw7jRb4i4ZjW^oz#|w}5VeYcGs-d}>kNTWH zT~}8%A~*O=tE-x_ZKLhc#};nbUI;R48!PMSY|Jt1$i~u?>EK2`^xU{@9C~a+?umxC z+2e1J;3bPjf|pfM)5L{?=?O*eJN{6Qx&<2(F5fK89h)=kkLL;a2BnqmT@}@b7Xu^ zT((-}6!TmDux5C4ubf1qd)r1^{@&<#ACqYOIA^?%Ni^0+l4R`M(H7^*i8b1n8*Rxx zOda~RUfbw)D_1ny^3SuQ+Z}^BG(L{slE+YkN*^sbEl2mZjkYT4P(@j~Dpoc(Q@nv1 zZh^)pmTXqEGAmF_^3A7GWTn4mDGtcxnp@gh|FdUw|7exKsE}B&u)b+EqafeLn~fC> zW1W-{$eRqwS^~cB#EKUGWG1C7D`2uWAa0sy_!6_7!X1ArJJG6I+m=o7zBGrRfuxTv zYeA%2jGHk-twTg9xOZ&s%(S&aS0!sG}a{DGTy|y8E@j` zk2k|n*+ld3b^cqaqgz{F0o-OUFZyk_cXWGAeU@ICly@7-YSSg8B#ibk1tvd%U$o*> ztmS4jx2!@?lWRsAOPV>Wv7*AGc%-3$t&<6f5i28iLJc3$Wnu%4N9`vGPU88v*VGr1 zu#_)}%UAh`k5EZRw2{IE;!Na+3`-;_W+WC$oh1$`?kNsCSu)aL_@?sRUSbb~fq3nl zC;?@CIH(k!5fP3OC!~JB;`MO9T7l4pOT>eMdJ-~ZXGw8VYC#1ad1E5)MC=K65V4}> zmk0=N$x(ViDqCIw5l;de)I$(HCssiEm+%AS{)CreB&!HS6K5sWz#}2)AboSnC=Kd; z37-;e5OH3ZA|6j9m-sr-SAr5U`b`9wFdRNDk$&QXG8`*Ix=|wgF`GDQieE=0hXHu4 zBgU)pvKWxuI!6;QBW6vd2Vqs2N+VT*lTVGLOml<@<`5<#EJKQn>Rbk(HpALMc?2R) z$e0v?Num^_e~EGuo*;QlHjl&^SvX#(B(h6bjTgnJM5SDpU=20u#9G6=Yrz2j`=taV z=(h1px%u6fd~gb6cWbV$zNxxZPTrH`%Qa)&?O1gG+0i&=ZjBu@YPgNQ9klIv+qOAn zY?F8r*G+Deh!F`L0?-tDlF_2_mI_aUz#O3z0^+=~OfZmmB%xiBT0~K(mHls*>c1`| zchs_UTI8xBiepvCo-uz5b-DE=cV6Bd+dEzkvrP{pFo3zW2N$TV+KN`lu`A@*6=Nrl z@2IS8$@)U^e;*>vK6z7X{CA%I_i^LjfwMV!`QzW=o5TOH_rE-{u>Y&S^E^g7eY=0W ziK?K0&Q}jdnf928KE89Sy+7XLO3VB^N0ls`@B>B<8#Rz7?C{afblH-nvIyM2E+zo) zcrnA|qv5yVt!{NpW#9NqV6wd^=^vI{usAipZ0>T`-&#^q%7U0%R^o5XCt{YE?>Ecl zyA=2N=au>&rGy_a!e!6ceabYf_Wx>t-&fPxwwjE0BwJmz-8LrucH7O{Zu2{&Un0jG zKfn6B9aR5)=LkHQ?aruf^tV?M5o>PRepg$|cH8Z3ty{PG^0E1|?P#k`@{jNzuI%5| zcaz(5Yqs&%WXCoxn%tDO)>bD?_Dr~Ga$|HRwPNM?=A3k_{Ij-o+_oOK)jFcD#cXS6 zrNnmZlyQ%4;&kK+{-5vJ$-b?Ap4|G*T#{Yib^gZszU%(UZpN+1F}}pu*E8G3UL*XE zv9|BuZoF-F;gBtCu8}8?Ch}#=Uy*Wg%Z*iQtFkS!QQqu-@Q$I};`_s+w^VK|tZ$be zqeJkX*$zavJd79i1ur4-W(+k_o<|{#d|`&tBr-y%8`6GMVNqDlySOP5wNz`wWtxHX zwS0Ay0uxlWEK?$=E=!4IbqyIHRGiH#t<*^IDvf;O!A8sQ>Muo*;Q(-9yDT6`r+Y=UaJ{RJ;bLL!byf&xIx>R*4ZJ9kx~3PVZ9 z7Kqvdvi6Yq`ooti8?Twt;^_MdE3~Fu*bz;GzV+@V=B2RUNQO z!7US=2(X4SqXMWK)g9;oxl}+oR|xW^I9~(zdmRpX*veFg3YYpY9+%RYVm_Fs3ZRD% ztfmeTs(4Qo@sUbI*SH1E5bb7P10Z^?QHfe#_xzz1q1Cu;C}RK^?jzO}(g-0P{v;?) zj~!GJj#y!>|L9{t*wvr>bN|JrQgUasEIIw*FNX(zdHwS{KJ>;W998PTIXQU18}w@U zmH30Z9NY(7%4p}X30^N`lQ)PH2-;;MO(-B5gjp}t;L+LC|KT0x3k9<9_ z1dLs(0a#w-A2a|C=56~gTq7#zkJSt$^jpkE59V#G~f&{3=^fMK9o6*4b?59$?( zA6)p3X5ogOGMoUC&y}c|23-fJMG;xxK+I!P^A18tHf+`4)$asc%9Ki|PvB%=8dU7n zi-BIfAi58btte9>T#yh|f3H*rQfD?K{Jiv|$Ta&eKSzGbVgK0Y*S8t8i{1NoFlZfv zm1NB>-uZW$lzdRbNGoM{&(jBnxG>zVD>`d_p=U@=_36Wze~#oNOH%px@!UV}73|BDb3Q+zeT!Yw@28h?9gB~zzRo||aeDFb^yFW%6;rWK>!ZQ5@kK8# zN1o-!X={2S^No=sJ)Lh1fuQ;4kw%?=K9Z2p;#WVP9(Y=Qqhq{wyYGeUwqBi(7U~0< ztwCy7g;fY=!OV(wk)|18|5Ix|0CzDj*o0F`(*l|<=7r#WcN_jPGzQoV2nOgTbhhyh z@r9v*5TVi@zd*TN&pKprKxCdDz{9gZ+-Cs%nZE7<949#@c!uDGILBARYz`JGpl5^v z1#vjGAnYC@AwcsO!AkBRBr{wO=#qpwKwX|;0zbhwggQZWMc|Av>6ysl~#t2v8e+(hyAcqCW2!=pTz-WBt+JVl102QeDE8tpy$|h=83ET?2*L=(_ zLa)VtW6{te&AuSpkW>&jDFg`A2e<^z>WQh0fCeljR6Fn=5E4i)$IZ__{G5EgJUjmP z-m#Ax3=&TWlAZnv{09tugb=9OeJ_bF0iipV0o=uH0$Kyxsk)b;AnU;pdgDkl8c>`c zSbQ^uVm|8&ehx+n04?a--zf-8q%XPn?~Fa^L; zRNX-juoZzI;dD4XF-(EH$W}8DV`S2E!!R1C)UbpQ3;GjmgZFQC3Ze(am#7%{3;$s8xfm_B900=g; zx+c$B3JAD{n~O0KZr!Yq!{QmjX?x9lB7WpE-4DRp1A(DNi-;0h|gsD<&VZ zWFLni0!nOA?hf|uQ*Jw<7&s)U5S<4~<8f*;xox0Srq3`iJaIT-Zy2;U`2izZ+C+Yg z03<|>LI1#Q$Y+Jz@C+b%4VS^{)xaP&frak@0r5D1`oL*`Io${r2?ql*(kuze1$nw< zdV||`uAb(#&;^@@#NYRheZ2Vj9l4KP8e5OYhFRL|R zFp#(>P6(z&(2})*4#N%bN5+lE^H6L7Y;HI=QhRZ179Yuj;5-7Mhq2c{l6lW*CdMv; zGXx+6Yhnx7Jfoj@k4D{^>6_k|Kni=Go5V%nZ%bWz%AQ4NMMg)Oht$J(8P3E;!5-5G zHUI2s_*fY}2y|#CU~(@SMWF_CWX6Y8gd{Z-i>3G{j2a;-^kZ-&$ZV6o?f@QIzdK+? z#chG>xY`>YQkSPkAbPVdy!74er$9&`SssAL0*VPq$HcHBXcrQj!+)XTCw0aOL+6=D zDM@u5*w3J;WjZm^q35~zj6Ioc zB|yc4LII&eoiX(3UohEo^QxCA%NI@~Q& z6a6x>80ifFhBk+JSQ-G!(pNdhVcG-(!eH_M#vi!6mUJB|OiU%(8hj*NC?C{p1wW^k z)&RN?(K7M!0S<|f1gj5{9DT~|vr!<)gbQ#wj3-lyISfM{=i2=D9EGS`=*i>rm`nh) z0Zxm9aqffWQnz`a0|5O6s5CS34Lt}7etL*Tx#OGOF-W)Li{Z7{7{hC^aXqNUp7epn zhYh1klZ%Y+k!p-g=(o?FALk7^a&+*`Py2^2U;2F9w%z0RN%Yos9B}MDemonWKm7Wx zU2jQ&_WO-w=0zWVx;X#*%f7hV=Y*B~(jP<>nEB*|iv4tcCeZjCyqhcvd>R_sf!H%D z=#DwhC^e=V#TvN(h8#p4L&Ng6%pQ6Qe<&H%EjV(B{U@Y`;IlakO_@9D707;hCEgiK zbi|vP36sYJVOSh0Ucs?v*L6LJ$e`-Ruv9dt9%(tkpA0{e5j`8>3cI2KB$7mNbR}?k z7Q!z+^}0fGcaCxQ&BZ4+8q;0Pr++iUW4DW)57B|nMn78XV+ zi&10JA6P`{a>7T7Dk45{W>=(QjOYYj9dFF23JwZk$?U+2v(O`*#Xc~>(8TqN#Ex*Q zoXw-=tnGLTrbR;(ONROOcaCI-&GZH)C`AgCjgb`LFp4%y7Kpu(jD!GAQBqPfleB4&P?>me;I?TD zaU#(gc)65lS$&RSi>JfV8)sNh0#@Tk|Hf#6bdKa>AhNzZ z3rhpk5MtyAF$l%m&!E+n6J3H_B`yl6v+gS&(nN~Qz;6aom&4+=pnh0)-o|ZV>MAB#8Wzz5s(4rep=7~5j8j8os z7Pv|yo*9k9Yof(tlDv_*tX@meijl^FaUXITet0SJqwIpnheZ|K%DW-xsmGRIVSdE7 zPEO!XwgeiV(=8o}!w^24Nnsdt0+}xi;hgfvX_iV^M*)!CL?tp=V8c2fRi99l&n%h!*8@4bB*zsF;~gQX0(MHMn_YLzUb7l)TQ$&h6EW9UFkjD~H!M9y+po&r5*X z@$(n2j#vu^7q=tXrSIvImn#RC7uA`|jmQ1j^YKrG9VM@CZG7kF8Nj_?uclwGk6-Q| zeS7@f4?o%W*NdaCZ@2&RA#tDCU*Aob-4~yKKA-Oacl`PEV!m;3{^j@ZZ#2D!lK1Sd z6OQgbR+oABj~}nj;D66fFaP5jd3*UEFu-uce|__x|FHo6N_!3n`@E^((|YKqxBgwj zO4}Jg+6x8W`@bIBc^Ons#AgY?AzAb4Akb4A=GkXFLmbEwhY_D?(iuHGI-IE_^*ip=v z&n!$hK7V(;-5Xrpma>?(zr0iLTgAS5BFFD_=T!h~mvCz50st%a7)r3x!(Qme)9u-h zpD!05m)KZWwu+6NZfT^iePvl}mreL*2!Ce;hF+~NIc248?Zxt!^Ikw{T@Wbk_=5A_ zPA`7=xu^H)EveiF5{5zJ{+G`3&uvhxA%1%H@%ZDpqhUmsA4n5S8}B|ywVd(LKKbYW z?S7$B{aMQsc*v{ZRNt}$r>^%k0bh{Rxjp>!&tE>dyL11jpsVid)*sHk`uP6(iGE=4a2&AeLLWx$76H5?LIy|yLxqc@rm-& z5fycptN{P(#qrM_`p21n@4~Du$J$?>{%_|1m}Ucxe(g<7FjHR+gAm|!>>H5M`B6xN z`M#~4#V3f*@fTEnm;2y6=ljN|(K0ONl-r_synUSExDX?szCR}H?+94qN9gRU`Q{v; z=~kXMy_)AHpr*$!My4u%UTYWrVf~#23hCE89C+HRw5gO3Tz#=Q2PiEzjk&5PKgW3q zAL$w<#O2hDnUsOJ9G5o#v_xDkziNTFoO+qg)r-@MR^gXS>Q^@aZK3tqTU z?Bf7H2CST)5Oi{W!u0VF|B?xN@yzabix1PRcYHC4z#Ti}-M>Hc8?@5Xx$+N4M24Z2 zJANBmD;UY^qhTUfk51cRB(-&}$TKQXX1P)kCf@PMq)D~Pyb8JOceCc-`t9eU^*l8A z_Ph;0`~ULK0#LcV0;p8PI=M{6hdt__Rb=J%?0>o2xT0xrsH|4%m7j(T6xS$r61C(L zqtXh8h#_zk!7_-KR3{YgLiQp1l7JA7P_7^iC2>IG0QgV>P5y&y2F%1ZuUrLL4imr>Q{H8j* z7@Er-IZ;?5ORkD3b4%jh_2V83NjdFe(>?J_A_EqNP^snT?vSx^O~?MZ9O4@Z11;1y z#%Jt(&{a;i;tju=aH0@_B4;HW;E80p)K>(S1np%ciFAnYL=8|G9+SqT`{W&l+Db4( zxC-@>QWBzip_OMZ6oXS~jSVbabzZ4i5t0{1Ye)l9%F+Z5NDC+o$Ty~bB2TScKq5^6 zSA?B>q^BjKNSFcyqYQ<#hd@rO=^s#-9Wa=k1?**qtvH(yFgpSM&w>-NqR2+P(_+=o<)pEmm6V%5AeENc>^#P0x56$1-g+W`{Il%e@n6|Q$ zHJYQFIQdNljr_=}4(UKW@vwU69q1_bFr5`k)$3KwoBSmE0s_G*; zw!bQ?s`w;tS5});?!JDG2dsDq0j-`)n_V_h1M(-jvmYRvdS4PZZF!>|5G3xX2<;et z>JVb!!B-|mkN__n3*>|3AuOBR7YJK*!K7^yNK)~0%kZ6Kx1{ELQ>~<0M%qN3!ow?% zzx@zTTqOev#9yWq6n>kez@f@8b!7N(L~~VHX=n```2l(GPc`|SncVJ3{z|Mpl0T7I zCTSvjl$c2mA{{XqLD?aBQW?Jx=Y})xwfwK7t%SiybP?K+0cE6GCqn^b=x3;TOBG6m z$n{!`1W-#NR+47RJD_ai?P-8goys1xy!F~sZrN=Kdvkpl+NQKdWh8i~mvW8%KB{biTs5kb3J!6fAeuQUXCb(|&(4NxZmP?A-o zx?t}Rg96tHtYqRz{U1_At}E_2fRPM_Z;nsk2!t)bZj}3DvheX8pdrnmi_4pM)k9n!Jyv;8I`9CJ`<7hwIB5(6Z7Bhxyvtw_6_<@Q zc`jS+Qxh98sXlr)x`Q{H0FsQq_g^8=d@l!zg&|*_9wMO){Vw$ydf5sG%AOLmWR_>- zCcG{M(&zC4JOG$Wf8yCmyEd1rYb#|F>1cU2sV9?Mcrj@?$bd&XGx>P>0%iHkj|zBF zs@10rHK|1iFV=%bfjTq_0^k!}5CTt7`7nY?t{QMj694kU-ZOBL^d9H}Ouyclt`RJR z95z)sNqG5f)l+F@Fpg9U09!QhfJd^Qzr-#iR%2qcfS@s`mw`rj;Q%RW8*S{X;qZC{et*dz79HSpp~mxB*VMY>VT zQ7{UI)H9-3K}5>ml0PISp-}A7=p3X#m2A3vHSJSIQKHofhZ3+dzjPcxP80C$4W+1zi+-L)l<79DN zrAf-VOW_Xma7y0nJkJm{N3lOBYYAc6bKZj*j^VvyS;`%Yzrl=9m@_8jPsyLCrDFma z$OL-iz)da+>XPgw;nhwsctWgT(WndqLCg|`qIlFZPBBLztHDdtL4j5V(lnxGPx(kG z*=v_xRV7v0GbnTqF0x7$01kgpW>}|y&)C=7yfB%a7G07rPH!nRhpL*(Q3>Nx%0bhT z^*VP{jb%y|U=3aQej<-Ru$=4)AfNzSGQm>YNxTJi;&KJ2P0-%gu}T^+4EWfP17Lb- zx0q2il3`WRFk5=U;dWKvOohM`rbJyrF+-PBPt;N1RJ?CRd&3*Rkv|KH!rli9c!o8#hz7q#msHf#Pwk!QflygW3FaijlDXG|^pKQ$ zNaw1*s%0>Z1v;S>tEZSIDCp}`{PqncUB<+lSF)#~26|Ee!4P_Th9SmH4_Sc>mQD2( z@PJiN`*dYoD^n5=T{nlPDg1(>Bev^H%`70RDX=jqfw6%k6vcwus;2fdqlU3;hIS2D zL$eO9!ax+3q9UQJpf4;3B)D&E8B}lDgzDv}NbpeTW608JSs?%7SPa-!5qOZD1Z_83Qj_u3P4^BN}o)5@WWJji!X&~YBfC_f!P3KK;xT-RL3=dtiUz0ctP)=+krJQ zyHX%dC-RUt5UfXUqliSCQQJ<19^e=%wQ9hS3h2T5lM7U?%p=<%Jp0z5<>- zFo~JP9Kx~Zgt1HkGOad4R|*Oq`m$HWZkkR^;UYvar|y{HQG{0IY%vtw0rrV%68~br zFHt(fzmUQOTT&3^tCCV;b->JwOo6p&zZFK*jj0NzAtn}01dtt+P#9kHjL*bRrl<=j z#q#Cp!OO9_z-vHJaVsCl8^Eg!gt;XP@kFT65(I=Zd}gr0M&|~&$wQ0f(ObeHLDf^o z+D%qWJgj8SCzle@V9zyOa9*4DQv*>p*}5>HUCdKqjCpB`^qf*>z(p0jo^!T_yGlNZ zFejcM*U=!Zfok(mT1=!KI#e-f6)8tcOX!b;#^_zi7ywE!Cmasd=~lB+9EL&CfSF6j zQb>Pm}=|3r#4>W|3^T7MLZ2+yK#=6wzIP+fDhMofd|3m!UG-@ufM77-0N>x!6Z}+ngjAr*rPwNjNEMU0Bnu^JVYcN&w$_L6 z6LSSf&SQXDv3gMgu-GSHOvb{%iYd)o$V>5&Tf%ZE67vsBNeP?eY<}<$;3mw39#Sl7 zHqVrK|8=i7wbqcw-z{KIeGOSwL4)c@|$y-v2FowpumzjHA$2%Cd{y2ENPwQG z0fb=Q3K?3VI>VD(x_|{P#utqq$vXwXYo))~&_PW&*#VhgmB=1nSJM*0k|d&7Gfx;V zHs=ZBHWx+Q>AY_Qwd-ILdq%Z(3dB`wccyCX&P|o8outQ)k6!K`A6Q}Q=;ihH-{#xL z_I&RUZ_fRy0P8aT+te}1vB(IOeX`^4JG1}Mpmdgk)<2y74}W!C zk6hWhM>bR(cV?7ss!Oi?{o2Ok{{FdtzVkDxjUH*$ z#+}(ueinsAtTG9=%gTv%zCE*C#Q3;BQeBn^ekVQckkpuT*nIiwok>Q@L#qtvJ}U!y zVsGv7qljkCllq&h!<1%Pev~jf`}z0f=F8Lf{@~m{YUbmCN79&TV_H+c*qKd><2H@H zk>kIzr^?mN?8DCNpF4AQ<<7_TYt-IXyC$OMr=8hE-~7-*ke4XP6PMO>cF)r!OQkNb^ss&{dJr>J=-od$lem+y0IYF`f z=jkWY>&4DY$>qb<`Yy6iqs%Ek57X+L)xRpscjK?7T|Zx(zdzTkT>W0BELvfgtva-_ zBdjH(w6aH3hsW2R)o+^itY6=B()^o98a^3+G~b3x8rzF+PWaEdVJlxW-O=&Np6TjV z_slPEb=Um*)^<&8x9bKmdSF0*zIi|;?8LI5ZvECFg`8Bu=0_G6KV9lwC&Lyze01x; z{0r_nG=0J1$l?p8J0I;VcFk{nZQuOfZ+&0oClu2sw+<}s^o~RGJDnd{eCzx*-D+t1 zhP`bJ=k(JENJx9jf-};lqwQWDSb!d6_Uc=K*@t4II5vS;yHS9UGl>gukkNB4(cSBGKSYlE;I3xjd<R85T9hT*9oNOU+4);<>vaK3BnvK_ZUC2twfDFox3<`3Qk|w~_%ODG74{sPBxT zi$KG}lVAk0eBi&`Q46pNwWRrmZ`yp+MbA@ieE*pUTx8hV35i21Dawq?qRAC;iFuYX zi>2V?^U9iKna7#k{TvwjbFjj=Gd#! zwhn@*4gHQsEn<|omS52a24A8GLHH)mBN=C1`dtJbPP;?_ps%gXM1BFWB6lT$1xTl*7QgmvIzCj2y7+k(Dl_x3FWA2m-a1O-Fqyp(0m{_O;!zztVQChAq@hab8(z zLzi+8Ur@l!7&_S-^H!x^6wGv2^=`Qc!L%!t;>P2bI28>dBFGFVF92`qbo~RZFO!U5 z;OSM3&_NIw^go?XWSNSzWJeql=wD!8_<5Nd;@I-|GCsRAX%kVCM<<~yS*Uj3TH%60 zSW)ZG$Z|{hi|@kwiptAkSuDW+)viVZLS;hFvEWMPi_4^4_#;AdT~HlC$lrR(@(oG| zBC-(^Qu0LLewx13+3 z9O;!!&%47563aN(hFL)+dvU~TUv3D^_ofYcFXN1(AHP5TKL?Z(JN;VyWo%x{zf7kN z(UUpNsMLL9&n~VpUe78A75XdTwdOm z^_dPnTaoaY_8hLs`b_)QWPRFoQ>UkWbK#vnF4k(F{`!b-W#<~g(Ds@NO((3V(6qU( zLerk*&9`wa|IyYIh%BOy=?m>!4?w2fVrQ{YeUspZyM zY$w} zye}-fLcRnH?UxhFl{!3ikmcs!^)I)sH3*z}twCU;HlqCdxFQ*_Y;S&OsS@D8QYFCW zuYM$Yy^H(`1yv+_69c6;%UIxC_y|i7dAOC6ls{YhNj4rh3s6f92jpS)s(%KQs}C}T zipt0YiZhk_bVOJvet{f6c$u`l#4R`p7fzigCWr~2k03?p3pMZ>QwPmaI`>dwSk=KB zQ*-8bJa{UVg+pYvhtj?$Vobq2|xl(n3YfLy_LqxL&5q z;icD0VZxB~tY2gL!q}P?@2i0$Kad)mh9=M9da7A$48G)I{MKZa_exTl?YFPZT66WT0+2Z(Ud_U9a$L=8WtwvxEwrKV<7+_if;hl ziEXKl@dRW>1rZ-Kp$9Z4-j}Sd95Bg(4bw+Q<>)ar^MFAka`9SyC>9Z4+KZyf;5~Q( zmZ|~W03K74^pbrx%XpC+n(yNGNqguTh9oab^&8ynDOKB>tVmX-#UK!KW`4FkKb=EH@;nd`hNN8;D_&z-t2$> zrjuV>{_y$K?;p`1PcHiemVb_b{PyGTiyae$*P(-pQ%Oo6j{kM~?)xLDN}sQO8^Zd4 z=##iiO~{7}i+x%ZE8co@ZV%bD@%zQ)KTa>&A)!ckW@r0qZ2W3Z%EnL6eo6ZUciE0N z=WCmqu=Dl#RlkM?O=4xxn3s#~AHU4E035$N{rt<-2YJj}Cpi0h#;|ccW7rrohD-4A z6^(kIhIY;T$aLV%`Sg2bN^yKSi}<{pMST9WBzq0zU3*|PC-&E3M@fnPYCLD)JfFix zUx1i?_4!L5+R?^b_W9pFzPX@<^5wkO*Jjk2esN5IZ_`mVS%c-Y>OF#LnlfS_#(`vs z3bq`lY8>XB%CFfPz(v#xoy4~C97JKF#w`;c{U&R;h!-n$qD4c%DV!R5%0G9*36dF(`X$i$%JfF*5M=>*C548E@msiE_;+>4)BVBfd3b^#tejqEsTA#X+!~t z=JGDH*AM3smDjk^0U=Gy^uANmie4PAu)-Vu=5XH3PcPnlb9}J>^6&# z&6EHCoO?JHeE4(CzP9GWAD-DsuQ03r@Wko1{?w`6|DQVXPn~q@dG5ddbI)?qc`MKH zzSFvkZdc%dI%$mg;MditSF3jV-7VnO8SKiU4X1ns`5ZZL& z`iR|h=IW^3bl#u(+O@Z`skOCNu<69*>)3Qstu2S><`e()dDlaAQ-ilU#nM3B@sd|N z#HRCB-tm2>t#yR^PFn8-_no!c0q#3z`SLfNuzXebowWAS?>p&FwX%HK_qF1t{>(Yo zFZsTPZolBUJ^G{fd;e*xulMe=*5B^k=iGR?cb~NOZtp(j`qkcl{Q8~Tea`fd{^+dr z*ME0Qf2NJ=7khUbYw!NvQ&wL6-RCSOuhkcQo$1a`z9U6G?Oscfw+nFriBJ9!qD<}y z7*0I|dSCc&O+`po#U>;R7=5ru>-1u{C^JgOfL#*V757oKx~uKucFxn~)5m+#<>)Ar^zKxr?^Y$$??)yB?qxW$L%1eAEAHTZ&ep)l2UDLJupbuT= z^2dP{k@wDANw2rLo?dUyGQHl$R9A>#ZR=tz#x?$GRpkBpyc>bgQslie79#J~$S_SAnOWHugpL^+LuZlIOpmbCs3c)0oJhGb_C`h(MRPwMw-i8W9e5hR9w?qq76jd{50ZN0(@XD$x5P;wUW=pXuV1r|d z&(&Egck4(k7M>iH^gf+X8Gj4WOLtn#6u2LO6=_F3(?tQj%JyO^yCv06;Bu?OOV)$x zY8>VOh}(^q9E~Z6dy-DHG@fDzLIL(jIY!cls2J`LQ!kFk?pW>Sbt`WCdvCV|CjMrb zjkX`yo;^{m$Etu( z$nu!oxeEngP!ypgq#kO~Rq8sABN|0^>AOkgO%i+ za;ypfFxrSX_@pVffpLenNaj^mNJbUhTmDks7UZA0@yx0|ar{sdTm0Auq(SQ3o+&NN zcwc}Bh(w4dz5qntvepPeU>$x1hn~SuL@EPHk9UdE0PYMAAX6@@gE6-Y@_~qEoI$ng za?;$=)ShoSRC&)CFuDOKbF6wIf3E5vnS`bj5#4b2US+wN0JIq?V?{F9aQrm4eW3Gc z000D3rp$fGLT$9aDXa{+KL7s?^E6)wi?Y>E-QUe$#!&8geB!c z=nXg#LT8f3T|eb5GW|L;V&}o06nL+Tl4I@AY((`1MLBFN`yhNh_d4v4`$bq+JCuHs;8|f%1MVH@JLUa(f zMZ%qCu_BSx3NRel680?+b&MpPtUE$$N&65NuZzSZB0ku2sDao1dG++L%zzx+{`ZKe zPhY&bwHLr|vHR`meCe9~&+nalb~R33-P_62Zhao}l-#It4_nXJBZMf(B0vJ8>u^Y3 zOub%<;K(mQZ6#m{1*G8C%22bCmsA?PBIF`zP1QHA90`GM0>+_S3{Znmsbfq0!%iBI zsAgCK)WY=GnW?329s>|&i^26us`gpWIe~?`nL-SKH+-Fze?qiThQM=BoQ1>6s3Vh9 z1|Ui1X@+%4TT{0|JX2Xh!_;%bh~dgH&MI~)bGUGDM4(Fw0)BL5jkokelZQGUm#bW> ziNj*(c*GQ({VhwYu{}$Kc?xlHbu8M_hu)K!M`;OH(?e3)AXM!wnrq{o!Z4X^)O(!H zcIcttN4|_HVQ-bsHIQxvAPjTl3p|KeHE6fSgnDv{Y#svg8(tq8e*8*B7T_9iAVbX* z5FG%^-o-~K@<`2K5JjjQF@d*+_%25i_A0Cu!8I$~pcoH)Tx2^9NM5HQK_c0d3K4^# z+Xb0XP}L2R#4ObA(19Wm!3!a|`hT2s+yG|B5JD^f7O0TNhp;%gUHA<;3l5!nIhG6a zY>*o+h*@Adsi;2&VDpAKdTvEUF~N270&Z6fZPS$r*w`{@pjsYQ2_~iim*qV*?<%FE zZD3f;)GPx|>9713>%W;kbr!vZ$_KaswE(~cJSAG_z)WQ0aVlT|$OO{w~kDO zrdzu|E&nv>;?c(UKYS1D_1akH87Q0K7ojz?CuA4_IicIscFkMFq4{g6VqyxKV~Hhz zWf~ycn31ZzglTJl309hgoWQB(O&|r5(xE4-k=8VUPC=%GwPc))A3!%#HXH?A4*m}~ zBO)kKgu$kZPlYkcaPedydB9~ASQWpU{mtwqDJYc~bi{xd_q;C+P&^<$?b^U1QlEj} z<29=EYs>gHyV?;hmvuLDpkwa=Z6 z9PiOD1K7jble(xW1CekgLcwo~55DRmmbz!)g{CTq(cROmjD+WiA zIGNOsqNY{L*CV-G>1+r{QQd_ryl|5vfR-l!X&XOO^py5FCr5nasd^5mYbo1m;H96N zPvCmj$`iQWxqJfGyH=ino%tnR1xMCYCp?4VlIlUHH*1^a$oZOt^iE+ZS37J*^6mq5 z13$LYt@-DrABa_f1)YWgxfKBLB~8#`DDG9+BTHzwdonyptEQfStLh;dyZ&bP;SAQo z1F^CK3qX|vRdA?c3#gQ$^<9oWDGOkYLW@unmi-Jj%TJ^PtO0yJ$`=iEVZ3=4x{kUF zks6*c$|#2f;~G52jYa$xB64(ajJel5XAJ;W$El_aL%aei3hE^9QDCay60pcI@RE&r z%R5O_1{s#p=?-LRlc8o*TO& z1>p&mF!5lxjTYgE#qbby-UZ~U0X^#@`ciraVi5>M$sXRES0^s@x>+)h8B~&Z3$X*9 zJfLQpOk3cZ2`Vs`y*mJl-46TQG2j-`n`a0w~`MUrV(i$AtA3$4jy8`*X&g<H23(A%+Y~61VvcT2!=3?M2V>h@ZNOWcS;$-1YSkU1o{cH5AX37g zanM|yxOpCpS0otJ0BwR;^g9oPY+j&SrFOtgvf>H5=Yi?_Vq%60qK>Y6s6YrH(X8d6 zd0EnLqEuhS+PD`hj7W&kMsern7b5tft?8Bn9(e^)u4X{_pcv;+J_2K_rJ>7PMyeH> z01tIZX(i}Cqp)?ZJr<#oE3i6U>Mw$*MAapRAG=x;QwxR&!Rsja+4`c%*dW2L&-77S zbFn1T24s=5g$FJV$w6Y6HK*&&y!<*y?^2Z65(S*7@*7cCB3E$G#Ncu)cp!`d>9M5z z>2<;LL4|Y1I6j7{Te04xAyT}&8^GJhciR+f0B9v`Tcd&R7{!Q>E7A)uu7Nxo-94L$ z<_?<)O$`4k`(Q>emSn(M^wH@}jV@H+NGspsG9vmKfI2sdD1QtW%-2X!=tstcNrfx} z^ftY@1TefsLX(f4mhrXJ7y1y4q-WsnaIQFlz@4VQ6q<;FILs*_utDa6V2|O!PUtv8 zkYCpm|UHeGpk*azMw@Hke0JdP!NDxx$D`usRQfmpqw7 z4k;uu{!%$)-Lq{*lC+5w3~=vQ?Z%(66DIc*VqBHs;XW-}WiXjDByinV7)=7CHFEGdZ_M^dqYk=+@XVkdbT=1Fs~e1y4$*Dfu~ zNi#9)xF&R7H&@q$ZY(f!^4&N>yzQ1iFj==$6!XgRi|pgPsSYrV;Oi`rvfY}u(JP0KdSwo$P5l<4-#3ajWtvZT_6c{{HvOJO_>W1nOEWWhSGh`@l zSSrjHwlY8sus;Hsky>Wb7$YeUK7|(~p;zW31+7dmoe6p@sjI=FYDFUntfa3u?&LRCNukd%D1Xr2Dm74W4P1tywv zL>>rBQ1>*cn~O|&3DPLc2<-;H$lS^wmrS%ORpMnDz&hZV@`4z;60TG zVu|hh^(`sn<3eH zfBZk{EQBJ(f_x<^zFZZ)nx%}GYQHdAX^^FLNF_&7qBw}cjsVdb*9~;D1gZ+^@!QFrOrU#iDgvutaJuOT}g@&)L#LhnJxw8vEE2|kzCe#EyY59hY&?o zQot-BKsi;ysF_`zh&WOG)RNC#UW*knVzO)+Smb#P*M~PHVjjx#N>qG%aOAxGqc^6L z>xAyg#@6x5#`fyQjtvRjmBVWr4_A*oTHScuANu+0m#=SMlNl~|YYSfCdTT9{`%os= z_4*ljgir4uh9p=+4YcpmasN&!p4<72<(`%F#&X{>yRqD}lH7Q>)K}?yvw3sL{fE~= zC=VA2V>hx;Jb5^~@Pu3LWCbTNt#`h>^D$v<0oN{>?V4!-V zQ$+4;K9NzGJE;d+%rC9H2kM++K(>9KE#1IkK@Qk-R9$$(;mt=CJNz}f6-c6r)lhzw zzZF6#e;ZiB>;lc?1ml!jkcy9!7CZA}1vhuH@#`IwYkWjPoe$B1@75Vg1hLg}w`7Ro zd7HSSMF^J~xMkxg!VL(95O9bpXh6Wr=3>x^>63nEpt)3L4>JaJDQ+WQO7h~7ct^5^ z8DtzvF?(OU#7DF5{3J?o_T}U6uY7*(XCgiNFxGWw=aTcdJUX!)ZLU%vZLUxtZM{0N z&dKuBu_8r(GB`F*uFie_?(FjF=~!vS#x?(?Bg<8rZ7ldNK2EDP&-Y=xj9t_3ah4pZ zT`MFQThkrItF$p)@qA-Rk8xm0kMa5F%gMz*Kc13pvhmf=SP8&ybrnl6`yh2LEkf5( zI^FOnUKNZ)CgG_<+mOKb{#txF>CC_%oW7h1agxAdWAD^6svz?JFmsg(1$b7YN-B?A zT1$8h|PnN9>o2P)YjP&stKZG2jq^F8Vcz( zAdReV`GKjrdgu53d}%9wZ8$1@Bw{U1=Xy{Mt*%v!Mg>yRtb7jWK=eYzVLqPNU3yTG zIPZY3!d?kVYvBIx@2G*Bqvp|;W|WUnodMV=b*2s>H;8%4UlaNW4uOiKDb#*pugHgT zRW>ybE~5j@m%v?Y49@A;z)s?Wp*Nw}!VAd`k#i)aAipCF&t>9}k>Qq!RtQ%chYzuK zykbkD-kKIz>H*wdRUcMU;*{C0oK508$zZ{Nx;l4^2c-C4-Am*oDri+`{_3VD^9Wba zS!6-LN=oIaim*fSkpz6@4yqDJU)xpgd@NC?ZYA;_d`m{Mj^H0&PK&$cM5UYToyeW) zye zc}kNOup&`X!E83s0AF6#T2|cRs(eVXL*)ZiBoZ*{U3l+2DBmCv7jj6Y-7rX~Heeg% znkt_cBZ8E<;08W~mOwRwHX%glYXNTf{?V7sa*s=89SRfWO3Ba))@tCJp^BvnU^<>} zl#;S>f7k4&0rdRjqor7qe1PK=#9ad_odA9b)iOVG={f9dK6SyOSSOj?tl%yhzCL>y zr`@XduAaMwu-iSdnSik+z6DBt3;_YDHDVX44g}_L@I9@0w<@>BL+8XV7u(xR0BCXVZBr z{pP;Y*80qSCyno1zT*4NTJ0Pg?5&cb{_I1MWY5{Z8&a z=jQvr`>gfXfA=|m^iAt8|L!){-u=C&ti1ZW&sjd01?_e*TdWO;6|g%zLjkB$7jf9zi)`SwgW=aMb%t&{Ja3GX;}r7y2p(;KI*b?8ke zj_+SS`Aui8cI{2){i(**Ug@UR*52Qy6PNFD(@AT+dGm?aChrf^=F)!u*n z`kmZ;&h(J}=&beEe|JlNrj6^De|H;e@BZFXR$l$x=PYK2)fauu63)=y!4huwzdzxT z{)Qd6m{i7&PgiUEZm44}|873vEdFG^`Hj-7bk>VkhcA^beY!kZE(RT2==Ir`)9J`m zvcBGy1@MNChWBaFgm3o01(CgRjVrrWu5xAX`ky&xxg+*+?JXX~kp+o~iTODl)wf4;iMpAUxS z{L5}in7SetKS`MSd@W&WV@Wd}0;hI(c;%0e`)1;PE_2@M;=su-7pJGKnQ;FpEt61Z zct0_t?`>*vVC&|0jH=+Rqc{2wbye3p$lBA#h0yA_N8RO4q1AD4g`7PadGUUoIbOpQ zQD2_CoAc79FBprPPal^g@=nzue%e2L355M%mp32#;(f_On-JSlR-hVV2nk(ejH41?{rZGHIlUBAYI9R+qDzxA5u!)Ds$Po^8rCZrrN?f!E1?(^BZ zX@*$te0w$>ME3l2a{131XG>2{KJ{r>gp5lY^V<4YmmNaT%I8cm|*Y14v(3cNqpWltMe!4jSj5{?xCqmq%P(7I5d|OjT>(fi?mivX3LA@Lfy!E8f z`XVoa_9!}7iw@-zJ5{xB7aKxp8%YXKJK0}V9n2*^`lT`yrZRlIq7n2EHiA-ekSXYT z&=|1C7^VIL#J5r=%((T%68r@s(m>ozs_c-70VuJr02aX-35Xs5ClUe#R0@?E-VI9? z?=xM>mV|=ur0Tn&HsYY_5X6*{yh@tJfnoqD!J5$=D&JHzg<4Rmf!0D%N3v36qe|<1 z3wLQ?Dh>NEm4$s65|Vxg+l0)sRRR2uLsVM~Yj zgQo~?>xtk!pe4jR2NzYS0;Gogv&LU^UUj|b?yZCcA;_izo56cUOobDL^TkvRv7Br@ zR8;E&DD7%dJyS6Z7^(WP71BWp6$c;9U@p<*V03||723QlJa|jjLQkWvqW2zt_#xE6;N0v06&9ZfL4OnCBcA4qBcvY0?BBb{yhpF;y`@o=z)9wl#36)nZ795Q8qgAkD%oMkWT%=s(Ub#2Ba!DR5VB{3M6q%wc`lC zRZ_9K@vD%yk(mG!5%5~(D(#U-JF%<1+_AiP8S?M!=;Zf3=GbL(LP;xeC5gF4_~Ux~B~8 zjPdEUw=Q1`A*w|~<+fff_Ft<9x=>QnWJk9mS20D=n_({@;V>f@4EiH57WzL*6mVrF zaZd+#04&`y9f1$P9=td75bSgZ)bqo+`+E#5V8*B^K-&T8yi~XycuL68RwKmbKyYjs zX9EoZ+!(36Zz0eQ!0tl=g8X4*tFjw)1fDhU^6cW`{NgW9&n*e|dl0UVr+<0<_4AeW zV*UaQW8t*Rvv-$&`MXmWzynZ?u`z%Li;Zis>#^tlt^-8tLN@d*0uCFFG6)-vI{41X`u$9cPVFLXc&H^%01O5`(21hDk*?<@rT}(7H zj4_IHq!Z|uvA19?!L}?M-uyZ)1U%OOL{if&3BN#2INPd$B-sIm!4AR;LA=B1U~xe+ z!zY81#r@TTpEe3K09;Q;w-p0h%A5*G(&!L)dOf~_`2t=R%%3O^drP2!9fp~ultBCz z<;F}04urXB-h{DW&qJ|VIt5~;cb-uK8DwH;lUNBeWInPO8w!w(@0;F#Xe;nBQlNJx1+y}oK6fMr0wYIY9jeBdXPA5sg9w3~LJY5{D`3^4 z7-CdYCra=G)GpZso`N?C9h8;}B@^U3%sqHqFcB+Jw`!!hrO6$r3p^ab0GtUz#bVqL zTognoKmee?s_4qtgbWgVa!~CiP8}U+8L1nr2ByWTur27Vnb<;>o;6vwwkxe<#s^pz z;zhs0ufX-F!(h%Hq6Yv2(K#|6bYT-C_aWGF1Qk65?#v=PA)5{nUwBF8Nj=`S)p?Lk zyI5S2SY#j6sn0!|N(*|}P9NGUodM=PIND1mYEY9heKjfD8B)l`Jp%&2$P&hGc|D@j}ToiZh)(6xpZu@ z5OTmr*!esv<5UCsVrtcok!V84vrT&R0h*Y;X8~0M()tF>zitG=#KW;5h%42F=m?lm zIN1rQ=^s5xdC)9*cSjT1*zi@2%}ceJfkcOu_hhlh*3RI}omr zUlOd~*GA=F5di_g(3nFs668+$Fz6gJ#>OgDtbL2BS?3vlQBT%K$uD4#nDD(4kj&Nz z4}*rymsHcB=#U}Mk>;W)&|xwrul(2tIB&3D1Ls#ZwvJae2=HH@w$sL;wJ`h!pkFz% zwn44``fnbsZanS}{j7oHh1G9el%ES9FNYKejgf(h9cm*1SDAatSTW?VTE#e^p%z`? zyPEwn*buPXZVObvh{Kp7kKo)v|L}_Cwd?zfRIZXrMT`3JQ?2ip+UNRLm;mEGQ1Ud4FIiIm;ofGaC{vrW*E-!Fv zvowej+!ZY7tL zV}VDG$I;B~d$4V=U)XWv9CwB;{CI<_<)HA4-#}-KmjIuPiRs(p-r>xl*u~POtzZ#~ zEU?=$uo%R&x&)~p#%3P>kxUcZFbU8nvoMEv z6b)Mg$XxzS?~PX?m}|C%a&2jQ?`ey01=D2eH#?cM!B^Sj^d3yhdW=~vy)+jy$!$DN zKcr>$M4iu2{!<_0E+^P$!ZgVMjT-7=F-lqN8s8|}%)DBw9&~ftQ|IF{YL)~U%exb# zGKukv5_r=Sm^PP(%)di|NMq6ii@awrz&#sn(B9sXRpmO#pAmQ=M-;JM!$v8y#kFb4cy0^bLKH^PIa(kz zHtMMXH=`RTAt(IB?}0^^*f9g{L)bG>6e>)Yk%DzaoNs>`r-*^ak>jwqbe+(T_YtMr zJJ}b?pui>9!lkL(b14aDB0vhWCX(*@&WlT{S^bp`s7Y$u1#h~jugc1&w?Cv$}`pOHa3@L$q#GMNuR z;$^lZGZ@gvvrEDF*DpfESsy-rx#%N$o}OS~L{2ESj zB5f?zASs$ogrz*rI7l2yV}`Z?SKE8x6#0nkX{wNs^9qaIEO*r+;@7mR_Z*W*8#C|oe%0-~bcj}*hRm@nRSoKr1J zAxAN1O$-CE0lrys1(C5eKpR9q5>X*e^4NzkCDy$>>NBPc&|b_!;?v4Z+)RV8rOVO> z6DOJeG!XYf+$U)9q;=is5_K&XC5jh$n8R{bmI;RFA((t@BXu1-z za<{!v>Ix+MO675J%zSc4jH7GES#Skq(<2#J2W;L!T7zcML&oRt^_FkGTo%Q;_HpRL z^(MPddV{k`dXfua*2}kHAo0iq=Z!+MgIPt&0Dppag=6NOBkb);i0MG-o=W-4eli+D za6K7SnI#mo3~>>Hg3Ri()FEU?C8xMUh$RAIp(zt3UN(@tTf_;fWH^AVvhK`A=x1I( zo-GfT)2~l+qBJi25cOxyo{S#ol_3X`Y2-)fE|RXgz6WOEFsvIvxL^#yLh6tos3Uub z(zV1(6bMWp5+SAq>>fR_jZ$7QTrJT#C`o9rWbT-J*ZJIF^L{tjhIc6oYjR>z8PtGh+Ca)m8_ePqlL>@6YIThq^?!7D4KtZ0O7s(}Rf$r82 z69=%aNbs2ZrdaP)M8T>JC3*&uW6PVRK!LdLEhsT5O_AWqF_-c2C}bOS-d5Hl$-ylY zPDxd#Of8PrtAsK#aKiK&*o`)7y5Jqr#YjXi?FU`HBAk340#nw3FY6)ttCqD(!>x>( z2kr_Iy^%EM@T5c}vtlU7T?zdK>IjZf?s~aC69Js9mPPO$RVw5*YM_NQy?d!6YG8b~ zBn*RhB`j(ugcOK)>8jtz5#*AU$O2yofrVgbOlHrvnOnszMc-^u#Oj;5Ld(^j(^hvNW6uERVq%E2% zaOQhaoM13``=)KlCr=FJG6|=SnvsPpo{8v}R!Yk-dcPy%LR3X&$K-$!Dep)l+j|qj ziA7rURa{PASOdw~o8^%P8JT)tygW;YOL#~k%HEk3l5UbQWAzhP;wH)$mW$T!(MDsp z>ZcMtCT&kK%BdH?TK9I?yL5BtG_t)?YTl<5QoWTzt$B2jB|^e8%-K8*NW)3_r>|Bx4v1*Ro#Fvpi${WQd$-JydxC= zGI_LCI90q^qMSm80t?d{-l3)EJRKqZBXI~zBV7YQBZ;NHEt=!iD+fn+Wy#n(`W_{1 z6?WxAXQuZAc)2PvQv4EmP*I%PY9%kuHq8+F4CZmQU@AHui*BnD=BxPMHNc5770Cbg zMFc7&>ynGE=61ksyCw1HM*SmHo~OQk0aj;McLzZU|eP zgoVEr`HJgh_8I-AFE!vQmW%=)!NpTfPPd7`pfRJNA;F64;WWy8{V0QrCIyjAvo%m|h3jt^ z@&~XuJq&6eCIVTy1iMn9cO_|X7RqcFXjkv?q! z$c-i-h1Q=a0OyB)SY=PBDKzzblGY%4T3TIQM!tc07vzXIC0=wMcxA?bVGP8*+jxhu1b9t{!=`y79O_^s|-| zd@LvE!u(ulky1cv6fSLDXTlD8rJE8{#OWVA*^szOUw4)Q@c2NEQqIo{uWBR3q56@r zP3GEo>6shbMd)N!I7tjJ?8DHe;|y?QiNA2ay(Ix%HLFVWvc5v9r1mL84u{i(#tcgX znjOtgq{tNi_|j%?b9M{x@is+c9#*4(SOW=@uKl1(RbmwnTK zV2B-%4l1|Ep|SWZ)2?R<@68`N7sSBOilOl<0IivuJu9mC zzuN#)q3S0YC1|Cvr6ks(Abc%CX978%lXRv)oEhaD_AZqU_~8!UjT`RBtGA~|A5ON8B^CqkuWS!#uQi-$#}~L$ zpU3$VQgjfO*{L7De>Qy#?Ahtd#eds)e|hzl%Q#S;tJ|SGSGPlXu5J%h=jx7u>RjCs zsNYrMn*(;)hXp>d&zGOJj@`rf zc=_|r&T`|^j@{z*>5Gj{Uw(eH_z3UVsc!B3{b`>jAX}g27|7}48V0gm zcVHmf9o(_sOJV6Y){u?1%|Y$Hyb1@|r;|(a1bvvqT^{YeIdAJ4{MgpCYR3d(?9=b#`0>%z$<^7r{fmo}-(P$_yL$8cm(!Me{(qGCYW4o(8;_D7 z+<4qUz5YpBYy29sHCI;|K`_Yw6*DS!(~)Z}3*{Vcig19N!0{G6=cE?-@pC*ZKdP}A z>ty}peUud?D*(S zg!#8dQDWzcTVw5WrdbIyxw5W?6DTENh7!(H8&)Y*^`M$B=Oqs(43ZnRI+CI{z=PBu zXovgZbS9Le0sf=NRbr1oi^?Fk&O8|WFsDqeGgVeDp14LOLFlW>mna`E9e$(R=^r7Z zrvW|?w5s(yAhFcGIRu`c$0^w%!3!F}3-i6IB5bzERV<~dq=EbUPKmcAbZW6Npi^Vx z`xi%Va3iL3u0d2|&xS^wfB#XTN(1Ka{eJpx{Ia=}hp?#h`}rx;2a7Yj`ug_U)8D7f zPsd+Ywob>nNAFHPo?P@-##X=n-9PtVf;y4ocY(`Zo?LzS%i+OaUjO{g8`#taCq;xf zlZ_BO$i#}P)K8P51Id7GlRx7;@H^mYD5_9Je4uI}HP0qCj})QhvW`eWFtgARIds1Eb`lvQI?+9o4I9%j$E)S zQBf>$BOH{-@KMDIu7}?yL#31gUhK{T{%{@*lVDQBX(*`iIze-1g3D(zb6GX&xj!O-+s1Iy3s=ib== z&)?skUUULHUG>}EoDM35CORnz4>0b!_ek34yGmuUA!Y&k>l?4d!Rm76 z67U8ww7RvhBJ`GgY#~r8(Oi9*3L5 z1iY3;6BLW;nG^-h&{95pUYWxx+OSDxkB~YHEYKGb_lJ6fB#1N&+bxGt<|Z_&Y0DJG z^%r=E-t|eKF2RrnMASnxNgBt*!(-1TD0x}X!vrT^-Xa9NbVz+D36-waxl`32pdWdw z>Z7E`1TzVY8wYZ9!a-I;metS~rnG{TTB$f*z)ui2NE7UzVmFD)CqQQuq!hQve}X(@yl&orW6i3M8;#P{ehO4ge-WqRUWmQ|3{a3%4yj z6%Jp#CAr~QNopEhM!N~Zl@QqwPWrJ(CpK~WiONn8P| zq>-m5ks@t9Rv`mm=s8bcMaD~y15DvCROc(}Evq4S#j9~?vT|N*HAVnhBBS94pln2( zUTMo08V^jQzm}L`%vgp-E!1SyV8JLfiLPR#%g`xEn{J@nWVMYu2qft?02fbN#HvE> zar-t!YY9IfcLja9Y!*;GKzC9?md{jgBJc`1A!hYu1Y>K!?4;(+MUq_N-pMOJlW2w- z%gT8adYZf^19PCiGMWk}>Lw711TIr#o3ROr=}D`a$OQXlgutvc$SZIwOaj_jP=_`1 zP_ZNwlT4pJ=DUC;%(yG^)Fu^K4h4Tfv}!|=+=A<>7fP_|(b@^iUWSDJ#0M z207}8dRl-*`C)#nJ22tvyq*LXa=_!z4cEciK z3=#Ez2^p0x$$Aw@u!539Pu+rYpfq^v3x#unhvneWkC(cUuvK7XM|qqw`fAojkqO1^ zkuYp|NggL*efJMHr6cN(#$wX>5TcxRMNg@JN4OFa0D)yISMp(9CrexI;t8y%bOOAI z`kz%25})?9GK~YBfv=-NkXW?0A~FDh3R}XusN{gS)xZ-88~_vy6le<-Ju4z|>}AvC zbjj>NKFB+G&z#r9oLMwbQY-KToRt#t3}lMIi_Nd>h4ijz6o6OuS8a@mt=_3yw+^2N zXp&<9pg=>1wY1EE*O)ZCRWwZ0u7BX6xCIzLC49oBDuGrdl2Q%%WuIXQxgyuWm(c)6 zLk5V(o;ZY;p~9ma6(Ucjm^;sN@k%7)Bw{44WpGH18hBFvs)-ylm0P9Ysw>J!Drf=< zo?LKsIXb0-s2ECP%J}-2QXHh9^&j+a)i}jo4H#_bWJZZPyE=o^5rz^N5{P!-SWYZ+ z4#0ypp`f8oWX5rAyvAzr8hGtTx?HZP9yulez!-hdWM(Ifg&cQw0(;Cu^6aeNDc6W2 z;HIREbr#qVIeiELua{=LH)IGvPrJkcdXqRWwHN4DftiQJ*$ovP#$63f1CmLg?(kBQ z^dPTbI%=tv-K3bkeWg2Q#sYOz$H zA(a}yoYk&9@8GMPWqyelW{R@lB=jA_tkf#Z3`7r8s0r!t)<se4mX~V4)(8Bd5={c1#Ml0!daG#wyKlelkHzcoGKhxe=-J zt1W0);xb}G9=_5Z^alsO!8z9DjV+;iV)j$!(-UaoD#8FfOR$rFXHYAq0pRd?hA^W{ zCjUr`4}vSs1gwFw%JtIW)Ge9tYPKLTvtLfHj(>dP!#C1buSRZTlEbwrYeyDO@tI1K ze;1lFrLP#sNrG>#oOeS{V*T)%yud^RZx>;5C>wb+{SUW=_2A!6P9{ri`vS0B#b z|9JJYf$th)1Al+|+bfLx-_9?l{V%bAMbpnO&Of#P)rX6d%hT}}fX-j1PksGS^r~OR zf$=RX+hWRRCs!x-$cv3L=mlS%9?3hNUd`J+k|Mhp$5ccvPTpO;x;Xn6oTbBK@7Jzz zXeB?Hef;R+KcW`{40K)P)uv~Ve(_p}x_BJZf$3pPpO=yAO5m#L*XJ5I`m)eDelq*{ z%f<28=YL)Pdf~vAi_gCtH#EL_K`Sdrj;%q6o>r8+KH7?rt4F8pp_|Oqp**nvKs`z< zn7|Tb>|`3Nst1Ax{+ClKJ4&Z4TU#HB(ad4NnbTfsxA7Odi-+T=n!2USw3Fufb` z=1{hlH@^FN@vqb0r_XS0QW5H!(DPgwc|UOvM=e=$jvKUp^{wiql5$nwP)Rt=<|N(( ze{Mri;-F&D9>5;_B?P_s2@sUm%GWHk49#DlD;hEXf~@ESz)zmF+L|eehaxW1fVqlu zCC4d}A^M9}Yt?A%5bnOJj)|f)B480dDW1DU<8uHca{*X6E8BasTvAuS@sD$dd4wzq zT`@TJ(XbSLTe4E9jNc@a3hboxq$7%Vq;K|Gm0RAkgq8Ohf`s#xpJ8qVzL7>YIm+>H zdE`A>)HkA$w=b)z?40ROHSY3znrz}>jp3lM6 zfx^^4pl2(=30TC$tx=_TCpQ-_OIAP(%SRM>NiRcpfo+NmBv=qNs<#1YmKgWXK;HYn zUEqZfX|^+_872Hk12j3hP zXz+z)Ga(mhKx%sNaC{KvN&kdo;FPDOAXlT4>Oh9Gbe1j$Im%~OO7-LpA=hmBUP)%LpX{PgpUGb zfFQc%+$jALJc!FJAh+sJh%TrDF%)mD{FaDMQ1P?|IF64b|2@BJxkK!fEsn*|G@Gi=;mW5(_+=kf& zUaka$wRYqRz;dNnEwkeR0+Sn&W+1TyOt_)tJApk!|B#nfnbW|4p1BKwf@_2TA3#}Y zZTz%CNFXyqz$FBM1TB+w6{~qa0!ZYTJGGq7cqgaQ;4EL01uEEE8?V@*+OInH(io2prr%wQ`t z@Y0W5oZ`Lg7|5ee!(dwQl8+C|BSQufAn+AEBgb2jOFGI30MJ8o!D)b$aTSOI7CJSa zg&Lve<*ivtj~qrAkl7?Fx5ae%Zs;;eM)+F;T>K(Sqydp5oN91rz$Xva%;(SHE@5)i znRI-JNg)O-8;lehD%zLP%GBasS}BPQ7HV0VLl6U`+rn#Q{5?3}ox(apg%Qz9c@&Tw zLAC`IGR(HwCRU;u014Q43tB8!EW0RN&@E* z9KByJJk*1Np&ruUHnMd(*>X{Ag#piaZ}!W{R|-5r<0|n&<=LlSkKdpDYrX-b!c2hF zfxMdm0&N)hvREZvBt|{jbVkHbE|z5kcLvDR`^3<~FE_~KJd>7?Er)fLbO@(M4Gip6 zS`gA)tgd&Nnp%ZUw`juTQY#3EbV6||NB zW(dS*PukbW4P*Qki~%lz@8VLNGa= zyE1Q01<7P#n6=O(T>1D>hqCAXlbx8?LIUMsbB4^+$RzXr4Ck6!5OGpsHs}jD+ z9h;mu#vDjSEjtPPhE4~FV39j^7$%lkZ?-T$@v$1786k`lNKEK=J(;Q$2}vz z%LA(sNrlZk2p9-~Lt#Ui&Irx?>|TdH#*)Sf(;0-ja18N|D@Z`S@=`=TxQj}_yp-0r zHAR^285lmenL;P6S3{{`8<@niyaY}p=;f=+jZ4LVXPBH+xX+ui3WSUH+`V03{Vmg*Sqpr&c zV)*D%x_J6Jk|!=f&qI9{5V!zePlz35;2PCNsB$s`LOslQVrmbil-2V@>;?yra($x| zHU3NR5$_cnQCE(nTg)UMX^q7z}l3 z@FFmDN$kDFZ#kE4PhOP2)Y74*XHNpvi^WwRL5x$XRoMP3bq|YKkqH|OmJ(4?NkK}R?}Yafo<}+rP20>RNELM|<1K zf?Qdg59jJNl{k$JR5V_;*z>t(pdqdv!!OVm-r}1S<1oaKq*B*h1^^?r&^-lA)pe9V zyjtU0&u|L@)Mz#$Mh}Z0DUB@VmnDXHF!#tSn==HHUTi`gtP!u*U7Axtg-lb37Gu4h zAh60Q$_wNLymbRj+^q@aEUtMmf}}hzmi|-n1bL$7shJQ4g&X*tIGTwIDN>mjq1l*1 zlh!q`*tyT*5;tgaD57qKD7~1a>K071d|4()pAlDk;L27l{w>SHd(uBlV9Z1zC+LL% zT5v9;5RWlkN<6B2tbWV$wx)PtfAJsxq(Zu=ng_yJK&CKFo4E8Iemj^K!y&j)=AxF6 zC6B$Js+5h=UuD?^c*vT11kj0>$W!B0DgQ{#=v8pR=w})*b%FhI6F}WnaEjc8LFxyR z?SR4N4bN5ZZpFtb4H3XklNGHeZevni#vrwaYs_VHfp8J;DFIM09{KeBHPT)sEf zHuSGlsTRL}eDH66#LM&dUq7Dy=j^ZZzkBiN%g3Sr3MP2|`Pu2!$=S!t|D07Xk?(-F zV{|czc9jYAdf+@G$nY`%o*2~LGuKQSsiKmHt+>EFa0I_knOl_%^wk~p)r`K7!Yv;zgdovSy3&LNqCO2mOB&@MMj1y-acBEo>8Eb_>rsO z;edlAO2;jL4v2UZR#3G}85dTXQy_A|oiC%K0ZEyCCejS5BB#q03{>4j6Bfqu?>YRe z5xar3!<|t{^SlfHdE|gnup048TA3F|s9dqM4t#lJd(&v^>vC8;=T_b0%@zN_KZ>av zOB_g4oRqtOCxlY5Lu0lIBaLx^t<)hjB>$d8c6oeJ)<|%n8fAdQ+v*wcu2t;H7?*4? z`~QE8-Dz_hN3u2G{zVQ;vOVL78*c;%f)s`u41n6cAV3n5c*V^GX-o6#-{({{HWHHW zj74-;HwxXgWM!T_nT7QLM&gXe%i!P@cWM0$`!oBnOAsI+n*_1s*HI@?~p@Ja!Z+u=){_=O<4z4fNN}d7s@bbk#C3xNJroKzQB1%V_ zljOQu=TWxH#@z^O!4qGw6XBM+#?EY#h;i#ib)p>+Ye8knhjg{rfl$-P68a-UfGy8G zNh1ps=k+~-7q^rp9YvzO=*@5v1OqsrAGBhJt&zk~)JeuK3eK;**W|bf34SxWviexS z5-!1!V3D^9g)kF3E2|hlh*n1xX2?J@$_~b?eQKGotyF+n7OoB03Q|l(<1-%IBqI+BpZl(au+GL;Y~R5-N|43dvo&Zx2^kp)2bMeSRk z`qp3k0Sk-lnc#?GlLq$8ceDqLTj&Ex5=;jQL`gyDfyEed(0S_ND}7>gQfHIm6jDNY z^?*8%c-H`M6bmL0PBVz@#Q6yR=~kHHo^gSi(4gSzyDajsj{wP9j(eOnqT!%%K?g|C z5Kb)BKVw9Ib&X_E#fG#BIE^zbh-=M-@mqpS&GBGoj5W3$)GWi4L?R)mju?x{))(0(88irCfPG#PsyU_Vc zt{3AeXcB2=%4=F=HzqfiG%e1&&>z4To4Llp$myiX<3NxSSm(lV3L?h6lSC=@C?lrJizoP;s_Sv zipj~|`vvccR*K3%FVC*gPD`#pSu}_Sp;1;p>ngG+9ov#YWh4WoUJB`seUYFbD;e`8 z0JH~;SS9`wqXkHUVszvXiPxay1f@r_3t93EO#TT0SphE*Ap-Man8;F)Bu@T^`V?NB zU_F9c+_wk0iPtYgEeKvtJn(GzSvglDG0c4Sb z7U1*-KNy$;-fn@=kI9f^t9b4bIy-J4q?mBB+@#TY8Bw5uno2jT(P4BJ{e%UQDYRtc zNJFb&pYr1)k;I@&mMrNO!-}DhnV3wIG!R-g#mD+dYiHQTJ7-=SCSEe@s5hZm#0wHC zdIHidy70WP}10Tkv!y%ijeS$_P{#N~fw;@=^P6pFij_Gh-iXVr*2J)6^ zERn@lVMsYR1MM&sMO!GVD>ku!kZ8+TT#>ZFeWi22{vuDf2a`+3u!U*RHT4|F6+@l0 z0@%VGJb$20FC0jkvY(T%fKQhWok>UkZ& zo>bN`T|$XY%urylz;56~70^0ja|U<@Uc96?=2=-UVsvOD)Bvwv)TGfnuT`GnMp&zqVo!LV*cE43js8S zN_-w!04;2~glWQjVicA{P#FX8QSveik_5UckRyQ9#B!Dj#55ye170Uc8?+jSFx-em zhH}bGaNohUe*Qa)NEYe{x5*xgJVRp;d>T$CqS*}H(7HgF4uGxZ!Go7R(l?#i# zCN_wqZiqX?S3$DUU9+cYoJK&BUKM{YSx?Rr8HN#tsSu4oJybp*ut+Shc@u@43&d#V z!;AjMu81`j9U3Pe9Gg6>*z_?gjJKl1nQR6Z+(-o>O9YIfwpxKrQ!(G#DBv(L2oFT3 z^o}wD5QZKy?g@Pj#!on}3PAsHOvRl;%mDBRE9g8Dp+sK6txREBFL(o@i}xuT4dpd5 znDv_q`WODNx01sQouSi^sY(qHOw3VG8We7IGp+Itm zVgw+eLboAgj2K`gMOgqc01d5x2h;+K#Y`MyH(Xts3P&TrJPegwt({N^el9h4L#f)Uq zXj+Jf)JZBBN~L|lcxy;0+AQ5MZN~!QnIvUUSkNG-WWTT*Oq=i$m_=li)iPtB2PgTS zRDgryZ!E5Q^5t%FetqtEbd3n-^W^M*XXg(izy}^#%I8=4`*?SEd;c&``SjuOe%87A zbZfKu&(|*Y=l-P1{yb>M<@x2q)i*^V6v9(UIuiC@Vww#`tB5rWWYN*6l95FYTPTT1 zULFDIF~-4#Ax6f!hUHLl%tc`yY$I4&QV>}7iT>~}4M;!0=;dmlU9qj$iLAqTp<*uZ zUCYUdSDN7MWXv_0w7P^*qWs8k059@plFiUMjBscjPCT24^i=_qIE#ZR%L=C}nrq+zG%7(HJZD zphhIfGR!SQ_y!Ayv>3H`Muwp*aiqO@$ir^jH8kif$xPRDd-7I6L=s!Ir|+4_1cTan zd7-_@mBPrg#_9;8-^?P7eTDTwo8vnLSArBwCGIo+Swvd>C0c^MAbQR0qzm#H#;grr zire0Tf_028O&3crXp->pUIg)%a0#4aQDr}A6w4RC61z!a9I{0!9IuiJ`kKFaH}=Qc zPZx>Tk76z!g0V|B8B8oigN9v5W>f7!ZveHhqi8P48p(W*Sj0-s5}`x2KWmgy9`jG6 zK$zheG`{tH+Hvs z`rYq-E0s=GThTrJUMkJg_TCLnhU3cA-tCQk`FXKpcuN8QKiu?xsitlS{Et7D4fy`? zrt(t%bk|xJE_d0qYHWME%`I1--n8t}A5^op-SH3Z+Ok3D(;wUDvhP+k8V<^4W=pdA z(`xE~^7k3JzwULsbs{Wz`fK+rereeV+it4A_QP8iqwR-JyqZ?qe&y+glTY9IhrgeT z%bBvR_WBM#yk%n=ZmZ*m-(Ixj#uk%L-yuKo4>$bydR}8~(rHkp0=G=N(!w9^}U{P31%bLVLtKm7KhB{!zV z(;7BL(zd2HVt-gcX~xwE`c4%;zG2yrQT(2)HEXkK1W-X!M{P>=Y3A449AAf(*rppn(wT_MP^PM|ZV`%FQd6~5p zXX`!xmpj&GM}`~7YpJoXcgWLg%f5JeVdK@FUtGP_mTRkt@$_QtI)4|V{`mCpaQUF6 zEq-)y?IoXoarr76m%d-R8~_{l*C*V5ZDUYvd~5st<$YF-vHZqjac#1-xzNMwyL+26 z&n^ZBo#v~5e~x>jL)KR3bTaDiAHktck5|Uq`LG<@t+zB)c8Og?qL$;fn5|bg^lfj@ z5cP5nGQ9ISzRh1C@c4AtvoCaKchWoV4&M*P1nh2S?CI?A+Z(>=4JV`1!QilW)H@z` z4wu)uhkXSwV5a7SmiU-Pr==iTF3r#bI5 zSF@YRd^WrApbPU`d7|aO14)ePqxr#o_^luYi@i=%(K zGu^kkQ+~nw*+tu~&H2@(SYe}f_V9Rjd^^-3hQ^{`b?R{fn1%t?zxjAu@0mXZgDd62(#na*N0kgr>0IbfBg9I@@wv0#N_5t+UQ+MHMQE{g;G2?m&!_+yT1J>Elg7uaXf3k>z9JPfE@r zUNZz$~O`6QhxY;@o7{~((mxc`5 zG!Y$8LrO%CluVj2`vadDtWS*DYH#ok8z zxL{FP`jL#1JR3xht%)v#850jLOfZg%?5-nXTF8;i*lyyf05)cGB#s{lxVGUe;;z7L z{9W({jMFd#lEa7*KBYIn9puVPY$s|Oh8CZ22zq$}o-Hm?J%`K_QbO2o*_;4FFn~Sq zSg0+>6sVT#3TqvND`-;`26$Bk@kYGy8A{nsrIhU;gmYlZY=xX-;JwT?sS0!mY+jCJ z6bjfg4@3A-$YmVLJS<_&0+od%=v#wDHwW_!6%_*NaqK0a?>MW)^YEVI%S*0^uPtl3 zgA#54PDegcf&5~DQZ)!Khk#WcC6s2wkqol9$q^2@4w4+7$CD>68+&jq2~Us~BNfPe z!RI6tGs!P_k2Nw^CBG*sfl|;q+5rETpjcEk%#v^dfWiQ84(s%7e1xb!+?V#niJw;> z8Fv-BVbY+Zn8s1f6^f&&ZMkRh&yj%=8lMVE%lZtVOHX@p8Q7&9!6?8D^3Xi#eD1*a zj9mT%V+)H5R4j-%S$OdIxCMA*dR*e1j3UxFG7WLhC;Ao)&P{3N@)B_f7%ZF|yyd*_ z#;6GeTNi~C(uWbdgT5G6VU#MhAFr*RV+1w7|!Yz^9eTfjTivD88XCrLk8Tfb!#S@gw6=W=#vm#;S$4~q(lC2Ph_^5C? zkUUWN5(|VlsuAOT=c?B*VFM$~5}-EC&}BGQJXG9M`5p(ph*vZwZalPLU_X9;D$s}I zZgRtr_u|SCfIJDx7oH}sgt#H=j0idrQAh`f7uwaNLf0ZGkmnk&EAEoRLAOi3gWZBg zg^VW@5-*GF6*$SgEx-^nsN~>8PSMv`Fx(lxdIHY!P?*IblRFuPg{Oj0Vwxm#Y4Sbs zUuY;JAyK4gSp*P5rs1Okt{Obh@J!7kggKrkj#v&tVc2pWB+3u9BiY?8bLbW!PjDqB zC{b^!ARa3dAQ5ocnU^CgPDiA8)LHyyzYlHP?&%5PW_K-)uxGtqJ#F-7^z&IlcMHE%`WWc5acG6weAJ)N7bWD2pOWtUZJa{~MB+~1?k>y-V~B%IgIX31oDls&O|xohFSG%(thf$w0tNEW zgRr5j{gzOfTD>+`moNjK;cz<>>uoh|k_e9g=juiblcrx2i?bx5QA`L|9Y;$;$1`l= z5>JW-p2KX;=?&Ft|6GB4k<;Wi>JUF40u%pqLf^v9jsp+9gKQP%$}MLNL^Y$o#jUF6 z8=Q4KHUw9KAmJc*3MQ2hbR>dK0{h8(`6+NYlKl|ZK_)KEE@ym3pb9uDhNdSHtDIC!+?c3ls4B)Ay=OOypqOs_D^xQQ)AC3omXJ(+I7TYq zo~B`HTk|e?okG8)R?3@60Zk}$oSb?sDGu$mOp$E!V2Gxun@7F9P>836<1}bPe4oMWcS>`A?uUB@Dw*_K)z|XgdT^0!a;P zzI5b%Dvke_zDRtuexY>;s-yd9#;7aOg(L|;NWfS$p50Cwf^TrPv5ygl!%i`94e{S- zTKLG4s%0ldBf+og95hT)kTAf3L9nK^W|sotS=sN3`hspMs+%tF1mUVfq*=Qx!7)9o z>Xb0cw@D{li&je6P054*vl7R4oam( z^-MyrsFx~cz?gUxqF8+J8;wtuNR9yf7+h$~Wt_55$_X?P99c)Q`(E~pMyJNx#;IgH zP>u1tW!*HBMOlQZZX6~wmyThtyzFB9n<oB9w>Y|*j+x_`Jpra^0_Y?7 zl#}JmB(H^w%<>?Xm5WHD9pHK6|9La&bOIX{iJ8G_!Md|@g_O>u(slSW+MiItiZ%Zd zj-E|jQMw5fLkYDoG`r;KB=|`dJQtxBVaAj78EsU!=myA$rlQ3KL1tj|@3-MW9O;osYo6NTRn} zD8z?Zy{sUFuK_QZ9Gjb2OcEiu2?j)jHFJ@0L@(jy7nYnv*JK-IoKO&1MxiIUkvRq8 znW9$Tg$5}3zFcw(7LPQHNNC*S{Qtw+7D?oI0 zc{Z6Y2x*hEtJ}XnUd}(mA9tD){|F4b?_$h|pU1kQ1j!|0xTsWaFHl8EQcWURWe5b( zMcJ1gwLWpVR9&e(1->Pm(L0eXCOXbBS`Zs23@U4+#692zltZZ_V%kz=FsT?nXgVq| z3KDS7I+LY<+Aby|@yrCXh_+bh;)+po*&!jCF&oVYOQj$jgO>!7Ofk7%h*#>60fb~E z9us+x;z+3I=UA#q*GwN`w$dpX!vc&@7}@bEi26;knNbV?Atl6R_JXzSSw4i@S$~6p zP&kEx;H3!|=!KYQF%`#}CkUsX0w)<}7O|x6H2g}&1}QUzT`;MAGo~*_v;a<`i@Bd{ z$7W=n0JcC`WnQ4L;%cvw=DO4$Y%~-)Gu24PUe}c#93lrzkZl8nkjWfcb29f} z{nLAiS$Y#*stWc`{^3`0eI4_+!kixOG)JA$!OO|cNj-YCJZcZ#P5N;5!HK`4QgrwD zF#3!edNkT;jwk-?4Y+(xcbX47K_|x)sNsu=%AZ2%Pl^JiKPd_pyVI=T^Pmny|8@v{ z=N}>To#zmG#WU9GdRP0biei?C(9y?R=+2! zZNUQh_QcPTZx8td`QCwG7l1q9_KwX97BKiua|7~Dk6-?kuLbVT%?r4@45*r{l2E2s zi9&!?$-Km3K>kdyGiBeJ$<{#%rlZh8Ng$v|^jrUlbTpX+Zgp8VR4das>D3u_mX!c_ zEa+rlOaxc5WVkU)u)G$+21ErUSx$oW*jVWk8k0`=mJoJ2;fAVkP$FqDqr64R7kRdGdus)e= zS^XLQR&f@7QX^8Bk|aX(WpYSSkUbR1NISwREz#Z;TJ4fn6wv@@0m{SsmaSguphVPC znzQR`_~;4SrcA<{lfc)K>cKEtfxJz+h_oeu!2%MJ08Y?cA=i*lV!Z4M3BQJP#>Bw- zhG?;xvX?`4rt8`$m4vo<#$IJY0AOmOAipVZvrh_okIb)l6D)5 ziC-h=LloBSx|n9g#H3aFeK9%87^i>@AdVm!%D--6)d{OD6)-0lt;{HRHv}!~IpmX$ z!2YCwh}OYxXRXt)lCnma6fMIlnGN7*gpDvC)OW5btG*2A!1}~@C3gZ{OpvmHOhI9p zvK*)YU;(jPtVPhXnD19!!41rfgs~@|M(}tGoM0*I8?6GNnsMpX^g~NJJ4k8bF)BFnxKexi>6(ur70oq2|%4?g@;+ z8kZ7;Dj+-}NRqYR3JSc}0K&q!782=|xIk9B$Nt4YqJ{?EU>AnroVGVrO7?S)0eqq0 z3)nJoXsFr7lTt~Ii18mvx;aIoO>irK9hO7;X1l`$S*+{|jmX$yAVvdE`f=*3d1|=_ zLrJ-z!K8140D>TUaz zb62fOoC6{eE+X1Md<_dOi4huld4xHOL8Mh8v$8`i!=DF>*^qu4C8NgHRmov8%gmz{!X$9?jO_KLhd+dFu+qlr>A5J zSsK^{8Nl%t6UhXbhECM53J5bjDjZ9)Hj_>E4#BYLNp>+TE*3V&1J@43j6I$#0TKv6 zFl=#VSu-ht#)(rkO#sG>p6E1iWheu@6Mh3zFLpqh(QJU)CboQ8uV1xY}^e=-_} zA)@a~-uXf`JnEneO}gXZa3@`9KXuf5U1H*_YaZ?s|SU4czkH{?vnAEmJC`Rb za&rl2DwS*AZeeh}o)pes2z&YYT8^@}QMw?QqQcDUm6lJM7bxF5Pg{QnkV(DM`DFgh zzu9>po5|(HY?@-QiM8D9wY(YiSwEuQQ^!b7Ha6W@=$xQTh=UlkcwvotmcDh5%%XWP zwz=^L*}{$BbR}s>OOU626bY-JOw)u`GViU9AWTk?2Ur-C&Gj?Q zu4y0@0Z0N>1bC6?_P7yM1FeLCL)1i9kvW zcp@wDzf@q|#UZFb+T6!r{%6BeE7NND^Z1fZw1sf#qqo(i@-^P-b|_2s7{)dkp-| zSwpRcwGi^48*4>!mI$K@G-FXuLMRJY&|@nr!D~5(Gir0Nkza;}#HmKsX{h8XP68nP zm{u>FG{txVW$~S138(-zXNjZ2aRBfgN)X0;AQ8!0*#M%iQ^8=5R8PRYbzZlz0^us^ z1$ZY>Du_B;mr#i$)un@3CzzJ;dYRn}Mo?lx;ROWpo6t6s1BRkRIpR{@Dzn->3%6ct zTzvs z>y&iSw#n+JC3sEsiKP)8KvrZnRyH(Q z!9hQPUmO^$g-}=c>)@O)R~3vpJ4l;5?{P_O{?VD3+P2m`8rWu>%18=Fq>X zQq)cy7bGsKKMgV2aI`dgC*kV^u@2xfWw63Nx3_KC<)C-rYJctzP-0MbpX>sV;5Go9 z{B#nY(tVll@roy_C=80zN9b`fw$RQY_FSMKXy~soMZ3&G!Bk>)62tPbKp{1TY(~@v zvzUQ39WT z4?T!b1x-tZoC+uj#=b~c?xxT?sA9AMqJ*WMJcsNRGu*&Ii({_4OeOg>7>Q`?yh5DU zv|4T>ULc%7KM>)TRS-~a1OSj9kduVOlGU3K@wdY$5HWsn8}RfIAzeC0L$hGRF-RzE+oE}D2_?T zcI4mlE%qs2Xscjc&qw=<AP-ObXuzyFLcL%1!WUzB+7~P#u4sZWH>NanV z2FCNSYa^1&j}P;oM~kBu%OeMs3fx`8GBXsr{|H%j3*U+mEFdI(;q&wgclXoG^Vku_ z*j}QJYY-t0?rJ@|zw%S-H*SE8>lfB)@T9l%GIN&CxD_j0=hc?hdw52@U568k3b{_5 zZ3`9vVz;@X5NT!5JKTf^hQbWl!^UC%v2t|0wLxwmu)5?E{1u|x8Eepf{z8ZXasp_j zA&lZs-sFisfPoMN`bOV&F` zj>#4g$(=+lu^pN777mImb&ZwA@kFm-KA6$Tzk++}LMq_!3-2c_PSa2U90Kzp6j#p~ zyM`I7foTLWJGHAM0ofFTP)P;c862X?Mgwov)7CPsvQF}b@oRJ4aRMiBU!z*OAwh@- zo&?g$3Z*fi)=_hqw6sTBS3r{(S=pJxo?CjKHx|RkdWSN@j`VIxSrdB*;6h+M>;c;qt+r^6UbkRzvi3`Y z#=yfHqCg?3yi@^AlFK`}4{0WV0po)u4X5@4Xi-ox#vPWV5ymyfs+ZOzDH-D7V?j44 zK2{8{CYy+bW4dKZc{Z)S&pDDpg8To*=u#?=ubi* z>M!eJOL3a7=x%) zk=4hrPNF*9maTKp35-3J(}RpWbN%0z$Z%baC?hx(B)MfQ9D?5TMt-q6hQJ15~E25 zMu=N~O<2?@JbEfM{%a1?lb;LY zRc9w8qCiBzM%WyZqd*xnF9OVJLA&Lkkd{k51RG}@tdC*KrC|LIp&DnKL^L4uv9`)4 z6TB9oV-X?lq|176u=g$}qZ`Q+W^#@`kl;V)d4i^H|`Z=mq{MLv|#uV zMj_b%0<~Qedb-SrOiNawHHR)EClMnZqXAu8#yl2BdedNKcqTIrNlZl}V#p4nI=Q?J zbD|~LsAZ4z_VOy40dRYi?Qr`W13&q8VsD%4*_6WL2@xXZO$?$?*pa1M#n5#iZ3u!g zl4XArSc3}iOuVS6ooQ$J*W*~G(#?gNNL>`XqB2?vrGxl$jfAnMEV!kZIZYG*vz-$` z%ga^9hfFm9e$paAO0q1`F~e9wt`Fvvd^bRts6JeF|JDmcIn!L`DuWuA-gtx&duuy!#Q|e1S{vZEAQu3NDP~<_>?m<~HBq-bvb`>N8I9rc>99K&6fk~SL!$&O?+D6+qTPathgFVU!yt36cN4=Q? z=vM*vJ@rPnG4_R&f!#?UNS>AsmDI}1P7vQlYs?M_JX`c*Z^iz$DTNBcpCX6C{502q zcfxl7USY!}GKfN0GSUZ@w{}u*$zl`hU&zH0=XD5YzI+Q96hv0Em^88pGKkG}+FbVF z5N;(#jwOWsMd$K{jHN8h$zi1e4MBogg?yVkB90$E8~PcNR*V|EX<;uB@+OkCA=zjx z7;GylbVPyVP6h&j2Of`1rvud$2tgt_>Er()%{HTu!eNRNNf(k0*yGCkLI^<*f5= zGUT7_zY%j>O8Nsgo#an2_P4{%pd9x`i$f_1GgT6D{D8a$+K=IV@bb1=J#n^PUIHRLSbr2OqbkP8yKQu{Tk>xf~RiT|TJCGZ! zS9)0m)?N;WgdAA2tnvh+iiu_Lv}GAi1`47#K!SVQqf$S)PZ(1{A7AwHpO z;vD5C*{3-zLn7DFJc2sfyP+>rM2$Z4Hmp||)~;w5N)3%Ov!Hc)#cyT|SbyngLAH{M z)p+J46Z2s@P&;LQ&t5btuz*umm|u|)Mz)#Zr7SdD$Z?R_a)R2I2|{#Zm=eDxpP$n@ z2D8Bh?BePtli1wM9t(BgHQGFP9@zu33T{M=pfJM|NNsolq?WEms1TWU7NXdY4^+qE z@dwyJOR-b<$0#j~67qtkZ^b0HtscNnOn&ZX0D1ITa51$^a1_CeZyR-s9Uyj%EJp`I zHis8j4)iR06J;UWEGHi#DMv!YSV+mm2!a$6zS}Tlk1YA%vE9T!YXZIpgu^U zMq)qhlDUKwcSd3_IIS`QKn3wF#{@{Qt3oVRJ&@FHY#1s7+5s^L|Fz;1SUi#eM!bV@ zit5Db%AR37vwj6TIqYzPGi(>`4~|B7o<{;K5?h3Jyd;!1%U;-6xSdo5`$d%^|xT`c#+`1Q*TDic5!t zUK<@iA+jGr4;!JZWCaWlgX&M41OZz2IZ~{xxG#wrwh|BllEhPD!beW(TaI2yza`kmv6J%{>xeYcL^52UK-Or%Yy1v+K2+Xu-Xy1M7$^5ny0 zuYWl14V90^C-tK<_U)5Xc@tht-gFN85;2s^tMZrMDS!2yav2SVy)CtDD}A-R_XDTF z-r?HU4tno-Ef2e=wXnSAv6l06_w(swwqS@(Ej=!1h&ILr zX=|Cc<3VPh#f4Uw^!rvfG^Q|kjO}Ep2D2Ja^f9xUkQRT*&=3M3gC`qV>?(N6pcg<$ zvEcnLu{(uv#|50XUWxK@#)hxv4HD>3f3oXkDYMr}?~ z{$>3a+Oj-+e?EVl-JiO{`>jqcm&N4`A4}fe_O8+DY`!`ymF+ge*{93-!|Z-`p~~+4 zl?~)CFXZ3M>ATr**uGqRf8=` z+E!bfrj|uxEy{f9Y0;t)7q>hv4)Xn->8JA4`70-KK2mJxI5JA=frpbkxgOhAsvdk! zQj~IEn+s;P8e_G5HOlJw?(F0A@ham|<(-R*-Ni6z!-w$rWlO>W6wI zqp^M6r&sF8c~W{E+J+xHyPw{ie|FHlpXs;b>Gf=Un-Kj?k|YrV&h~Zc`pNO^@74M7twH^5b$*TXviM>if2;-P)APs^ zRT*`g1=o{@jT=sVyL)?cBRhBf#`WYv@)rW>NvW27R_Z}L+s2Iq)vB)HY<}0iT_)pj z_Gf*&)rpQ+UdYqu8@G|F?blAu=XdL6*OS|4C9+E`zf6jDK?owj%+k9d5BwEb*ZIO%)e9PaO#K%E>3rk0m`n2bH zUx)XzPu++6tBIUUZAl&F?MV@W?Qw5DZ;y0td*p}OquOd~m225Y>xvTN`;Z)Vlm~7P z!_hdu>of`RV0Jc}ZhGm3}#&|K52xyge`3 zr<5$kW4*y|rTt5O_qlMFS~{;EwEL^$ zljR}1)4S=}<<;dw9u?bfb&{*Zi*)oMA7)PGmYZiK$8RaYqu+j^$o39*n|j)waNd`z zi)%Z5(M$C{m)6g2FYM&^aCvw2O&!z6uamg?JI9*!_380}9AcDogvVazNQN?{GAjD` zqo~QvQE&YEq>d~?ulVejc!>Vio!i{?yK%Pf{&O(SswzEfkWMcyzao}3fJ8P)0$x!* zxFP_0^gWn}z)=Vmh(sP5ND&j3`OHTO45W+Fwb=aJolPQUAzoay)GT0-Jt)8yj1>4K zg1C4EVH|97z?Kj*cmQ>kju(&!RIUOzK0_*eN4gwVZGcl|8E-HmI`((A3#H@={ov4H z)JiQb3I83=4{u;A19E2tsCC5$bP;}k;DPZ0f`(=Y&EK; z3X&5fQG)`=>EeEy`vj)T$pK=DcOJSD~jpE-eQ`?|IHYOdyCmG3M!OIN*8K9Z63&pSJq47r$k?%mQbzTuI)ligfYsm{wtt> zp(>;u5S?vgRS%L3NT57PA-n?a5xp493KACa0Q9xF7%xw@6ARxkm}{Z+h69xVOUgF( z8{ZwHUIiS)wr7>!0{jwB5^bE0Z8H`QEcsc)#R#Uxlhp3TQ%4g|h9hQb_D)j)S3RR1 zIZ{nRL&OMWLqZSXL4+*W#v|zf_`u`%Pa*;ST*h(u$O#AX1a@lBD%kC?9pRCD9sx`l z2Beh7fNg+q0R)gQ&^D(V=T*R3P(H{&8(?K{=Rldm4|sQ}2=9=Z@fI zk;noqOhZevBt4W0cDOrPEV@Nd5WGWwV+eq(LD3;KbFF{~xEO&3A~aDrIh9cN(7*(S zzU*P!qakAY11Ey>aJbk)H-vKXOw0im2@4-C;IVXR>?6?|1~T)`J_;%@GPobiMe$^i zSiUK5?+7*$5Fi{Y^eZ?SPaxEdgBwgMsh9*A@mpyFyf(?t1O|aOc%b9XHCnhzvHtko zGEO9VNRlEXIkfZGUg(g>r`ht-D~OucavB${9_-6>!e`LJCzpeMYa zCr(jtuSAgXFxiy~@+Ay|e-FhAuqDR_8Dq)-JjFbPRl+?OYL`YdN7ca791m`?7}=5* zFEmyqpS}?**ck6icmWz4XQyoinUuI|HKM5sM->5I*q%HW+@-vZ%xWwJFd-laLDkOS zoIC7g^Cc8M13oYz4?rlxCRyf$$!EUcd4z6)y^?qj)Oev|?0)21-USRxsJXEQnaYM% za>M{ueNP2|MV*v*Jqg!9OehwC`oW_hevT#aa;Y{UY1v#srX&t=VL%umBubtjPF41L zVJz^*L7hw3w~HJZ_G0iV0J4;ijt*N?vCgyCH3-XeQ+@Vu zNeU?bXbwIjD;Tp1puDh19WGZ5z>U8lylP~WByI?wOI|S8x6b`J8{BY*cmpmo@F$46 zho^|Mk!*RSG_*g73d7$oIirwQg@0f@B#hn~0WKhElO)T7-~z*9O)5S#{tZq;?m7?8 z%Z1WJp5QTNdo+^VlXB;3Ri5sog5ctmq5)PJ#9QVy1A!=qK3J(l_Hno&ig557+t}2} zpohH}_Zo(z35q?HConDvH5Fh6h|7ZN^R{XjkTH+8fIZM-e#JNA)WEzzn@J)+o-I$~ z*+Qwp4`>qK!Bs*WC4@+5D2!gj62vB1lfeFAoW}1*921xE8Dd3XjPb%+Wep^;F6F7kC$R@7=O24QRxQuSllDTDi`Pk5O%`(`clmeXfyuUjh(MrP3mJ|a(F55gRxy1z zM7bGoax4^y1BojpM6E(h z;ER{{P&k0+G5;})NLAgOyfpMa#Ceab9;>uFlRA#`izKJM2&E@f@2K#0uE_7aU=smPxb$uJ75)Mqa)WWhe|x=%)Yyf{LR? ztIz~7AdpVHO1#8eqQH5}sNoDPjeFgI;Km5#Zs&1NkVpk84IMQxY~&xJ51ilYgkOS* z;jI$38Q`9zm9wLvF`M7iX096&k8{cfZz22W()geVCw|oUP%Wcm1jms6HB`9rY+`Iu zBpQ>G3~i79#j1e(LRC&GKTl{?Ct$pp^-Zt>os#1s*|S^-Jxm2fkN1gZ20b>Ms4D`# zjlr;9=umOFhRh~VDLsY`gMXmWviF$?4jxZ_wcklu&Loj0NgrueS_{c3z7|X^PKb;s z=q|oR+ATj#92HlU=Hz;kh*QK!l5?59#YIm?l`|;On^YEV_)vQ=UQIg$d{jJ&19g!T zo%h>x)FHwZtj4(LAY9_ZE0{EBM5q(l!V$r)Fb_N}+FMdXWxGcq*L*G9LS%76@z&3r zfDzx^C=_dy46=}-5kDyr??p6Q*bawwECFB^O$D&>pH#p%fyp%VT zdx$d^Y0BD9K~n*AJ^=x|kUTqFc6kpvg~3bk{VW`s9)(kd=|a%jRxTt0WC?-m{7O7% z<^+#A-G>5iC|YwcRrq=RB1wzOU>xwsQH(5RQGW`7)~ZQ!rBm%hg{8P<4=Sp%1&IGQ z2{dB`q_tR#WB@&$V>1JbY7l0zxx)#~K2t$91{hghq%?%Sn zLI5FmY(i_PtEhzUlYoGyq-WzZ zPMK@~nf!jNDiB342&_WRYHvju{6R!C{vPah8I9it3dY>TIQ$; znY!@;kMkP@qugahjPQBu+y=3ZsQN#+T-${w3dDBAQuVNd&1%Qk>NWWg-%k*vx#=bh;-#`G*yT zrJo@AgxVzqF+~%lS&j(05-CfPialiW8nG~)5^02yXC1^X(HI7mFJq6;37Dg3q?Sty zLvR@?9g_f4l7+ozdM1b}iO`G-gb?;Rv!Ev*Q|v_z`w|@t3jr_PQiYm8)j^A*5TZPU zNuMqCKns)~v~sG(OG+}16Rs1HZ@^nd0L|U<^3B?%F)T*-l#XO#|(2M57-yj2<(!~hHMHPF_cHygmtx0rv%7( z)TB-pJZH;Llgnf_fUPG=rjJWE{ zJCV5kgI+TZCtyu*wv z0;W$UyC?2A&Ywk>;zNMU{n;C4I*fW8O#CH27ds+2Ts3dEHyk5QbWsmd+P3a`X-iCK zI-~B%aoxCLn7fZp(&nPYIyU9offzq4lq9PZRuV^h5`sZ{xN|ys-yVj#Hc5LP_ENQM zTB0d^9?R2}qp;TMQK)P6=%9Wa){la4vCz}H2P zIbefDPn(iiSa}j=vNOe`FmRK%Rs{@6Sda`)h5yxo{n&vPjS$lpj84_2ATwZ?_SPmU z3mlj)3gIrrQc`dTbu$|dP|NXe6>^%mRYESL6O4Amhcn^i8U6Wm(AfFaH(-$U{3lV+D`NtR2s@K zd!P}VCEU#geeDskq=R?in}y~MT7lKUq>}%amcT%zJLzbE^DnT$RFDmVX@u;48M3g7 zLqLRriMA@k!`soFa9xZr!7vG(2HGXkjj;q%-)jPQ#cvSgPDEC64~45M)W>-86f8WE zGc?Nt30fA|)T|4ruayPDyC-Uz#vZl~9Um6TGz#@uynQ0;lZ`ideR33zXgKElJrG+F z2!ax=znF{ECT3fbsk2h7hYmu#ro_ZJMNmg)~RAcjTPIM`~sU` zswpg$8qWk5_a#6=1spe2A@(7po<48-b7g_*Y3~}tng$Zeuz|0!QkhWFM~R^Tt#zUFGsSrXuPI4cYhR0lz_s3i>$91>TOhx_+*BZj2`ANU(=Wg8a%FY!*KNz&55 z$+ZVhL7dY38%NZ4?k_?6C@Qco@-$lj34fChIXQBKfirYFd$JQ8TF4vE z8;d-&%|z;nm6X@Q=Iv*AiD`H;0b~{%ZYNz7_B19k84_-OoUAf3Wce(HFxkMX zM`I$$GLQWtsi%(^&=eo4QOTgfi7C$wZ#sLAO`9be_(;km62* z$ELP!(a4AoV?ZM}vhJEUXA3O&tP{8_m#kt7Q?1);`Gqp~ZL+DiA9O{D4YZ0xe5r z*Bdw?cY55h++zeq;w3yk<}$F1rjiI|D2E4BfkhOBg2;oXVZ$4>6f{9|dFsRoh}7X8 z5XWf&g$%%B>d26*@qd6cy@kCQ%>~-hNQ42jL6Nwo`sOyAB93=JfQUq%Rakq$BJHN$$R-p#ru4Mvnein7XOj{q@orq?+K?n@X6Ii}(mx>~7xv$QqVObr>|EWR zf9YOgtKU1LlFAcDg^)CXFCk$bi5^VCJ-?7Ni5fctl$L`M!VO0AfhZUPTs}QZX`%{6 zsTpyyb!JWE!2sdVWZ|;Oi)x7!(MPWjq+>P8TFn5ULmB;9hs;frD^40qDH?$39d~PV z8ODw7OA5v$RRE?@WiVGH527)ITa*ekDrAt;j(*EQNS!jm=$iD^Y=S_y$2E`8)B`jH znoFXlUCK5dDmc8IUhMBRBTT-T&)wV8yHJz6!$U$T0L@^y*StMJ-pawv(Ri&42ap{YH$zKCo;c?@N+g@9n$P86F(= z7`(lKbl3g;bvTB_zo*33n9WJ2H%Q{U(Q6vnZw&vV{k<1$!3#F|ZvS}WrWfrkTP{t0 za4jLyweD?S{NYvJ%pZL9hgTD?^!;D`{?#LM;k(~kHPg0Nd46?p&>byXvOhY3xBWJG zQt+=;!CHqTQ(pTVZI7&OQxW9|ko|J;6pNC{vgF2-$w`IZ_6r$zznShge_hW0esy|t|M=Nl%f_i#wf9&1&0Scq z<(K!@W^(S?*ei3t`8@l&FRNPq0KASmK%JJq8V0TXcYV)u2M=a5h=hl7LcTKl>s zwU@&sGk+0OR7%@NTjruKg*)f6O_rn4><`=u6W+!)hZ?ydJGYc!M`|WhAa-(NBpv^ z42mvUK3*27RHo6VMSDX@OtYzQ`y(xI*`9@!yzCJpR1-hm6tH^LcRHemS{Q*XsrYsC z$It!dbf@18UiY;D+_>K_qrV>gS+-oB3~w%8OztLq*V>YelXAJQf0vgRUs$`n)Y@;( z`pt0mvA;FZU`dqsj(6rK#P2?2ynL*touBAuviFfz|1KM=fl&_*8eQNWIy?AKh6 z_71ev$w8|D9JC6+LFVp2MX5HsR`6bwk1iu~z z`svf`V)w8WTf#i%5BHDfLb{ZdSc0U!jkMmp^cI(X<`z$PD5>1T=aY|{XCh{9FA(pQ z4rkMg`a3oHOHsKxdp-M_%l-cxVynBTtc4-W??Cx^|& z?CN1k(QMA`gMKA?DaVt`n~Tdhp>lHhFgbtZlDa8py|3bP&L3VA?^)$e-7xMNRindZ zbRcYYtbtyfdZbq`^Y`U%Xz&zC=Iu>$Q#_=kjq1}Ve7^3Uf9@* z{%MuV-RPfQ*!nOfP4V|0X8q#E!>nI;_Au)gHy&njp?cQ*XtFNv`|R>wE^{rXV3}(< zC!^tbP@Q~>H`@44YF&P3akBZHMQQorQf2A%k!a>#RbcngS5q92s zlxNes6MNDec8;5_|58Ek>-iN@&&6(gKA7DL4vly^ZccCbcYQ{j)!+X#rN0BG{O1jay2!%@V{nlt!j0GmLw>#UWjN48zRg5ggRQClbx}SIF8e} z+sgr|vbS@(m&0*qKObdD%0USTsu#GYt>bv@gk>~+U{;YXdm z_gOzVSU;&cUAd`_l#ZeJesVfU)V~NQ4NwM?G{`TwTHE3n`@~_K8 zv+jJ;o+r)d+x%g6{Zs#Bs|>2v%lEFHJ|uqcayHKmD-uQFVyw|8>iLIBZ#X+YZ( zN9CYDznmXEJUC9%?+yy^QzypSWEu7b3(bTPvpkErq$|f zcrwCA$sAvnT+0>BpSR^7O}uN+#^(&BxniS=#4r?VDhc+Z+1g zAk(gv@BXo6xU^-|{S24(Y-5D9W$l<)-2dX|#TaNy+7YohTOTED*^^Q7$6|E+QAfug z+U<|#gMVNBd-um$yR9Fu+OU4lHDl`!-FkDOAL{{GaC66-?Y2i_3RG= zhR!f|kT(8ft&Qtl^Zpeyye0oGJZ#vfV_eUqEp_m=-Dp-aai#m47mZj5A>PZ`W+@2Kj zRXwJ3teuTfp@!>-ayqBK3v)=eo?$Z@SN@ zUp4orF&oA&cgpAW{8M}Mrq{h|4;P<`{%niN=Wue=r^1hj!sFc$5L4jxE}I%UKRMac zpo?-E)U&e1RUK7Ax<@UuZ?ID{A*nAfl%zxBVrW9x-67t{6&yrbJMh`8Q*VF2m>&V^rp+j?P4k=cGhLdvZd^5(YueE#MG zF}_FEY+Gr!q;~iwv>$sPKX(aeuT0D~f9UZUkyQq98VdvzhHoUrfZ$u1@ff zMq7NeWp^WTEvDM2VR^xs+YNV0<#^6eOTsnS%imaqC6LC!`xeh6xqOpuO?wdHcnMEwy=b zE^fb)<#D~*dT0N*E&JoN*ZYv9a{6jf7_B7O{YGK!!o@A=v&C`ymfil~H3hc!ZoK|; z{o`GJt;v%5Td%cWc~V*psI7%i5gTEI@iL$(m55&Q0~hduSfDGb~6rv zF`dIx6JxOifmnBayS!KTbew2_HC@F?Jpb#9^B!1;2ecN|)pw()uDcsWb;aE%sw?M4 zQC%}Pit5_8QB+sGjiS1&Z4}i7ZKJ3zWgA6x5!)!L%hyIxT~s!T@?0+`n?+?=*eogw z!e&uf3YJAhEfR=im{!HCLTDsld@G$pqWEB9tT?F3?u7g1uwQ}%P6vY%B4T&43&?NU z_>1pdZoi$JkV&Gs^}E|Ie*brW{Ij|GyWh&tRY>G?HSNk(Q$3}r#7en3PT^|NE3>wI zySR%9Gz*433T_3N7qZLR=HkM zHM;eZs>-dGR2^=;q$+RAlBIICEUE8R=T~{Jilx6kIXVFo_1KwfLn2mv8-@hYfqB*` zLm)CoN`6*|q6Q_@PT<#uemy&;YK<2zWmRZyl(ja-W?Ajejk4CV*et8!b)&4cCpOAjQ)08M z3f;}JDs?x@s@UBut8#avEbT&@-z=*mY_qJ6vCXnN$~MYc>tL;HIri4dmZNW@tThLo zlogTxq^!vNCuK$GugWUH$N-DYKWhH=Z|NW>a-^DT{$^a?NwnrvxFvoLc(4Un9rGAO zS=sG(#|bA)L}op%M3F--j-`6R`wP~$g{&I_FTYVd+eI@4~Iz7pvFr_WhcF zr2Y0?a)prp@~=r3#iW1azU-;`q$rwo^Y-@s3j|djJ*d|7=Hg)7?2KO0J-u#)%^U?Q zs_WwlU+nCA+)!9nMOK%hc9&91*C1E9Sn@#{osva`O*0~GBXFDUdvpp-ElQ89IZ68= z#tvT%TuP&pN5k6M<>0@UHxHBh+1caem5pUxS-6$sekrHL$-F!(OI7jB)Qc9bwX^BX z?aktJG&_%=GFgOE{^9OD9l5slyw)Su{k4u?7n@L33ZZukwM0;BS)J1T8yDi8eRgGx z2mHJ=4&Z0Eu2%6HfXWIeSbY8M_yH~ZBh5bPcWs;8=pV|;;w*f%Qk;6rK_Pn#-FW@y zB>$UM_-+T(K9F#R(BzjfxcpJHF=bL&L?-D+XIJt=ygZvM z59c|&o}TCL1SapZTT;u5f3Zw{A5@0Y;^YE^~Vi^F+`AjG{ZLB~|#DRK5KZmT_^A_K){Y>K$v2j#E#u za&$Ob%18jY)ANsKZ<^cunO}bLJtb%7Z<=JA9(Qgo@`SiYdEtizE9OmeskjuCyvfs` z@VW8gw$um5Emf9JwT0PL8t38L8xYLf&}NvXT%+aiReN~2|F-o|pEx>r+l=UP_1^Zr z_WIj065g)f^+pZmG6}ZKGV3Nm@z|PxfC9 zJG=dNb%4wV2M)s$D=+%4Ir=(r?!jm6$9t|JZ}6^JI3X^}*|KYW@tyqIyig?j#nE*B zMJ10{?*tfM=c9A{KFqIv_kBBSDR0ff*S)~)e{CU>IO*422o()^TAG`R3qAaR@QqTi zl2+@I(hv0a%Ejm5?sX%9_iN633}O;&-~Ah0kSxf%$0u(_2k$o=?YL{i*~#%?>Ue#+ zhqre;DW?un$siYY5Zs1d-apUSKE|3vEv-Gh52rGt=H2_!6YtX#?`u!AAMa{Uyl;lH z^IHz4_ZfPcYcl#^F7MnwUjFaR_`{Rx-hU_y-Q+_EEf41}f17;5@oHD*A|qTH+*;^x zu0j^$$NbQIQZ_yR@`0!Hu6(ZF(cXV}d;8__&hJgbFuVVdr}@x4{5)yqFZ_EsY5tmC zJ9A z^9y|&w_}IS$N9XJ{~=mRhX78i&3$~L_nlmu3B@&7k9Kb{VuQh!tDUPmN7M3m?&#?9 z{{Hs^y&a7p0*5UEN;GKqdK7{sj3> z2KF2u?f$Yl{B?1-`f~2kf|W>~lLeDJ$LZLyPb95;%+4?T{qAcVT)KMiU&&itzTA7L zVWM zCc@S2;;vd5)nT(ZUru)I%~C)9ywH~F8#TIVv%o8E79_@F{TcFOVNgg1KVQ80r@JR5 zs(n%tQM>k)_TjKA%El43c?X~FIC&5`>ZPME#-PRwv ztRL>ay_i+smTxPqcC@1xXMc4PQr=My$?p1q~K9b(N7Rzd04^lcloBK%&l6fuj z+)s;4swX#99k~49aCU9cD~DIJ>Bn+l04*M-1R-WjW(1Z(y8dN*xEK(noJ7B4f#N?; zjgeARob`*E4E2`lQA&*V_0!7vIXOlrnuiZk?xA$f?iVM8hrV}tWd{jSe6#!Z9-X;< z`kLu$@pz=Q&$6*dd)GqOe|DkN`mD5F(CRw(?5ZaI&Xsvt7@vKit*(2O)c4P>s_r{i z>MjJ&zA%6Kb_sqe4R$@T8^p!!bvel_E(e$O!|$M@&u&8GE9GJ9_pa|AAO`EjI%-R) zfk0CwduQ9O)&aHUYw5z(FOqR>fNObxBIMQ!S`BQoXnGMFEc18q;HA@^7J6)(X{>It z@@$;X>LNM2KqmT&uALnR(6Y*dpl&@a%BBO0%Vf;i<;~15w(T~r2dDGd=>DM|vjLaK zpUlhUrWNHYDbXL#3(HzHoc$&GW?t^RtRMNp;u@W;7M2cMG-xdLKsHw9mM-&feH;=wp5E{ASrMW37z7oDDSA zO3IVXX7_7F)TPaWiv&QGr$`V-IWVg3C)d-vJJ8F9%VUXkusi037)H-~7pqyNQ2kzSNHr4AXp_ zHs9Q_ZfESAHJAHGXHou+&#Il_V{+Dvr)SOl%AY9HcQ5fS>)|I$l*7lH|9;Hp*%^qo zGn;d?^v+YwJqGGSdseOhy4S9>XZ1UB^sIfSp6$-&DLnCSe*I!H^Qm`ZDyg)b&gUOr zy{bpo^Iz8QPsj;7<+J90{@=OjcfPeNiW%9P zo;NjI{$DoBdc`hkcIOX|@?`w7JeFo9#~D0j?Bt}IKZ}9CzdSJr5qWMVb&U~`0z-c+Es%egN!|i#%%Zo8( zFC}S@zbG&FhIFHzx(2I5r+Tb^|iB|^U?h3RXJRCZ?#2c z7o{{m3u?Lt4CQG`S!t`yB1_H2EwzI;>n&%C?kgu6BBqcHugYnEUOq}-eK|UtUKp8F zsSC?>-Y|b>wc~=m-;{!NQJWju^CD~4&Wo_k`SM98pL2R$KkmNdWPfjuCbM-;6CUDqWoMm=)nTuCfWvT0yg1WNic(tPlASi`lFEN`SmybFg|`jZzCdZhPE5ZhN*`#x}p`H@PsdhCVHxe);dx0?OEc3gH|%lY?JhU@uHJvWr~kiTRyb>i`? z=Gj8E-CgwH;=H-Kyp{wA@@J{fwsVW1Bc6Sx!}ouI*|7EEV(zt3wz-WeQZT7DESpmy zN=i_wMG0sa;5+#;E88_zJ?D0LS_uY_c69|wQe%mNn4Qn;czV(OJiYletHXK3LMJ9j zqV1Q0-#@#bcb48zb5QKrwL4-_>HXawgWrro`xTyG*E3790MDkMS82 z{{n<37vI2WO1BN6H8_57d4A)ryZVtIJAM?DqUfgmY=a@Yw^zyL0-~7zRFng^ z3=4u>?5ouo>F{N?Iwj;*CmayVZwneF0?5a8wJtt%?h4<6To3q=$ zlL%yTPSIR1d$TOreGLc(gO;Z%UKIvjd)l9`6%=2~`nBEc(rRnP1O(Qv>)}|@+MyKb z$@SCSoB1Ts-7paM0~51eH`kW4pQ$a}Smp1_{5`z=`>30Pz5LNp`S_SWzLt;cyUTo- zoB7a6`3PK-PinbdT~3{u)1oQWsgu*on`G{8?#tiq^{+{Vuu@^mSytA`?r>*+QkA|* zDUIi9^X8-Jc_~Pz8q6!Vc_}5_%csG;JNv5~e#~!0NiVDTV)%`|DUCL3pG!v-$|{F3 z`p37m_-Oi7Z=HXM{-WLZ9qxGXL_gk4hIjusXkY)Sjs83@0_3w}6Fhx*{%~YxW1468 zK0;!F4^$nn6fu3|n%lT4FmpD&u4PQ9_PFn!+mhdB(~69eboWqa%94qIg<-}@y?fLR~u(%Yp3n1T+Ud} znpl`b9i+2y>jX0d@AGf=N1wq>=Rp)@Y59S#m!K&Yg-KJ0OL~VA zl;>=;GA3=`H{fD5!m;8S5m0l0iCsolwJ{=6-qXsYZh8==6Qc~}MV63}ctk#V5JaoW zS;Y}F-U$>mG7iL9J9=KyfDwqITFoOl? z9xilp@&Z}1J-Lv&l>UU{w&(vEH~&IuL8g~w#8>?yIR4M=|Gr`*k((EhYXQ#mW9y&w zBqzunEF`)z(eP3wCZu`XJQg@X z#)2@K@Y{8T@SZEo;%zW?K~3C*VxXt*2+x)%jI9!u?e?3;49*)LK+&Wy8X|aGtR^C1 zRFRuZri=u=XDL(E)qFW-*oFC~8+i7NYSJL_YLat|&o;y9>ozu?gypV*QpFjN7<>YC%Hh|&Ifvi%bhU+aW8%tKraJ@_@&r)I1@zqI| zLo>Z9p#H$xb1+)$q$69eos90;0UOnZTrGnKS@cq0nJj4o=_ z#{>-=qTLeEIKbD-l-*2ITtBJDJ+huYY|1#CV0mZ;#3T;a>*hI}(7F$uO6&K%tm$?K z(_42v$U%kE&G*yqTJ_W%m3+42iBBe7%DoLmr9tXUaf3+2IH}l)y_4CD;6>(Q;5v?p zoU9d?!rqm7B@l{%fOvnBc%dZ^uHI-gNlI&8rfF%C+hnE}IRi%*Nd08CIb~A@R#f8W!`d?%jUF0M>P`KBx(?1&77{OmeY_=G3rlL%A9K#@8vw zZF=fvy{Yhilao-wq4Z*Tvl64Xzg#jwBoTaB*>GZIVA7t-Q4Pzwl83mgH6xs%Oq^(f zY6F^=mZ&Or) z+4FHC>LVJGx@IEy$&BB=cq$Q>aLy1SNpy}^NJoCRvpT_+Z z1{BGBOzdZd1wLV1tpj7!csIhqO<%kwp+J_ynN!jHhMRAqAezqaS&s0=0|#}$m^EWrxmWDxRlZz7Bek70lln1 zbV1|_>{+Ob1yexLS z`#{lGFGmYpEyLZ_&EOK(8|aHfMTx)|*-VGuSc-fdPKPsg7&TsEm9|>%_L5aE9*Tj} z*{P5(z1Fi+R!k?`?dUlt-mdp)Pbp`=IsZ({(!0>q(suo(f8EB-)k`m?P5!c-w+R~H)Gl*f?p;&lX{R9sVkNL-y*ty5sN*F{hS4SOwrE>1MKWt721&*H zWGb=8ibqGu=MS4C6W2oILVe_TA(48UjMd3gKiW3cj<;#X3qrSgn{E+td=1Zjv*}hu zcpUhIM6h%zgpLrzx>!j|h%?(MUz35}Nlw^rf+}XNMALwT^1}!htLACTKDY4aZ0fY6 zOEktOoEuDMVwkCvZ$|P;HJq2EnT=7_eKo_1)o!WjiR?^y1|XH z@=+n$7)VFesKbX%f zamdvk?ak{qHLYbgDn%wTrlUeM!(>4b)e3@Ewjj0}QX>p=TWVmITX;aAdSs5#2r~EO zqwl+-tBqEtQxFC!V@QiSUiXLtVAbzakN7a!PQEb?VfAMRzA49={>3gM*UPQQ_eo|#hR*caCP$tJN9(Ln3Z^ja}SZ|C9T<_E!&c zMp&5ZSF@MsFJ_PI^*vcB2s(QHZea%!M*F-u!~jvI#$(1-tBw|k801VbMeVNV8^+I! zddKU%r(f8ErmM-0yoe>Q!e-r{!F_9 z9PKxFCGk;U*hd+Ij9QZB&1Zx9NhX=YlcB((S<+h;kg zWMd&b$-5X^+T)zMt#!(o%5COaRa%~;Ue2kOXOG&-ll0H+JbP51(Ric2Ud-IMlfA<* zx$sFg95g~YYtyzVQLjfDAEqLVBMHcz^W;`e5%cNSX@5pXTz~UcOrlRVry(@Oq@B1J z&7U^d{5?roUpG9bns#jvU*E)UoK_=%;qt!+wg5!<|I7b8Djo1z6|AN{%!o z6j!!Ub2^=_3JEDWCtrzfszz<5!agI}BS{>EGN89Ju3!-ii9<;kxNs<`DHtfLbgRkd zw0uyGG#CPZR=d_GgJ68ahdroxKThsN+C}FK#gd*P6 z+X_X%W~$43)65z{npXi*9Z#l$>^l)4rOXX0DZLKix}nK1LYDV4U0AcEB&HX-f-uu1 z2CPh3N^a8vW0;XN1yc}Mp;e5Uk}5RDFNTncSjM$NmjR|?`Q(ZnCKS9`Sl#7nyQ(J$ z+?u`-$fz(uag(tg?{SL1M1|9uEj$-evQSh(rs*?LVM(N+;zBhQwVp;qQwx;`7m2qP zYjj~7{S2%h<2J-*VBzO$Dt0#7jP4>5I>WcL9J zbzvj^cZ&w^k1^0A|8ggy3ar4SuutQW4W>R0bZ89yZ6?)1eJ@mZo>L98M-->*+)~?2 z_fnPRXK71W}1Rj2}V71ScA71Y9Q71Uy<71RP-71Ux{6_f|N zsY_O{+el3Ts|vHlug$2!autMlTm>NzS3!utRS-gNIr7IF&LPbWL)gP0A9IFgz-lo< zj(LJw-XYuN;NW|A-IzMom)d?82)q4IN@0r){rtF2M4qcy&A#VF{LlN*aydHs-^bzb z6s{F{zLYw>#F!q~?1nESDdX?>Q^+Rr0~X5u@V89=`CATN-{10ZD=qh2v+;u}{Vmg(CRS6k zHv6sViv;aJdEpkG`)}FYR?STRLNWf<{*KFU_IF&aX?^)#=xkq3bZ>>1)4ig9$K@6M zTdwsKU(fwU|IE@K1siH>X8Qx_XbmK!>}Mnf%N~(o?|2zwCG(;08V_8(dWFVtwCJRm zs%Sp`LHh1g-kW8MyO%hansr4+6DP>TQZT;o%Axu8TQE_@jk9aOM+b?qX6po$up<_; zEgG81!a!SKh}dx~S!prPihakXnw1nnO(43#;OWj&FiAQfk^F(N4k=Gj8yxO8m($tK zVTAIV`Tav+W8<|csrT?S!(mB#@XSCOlGTSbI0mnUtHTg>x|Wxj|)|_H&w=z;-u7 z`|voKp-@+{WUv5b!U#}`IT~pEuxsw7$ihND(OS1_G*=wU$E0w=0VGq%m0dU7;*9J% zxR!F^7B>+xR4GYWu%D7t0+FJ3Jr>I@mlKO)ZeB=%jSIF<8?a<_D;rf^!S|S-6J(Zfq zBFgcy#Bv-hu3U5+SsdTlO}F-od%fN^?frDeKK2KoO@=5ZXywqtEV&GZl)GIp)qQy~5V|4ZeyD4iE2`aXjkS5oY zv}8FzW{q=zYVHl=HQG9be~>Gq8%bw~m(7FW@7JmbpXC+}C96D+V0M+Unt%_56OX6_ z5r$ANvhr9zEhbpW$HG=q&PU?g63haQ5mVn2N=k$1u^K^{pB&dSg_;sBUbPgKI?YM$ zhkUK7p@GtN7(hk5-OcgCNQ?t^wKJd22~%d+?PxPamNwXC?SS7c<{KkMNd{BI>2rc? zADJNo)YMY%O8KLD@ihi`ArC_g^np&>C{){MrLZ%NcJ!nID9;A8z=N-i#gJ2FQ56&d zv(4vjKd}qS>K9Lxa#CAMHAkDYNh%T<5_DR&h2;p63@|vSwHb)?SZG^s2EDXD&em!X z>yzjLSJR4s*LH>dyUSS-;0)|N(d?X26M%}=fc#B>UcCXjS+bDWB$E-o+6+ta_Uj2# z9it^K(YOG+O~q9V8CzzETMxFlSO*(uhR@g9eWM|i1$3s;c4$8{ivBv~ZIbWsPTz5V zJX(L+0un1YPCRJ9{?dpf&i~66LRj1xPS_ly z(G!5WXYE&d2=Brfs~#^T5ZCV24Eq$sExBQ4rqSf9-SCWwlD(8yGfWFUw)rDYml%|g zuw#T%#csPac679vBz|C^T5fLfy<6qHsIf5lZZobnyx2AwM<%K;rQSBXDwU}vW0k$* z`lD%1G#;DfM8h(g6vI`Bzq>Y#g^<0`LPc8YP^7xI=2|f-rUKRH2C{zXZs(U7G zE)iywMkBbAI$+2oWIjw0$OTW!n<`gtu!*S31a`kJ%$2Rsci~2|M|F0@@4pm_PUz4QMy|P zWg*c@9yVT&_wQVQlHeQADf<+sJ6?taE4Fql5DN?}=jsKaEhS=aYAz-V^FH$M^6dW$RJY>W(0k6ww?|28dM@7XWxi`2w#S>@>X#P)OH?(LW` zc`@oc88es#s+dJ z-d$vxMyr%QAr^${NojJI1nM{IH^)c+z?!0g2=AtND#_2*90pKh>wSixnzx8F;<#=e zk&@ZyV%y;@d9zPq!g-ilqCy}VpnAU}5cz{)GIbQpN2Rmz($XAzAK3NOsmjl%B)^l8 z_7y6)M*DSm7}b@B=#mugX0&m=n%xYs&J3`Iuw!kzyPJ-7hOq^#!gPOD!z^M)=8<3y=VB*PDQD{tSR|J0M!}WHqVO5&XFIsg%e`UlkH&x zwueoyR<0(sgc@l+WWpH+JHd}i9D0VxPMS}?JD_zFK}J8boVOxqcg8c@A(lw;K4XnB-l53Akp%Ccy%dWc zx|i%yht3D~+NG;M-^7T-@)$aV`q3!pShZ_cFT=Ar`y#VDG@F?p2bQ$q4D~4G7{iYC zY56QlJz#G_R6}^gIC=Wi+=!hheV+Tg_2g%Ma__mW?q*LYO?c?rxn>s8k&q8iafQ5a z*<`>HWCWjitGJ(wDUXOiXMXZ|&EQn^t-D2_cMy?>8}qum~DDz!dCNiNN_TeM3ir1ftmP;`bLQ=JthV|5gsBRwJx z!i3^FMnkU%AxK6;_lqL~VcdSDN*J-F0T9I{9nzTKOy=XZpl`5;>;n#`UP_NK@ zohn?nm37*;Z6&UdYWEYw{sMEC>@H{e9Fc%gc{2i~;;GdhZQEtitx^WtnVOI8+-|7^t4l&;35mRk zMBl)-uj@o;tv35);%22656FD`N1=Vh481Iw%wlN^y0qU$&$hb6gMb(R`P}bCm zpLq^TfPdEhZezGcK1w#r7DM%pC1yOe5{q;kMHT@$2)wlVb|@z!HnvYBC2c>fB{Qb4 z^K%K!9;NC5r6^jvZWVL?ty8V7!-u%0xB7XKluA=og|-vZi!3*7r(@PbZg?^K(`uRm z9WSHR*qI#=ksp{w*=U}k;@Pr|m=DY)9NVPP84(%((Q6j=AX!XF1m<~9k$xDJnQ?^` z&{qHbmD6N;Ln3!XgROIFHKJWJzPjrTJ}DkxM!g^o?NAi#OvO{UOLE3Nwl?A3XYX82 zRI?d%3P-tIQ!w7R*4ZZJoZD;hpt-rGowq3_o^)8+D_VsT)#Hp<@^%;x5>e6DY)R|n z14ErMK;67g4WGO$9?8tO#1x6bOjA`|K$kT;iyq`w>#EnIIz-*pEh}d4{e)(Zl?cIR z8>3z7+LU3grl!Z=o7Yk_n*9X#N=-ekSsqU_Y<01|pJ_f=wrP6QY*sP3H@mX1Xe}vf zWafwz+O?I&fOkoN>VyisvkElz=VE!ThVkW20E5 zi99pcLTNXC@P@I$N&*o!CnafBhVnaOn4m;P<1(B)OlbxKxg1(CyPED*cEo*UA#8dx zOs8nSmv7(vWRgWD9KC+?rcL?vA8(y7_RBfXi7WS9dy%tTVJ)uB+s)ImognG^V1gcN zkJ}2Uip@)bqu19DY&XQ^ZA$vd9h+19YbZq1 z9r1A$im}C;N`e`ozCuJh_!27f$|)P-{*GrLo@8das}XITV`l-or4$XIXHOJxGO#Ut z{ifh7m;n4-DuaDhX5h)>zi0xT1;+;mSnZL6sCloCZ}kd$+i?Kedg>n{oAK6-=H58sJeweeL zgO-VY{&0urp2;1epFrFt+7ZNEqMbq9Azqt%^Bz%7&K;s3L)<0WIm8{JA4J?Gnrn22 z=tmKEiFTxEhvON zhM}?2UTW4YGM+8NmRp8&V@xL z!~Qn2v|=Jw$Re#U4Nt4}9XbMwgVIA+w_Mv35dcqXB!@37s;hJ(eUstqPkr2u!?lY*8vh9hCcJ0;VY!D0)O}Gs(gO@CB>`cLS8y=fpV_ zPA|Y4oH%%w=0YraihBe}`xb&CkC)lA_2i%+WGj|JVANmSYU-d^>Vuc3c$7(ozhOo{ z#SX-6AoF=6Mzc<-$akJPz|nXH7i#c;*!E_7B#&je?6u#Wk#Kn?L8oVh-ieM4WK

`6kF_XOme7;L533z?D;XIde7>&g?>!=OGc$TK;HBW2iD!3!us> z%^<2gx_nX=ELk6>{AkE%=aTzn3`A^5E+Wd8KmaUgQ?ZCBQ>dCT=UQw4Qx0p+X%|L4 zTHYD3bnRi7vU)*+P`|cHG39`Jl+0%7P_EJZFUsh2E?GSa=rRu9j`!+9k=Z`$PhyV5 zlrvu*Q>I)+>%q@rAH8Ym4=#R+>w6e#4pf#b<*{etm;Vat!P9fNVd^yR4D^m_56*lXUsi7YK$M9VQ8X?_dGY4Dhky0Jr=J8`+)jXQBy=F^H!s<)@(X)~%$ zyHJ`|m8#v2+v%-%ZHMWqN5<0?M3GRITIY^CW|nP>u?sX#Q&IM|r-kghrhq*vF)omi zMF@pryWozON_#f0w&zgtnRwRfr3vSpIV<`CL$cX;E^b@Zb0!-pseRzv`;gO2yw4J- zO~()HbAGfz5TPlfJrDMKf@Ae7v!+<*ngaDiOu(OS^PdiVU$q`OdEzHgr0$)+wLh#8MV(-csoDgP z=}Ua#zq0~%xE@iRoOA1ur=MSsxXo_o-g@Ni9M@m=dSrcaa8WXjsq;8H?}mEiuB=J+ zoQ*X}zo1`rOA>wl^xdh9{UB=S!eH&+hx6-`G92tzREqe^*K#HT`zgCilv=jV(t?l+ zb=bfT?sW_Gid#}w_h0DN>98v-<6D>CPyo}wYv>Z4^z!BDLp@~5y-(D!V4jN-6;gE{Z2K& z12d4)sS+*ZuuN8XMOa1&$KP`+o3v$HSfV%KEj|C)kDDhq)I3}pJsWK^8MIBRmw8p; z{(dhRM=MQ5$evA(?Hy_#5xka+gP87(YV6>3k4M{+)UCK>dPz5S2iN;wF?7MKtDsRq zt?eUCX)St`uw@F2%V+{s+cLdIg#KCQcw&7XAE5SR1YKb=t^0|f?ss!-l#RF2*j*TR z{8u>s2F~iqpTiKLOW}S4J!bRG2rCr2cYgFhIM?UjP5!ongK=)r^zmGj+rP(EmMxm5 zH$M;e%IwBVkrx(C(+?OXwwFbkUS5K?1?!zgr@8-;q3Pv+LF2#cr@@@&l}l-QS@f2dp;6(U{$sXCq77{d zc_F_zSpf@dm`HvMVniEnp)f<^e>{u7`}o_D{|Qz4s}4e*<&G_}SiX}d-o%n`ahFC9 zybn&57GBf4_v&`(_*)Q>975Ou0G$7wIsIO5l63!u`Z?Gw^(IKT0F2=HTS$!)|8R<;&_pI!;)!KtD#E{2SY#u)Wk9J|7macK;LzmQytMD<8%W}_fYOYR7_ z#zM49U>lSlxqOJt1|D2fUWVUSqP_~9*L05@Y~UDeG5J~vR1kSlg{?25Ca+hp(iO)p z!tei2-M0tGRaAL*?!A3)KV~M=nMwD|B$*k&iI?t~JZ1zEl(&F_fP%nGLdb)cLCFo> z2_kKe13pm`95D(QP@{soii$3}J`j9@_=2eGx(F^HDk>`b`Py$+{C>Z4>OQ)s17UZ+ zKXwRRw@y`^I#qS*)TvXivyrZdrmW;#%iFaf=anY&-r)GZxux^n()tb9eK0jNbY8r7 zLfU1N?ZUI`tMGyPx{>wu71q}C0odQ#+GF|eLBi1mLZqSeM3%E0mQsDx)43OCF=!nc z-hr+)533r%DhrmiqFo&HuRjE@Yp~Wci35PQ*{*oj)9a zpTr}7xq?^PG>P#gH;K^%gVbc9W3s2ScrgT37n)>Cp>wj3oOyfagRoDl|^BP>FM{A5Pb zE@}>1ylV>z64AW4Idq9)&QBH`NxY;f_!8T{i5x*bzYFa)JP$l}an4$OJv`QyJWjdq z+Jd^s;pFlqH@T>YoI8s52vHM&l_Hppl6zAC^~E5RSyJ{#pwS7}MoAHEAcEN_832o7 z0LlZv;uwG;2%;qs=xIAG<<3HopWuJoc}n)TmHiRMINUQaH;#4@*)t(&)F1DeIDvhK z%uea^VbAWTGun*g*I<+~Q6fqXIUbA;4!pFFE)W#jQyaoF3U5!>}=L0w|}#n1^)ZF9=T_b+E3K4sr3GNoZzhI1JI7OAYkMc2KMrD3T0j40F@t;93X_ zlV>p8;xSyI3DYirIqGf9>Ma*CTF+cLCEEsH1gWx)8vB~pQQ1vw?KwnJzEC^G^U&Q* z9~$)UotJVqme-W351`w`tDiIMKF08N3wkO#neAW~g#QtGPVKdV*RcNvSKzlMx-@mP z+p&k>Iql|nTzj)8+RpT(3*s(gUN|h@9kwoUvQ(XR5W%6a_*WuQ@dk<36R)!|W)C(3 zqKueqQ=i5hTb!&qoGQt2ODwC6_GG0>pvE}*(OEgZ!1v+P;>El0!VP>$9+Q36As&&J zCQd{#e#L+!;x}hHp%IN@D$SHV^E2vpS6+xEvu-TN3Bqo}aXs@3-5L`wi3yHG6!j~D zLbn8KaSF2XR`4lVupG6Z(2bWC7O1x%{cW@aKSk$gSm)tJ_$-#E3WXlYU$OOKk*84T z)$FrajOqdUh3ds)2|h&za_B|ppe9_v$`qgn@UvG`Agn5J)`B+^78Ht7J}sSoBcTBb zxxE^<1SBTBq)8-Hzk$c|uV=Pi!Gu!zE)+CvAJ#?(3Vm9WrqR@=M0drZrn(cM(fqh? zPfx$g^~OSfO=o}{C%n{SkY6JS9!}VhB37JhLy|>#@?mU468_aHcGO9$zr- zij(L!h&4dregi}gOvKD8A)TfgJ2mKoAbYI8pgvPyCWxqLnCgfczjIHb&8^hY7`ynV zGU|O0GB^X&vV9=zNakwv0Yu{F0!vx62MU1yMnUu=m-LCf;hcSeZHd+lb-wnm*`Aq1NNOf?f_`}cx66GP$!XM<=> ztd#F;N-xRh#5q^v1hXw^G2{i`MF#%2SqJSfeW)eH$1yXWL((--BqeN8DcfWy3*vAM z_)-%RO{3-3B3>BbYeBTn0JdbZQZ8)DLEB3d=yFI@B3BJKNvSpP4*;RsEx2cIDdd(I zf=WUV><1>=x5CN`{+Ts6ZuxYBEleGHw$lT9E81sV72pa1!+cC&MIX7Tt$e2{^ zxC%3oPOmTc7=pNWK6pO@VZo5lLVBvlE%ta7xpi;Fjj&iu^7viB9sg~yCsQdx=__O~ znTO@}14wsc0hT!^dYM9c{AZckbpVZ2Z}wALgXiOO=}0y>%YiEdz_7x3@1drSAtzvL zw71_2fuDnNh6fY08k8?sjM4c@l`FJimQpOAYU)JwFBWK{!Fwt2z6%m6{laGR*AY#rI$A#oME~3OK zEt~Hw1SlrX3Vw{(*r7$I!}5ar;C9*>aWMx_B5+e#X#$!VxPg>mS8-lEB#F}j0}q3- zUK4(LNJyp->iMRgrx>!8UP1+%Km}E{*pn;50G1YB4t@mm#qs_^X8KcXGVv}?J3aYI zsnnCJEGT4Uf#1Zl5rCA5sEd$NwhSmD&$-|tr0rjbw7p3C=_FDHai!-rG^ez8rjU<4 z&{I2Ve*`&{15WA{Xv?2r{)O`wlYMKu(wOJt4%Vr@sq|z(X_2=*;hd2!NBkX5M*Mgs zguFH4cBQ8z08feGvosal4)7>;s42aS#J<;+R-wl;FN0{`GTRr8Xtgw=?f)xilbJYf ze;r)m3>{dp-GRv+yV03pTrYMi*JBf%aC=XB)q7Bcf>YFtU+QI~COMbWo*=Sg`gYdP zYe0;6MU-8%+snM8Gy%oF7<^91thA7w>8vEOBraGGDzu>MUOQa7&bnF}qZmx9}n?X^fgL;epMlz|r>Y z;f&6g>daZfLHSCD(4UjW&ErtP0Uqv=T<&G1(4AQ~QRiMRFN+!W_yc`Ax*+>Tite6~ zlD7v%l6)bnQyb8B_P|6ps@0wTB9~~U@8GY69rBNZ6JX80L2d3l2(88Ol1q7+kPM7hA&Z z1Iv5x1{3h*Tv?lP2PUk(a`o(}T&y!M^-ZK(#OUpIhMkxK?DlR_i+UW@SU5- z?_G5bDkqk*C?|Oph7vmIHCWs!bY|A~ba7^i!>6E*Kj&Qp*$Kp5CNt$aavDLBmw>Qr zB!_20DcfPBY)8nll(HR$Y4JggpQE+(oS`F+I8Q`t=sS}>C%BA}aG|q)WGcAIxpeXR z(NzU+b0M>tGM0?bm{sQW`lAit?oV*9-nk=2agIo%RvGxGa z*^jZMgh_kpe6VT3xj{H}Pr*T>U^Lp2y?Fc)<#XOFkq+)c_vb@hlo9=~tMN}nlGN1< zRSDHFSambh7p6Z)9a_Bq0PlbZ_!*9q)oUPhW++x*&i^)hqH5$K`lZ+I8R?&9B2iHc zWRZB1kR?Ju5;Bz4NK|D^vZ1`jN`g9tHi3t+=4(O@o(h`zN8~@ajG!aM z(r!@JZ#)ZkkMt3+C;UYEDnqe)BY(BCF`JqB65!MFv+979GdzQ!U=jfe zJBF~knqemq#`XI|yvK*=j=>d9U^DCq!VYSN{g$xjG{b&R z*wSX$lY~9D8TKe)Rx^D+Bdn!tj}g{Vj-M0OQjXs*o(uZ}VJ&6*BVjFN`x9X;W&1N> zEoCb$0jwq8MI&=ziwSGVcL`xF`3@4+lJ5{aUBsh5p}wbaWd!dmL(Ji=P)WwH&vsW$l5w86Kw4Zd^Q;9J)Q-??q@t#5ZYi+rXyVqB=P1-lNfxWd&+PAfVy}eD^9c^H@wSj%A4eZlxV7In`eVnis9{MCiGK`pxU+ZB^O19Rv8Zz8pA4#Cu404JFX4vZl;y-H;z|hycXEo z+Q2UDNX;o*ov@a&T}@a^+1^7~OW8h5SWDS{+NNv^+mz#%ubx}pKej2`x;EuVT|T$G zkKO>7j305m{E;vjKVq2oUchAhh++MN2~Wnb3Sq)mG3;!@gs);)ejM%55^spGmUzEo zU5U(&)1EW|n8@cCwuLZ}&oS(Cl z6-UBy--kUkfcf{H0My^*50T_6hfaS1bzvf9r03@=2TZR(jYdGS15L!7;JE(fsZ=G8 zbr=gXCz(qv1zlaZAIyfmb^_g`hDliN3MrP2nccZ4$Ef9&?b>Te6m1#0ANxqo|0s+Z z3p<^2V>x{JdqMwb2AiHGgI>-Toz%Cnk?jzaz2DOonc;m4U(-xcJ&|2{7s?hqXk%Cq*hD9~2j@Ns*t#~b8eyFKB;wuJ2KH&fI3G&H z`)M0kXV=_#BW++u5ym-T0$j3<2JBAwt@9@10UzH34F)3fo*RCn{5NbPLr+EoC7EDJ>CX3QkWa>y_COohTJn8M9#^-4eYOqku{N-D4`}8* zCqeV`+rZWl#yL_V-nDIDw-d%WQX<~3+rT<|5s!2JM7;0aiy^cHHqs{E5rlERn@BrF z80WhQ*mlA=-%Y?~3FCY>0qdDSopOGffK3p_`Dp@n24S3^CSWzfI6qCmt|g4~(**3Z zgmHeFfIUDM=cftS-w5OUGyxm?3hIUP@dWGxgmFHefGzwgV4Ip@R};qhcp}~v>PR0#}h`oQ37@WVVrv> zVAr&P-ANedfUW)! zV6;OeV22%o^3ryZfL%ZsZ5IjH*Z&!5Y1>P{E_w(s+RhTNCw>eV?R^Q@k_P~zy)OYf zj*o!8CECkCC#uK2|J}3b|+zHG{YVy?B&fcdlg`3Hp31k?5t+kX@s@V zS0}86zFP=uq3;30TIid<8n71njv}muzJRb6`feetg}$E>Mx{GZr@tWViDtR`Bw?pE zmo0lZV5c?14j}BM&9EZ~ds#DVim=}{=W!8XXE(=tJz^OlD;*H$qX%q zeSt8UrNyxC6God@qKzIVj5e_Z%zgo2w238P2NOn{n8G+u(cw-fq2iZeE#a3TE#a5p zEa8`-Ea4ZmC*c?MCgB&=CgB&gCgB&ACgB%#CgB%VCgB$~CgB$qCgB(LCE*vFv@Y57;j0D0Dm$dk4f=`Ci{GRl0ZSh~);-_go8Nb%zr%65;zP-hN zS&RSGEq=}Q=KBwWNpB0mug3BQPigkMBL!Y|?=;g{hl;g_K);g?}4;g=yP;g{hk z;g_K(;g?}3;g=yO;g?}0;g=yL;g{hg;g_K#;g=yJ;g{he;g_Kz;g?||;g_K<;g?}4 z;oscS9v8RxMNLV>7ZoMp7d0#4PZ$BEJw?4rgo|pG@Qb>X@QbRXewvrm%#-konI_>E zGfToRW|D+o%p3{7m?;u|F*79mGL=sFWeT3~%M?1{m+4=^FH^sSU#5KtzfAemPcw0B zn#@hpuvVq50*|o!X_(}K_1po$HHSD_UZc@`;SDWLM58@1j@F^kXwkr#%0x7pp5thp z8jVJd?9hrtG@7O3Xt>`(h@;^Hr$CQRM5K8-5izY1X$`?Ot4aE3yiP=PH6l$UxEMM~ zAx+zfh@M8I-2?|mlQh!sorsvxh%~0)8TBNMVx5fld~9H?3M}695yRp=A2BT6^AW@1 zJs&YF-t!T|;`RR+7O(%uuz3AHhQ;guF)UuMk74n8eGH4&>tk5FULV8a_4*hVuh+-0 zc)dP`#q0GkEZ(0G!{Yr3F^nBU*Oy`#JBO|>#W329bxkLR#d~IASiEN@hQ)hkVpzPN zAcn>J31V2hpCE=&dC|407#8m*h+*-5f*2O>Cx~HGPIUb+hEYLjg;7b-b+R}f6_r*P zm66u8R7hH3R7zUYQZZ?TQTb?1O9iAAMkS;*ZM;t)ra9gx5X0hq0x>M!ClJHpeF8Bo z-X{>l;(Y=!EZ!#&!{U7cG3>2v%J#N4u(!7#GR$ow`)!A{KYz~Qkv zdg+QEmtF7}Ct`iqfW>9Ly~xw$n%edsa*w;7F2i#RTys0psxzJ~zc^FYvtnLqKH|R& zXU8-shh?<);mV-$WtP7YNi2J0tYc|);naGvg;sGT(;sJUC;Q@#p6$*{}+31i)-=_;cp+C4DoECZBoAm6a2QB?CR`7t%#$I5wiVewt>-dL)pX7X$M0bO*CO#13IskT!li7J zlG?R*V=I8{1^DmaC<=~r>P*nVKa~ zdHIWMB3S)`A$PGvI&hL|6*CG6nCh@(yO0|JVTAHrdCGd%bpF|gl{p>Ejk zk?@}o&}KL=EWl)7fe$fZh=qAbJ?k56Y;Rl`z>FqGtUB}Exp`gAohmO4!N^nZr&it& z(lmuI0F#0N6cS>XI21>|71<;ZqLscJ^N^NHOQo@jsA8P!c5wt7W&2+we?Y63`VnZx zR2+w2m-8v5eDwt*7|V~Pwo@z@b74hbgphik>PTmf;+YS!tJJ1|b?C$cBHdOxGu3T- z>B-J6oZ8yCIXktohvF@p_0~;EMf$HpWi?fxh(A^#ZY~qHsSKM4HV%XF%}}Y}$(-Xc z0}mObz2!e?CLta67`7>Tj5ICx_@qVGT16d?od$WxScsRYR#tQ*jmFoG3cQ`84D-dIjZC7EF9p9vQ4TYVYXg* z5z2v+BSyxtaHEhT(zh-|7cD+Dulgjo)PF4yvSJvcdhn5e%&Bg}wo<1{jVDZ^l_;rm z58+M{isPTK(DS;Xt0Mo+n^PW~)J-O#Iy*Oq0)+V*x-Oz#z&_9W{CMvqq{PeF%A$0p zQPp$0my5o119%T%==65c-nm)C7tij{gJ9|_A_JThZaOe{jg(W&U>Kk#^EGu{oN5lmsb9gP?X+aELDK=t8W^S0uaJG=2O@Qbf@G`lRu zP~=ZNe^p3Fok@V^^+fYHBSAJYhu{%-{6F9aXZY(xG?s?dZK{l)Nsh))_ZSa%qaaRi zv)AO~q)tOV{A(WRN}W zWYkm6=D#&1X-Pa_DyZxrkKo$WL+S8#x=go&AChio^mWaC zM|#NE7f^MNzBZh}I#F#un5^xGS#wU~5mp!~onE@ezkd|HGbEcDx03y?=xdiDo3@Ga zzp*)gcS&eCpop5cCG?@(_u=Ni#-l=#e*cW$>BeIu3;mTr5jxA_ILhy0_PP@(BnMf_7%op_7Xdlc!X^wWEkDRN*y~dm0C@E z&Gaf5zGX}K*s0d^F_GsOYx<A#40*>>3|fgeW%}2^KRko_e7!!Ob&$E0_>5qjLneWmB3v)l|8oFQh; z@`Cfg!Tyo#3eLh_*h(sx!!TuMI9l;OOG&4iICz z*Ib2c{G*8y+?-PtAd2e-7a_!|6BU!b0zP36s00(Yl0g^}YzA2SrIWe_^c+qeiG!p( zD-J~(ZChNvet^lsyB4O{i;;W`uTjo}zJtdv_wJTE*_p+UE62kL`*E?evcjaxUc10) zPM7UlgSVv5+q>JvR|!{p?cFWc?-0$sq9=H%H-NV1x1+60+E^337=EFP`ID5`n+;wG z+HQ>Kd8~2*VwTpA=BqzqTZ+O&3fG#{9hD)xue;Wy&&&>PvT;q$>;}anFYl6|2V$j? zbDmG5U%dcyIeMnUO|V{9Z4FeW5o+U9oiv93LbQI~k+U4ZzZ?MzX=e2paAdZB%}rT{ zABayk(O0&+iN5j*H_>_5x{1zvs+;J%#~@9}|6|W{6P@?DZld$z?*6Frj&-_;Zn(s= z?2P{+^!?xz{M2s6@Ep7pa%k1vXayi7(N#{XjvW(r!DJ1g7zt^TByJ%x z(oUX0lL1i50_!vT;60%72(+KntYrk0)U5GzO3kWgwA=4MHPmj!Z3Vei=;bMCDihdQ zd-uvPu<}Z&&nV>PIOJs_grbm>z#EFNa+t`GDCD9z&64Um6dsz3vlM5H)fV&Xa1aNpT z#33LBL?(cu-2{|sPb~+}YdY1?-bigkI5JK=6JzDj5(gx*<1iBkB(fvd#sNuGC}i7! z55}l^YZg|AM(m-Zlhmc+2+fs2_HBpqY1`w}+EpksI({OHSZVnkn*!pOZtz1$fbte= zw9^W%hYa6#dMdT3lJ+q4I0hz9Qr6=<*nuMTq^q;wa$K6T0acOU zZ)-a=KU@u6a94j5q~lV*zW`N_SGyAoJ2L%yR^;dSS$*{+?tT1x>*pU9h0s#)83f{5 z;?v>uWJYj@Af<2(6E1BM6!>t1 zKZ7jX>GxZx)CO3UyuUz-3fbTexDpV25ntu%U)ibslb|D44>}gQ-XYO2Ucj|4DcXWk zsXI~&zL)Cng|}x#CUvcyN*#r^?}b)8JGA2H3sG^F{}za8+-AHB&fsJAszWs)?F z9s)3FfWr(&c5a@|E`Yw;iJwWNKQl^Sto=bsng>4>a@A8VMwfXN6sXZl4QBryA{gsg zTD=~LkijuzRvCYI8=3!g2_9Nn{W62|`xvTl%Vk8-Kaen7=bWr)9+5@%PXLEzKaKc| zh9~Ap@vmz6xz}w2}8an4cPE!o3Wi z0H`W(otSQan~3?*WkxO{B8L;>LQBe)ze*@y`A;()=Ga|V(kBmBA@B}cR(+@U@JZn3ysltVP1!wdO z=y)81deR2nA0j$21aL%ALgC>66nQxK7>^TtEcAqBzzj=9iqV4%*8dY@#ffAVnG%1l z06q@oNC;r+O2er4V^r2k=gLpBD!o9u7OsaRUv{X|Shyr-kUHC=34VaK==d`5fb3hAT0{Mnf?fXmMKz?Yy`xW!7^iQ*%48_GprmD95ccg94u#uwfY6nG{YJxA8HpZPW0@OwRbnHoR0dO z(j@)V#|Ke((|>v__gY;3brwj$?RwpocAuS_$A2OHLiaY9xFx*sHF+&>%4GB|#&;yX zGya&^dpMNR=A=d@7$^KQ5s~IV8khXF@E~4V-c6EtjDP3biN4#I;X5yt??#Z zTwds}Lt?KFJ42lDU*p;|RMk-T`BGPdboy9D7oy4($~qG`%F2)vs1I~+vPz(EgVi^? z0T-{?0wKAC*?l0pd>QffDPG0Yr+E5_Whe1VDDj(|kb&mZFF8s1&;+R|)aOuN4oO_x zDG8Lg-?rBsoC3s~3}G?>Pmli+x4R9br}{VX3QDZm_!ml50at6A??RhjfIw$pf7*N( z+nmw67{7g+52)<|ZGJu?s_kW=;fDdVe%R7z=T}ICO{~j4)+K6`mC5SsN9Ey)ehFZe zN)YQ*>b9SCi8cg}BkTM6Hf)&T%Fw^Q6t|K=(cy^zK)i zx5Sn>3sgNgPnK*-toBKy!bGy7jB1rnyeoI+>C4!%YtY-8U zSq*tSt1|3Zn6;uECZ%o{}B5U&!}bAs{o8vv}ic z{iY2$xzyYl{{^cLa-&VZ+6Bz}FH2iuH}lH5!Yj}O@w_K^BCVBX5_@YI=en1G4dTQkh%GVna{4C@v zqtB7AR;md6Zx#9aE=0&dz_Ln{^3_htR|cXoQT${yVTqd&)GS}03*0YMe%PW6QH&X4 zi#G&09C5_YrQX``!-s%&|K)5;>}Gy|5eKpZJ64n*ZUJV)4|cSsrz<*G`u!txVRXWh z?an&xEau_=0sYX-D%q{M&hRKUCH@KV@IBe!M`GQR>G5=%cr6r+=Ue76a25jNkTaGo{4&AKwviDPGd_J-T`RJ z+-2sQnYkDQG%#KN;^r$t|;Kb7Mp#LclhH=zR zeGhm}geZSjw7T{>%z5+G8(XOFXr-QHcVesArexKYre=r#hHTgOICyAl`3VKbUVaRo zDNN(Z-Kow}51%HilndChz*Z`FCN`S}`VvzW{EpBT21TUt5R6q@uv!Ci9R1$!s*)pbzgZm0-#U#tH9|5U8%>bYY{mZAF7s@kj$fSuj_5caCL z8}Q-QcHz>oD;Qtxw5imB1*xtfcu%sVbJ=3m(R=`6?@EHO1i<13zXM$O_)E;5Hl(9~ zbCUf*BRql#n{Dj39X-Hyr)2EYkc_`2tRNZtyp_L5;ylT0b7L+lddfH1{+E$UxMDl~ z7Y5nEcH}Vq@2q<(fPqoJ+*V6me$a@LBTO#l4=zU$}ou&6Rk@ti2 zULARFr}u54SIRao#e2+;6xl=jChFL8#jaA8d+XYVe}fIx%hzxDH8N%|?JfIf!MIXh zBeiG+0osL+eOgl8<#7f37hzck9;tPID?P`GCzwV9_}K4R!TwV>H2}NQ@f_Pm`<>D_ zrgr|fkk5(^yEvXP8^iL|yHOe)GScDUr6n(hm0{}NLMuaBEPP$@E6ad|VtZpV+>DZ0}BELO6*(?cs{3f<9_)*Tn)84G(&D!hnE_2#*p+^s%hlk{+uuXaSqhNTBBj|f6 z^!0-6ObzAm%=6C8czp)Hr733$4;@vYc4lWU*pQP=3f}mWc#VFxZzBMh@8Wfc`L0!P zW=m5WaE8qZ(RgrD_2La?6SfTGgn$#K4Ia;mW6Ir?FbV*Q9m z8!^CN%7Xj}OTSmxV+IQK=GF*`3I&mxD|W5zMJf!6LX;icOW%>khh4(oqW>hTPSVi? zNEn<}1_)zhIt6i-dNFCNlSE|wWK@TykoO?DF}_I#SP=L&Kua}uH~2n0`6PC)c!P)c zn@&X@9S>o7<${yLRzauNK7UkdU@FzYK2I7pf`*eK8c@*pMk$_=hPY66a0+q+C2R`3 z_a#K&sdNyLURvx>kz?{7Px)(7Y37d+T$=Jbh{zRvX~!SXgP~zLM7mYG31}OC1JZ={ zF5ZFaUvNVk+uBK8fij$oGW6!`(Ziyap6>;p1*_+@!MiO-mhWXH=C3THG_jRwz;xkM z7CW-A(wRjoocNL`G%KOFn|8F@sx#lqyV@QrC9T~QbI}2oizcZ0fn5mbwgVT|3Wq}4_4!~T4mjPy? zpMg(2(-d%VrvO)$f7q2hc^s}M?HQ?rm;BQ5rj>MM`J(JH(cJJ&uR@xSdbxW<*50un z<#1GEU77P7mBTBrV3&@7=@{sx{XAqHb~i}wCTGWK@}{*2J=hhYMS|=R$prORDvZlu zvUNaIL=LD3typvhO$<*mQ4^|6-8H~&Vz z#-Wg-C-~H9;ZqZruR=-O>mp`#$gJt;{k`Wy|2wU@tWwgPW>Okoq8Li!z33{NkZ*B$ z4jd<6<2<(D+5u|>#|Ox&*crmt-ju02&{tv~swk>YDW>|ju>oZTx5vR-S6Tl$tQpFC zR{ETBv5Z3SF*lC1quao*e8rDi-RMOHZ`xvw3N#@~JTGHA+l>qY?9mrR$25!B zJ3#5*@DLE5;Kx~V*`g#sN49z8JqY|n7xWs;qO3ho`e&jCW;75TOz4O^80g?QbT8b~ zh3`lX5U)HX9DPf6vG!8b6*d${X~9<&q+I?LcY)DOWR%KJYn zK)UU_TV+)0b7QDV0!Wkm37*+A(C|aJS$<^`j`N%62P-X?}PnIEyB+M^B#1Z+QgF;Wq-6-t+gb>X?6fu@&B+mIwAzddRjEBH^aQ8<4s+0uQnLrj^9$b{!Q zJ=h+fU@Dm~_4YWu*iv#ZcX?(dH>*ziYu^E{1{HvVQT#}C&WElRi@I2iLahW? zbrKc49%aCRd`W|ywRO^fG!M*4!vIZV2X9~^2zz1+-bl|kEdMz}sEq<+$;{wsYGVN+ zp*B7d)O{C%$k-XKDky$UUW(zdc#U%t9dV$776aIhbjeL0Mt00q0xA^8S;gIGgFPi-C>tmF*Pa#I&vU zR&s3-xoA6~A%4Jl8riZ0Xrdwd7^%0>4@Vk8{H7rY#M92EA&4$!OYya^`b$TPCyB1A$B zd^Kdn#Y8F{6Q`7{#uDP_Z4APZ8W6v;4TqF00b2e+rUsZ>%!=ZRS@9TlO!6Vm-76n5 zBUT16qImr;3oB##$e0iLY1N4gyhTq?!g$fljNJ)lB(joKXQ502FynkM0hy8cHzHnS z{;XaW4f$Uk=TBk{FEZzl7oQzjiny5;Fl4`B$Oth6M{Oq=atK*HV`~i~5?bqLA-^5~ zf1O!pqSalK= zyp5a(G0HToR%VAZXPh23YnVppkTiDicEh{G7VMxWeo6E>fWfYYM?ScS#Bud#v>pEz z?SSQW-bk@}E0AMaWz`v1?!?(M(pO#dqIrP!TXiNC8{D3efojjusjYkbw_?wv*b5T2 zx21>{S!ABs_I4*rG{^S#9}(0F4nZ9&O}yZMwj$WxNKD*4#20oC$R{a)T-}56v8>0z zRhdchP?1NS5m%%XENOQmAhx>^nV#q8{WWUGLye`*5}kzjpp$&!+2PasBZ!YrCgZMv z%2Gd7+unHU=xStJlah#S?Mur0 zkik9(M8iKGZ8>91MpxR>nx$Sefu?Y^)UoKZ|yLW*h5~($35^VPhpx zaXX7IY-gNe7Sg%)_-@q4+0q^)3+>za^0fEN-+*&5S0v7*jHh0D6>OGbhjs7{ zFV-w0n!!VF+h3YXxF_rvFJs`M z%rE~JFE|;o0DLhctFctl(RSF016Q0dYvw|3a600tl~U$61{-7dIN@X^OH-7ic2lfO z@0&)Q6cxu|re>z0y(hMsh9L>lDdzl)s}G)YPHadZ?eU~JE<=bqIem^ovEV2bfF+=) z0-zzX7PqnXgENs8EyGc0j}w{?3{Z`%5k@nI4kyo$wU*pfpnFrqjB3}8S`L}B44GJU z{su3EAKPiApEXtxZoZe}yPPYPmsP({#*mFMD!*@seQyo>irrYr1Zn>zkQUhXV=M_7 ze6h$mTnh$s!iXa)X?a~*gSmy6bve|kGsluFjVe7FDmQ8b$APpE%6RBgba0o;2~kKf zI6nIJijdKXuuKusK>en$Cy7+k(hC*AiAcRP%!Hu*&swP|Hlx@Vouy|*=gI%y=u}%u zI#oa$t`&L5CX{y4=@GW{pApu_z6#S+atxK;RM;|N%Lf-rN*AYZFxKb1tX>y6ehap3 zlT}~@*#rJ4t}H;AV*biF%dHIdWohf|E9+!?8%}C&fd57tu1fvwZ+~;~0zBjf?XL&J zeHGWy#r|q2rST$f`?wnwmxFpMa~aoD18hfzS9u(X(r9`sbq><=FB<(P5Qx!#TpS(! z59N2d@d7wq4sLrG=Au>OFnG(3e>v(3r!VZYu&ZGNKjy+4$A2{fG}1EcWJnkNGXW^C zvFb#vYQqHL%NaLak6n)TJ_qd`DG9@7Ayy7s>En+OXJWH`9*|UC;cR~+q)^LOWFtIiTFJE(dK$v&1z91~mLnN&kC zbF%8J7Od6FEso|`7i+n(ilJ7W4tEBqh*~X4SV2-%6QhVh81t0UQ?~#=GXJR({?lZK z!&@moecZl8Tr1efDvkJHo%EIG0}FD(Chu~IhhMs|q>hDk__=oiCttAf5l11GiAE_` zohdK4se4fW=L!xft?b95#>p$p;vz0z9)AnnJ-E^dcGR?}rBjHjcfz>-8c5$il|NAo+Gb;_AqFi9SB*eb}m^+KuR#?KC)HS?uW#q&xb{1~jdwPKz zP+((+RyXm8TQq?~uUo(O^ddHUignWi|mm)!GZ_@p#rX3 zzPUw`xeTtk*z?Z^2^vYqS8l0Lzo{*aD_bo>WM~@Wr#KkRs#xgYMWpPPA>Xk3>xL{% zr~`JKK};rw6q1=WOxDK*h*$>LCHg}tK+2~$4=`K^E+|dmE*yz~i_>tKk+gCIzy*m+ zqq94(1rZ^h0Z#&z_PT5lJQe3d&Yl8t;piThEWN}e4*44U!I~JmfQp%TAZzlNi%Kch z+!EzOwW-+CB9^A4r|+(yp_A}Fl$e#m!*0zXB@+_Do4S^AizYCXq4_ABzy_=-?OGZ^ zzl#l47-d^{pTh>>tw<=euvHQ5Cw*iAc57m9oBPm#ON%|32R{s{s3Erh2~4RmOL-$C zPPhx39S@?0{SFA&?>b-sl3v*>y;9i~jUV=9a4Z&`VwT0eJ1(CZW|aOuihfC6YLNm@ z$G?li6y#=9k&bbaqGG~4@VJT-unl=UfiGrbav}4^6{@b zWyOVM5I#X0nRd47WCAU*wmG#dmbQEuE+&&BNKVO?LrG4bP1bk%ZJZJN6GKgs7m2Mh(Q>|wd5&EVW{7|2qCxztX-6T zqXIID71T(!&INdm1bd6H&hugh)foX-zCxDh7d_m$V!Sz=i8pkB22n9awIfoBo zz}2qfLTy4A<0>c9?IFYuso*m7cd1D92VceDe&@27HO$wU~CRy7k5t}7V1pY6J8MH8&tc{onLy}^q z&202(g^VJPb+As6I$Ss)6H(qoB|vc@Y!1H_jUCDxVHZYQ8O1UPX`R4%Rp6L&X{1x%9j{G(8A07GcM~5E%_F=}a_NxOxH|-SD zQz(fg`qzR~kOuvuJgoCrBm+)Q#0aoLl*3MY)PU^xDKGOcL#xgZhv-C^6mk{3YUa;j z{B`@*;1pJ23bw*i@T2z1aA}5`AWqx>3D>o84S?~WZe7{yx{}1i$6^5?s!JD1bH!b37 zs$B`>Giz8)TX2Bs)I;#lf^CcZ=@r$hwu8B!8iqOPUXUd)Tt=Yr9nADdtU!+hZECUc z-^`Tsrv;P$Hu_IzxTH{^BszhSa5j$SWdH4qwuaFFLdgtRBI1$?0c)gVjjtD{;v|@M z@EyRxzsQcgo_ypq&Y_cTtfSNAG|m;T6tuBXVd~i=o^<0paoygh?8f>0tn+s?XLY%a z3-MLyw7T@tdGT6zEn9N%~7co(n*Vqh~vk7LXvx#|~DKnek+T`t^^#aZ%!Z*YY z3U`EtyOK8$Q=P>yQ;f=>n!wX)mrjK1E(d4()oKjSW`et6?9TJ(60P6Zy&oUkD@`>n z5X}6AKIK^k94ogGhEz$(VO!kYPw@dbH{+zy6Xe!A8TC&<9ICG$+lqO}r?U znr>x>NQ}y0@UKHZ$m0Mc>}kZ2H6yZ@<(R0aAu$3!f?>Tm3zJ@O%Yp0=^&q&AZ8*jI zqo8&&S*Z7A_QGu4jESWo^P(iug*1MMEHt?hiT@6ES@1F2^)cve7yZw)tFU~m7#fO0 z3LxpGjBINroB_{Jdg%UA`q|L+*6*F@Z$^Wa3ax-;U10vGoR~H@$BSB1KQu66)zXwS zBu}!oG-M`jNEmG=9?^thgLrHZt|JU79EV&-Tj&T6ytd?Mn6Yna;_Di+uW&3(v|uzs z?qKCz!~bC~FnHC`Ny?F}8AvxsR7i8*HKBAQ{WEp8)-EfY9d>#)NTQZ$tkm9Wvd4_* z`AS|PjfP}c!JXX*i$RTN!6KRKg4-n`$6y~G8q_vMpB5NOweMG!QdXtD8n)wJ?JAP{z7@GN>HF_~7 z3V!t15L;;G!xJ=69U2!SFAUzVgfnMvhb4EJyj1Fw@nj1Rj&|1>TQ>1vZx5I5OXI)7 zhAWApyLle)A`M9FdFNv+;-Biu*cqFf*$N!f{1K&jdKGRm2%ZZiQ(;t`rw?m}JwVv% zW*8Of>6-_e(o(6Gx4#qk9wY2PT(zBmQMsNzs2N5Ddzw#pC*o1bo?hAvqoO_i+-BIL zHvq<0y%TBwNEjdRPQbkP0>-zx6R>{5D$TIZkv+y7xkdQ+(R z)jm9db@^AJ4Z%TM=%gE$z@auquWDltK02?-wwu_G8X!`%1>t{UKkBWZ&#IH6#C}wg z5-UyOODIi@oLC!mQYV8?b>BA0!y44%)X|JpM2eN%uSh_ov$+1pK>)@+B2^I+?@N6W zcsEO5B3>A4*R9!Kyjr4Ypw7ee!i+i|%3#asgHW+jTsguRm+>;P8MUl_3{lhg;&|N$ z`vzi|&vd3_w4J`Gy{BjT6P}{3T@K(#8xI0a&cb3wXCw9rge&sEGX7Jn*j0KND}yL7 zVldoTE(*A)vA~1b+SQEF1edey3vYsM@a26KMdwcg{f2k=KnfZ0y4 zoR|TF=W!kXb)%m zRjSM%lnFixWj>b){+>ELDsDJ6oJx)NRlmoU5-r`yRS#Z^wTk;~#IRR?236nN>B`K4 z_M_1;>t>QLJV%K>BRc5yKUdmtU5~}$BM_Wb!ryF43?Lq zWObsEgMC>&>T3mG7S0zq&p>IO@tLmpv~S}wq^(Rmeby#~@ebtJ zxEv4|$F{+NeDS|1R4|(9aMkVH z!Ny|<{~~&S@HL#X_3vyS_VxB*>=;s{Ht8hn3Y-XU4x=P#lSIIAY@t+|L)W%odlWk% z%7=68Hf}81D;Krcu5q1WSahjJ-f0CkM`s)X1xO(8H_BBm2bxpxcn$9u9uyWn!aX;s z0DR>5E04sc#9g?q9tTkQm_*;i^KiRmG57}g9A72*>cHK_nc%B{IkM|ybbe@}^kA9H z>?v1o1Oq25m{&nb3hrj*+G%lXxnscQBNnVq|HLutaNED_Xk@p>FCw74Cp1ZR+M@$c zXO+B=+E?N^iakIQd>t78W^gLc4si(Fd&7Gu*+&}Qwvj%XMM zuYeP;-8Y!_nbz(l(hXLfNmZ%StA2=44kK0n`ZFkbyrx}k%&$NjG`)j~8(MW(q?N4k zTBNIV;M_svv+BfUlr}IlW5kxU#x%Tu(uoaZuOl${K1&SK7-@^OJn@CY%?Sy7VkZV) zrBjzP{+plC-zzXn<2j^Sn3i6N3p*f~W|i&}0uW zEl;cpv*l_r^kBwPkQbU2NJkheS}lfUjBx0uP*k5e9hZukDKFpGlwo?>)EE-Ni$ky6 z?P2wq8!)5>bnO-ky0}i_HwwdjX~UmK!Ej%^>Wn6Oo((E*@gaa+7F5WtQ zcI}$eg#CP+(a4qje3TMmD|h2nTj<9Q z2>%y|gZG9z9oPpC#x4OCoYN}}DR$W0HcVg!JCHkOcZZh3?D*cc<$CccN(1{4Rs&PG zEs=5{Q88AXwGHj*;z_A@x2d44Ftz{c+sT^VshS(8jUW-1UsBpn2D`RVRxi#$5y z7e1HHYe45%2|OVstFxHF6KIzP1FSlKp&Vcf3QHUu4P~vk{6K~G2FJ*ZuT7k#Erkus zdfr7^R>%@?Ipt(ygS~&s?sLbNP&xP@k>Ma+OzDb%|Yn4#W>FDFj(w2>z z0HB&l*Er$1f@HM!wWfPN0H{Z<1M=O(%OG2=I)Ru>O&o63nfY*P29~Xr*1~dn7^7-x zkaffn&?sLE|kO}i`7dQ2%v07(d-ayx#__dZ6 z{ngaD@R==dz==HJ*|J=vh^LzC=mjGuqVwmy)3HAd8zFNTj(!fMH5#rz1o|;tf0$0X z@j*DuKOCnon=?)`KF4Xg&u*N4CS3ouaXOuPDd?Sxn+3u^Y}Hw$AOr3xE+4k)1P56% zqbG4Yt=RenHIm()3=HUsgYdNW?p?*$AOrU|~}r3D-H})tZ%=Z$KnLZsEGGk5UMTO&x=}{~V^mewY`m3!z<3@Bs27 z@csx$mG4%T!ft$=j??%Aopj?i@r9I3HaQTxn4Hc5PnIb~JE zf12NzI|!^!pFaQu?4JJskJunS1R7q=u^I;g^q@T^zY%`HBZo>N5bcxnlq|;|0XAuq z%HcS4znHisEM3Iu%bQVmOL1QS7JduP8r8IW(wU-1Z$bA!i`;|P9lz7HJ--0Uu2&T6 zo2ugw-033ro{3LvakQ=N5u1tB8Tgg2!{%jbAO~DqYfBcvG-Pg7$hx=&hLr;Rv~v!_ zJsYyBltazrab^G~YbSHI-c@}*Z#W;x7neT{n(R~saXDwP>f{U=d$8s|A2STAPPSm{ zfQb${Hym`pn3RWCfDbPhJtV4K$!*Y^EKRzTr73r*9j;BHrzF*WfwR<1pb5_gT1WxG{;;xr1va z$N#}ll+8BfjYGG<1{jb^a^3ELv$=+?N|vEcNjgkPloW>DTIj)Z!iJuF3q8nU{4SWB z&<-BY)Tdt4(hjCCsAIaEQ^dXrVQa9YyB35wl@Ty3ZuxW9pnvbM8wGMsb3*Z3iC&lUKj_`gvRBHPW$yfvkVKfs{+- zzc@>7<*XDGI$j_uTLCc~e9DcWE5z|VXZq0{`S%4YW&ga$T@kP4Fx z?{_uBxHm_%f8SPbIpuNZm9fbV&vb z2+zuCklb0=qv7z#t{x1DO=pnj;o(p0iSxofi1lPhHuxcQV`0t_yMWtLKjsXs8-2kb zey8N!5gzUE(}mn@PaeN`hC0J7sD!yWF#_oFo=)P%7G+7HwnzjWd zY;n0AvW4U!Y!Sh9Y_~y%>M6YE2==Xm!SPK>XIivNr&+tS8(&08pibUF$7y_tj+9O} zQwcLP?j+P-35-ojV;3&z@9T0xwb80GUaT74#Z-0The9bR>IC@}N+9{f{7Q8P40sGG zX0RH>k5D^kMfqifDN5)Iijp8j361J2@L=gsG8J}()Kz)*64Xx}ybL*AEr^yT)7Q^vs0!7gAYEwk_1ueOY{uGE+I51m_aP@W|ZuQ>~^*uNVaR)~$~&lx3?#AVf`+K62@ zcVakYK`+VbL$aDA8zx~82Qs=C167xH%UB;J+lT@>Lgka(Ge`NNxU4D8L2F3YqMQB?XCPOlvT>f4a8z?h!$dxka+8?=c4~hhV z@=ZGF#@%%MBars#B#0e6iXlcMa^vevVb$rFyC@sqAgIn?b7Pw4uL4HSFYg7AdpV2# zYCvpUY-*R}6Ww3RaWh>0S#=>y@ak1(4qJV$P5F!IYwZG`-4MDpuvz->&G)BNsSVDCdUXT?l^mC+O8 z>jkJI6<--@->gJEc$9~kM+?6eBDq~naSnp}(KKjZ)zwrOO%eGtqG#N~4@a z9sdYAlm@K9%xpQ9$o~u3G~tLnx(PfiEf|XxLzzYwGo?tVfk(KPWC>j9o5Hj@>*uT6 zO$-MCNXCu@Ah~_60KD)3a-JxgY>8pfD%fmjXJp*#ABP4g->XKiG?wD~)??;=1o|f4 zD{p)U5z&LbO~+|`mkw+~-(qsuI~husaJm9N!KyQQEFQ{N@dC`}L}+WLAjc8QzYn&I z#{EqC|Hyk2Fu97VZ@hDF_w8+FGR;i7Cqptl0VZ6gdy>pVF`z(L6%<4ioC!iQNkkKr z#vTGn=MDoZn-Gk<7y(6b1;hn+1VjZ?)F`5|C?L3@;O^@J|KIPNs(WwubVBs+`+U#y zee)z;w@#fpb?VfqQ+w3|MA$b(z-wvcK^$dV35bF*xX7K|6z{u?hvN!#g%TI&>Cw9l z8AQ?=WKU7jW^aTrC|f%)NtU~pq?6^oCH>g&f$d5dkBDJ%F)|sD&&oam?^Dl$`YB** zZhT(<3UV$idj5-%H*7ZkI6*SqCeK95Q1}=G`7fokvBeuj9{E}3^T8MT$ORYk#VG0t z(!uj3-TOwNB1N#G6(`1Jc^3aonx%#MEo!{8oI7rK;6>;byGKdqQ^ z^Oat{Hs|22+pSN|!G2-$mN*^@U(RQ41^*b}PYXV8CvZC}EBIiYla0j@f4SQd$!8?g z$wr_Knd>6mTfOw^%xXD|c@cKuy8bF=EMI>j6womA4AQump-IhmQ!w~pTfjCPduIwF zg8_6Tm#ncO#uWPqnXU9Y8Fzf^^xY_U%)Ov#U&8CyGBQ#g7<(v#kqaQbG)toiO7|{stx|e zu)Hg>gq~SE+$e^ZxA8xYn;iSq@M(iHz|w)hcZU~4WzkyXW|#-eEkh!cAnniRzJO%# zLqbu}#68WaZ9>=xUb#CRt{fM^CQ!m;2ix=?`CR2XdRIXu3T#l4!95^>8vP?fiC$6^ zKlPA#I|t?OI?Ev~chfO)s7E$$dj2a&>`bS@Sg!o~li-#up5F`l*nmzNlqKck`11h7 zlnC*wU1;g}QF{iWK%~gth&Ol@aEBdnWPgH~rQV|dDkdc+%6wB8JU6rf0bR-7UcW|+ zLloMTvH-NhIG{_aMVuE-DHeR}^E?yn82_Rf{ww~7Q_5oOc=X_8s+9P!DDxTY^upbX zYXPeZn$+;5-ao6`Uwe~obRDg3i5=~(zl$-1l*C9?2U1ct`e4vlcl}W!3{P64fk_St zN5~QY>!(K{sp_(>R2X^&Lw^|y#Zjg1`eQ9n9>6lUZ2R{1myr_ zxR*{UxQ`C(`UB#of}e`(1^0`a4t@rgO2Sxl8I3OE(Ip;T#;42pbR{0-ZS`sUJZX_hk%ek?<92?7vKh3Q`R}G}`%9G;^ zV;YePU9G5{C{5_YFw1wxw%qq2K)2dgJ4$GKEoiNOpY&#%khmMML81NmEU%9Waun8eh6}WJ?Tol z8uF_PV1>}**c&dckEO!{tXWKF`_G4Gak}{S>L2jq4`5cC{lVgBnFn%;{t*n$6yJ`= zxUneUmDWvS^V!?@;OlR^P6>QU($ffb~_UfO3o%T;A zt;$=yjK3UTG^wR*-|C6s8zXzP*E(D3SR)JvSi!@{C93PsDOmKc=#4^I(zq{7n5L2o-Kf-E`9u!-U zdHy>{_LC}gRPbK`3DW&FomB7}IxvQhiJJ<33zs<|7G0vzwQ%CoPkh^_elHVHyT|?% z|DrN(ZgZp^kb2I}chmxbuit?p_?%WzJ$M@;(5|`(-XF(5 zw|Z=o6@3;e8Fja{5nncd06tZ!!D9dP0L%t<3efjtHPzL0$N~1xW-+qb&^7;~AQAl0 zWK`TJpBKjR$@e_Nd|ro0%qQ_(`E8d^hBoDs2NLG_H!$+{NY{2?PyU41(5yexNdLcIzwRo(cg1L&v9UnHAzpKRavV?F#?e{3t{U%?KG z^BYJi_#b*w!QbhiHvU6gykgG5?BC#_xEO&hap@A7E|Fn(Hvbc@GyR8ZA~`ftT0H%G zd_3u0iG7GuuZ4Xbnqi2KT|ap5-D!}h4r9YQa&b)ltK#ziDusbMO$J=~t$iEkkJyhQ zFRuL7-i(7iRg??aa^=_7>4*9U=b(>_ITBLPI&2{%&MrFwcgT3>vB1A zDaXdT=eT-mth>Gf2(fN99<)Nf!P(1N;4GPcAuDvBH11P$A@6kJV3RKN@P2Ys6Talx zMOV&*y0xze{1*KB58($Di{zjrq(&tXYJYIh&;L&&Z)9W<882toM%E1yc%X`dME_nS zoAmieAuQ`0LXj5<|Qf3F>UhwM_5It+RGjD#zOl)0lB~hG2|pE&Zd*0 zY(pc-#l4l3PiX`xrX_lbNfY_QP8p6gXVX59<<*9^FmDo2df=JKG7n6bZj=tkr#sYt zT=kWs?PN4perp89$EWCfGt}5lj!!3Pv6w&=3lVhPsk@N3VM9jVeqKj{4xF)GMqo+pJ8q& zWRLFUuymrPNzrjV_nZ_RPu4;m#}ro5C)?0BiFTADgY(bFK*tFPL+~%efU67;yTfR? zZS?&e$sdgD&jDqxvXt__M+BU&f`j9D_zlwJPy@HO4@5QuNpzCB@}`)r2HX1KBoPfI91tcTah8ViVn|MZRC@cPAH_&03&H_VMI`a5(N?K*{)+u~`1yB5O z0?GdeQY-p@Vw%_OC=TY;8L22FWrlS4Cp*9u6l`UbL|%mrkJy{M?By^D83zpn7EqKD zZp|QjN(=3uptE^JXL54~*%eUlliuIK+0^UMV)~oeR)iR@5Ij8?KKX|c z@Ep!D9**_wL<;}8lkFAwzv2`4;SCW5b_lRL1T){tidV#Nfjb$y(*6mQ3MYvKOY9hy zX=F1$R;1cQWqmMl$9FNVyh_=Xf?VE0xiqsP^vKC7Qd7e%82e%xa|E!d1hOY>e_VX@;de2mW7A$2*2x7R7Q z$$78A>iKKnXK+>FN(u^fWnk;H#Q`3n%fWyD2?0xvyBV56$g66VkX=^%Cfd1ND zEW9Y5mb|}y7bCVS^=4xJDI^su@{>_J*8nC`dJd%|gApwUClN1ymVqsQe($*amq=xq zArcoG25&}BFtiwR0ac4pz*l53==0Aed$bq@QIs5hX))TzLH%W^9V)9LtLyR z?Sn9`B<&4Xrt+|oL>Sj9)!-+CqKPA}ov*nx)>nCDysxq+W21jXY*&7FZSIcvGIz%h z9bRIsh|?R`KT4VpMML=JskElavl3!sB-TeK70idj>=H_sV7daAI>Wx=mRMdk#Pd>N zx({e{SBPj|aSx^B3OxnQRidttmMfX2HJeEVjNS{{VvN+=Jg$U#TQ&^UR_!L&KyWv3GF#R{j(myqx z{xcppjg~G31`B|O5?n~fOt1{_8w2VxToxZu=n{vn zz|m(Up3a%b{Iopc$boY-KRw78QY#Kds{R4^L0XFp9SH?J@vCiH3HfhDzFWt1YmUU) z0;d%gyrOAP=ha?`5G??(JiJ(u~5)u%m}+G%0^@{d4xKp;5T({Lg<;6(hivHvYT z%J4VP?Fd>`=m?>RBcX7K zOuvN_P2}Yy4o5k>mHMy=mu+CgUfs`=Xc~EW#goWNV);T(NGPG@bG?S7C%cOM--d^^Ru{t4fV;iYjz&;rWxjU_A|by^;iRg#1bifm zWAMx1!)-gpV$p{)yPZx^d(Wfr52Jc00r8iMu!>UF2gXR)_eKVi|Blv*^1 zj0tjTn8Xzb4{52V9u~+LU|?`LomB7)IFvY{bP1*_@T1VcruTL~^9d%3}q*yqienL0>8g+)o3IDCz4;4lG&N z#XVak{Pr!u^HA6-mangk(gFAT1CtqJT9gxGhH_#Wp(hFfxVEV}6VQ^!4 zJw%Z-)qp;n`3w$)JABR4Fwa68c_t&wGZ{01 z^JYjSW^LJ@^ty5he)YT_3CA!^2=m9OA=L>Kv`THcXm7cGjEERK0eY%0eD z(9P5hnT|;n$1*AIG@k%iwg&1vj=5c*w0X}3UCbPEuOa52)yR~vt?e~`0!tc0uW?as zF4q`1ItFcjjIFQYC>>T^p@daD5{~kMTQ>xhlEVPh*@M-7E1H7FuZ8k?5mb_BxW{LdtYwDPy!o2yYorS0z9 zzz65_qk_4M;?zktNc@S==gPgF*bynNOztDRZ-ccmsgc2{gK;;VXC|*7#hg{`*#u1e z8lT;6WxkhcihE@k7p>w^U=V8ad^)M%1#qY}gwiFLuD~h3a6Kh#BeEOgeD4$bl<#J) zU&v5n8-t^nWHLC$Btb8e694ZYFB!MtnA=IE<@x##!k93B+zYw*)R0svcp*ul!X9gA z&`(<8KRfJh1}`FBDmc!dzu2G=OmypKIC`li>e%~GW+Z*Q#7PA&F}T(x9Cx)~sD^U( zKLTtHvp=-WF0?|#CopPEO@fy)&ZAoN?Xk^$;)2P$02l+C`$CPUKc;u!or#z>X)bi@ zZR|_ntqXVRTCjxCq!TfDondmHm?;Rx5x)De)m}hIPTaQ`RHpS5(t@?WDyoLbVAAj=?)#1OYrA2=5lan!iZI<^ zEijSbIun6jW=i~Pv0kQgHh8y%cwpn@27!Jeil6p~^F+9S6aD6kqY;z@d-BW87>wk} zuN@$1KOx%dMRK`tuNU#{Ac<-4t}fZ5n43NYL3u}Ji{x9rmS*HT=isoW-|1lD5WR53 zVce78?!|qu9ONL^zZ*iwxlaz;J?yHzxf#*%??O1< zXa7SB+@^It(&C@7chq!AMj92abwB6TNK~5`8Riw2b=QvpEXGYL447ts#E``jccecR z<>3>}f#}SB2fPKZfOueEUkQh$L@!Lx5M`2jBIjAs}5pn1JVofJ}XYfOQstDzX7Qum)@tHx-;JE-u9vHyylM+)Qv9 zT;VmFc$TsXJ*eGs1c&su%I{a@_crdyZB9b-5TH)316%e^p5R~Z00%ArKw3}CZKtSYKK(gtRysxqH^2w>{kWcok4hcs( zfvK#RZ*pRB$b$W5+V-1?>NoZo@Qwhfe~zhUyVcJ{Stn&>(p7tae*RPgBXs{^#KfRT z+A#DY&H~?*xwz=q(k=Sua8bwj2Goa6D8tGf&ED>2Z?Af}|KeT9|2t83QDYbNMQJvo zzNq%*axMMIs4prVQCxf+ycP^Vd0$5dyQ#m7j%czbJ=25GGnH*i!PyK<1&tU^sugE5 z0-0&jqf3Hx8J8|2(-l0H<$tk%8h|$PFl~~cmr02~91sNODAtS#wJyQK8^gjoPq1X@ z7MyEP2_r%AbM=u;YPkCNdW32x>5YsV?pL7C|2CIWxccafaGC0#)D)~fwvU667b6uV zSbLDzuh14(pjdsBM9k{r)lkrA^-(Bc1k3133&T=Dm(@qu_7lF$%hhQQEfn8W<66{`kG){Bbsf7_%3&F9cNcbuqpom7>p2p4bxnx_8r86PW^UgIz@)8%=w+$iB7GUAx3${%tdfSj=PH-g)PYOO#rLx zFTqz#r|2z8a45&NxPk&X65s07{CZrcgc9V)=%E}1J(Oc}ogt#lAHR2;zdevKV#olC z?}P-G($`;~0tZ-2NhO9*=h>%{SsAk)^FUYmGLIis|(51leu(uwY?tU zySR!k&X+I@6E01(8j>Yw==>^m5$B5!0`q7L@99Q~<6>cm0|%k$lgr&y9e%*TVsJMc zN_T1n;wk}=5hhC#?_%5R20sK!Z!RfM!Q|7`xoLUiC6|_K@4FqRwuZ?yn2c9jDEiK{ zFd4-gzKqS}ex(VPlha_5IBDxN$ikKlC*jo8Je;$Hr#J4wW3mV3VpK`J7s`4J4NfxGdQqpz__zZJ31 zPJn!rZfy`L@>+h1hG~h=`AQ6vx{q(fbYtw5_;zCG5Be?)4Z*ud=bWzkm}GxL?tJnY42YrsRy%7)+u_4j!bnVa zw7N9WmQnCX`#1`h_6lj@Q4-wHlG~aub?UmW-nix)Xd`o7iV6?X%X`BWm~evIn(+V7e)rcGkr6G6 z`AvL;1qS0yDOY|}i$#Nci?xxwaI}>^#BicFz=WXNjwUk zaN~_c76kuL67@DEG5D=^cxTX^sv&`#JCdII9lE34bG$Qt>0)F)bu=`dgc%zXbas^Y z7IKnw-Ki~0k@{O1h|wLPZtYr7ihS462LcAizZnl$Ia4nt*)hkz4L<)x^h86Iw+Zdw z?Iem`JKd?xI8a0n!{kOB>zTTgkuk(cFz_j|%}1lBz(kubS%r+>*8 z8<*kvo$70o{--dHfk#U|4@c6R{#6({Y`|o~KN~+CUhQ)hFC*AfXMy#6WkD<;MoUAT zbSYKa2)r}?hLY6GyVDPa$#glT)YPe=TwgGS7QNaBS7{arht)%uj0|9%q z3E0GdJ=+A_!T`Q3+DfW(lOd(7QN@L4Z983ggY~`8A<2V-Gd=<5LyPqdi09NV!>|8L zNz+(@rsCtGu<=1V!@)Fl0PhGe_ zO$}b`#nN0?a=Hp3i@_P`f^Wq7s(U$Ni})M^?dtSqs>qG7FjE1%!F$1HWz^t^T|(@w z3xLC5`DqtL^Pds}k0n6b`QE8_@?q2~L>l~!NrS0S8VYWah5%cn5j7B3lAaq}MG7s@ zq(YBlpH=DBL)Qp#!2iTvRVMB!dr5ScOVSjCgy_~QI_h?G@vNo`<5cW_r-f!E$ zhKjTCKCuZp`Qc{SKhXwu$eF;y(T=zb&T9kv0byTk=H=&veW@8X@-Sd)nqe;|?Boz8 zMqk8^pVRwg2oSE`)3jLf^-c z;Vt%V*G0IV?$vv_qvKF9<1$Je^!z(H?7Yj4J^izx5A`|dsK^KrD#8~9{ z7h-%{)Tdj&jt6HoO1UM9vURp2ol8f0lZ()QjCxy5J33_`D&r#Fxr~=_wEH^jL~1pBh|sR#?emg5cC<|x9?>Bbe2NP`>G}<@Q*;`P-m84R zpA1IZ13t=FN4aOQCR(>HZNEGNg=b2HhG6A_aY zboGhl{ndjJ$G15CGGRJ*6s7WIrgE)GMO>spw{{a#d1rGf*O^r4`V~H9h*ao7D!k;E zsn9F;74sSRr6_ewqWzGmT+hhJi?~RIZtWhXaz%40pCBSA(XGwl`0xnFzY;h@96o^} zHtO2o1cO@=f+3ZJ#6?1MHK7kSC-g}oV%mK>=7oR@U3b?Tq@L2$u0LiGm zOD5g~`d4tysqqCsS?lN~H@zOyomiOKH(mQArcgBsTPnm$)P^;all!avA{;N+;FbQ z0+kTH42NrZB`!{VVzvik=^I%P=KC}JzBS~Kk^rOR$=;6NpfCWV1K2jE!1ZR#(d&FV z#>UK8y|NMv1V|8R7*Mq4l&kbR4N|wSoN-+U@1z|k*R(47*!?!+!Uo95dWw)UjL|WS zX}P2lTfoPO*bB!o`qT13B$FTFnpG}}N<<^+<#565?npEtjLzeI4(VP=U3G(-08DN=QkC=>PP+D7>`Ds00w}(Z;P=h^#$)ZZ z!{PI9he8Cm0F{3e+6~ZKjooV_qVE(&%7|j;D*5_b!LgY;L-Xkc zw=$Im)7`RR;TZH@Kg*riPJ4HV>(p+cLPD{b8CC4cYpr5Ks|74t4NF6Ly0yEgkp~(d zrZb_KIa*SRSVN1$XQFr`1vOy?7q^O1pJ80Nj|%mh5o_2zki&|td_M$={-5xZ$BFYa z%;wj@q+#_mO}MzR_6QI(g7NVBPZlLA4+Sb#kd>3`T>1_(*P~j~aK}M~rB=|y*^Eq*2>s(TC!Y(jBp6VTma?iN#p z!Dl-0Q74hO3cA#i2Fnw@loykfY)`T!nk&!6qGq~(X|*(vUd2~ZdeX%nED)2c5uE7i z$)xanN+CU#ufINw9ch0Do|!65EW|rNN-hsuY(22(HY9{FQ$lphExvw;R{|Y z7^#xk6du+Twc&Z66+;+}?5qH1XVAs%rXUelKvJ@KGa<@^rqt8P#ZN{kbQZT2I&ouu zX8~7b5rT&-f4+-SK(a;*-;IeOPch_+d}S>;VdVgS-pU~_a)^h=Vs~IOFw3vdp`~Tm z?2sc)P{Z$p8eP!+z^TM7kl6dAy%zH-vK&}7^*fX&w|`J|YQKl4+%3wVclVg4?yml=sR)Y>iKK8$Aika3P7#dgeWe;6aYoGA=1*M@TL(SBjMwL z6B*g~2rbVHtdm>RGsR_(;GS9p*8YT)l%4$^gAzOb9pH@j&kiQATXN}qXx+fN?X^Dw z4WIhahbI#b_k^eh$ZJ{_r`#9em%F3yS!5RQB}BFe)p-ySKVZG+ShmFI)@Tun8%KxL z_=&M(y`w>vFtL_BX$M7USEZFgK_es2Tw>rO&?8#I*VQZL&0$SCc4Bl4i9{Vk> zhEEATmJ^D#KO+?Hm{kX`Z*imBJlI6Lpu@~Vfmbt9XBh4z^11XYr)0)we7wgFl~J?yx!lF#uwW~ zNEF*Q@W4$+8l^*T20qi7p|_Zkewt^xnT~%q6iBYUV>0H+QE7*gvZqPgA%S0b|vVYiYDhTX~^APw8C{64?get3x+kss1$*NgF@W>`9Q0t)+y zDhE8!0Yqet!ZFX{;R^owh=9BG1?p`z9Y}K-zmOh#Kx1k`Ar%ti!$C|+EsvxuMIkAL zfr?`UDVfwfeCEHChh5~3lG-_QyDALHLS0n^;xtlK{gGT^pfI%_gGT=zDKPd+Hc2gj z)iE2HgC9U_PPs$7rgw|ySoQo+JZe9Kd+3oKE$1>m&-x*o{>$C^;11sT_$GiFBJ%^z z)H$Q*O>SpIH~2mr>?HataHJmaEv>6j547{znXSGcD?QX6H3Nm@l2U#C+0N-nqVbL^Z@k!TRU4{LmK+|$v zZgl}#9~e=}yLK6PfQ@@m7>!S_YFM#6Si1zltow{z#F+=S>`sG{EM;o9W8v6wN8H^j zb4mf(bV^14w=7`JJFQ0fegJYU;)xFJ;}RvGoBh#W_SJT zIC5RT;XuHosIj}!z~n5p&TBP|B_!Qcadc0ccB)O+SleRUf#7~g=2WjK|pOb0P zrJOf^$(i25m0v;_b~y^)l(D-FdE!TDJf9Bufe8f$qf2&}W=;LY@=AS7%6PHg``1F9 z$mNw5$vk^g?iO0*l^9BBkfzcmx$?^8K6oG*ZX#Er6lpSLlPpHWrj|jg&epNR5;u{! z2Kj83b60Zle;qsGtXb$026E3boADbqCXl-oo{DzkKNTGvRr z;r>%1VE-wd6|a-1{4B3LXhzC|*)<}tc{W~lOxp?Clv;V^#83}j-avfwJalm8K z-LbpI%wxz7II~%q=*)s-j}$10PINS9C}V8F&KdT`g=NffbUUa(<2mfm&|0F_RTasV zQ4?@~13l6J;tx_J<2iKXM(<2!m-#lP6-*e7ngT{`8b*iE`yEeCh(s*wuw=}Ou)cyD zf2ppLyB-g(b?#E3-G*q86NQs5r0HPHNgBn`V{JMLuusyp79fUUU=_tvDPwfVa@}#( zGo$w<3+ZL^BgEFsia3Rc3bVYjkltCoE1uRI+iyA;5I={b0w{MkO#)cpL=JtbXoT$1rb#Z0pu8sY!bhAQqkS3hAB9uK&`}sr zWY7OBZj2atUHuniUCuMuWX7A5QE+iIG%*UW)QwguP#Nf#1Wj@*T9z`JB2gbJMyFVk zHcw{J$(Y_Cve+pp9BrYKI0Yj45bdzBz$I-1a{$A*BzSq~ZurmkiT=uBXMr@R!%m-Z zYd=EX>!X{^6qT6@QRt~BHbJF7?8v)hEoow%D8V#sx;D*Nx(oeBkkGI*-?;xyy2qj7 zu^se(#o}_6bF_oW#5fY={1Fg-ViC-E7zUMbaMb9%n90|0Jm;()8W+BxrsZLDL`vVh zPG}N2*-a%+o7gvBpMro?Hg58?aqRE+68oF9%PY;~lXPD|btQ&{-)0v^=mIb}#dG;J zKB4$PjtHXR_d%YzKR%P#d#T7y+IDteWW2Ef!$gdmaJqx+PJahKVA-1xricH6F+h1` zr0v>!!srvT&KICt%GR7RDX&~~^HWbfbr7g(kAgrXEomJJmw$*PMzKA)3uEL2tsTX` z(^RC|62Q3&k%!?Z)h;^uNy>ZLs$0@n+crbw5Eaq(GPY)isn?8qL{78?x=_b1Q`c`V*A*frp zEke&E>G(XjpSi@7R&e@O>3BxYznD`=kmtX9-e<8rUL@m}SAnYu)U8?Pehxx0;J}AD z_JN)upM6Jl)6+M?mYxVv=CnQ6Z#f=gF!dlx^U)yA*K*$iHe>vE&IKKKHF7}{tIY9q z&d!ep2_i~VjmR;XWFznQk-|$co za>BmN0tszD8#{Zwmuh&RSO;z+mAwND#VPLq%ins zT@MH%dZf{b8X`i;X0qPI)@|}`qCz;einpjxa|EG~H1m&mR~MLAbh4*#l=|`?84h2L zYG5RTB?&4K-ml`Uh@K;=G|V%-m^P-q)gt_&vL-S zH_~7Rm@Es`!}PrAVv;IT)U=^+QQ{g$4>G}6vM#+6LgSxZ_0q-JpRpFYQ+Y0FKd3d5 zAgzx-R{-I$H+vs$3w?-CSJnrD2k?yp1r55jFF;H>t92R5L{~^5YQe#RlHlCLc}DO? z8#*8Q@e%5W8~haUb@~CTU&9%hPsm{rOqR`l#RM}lC4rg0N8roIyhFZnsF_^gOlK8) z_&J}D!(@WQz>9F?icb*tX*6JPH8_t&vv*T#jIkQQtg=X_LE7ULltUz$N11r!$0R+e zrlpO<3_lTZpb0A|1ZQDU2{ElTL0-{=edUl;QSka2pyAuX6iNxr1%c7>ic|X+jjoZ? zF>s~t(D)^=s&jCoVZbj-ki`@d3_0i2yK={u%axTFccLq)#0x-M*W#jx7F&B5#e8KC zJa2|HFd8PCc7Ztc!Y7-f%{eogC z2EV|!)1agwKRTwxl!lZk15C#@q)~B1nJl%-BxtLks1VZZ@dMQ8pjdD{fGpaJT=VsN zsT5|3jr^I3jEfx=d>aHi+i-~E{{uPZQkS15QO?J*{MasOSP^F`D}6*sk8d?Y9-@xt zM1ColCMgpyaS-aoWEjtBP&_b@Dj2D+JX@(kW5uby8s-4+!r^l~-mRgujabuW?T0TT zo3=vrxM+qN{y)jSNLh@Fl2h}8cM1?_Qy4ZMO#_>dfr*F#gExu+CWUD@F;MLrs!?di zqo%KO$c{3nXg0o|f$Y%hs86HJbkx(tCk_R(4ca5rJ&H$!2T_VnalaFRl1Y`ln^*>X z>C>v%xk5}4kKmbKL0*a{1}zJi5z`8VSuxt(CGF$N>arA$jVY_kgm86P+e*$;iLOX7 zqqii5j&?!H*~46sKvV`>Hmv+O@VTtYS{sR6ndM|>tn8tqTv|nMki}pliAk!AG?h)v zJhZ0}*SZm;5*bHRhB1jE56q(QpGH1UP(S+*B7PaRLT+;syf5hhwga1P_)iY*C5Rme(Yc1r#3t9hc0NdgXTuSvVQ?>kf39y0$ntta$x-k z>*YX%jXI8v9^>DJWUJoPH<p6)-wkANl->;<10&c+_*-TJd7xMG@bNGYV4L9Ku5!2P@^>%>_^7s*k2892 z@HaZ8;0ZWk>$yr>&%A9t|AatoJh>o<)xEg|9YQVh3dwB4rV!t+cd+EcJ@YAPibc{yTX|ZB8^vo-sxnu@9|eaFm=zHI{y}7R}aT~ zeF&)@psty@Un*|}o@0sP?Ly+rM@8g^oy=bnA*2IR@v0p-i+d>=|A2U26&pq<+>$4{ z0Ctp@KLe&AcySNkFcIQt#|tl1N;pBa`%#q0{O|Bx!6F4=Fh^%cDO;LHYpR~0a2a0cfOG#X~!DrC?y z@jqn3dj1~w{3Gs}(*v|(;E1y^aE+U!tAzEAp?@-x)1ZSjkyj(#;9o-L$>2%4#o#G% zHU_BOkSJ9|+FyPk&vek5F9eHRPo^GM(QT(2yQEOHMM_5NUXzVw3bfCx3u0qMwP<7c zAvTsD*2cPts;RdaJjUaSZ6~I&?GlNr9o=Hr-NcK-`o4rBZHSN>l#xu~^P_}|E<|dG zwR|5)Mno)_S`J30c!mww)i*hMQ5$*kLv+Ej*w*f?Fffyg!251}1Wqy$t`D7_2hGtr4*3)a@j_(H}vfJWjMS zb(=T}mtIk4)3%+9{P9oe6gTd>NSOjEm5&ptnBK{mbOKQ}AyOR0JIjKjdAGDjl&c8w zv~d%QAS8g9D4~ou8e<8l4y6)51}|#ARo*l}suEJ3)D0<5exSG+j28EUh=paC?s#iU zT}P$=IxD^Q8}Z&DqfclxXi)n{F!?ediX)oZhb)mK7kCyEtaYXG%ouzxK_?cKK(JM- zt)Qr*TE*}vA<__y(huoUNZ)77O(Z_X_QGdAQM}QH0L^a7R-gTRbUD596PU#-jvgwV z(|9Xsin62zH+T=CcKCcvBarD*XpPyJq!KR|6dr?i8AH#sn4^g1T5=rzp!YQaO zq3xoc7Lrg@D=9K6GVDURjCXTe7!)|4#!pAZE*SWGBlBqY5F2Q4QpwEd!^!lr-OP%D zP=Ex10EL#71M*=xh0@V0oUYhTIcZVg77!z_BW4SXD*HblQTAI!uR>KvkA&WGRQot5XrD6%ic+RJTtF=VBKzSxai zqRZDiW@3ZYJt2myOqTFV+R9{teF6T3eKir&|GADEGljmhwn1`s!o?*%JiW2-as|g=7Sr9|BVVV&s<|&*&_ce3!`-`j^a$5mTHf; z;!Y`6*x#)^*v}6a((TE}7t0p6XaUB;g=MAi3ijAaAsv<)=f_Q%7cwzuRKiiQ7rdy5 zb6Uhmpui2XrVxdPLI!}apaDWjX8{U}TDU6Y0Mf!Pc>OX*kMbVbVRdWx3 z{+93)`+@xqOgj3@KtSghNBIR_ImdWDTp6QI_U-tpa9Ha!{2-}a4 zh|sW5I*qijqxZBBm_Z-v;>ahPfH|wF zImeL~`6#VLqzDbOt@A(#C*z8A(m{z%#y^H7=O0X=V+L08bu|j4{(dB&v&g0_sI&aT z%an*YoyX5=F!E_KVpfYJpo0O}O8m=M07Y557D8R5G!ir8(;2|riH#8i2lHNVo+YMo zeGkwF<=uf@NONQ%p5tXgHy|O@laQM=fqXK-jj%91#6b@Xy5FEkM&@}Uu#KDt=nVtY z5oeyeaU$}8&*_NIwW> z8dNwX#xuL*${sD`n(6SliDEWM|>Dd5r~yqQJlq2HR<3GJ%q%yJT=Z1GEL1B90`=Ru941h-oOCKhJIG@v>>|mMbFNpmX@?PE}%X=t^dB3zUv@|EsX+Ho;enf&y)`&>Sz299iy!9#b+3bD zZ*04VB_DePbUKIf0^^#uR00(v@hQ-al~eSo^+WWe!<9~eR$*0>qN;`H$b|fwxgDwm zZkOcZZ;^*DTU4ZoZD7+La0K16oFU`b7bXGY|6Z)NiQqcr{~(d3SXp$9*efipUH++)!<$D~~m*(dRQ=pz4cER@;MysVp{ z=*t~5?DP%(^{J=M5%|OmJ1>JXK%b$_9(<5rRF={VJ7Rq^>~&EWWBcAp!2p1E>p-xO z947|ACdnEI0w?faR1u~S3os1L6j7bV zJ211U@rX6{^9szILX2bLMTb&fih_|tspQulN=2uaUfp(3eZB4U>bX-&Nb91Pm_l8o zona4$@Mswc10Vy6ohzm6v3g(BLXb4@a5{kCj&d_58QlT#`ETdyu}DiH{R*MrfWv^n zIrtm5I1Cs}3Pf-kAz&#EfzwkQ?Ltg`VgHQc2z}+kzf?x33MXIv1|oXWhSS&?P;xx$ zKrCg#1F@ndBB3z91hJTjVm32rt}S*Gg5nu#B_&V~%aw@D#XCIh_Akk}3zj6^f#n&D za-IQ{;ra@cr0O&_uJrpG+4yLm^6Su1{+WwW#(-`Ji8l8L_~KfHfMN1wPIXszC^GCc&;WhLWT49^%| zvf#Zd*z%IEKOPFn-qRvEGU$5&{lq{rZEkL1_Utt+hiG-iM%58}O^Zav`|H3GGoo>O z14fH3Y8iTiEg5JhsjDniQo%j|C<(hMrNQhLxv`OSWcvg^papc$DL$QYKsA1!-wgK6H(uigXjY8rJpncKK@s@NCj=Z+b&AjrKm-yZ zM@j_gQq`=L0k=_Van=o6%cjtA!G;7PVG}D^wR6p8s`fbDpE-6f>@Wt}Bp&rk87=CU z#w0U#w`EGWX<}-)*l8(c!yYavW!ZN#8`eV^?~GC^SJ{AW26wbGnQ%ut-j~p>U9_V; zqWAqBb7nM9lo-pgD4sAH*1JdE#@l{qEbNf{%;s^phQ)@~XqUaj+prJe)2O3OxLlrY zI)p=2w6|RxrFv?w1`2~@c!bFdvJl;V;WSuxM$jr!Bhbs7z-F&vI^(}a6pVrU?TI!l zUiv5DSa|)LtSy_-9h7~$n*>`G$>=c+kWr*mXJtRTVc<+)VjVCNO=pYTy5dP;$3eEZ z?5iv$=ov|d#e^s3#k*Ddm^cK0AcbwgMrvD3!i7;|9k+3ux(6kQ!KVfkQd^A*q@Y9F z3MmjnsETCl2S)vz28|7Bh*TZfbW>{}GX`&b*nGt6ev70`O=}~dm_SbKK@U5Fldd{9 z>r-G)BO-e>Nx>VtBb7Yxjj-#*o@^+wsGf+#vLZ3ml%8w>B|+HJNepLAMPfNCu^eaQ zbFz|G$bnL>fMG5LGjyDYkrAS%rzbFp)>pd;v=N^h_{)B@6+JRf?Ac zu3-s-eN{$dI%XA`Fej}?4{VlDDzFb26{{392J>d&IU0jI@dD?*|b< zolE5_hr{0WtKwLD=i(T1I0*N~9R}DY)j0&WBwU5 z1|lj>db0rZRBO&1=HW9@4(%T&!#fU4`(tLaq*Tl|Rm0A5iLByqHt=L3oL!^23q3n) z`nM!g^T%D@IJ7O9>cbcA8^Tv<+{JA}_;9yxQy$&KcaZnbzy?q4i_uQb#q}5U(ZzF4 zuRR;y#gz`Pe(K}ahn22nc?^&G@Udi3<#_C!TH+_m%a)Adh~|?0D~EaY)EgZebziUE z8Tov#J}>h1d-a8p4~YzgK5c1B6Mg=}m;%e=uBNGXft8~0;lBYhZ8aQQ&E?aztKjrw zt|zU$dIInB-w2fL!)N!;u!}G-!!Fw(4@u6j<5yle!!Dncdz(8epN+7l0pBwtK(K%G zt^KooTLI4kfL$t*QTgf}jiOtnUWM@eP@SNe!lhN9ZtWgT0^!byi_A%gVhY z^rnF!y&iL`5Ee#8AUW;T%Q%inn{k}uUj$ucH;_Ob&8-Hub^|e*gZ@g0?1jgZP5uSM zhd%HOn%MvxvfHKo0pcUBhX7Fra@2Vt@i^r zBZuN8!i!mz{5SDe7;zeuD6&d9+HRV7&@E;xJnuIi@B<^Q$?e06*fvb@l^_oHr7`Ga zJSlfCJj>zuZ$RR57CwG+W_*etOMzU>J0E!7@JF2n%aBvi6kJ$R!nWB7ZWH1hSOs~O zcerwu-IW)Sj%h6H#2MeLBL%cmKfy@@k-sDF4vDp|4&M|n=v#f?egq|3zl-NASkPojU!sQj!#hBpcYz(g; zVhRww-~!B}f^t(4&kG)jtQdN>aGpnWbE<*lcXK2)#y%x->5+17==`y|2}ANa2QdiehR}H z93rvwmtaAtoe$%QvFn?GJT!od;s#DcXE8a_=QOB(>^4&K?8fZMk?hPJXalin$A01w z%$}5N-64JUJtppX zAw9~F(_kgAe1+&Q5)2lf43hZ)>CnL-`5nk4=LfySS5W6Z3%#>@58R>$LyNcICh%YE zEe$Y>vVSd}5iQSUkgdg7ECOU}`5-(8SDtI2gExbuEH|+1K7#Yr2NVz*Zp2=>_pp(lUy^IP6#vV?~d7$&01}h79=F!!@iSjNQ zDD$kb^JtVd7kOFHU2LL$Y_O4##rPD83kgQYFI z>iHqNMfibq84Qz+PS$%_Kx{i!LYSt1_^hV*yug@uGbs(+&BPCMe4A-V&oMRjHPCVs z=;oMqh;`9vFcZQDKZFl{fDZlNSxhpp_HJf6E{+bAbD4^dP@3EKvRBc;}n z5p~7@)KDwPC7Ry2P?X&zW)9&CIv1Bp48R7S3fjGYRo-l?f0yTpmIMt%%y3J`0jyh;Nl zSwL-LXxQseR9YB~8>>Rn8X0u0WyUxd2FWXWaZ;nOPaf*X$`IQPZX$6ONy8+f#lz@D z5yH*E-)2K$PO{dngpkwR94ueM`@7Yg%XhV;pD6vCWa4lX8UHH2(}>%7e%J1S^~cM| z%N<;(%tk?MK1MEK;LcB&oK5$s2&y4uT;`aZHz@p)D^#f@*by?Fz>_ zwjIT8-wz=B@pkb7pp?=R^Vgyeux>jSq=!#&Tv^7$1KN&T+~)ED?K$WZ`~gELf2kvF zK$12)@kKldgI+A=WwETiwZru?rL}3UBrjZFLhH+iYKu!BNe>U>!b`_5Fe=)2H{2T( zE@8Zsa^fl06ZjEd;d^l_;3chq;{o6xk2Fk~Zv}5$#& zM9bAne8I%0i6*>doliiqbv}`9gMtII6ql)ivE?#CXDyeJ4wuVBTNymKp3GnATTsB6Tt-mf?j@$53!%lPdy=x9HFycICf3RndI)r4d? z_@S$kPhVgMKRO1N5E+A?=QK_RT6mrB=TN5hI^WOodkgxRBc^+ zyU|GuqDZs7YLbdoP05nb)r*ar7nvCvsiww@FESGXMl_&BH9rm3RRC5mG4nwABxM*+ z9nB>6Lciy8s6*5-%DF|y1QgRTl>(u1Kaxs9WA;sO_1R5g-kafl6aI~0U?{hZ%Ry33 z0xuD*G!8`cqZJh^Q883e04rg#Yt*=aZ!sE_o2|^;FaGLz>aca zn7EYPRJR?a)z@jImKCXGnZ4}zR`{X}niVd| z;Py5dRDq%&?53qOh)>$cE^OFFPJwFeGyGz4w|*bZVC~yKWHP+6@P8j zm`8W2F_0ThU!lU_)Gox8ZBiX9DL$>n=oyievTT8GqlS1kcKJ#5j^-UB+ zm|X9|wnO-pg=n2i@_u`pyc=agez`|zmi=E1`quuh;kVPEqxOHY6|lAyunqv)f09v~ z>?Xd3w7wAQC*VD?G8%)}wbrqE3U~u+)3jUyTR~?AXnbDXscf*`V-h zc%6qrqGYu1M01)!e3rsQdUUA%{g4JWYwm!GQ@a^fQ~MXy-83KAf55pfnU-Rk#=L2p zw&=p0oxL$E z_LG=hF2a;*N9MRp^+Ah=OQ|(Av#vrhB4xvX3RMi(@V>6Ic|Ah?Z?d6Pop=uT{u21c zy)Cx`>f*VDy`2WJ@@{G4>S}hk#BOu^yATBD`@|@%>z)I&WxRdIzmt^glh4?|%_LqP zOG5OH`!BytJBCD<^lk}*BxDnAGnKW@BH;6HCPnA7knL@67Am%;G`>mavga|VT&mb6MmdVLnI#WJqZ3YOF89~4*qn2x0 zC8@I6uWgy65d%|*3)g2R<3vsTh3h?pn*uRqyCXU)NN1ouodM8UvFmi)_H^8IJ33fn zY@=)G@-p&M?rzTz;2tn+&Zx6%LFc8R&O+ht-km*8YA&oBJ^A!x8iG$l@M#FXGn1yw zDZxo>-NjCI6f_Gi$%0D)L#3FFiY8tjFF|@7PvKrsYk8vG)&DUFIUc)S<~v{3hCZh{ z&Hn8uh5rc1bUrkECT_z_nb~Tr@d@!1jB2-AT>9wn#g%!E+ZAqjVq-5D=pq6ISI0j% ze7>`pHFf9(T1h95cQ#Yt^wXU@e)tNsUSenDS49dCLk*4*D$DTC$n#fN{}&a^-)d*k z9_DX#&RKMr`CFZL7F}rmR(H4&dZ!!V!(>DedQxr_qsJS{uEN65s!5z{ST%{Q-K!>h z(l8UNyewe|fHeUuGXw{k0U!c!Hz!~)F90DFmIpAvu{k6iv4FD@nHUo=CowMY=2VOg z#4BXh6tZgyxiz+ba1>+^Zws8pE74q37okm%uVU>2tj}E1mrXcN7WrFSj#i)dJ`PM> zh>(Ejit5!a0>HFmoDaMivNi1(=1;29el2v;{(~@%8YL~%JJ6;%(x!0;6E1ie(^OQ5H76dOTr zWH)CpffnIX<}x6pg1b03z;*SZ4&+%6@B{>EkkcSy=`zNVwx$X29kv3w%5e(x1j~Gv z)@qz@i7ZG2?L`bhNruQcb1q5z80fOLFgOF|$F4!$O(rhE=dF;7i!~v%oE8ppamf_us%%-Jc%WpSP&}~j4G^EtLe&TOL(E6xb$P` zjA{05AB1xL8Tk9El-sM2Puy^d$+{2DN$eibOCg^Mcb1R|Eap~RfyhpSm|j#oLmltW zZBClT)c{Aza<Az)kqh4$GtKLr z`QeQ{P&veW?vfa#U}Jf&l~L-8cOri+3}%J}sdH_nprvH5FvX>gSq?M`VzTrK>Zr8c z>}T*VTn~9IXo{Yl#gDk2ozV)|)CxEg0IC0}#A%?3H{zppkYnTPAS8t;i~;UP6|(Z% zJlU)#mKbV~?EVY0;mz@%6y+wvb~0IQPqfCtFrAgqH}sgN)-?`P2%?#{^oXH#qxr2o z!dvE{1r(cy7Rfaz6fjIt*a?m3tvo_!t#yzN*E&R;4f^yOR0!f)2Z6fQ!KpTb*%YWa zX5y9T9Q3Pbg(G>jl@m2FF2hSVj&t0BxCwKYLOq&+Nne0VV8c?q`YrmiPPE7BPimG8 zi=CKz9dqU;r~Zm!3pUf!l?(#9DVVisemD)nYySj>POhCzXfbHOp>31U|Dp22w&lR3 zf?(VJMj(okWMklZCst zIi1kL39WIJB6w%tkKH%g=a|INM6)#1#tFBY1+j4?AvTU5v~gGpkg|*A%UZ4-WD7P; zL&$&aDahMogf|tkq-! z{T4dfZ(*H2QSO6@nALoG%ds7kg3eb4riSGf|A?UPYlnm4mn#)iMD{#KI(~GGJYw?w zep|$((jsQf2Q$RNGIg5eXG#xs+Qgz0bx)WNqxsT3>8~FREx>ZU)L!(44}J!e=3-*G zXfNJ`59wN!z&gQc+(9O9HS@wg zeW-$9Wb~)l*86g>VSBHVd_!V8;$ZXW5#WouDIBrB9?8mrzoz~2iat_w{2~~rUk0LT zVa)A7q>Knc4cd7LyeZrOO_y61B)E1sk}`qV2&IAA=Q%}_#E_iK?I;0*mq zoF)#6Mk$;a`(A|>F-*ES(n%9LWX;7%dv3zx(s&vl$|ybtLMC~*9Jh^d?u@eFU#R6D z<|Djsro^e`Tlv9J-(~!88uV+M@|IS>TU!BdYX!U=fOy}C>6$u&Hy)T~$yA+>@QpNq zI1Q44PKZ;x76FTg>o-7qaZ3pnU}CLgf4y)qDlBROnKgCuK%}AL%Xa|iG{^(&bT9J4 z&!WGW>Q@Y+P=wGUrkNjNnx!4$K9;s)O0r(N6|8Yg$v9%0kD|6S5ru_!ozF@)W{!E^ zv#``;fq&ghDYU>$3Kp2o@U7sRWg>f2Sr$H?!3%{*0E8$AMu@Bno;f9TIjfUPVmD$g_2JzPtF|d1L5nNS>%>Q5Ub@TX(M!}5izLWwty9{ ziFM(*iFM<#$(TWh7CQTu$5FBHXYkB=tBAmSi!zySEspze`R$H1&L{}qo0IC~S0G!w z7Gj!);GM`VPM5q3j_6$~f!T({H!%KmH|cd6)e$rP)OBlVAux-|h$ZaFX3X@|AvpR` z>tlzqzKPP5DT~t-t5yVD$C95ynCLyOP5rw!%%BK*V49#_+1R%14sH)SV2gTZ#m;B9mv++qtK|BPE2OO)SpJyww^NDx5dU`0`S}M>_2a4Z23H{!cM}ek@bQ-Y zyxNzsd%$Us!J>>!z1mHH?Drn#Nx!33+1y_qVK-dLz_&V@!dTY0bKy{& zTM19*v|!om7|4&);4gHaQZbIP+{5+JH?^^GcoO|mz=w1FM$bM5emNFQx;#3;?BL|b zYHT9oC!Rj&UkC5_*8a8F-z}g)Z$LosIcfD6`+50&$4^W}N#JJK1fFwyQD3%>a-A#SbY-6}Y}*FP>_>;aZ> z!lVZU?YY^v`70p%$sAS^#yhhEW51G(XMJP$!$#p^eQC+~h1mRt9(jrDK%4zBr$NeZB}K343gGPVx~cgtOBn5Dy5K-%AdvESfE3ry$Vf!P9o zB~(dmu$wp@@%X0}7PTFZ(|-uop?)uZ%zNl=@Bt?0)^11a;Dh{52G@wA2KgiM?KBuP zuAqDnb?r|B`&(i193MQ|GA7K+P+Of`@! z381{Me z4LPBBDId+cCM@)MX+lI4;v<&Lh~Y#MiKnXZjr5t#D)zjQibx>XD0jbVM{y*Xtk6suY7H6{5>z5@_hwY&o=@ z$8$FgrQ~hlr&#NBmB}ofT?c_+>Fjzs97KJBAMuH~kF^3mPCzlZ77m*FBQHTyuk!jS zc(_ygHZK9&+6&>k#_Vz6tn(x=&0L{Ql4VUztQ1_i&@JkZpmMU`(!sO8-OC3v#Q}Lx z8*i}<=vQz9nMenpgj3m{Xno859ie5)-6ocx<6_|$CuFV)eWx5sX~PINmFg>^^1_`B z)t#I`rjXPwMssYGhtr^NbdF5~D}Jahg&57TMX0C@^vFPtA5TAjPO>s7tMliK6wRMY z(FnzG{(J{)$ox4Yx6Gdtoy}ataJ%{QD4nno6k6k|6{3?~;wiW~e;zUIG^lAhe=by1 z0Mg4K37J3Vhgl<(Pp%Q}ARki`722~)4rpKQ`cZHJcP2LD%Gx{$7I{){%Q{7 zhhRUbHiFR(!N4dqs)cD@Sp$roU}lOrMM`wgh;L&9Y%0*Lc6?Uf_foW96EKR&sTG** z2|f*EoJRSKIDM;7HE+b{a9t3c27_yN0C7R>o1mvH-^EVjbAY3=tpqiDsWwqLZT2l- zYrC9Ga8KR>>3@D6p;Zi@W=Hf-LM8@QZS*hO7rkFiQlH@^CG{iQf>!GQj^nSUy`t?r zs+m6KPG-?hJuT@}oT}Ow&Ko`r-^CpfaH85xF|*G+3rG7+u~213>5?pBVDj zKzi6`Xh0WQ5a^R}kt_G;VCY*&ZuGZEsYrqNFGWn(pTuw8kx4y)Br&mzIluAbNfWpO zd366wa>>59Fm3u%pjMP$Xd|O$j0b4uYuS$tT1F6tkHUbL<;6rD|96;)7(TB}nHu5N zzDa5BTV5}nwHc;|*R9Pt$KVcljoSAHUu4*=ZNg&fNE@llrq$92B4hR~zlrvSXTM3N zy0^A9LpUgK6QaZp3YeQ?bXk0k9U!BgRJVkr)1Lnd$ineY!>`s8oKHpn@mom4)v0m_ zREGU0N6|FtdC6)Tbz7FbwuOPX(3m5~yr*|@cHk~!`a3b0l@IJ+OAlg0kh?ng5`~g3 z_pi?^Si5G>71);v%anW9XEEqqGx*faM7)W>Y`JSaw(RUi=s`~a`V~U)(&&1So10i2 z+)TgIAcfsm1-Gb=QQ^B)eT)L%SJlS|@ZCnAWNBnSZYmZwgRc>aakcusuD(9?eM5Z< z)OS04I8Q3&&ev90E+x<@cS{Ry?&}~lc73*kp=2zfLdslSDhU-*Dzrcn!UiN2T_7pF z#gu%pejkLc9+`krdsu%0P`T)VHfaU)Mdy}81D(d5U_+W-exT|7|JeH$ILV6Y?%vzo z_x5XM=Wfq-_w+n^7I*MEJ^PwPT@cs>1s`Z6h}bhL*_~z4umTM;V34*as}iGrijFcy zR~%t|#F)gWiHS)}G{*QqRNVML6a;ywm}sKL_!)!W|9?)^y|;U27j^@F-{+6{?e?uY zRdwprsZ*!krxbz_2bW$MrxaXFi72%hLB_l901mEYbnIL3t}|nB>Z>nIN(KT8J4i;r zEKx9(F!iOQwB-c$8&PbXC8H6?^T)Y2UWgJ}@JMB?+5-IOpFhI{P#1*fzE!y8exf;#!K*XKZ!s{+|*(*xMe_;^EmT!-9>D9ckj>vX!DJFKz z0$$4FrPEj#B3k$Pyt_$kZcCldH6>G1PlDCq};O(2F)@ka>53=G0B1B1|HKv^~j z*@|Ka6@P><%)lTFGcX8E2E3DH5V9f05Gwu%VVHqI7-nD)nha>c&LCu4jv-Y15yCJ7 zgD}j%AT$}^?L|LziBV6)%yWH^0vJR@gS7#yC*CBJ02i0aYyc|^hqfs<4bbL$Hh{U} z>IijRgL!0Z0JDd5NLCqyUNSh{A%z;mWL5wfHAbM!L(++Rp|jb}yKN|Fi+iDM<(tkn zEl&ZFBEYM`Sh=_C@-Ia-?TLIgMK!ID zeAL(yPrT|k@k~l#tF%XnXSmGhCY~Z991+S>gVXz*7BxN^4;id&u~yx)`_e{Z>=I}b zu42|kFK}8^(1=BXhV~j-`onl;y;EXRc_SuCsud8GdDeQvs8B~CQYv;tl^AOfm4uB* zOyo^%#hBVl^F?otx4Z<`wV&bMh@{$Z78(Q0x-~Rk!6#|m z5$rU3lXv5!dYC6K*hKltl16&+PQHrVmV6ZDz@9=K{hmRI_semsYu6q{lDQCWT#}3O zakAnu*?p$I<8ax9#pL;N$q(b9u96y(J}P7B*Q31rGYDbdheXoY21rl}Y{}w2r*#{m z4S--qZ|sZstUPsEUj#n?T?y?InY0k~G1)bBPK&-o@x%bkU*(UB|5k0b%xHGdCUV7mR zUvhCsjKrP0@QRo0*?Zv?Gi_$h!wSOll5h^qh=o-7Jx$p}J`o6`g8aZI2}Rxv5^We- zPFql7RVuZuIth-Uz6d$f7#O^hScn^0h5bfi?V*+5=eu4bm&rB?Uaco+fhGAY2BdWK zcc+H zqV2v2TCb&E!EGH%shd;*&B^h~O`OT&xMqM^QTqk-&TqR7-8F>iF0jIBeOrFjeJ9+~jCL(F%(pG! zHuCo*@^|su@WL1Zv@saww7%0BLx460!<^QAoiPMxV=zn!i{+xgyCvvb_=SBQr`6%b z{w5<~izFUUZgj;I0OB&suDF%%xB@_27N9HcKzCdLATHU#xK4{?RvGP3n@(H27?;ub zx;bwlsCUnZzI*#F-4KD4A22=mjHGV>MD!$8;K zR~~e8LeI$UTCj|;^n~a*?q?zku;`A=U|4OiCK_t}+n5^EFV4@L@gv z%0F4f8E?Pz=}zJeDBU5^6I|$q>G{6dGvClL_;6)WL{{YU9)g!TgFV?MD_{+MGM%aI zfFaJGB7$lMxA`F`j2>v^$#ho2PF=8J=IBBDUX<0CX=vX=x6q3IH<>u!=t6WhloouX zb?a5R7N&=7G76`DdX-78YqlFD#^n8mbuVSAWbopx>|CWj1dDg9? z3kzV1O^b@9*4+4E2*KeK60*TrYhpm^iCY$6B$I_MMj($toSEYJZRF=Go8vrqNt_4i z(w5UTO1k-eY|3j_uMqIbEj9lFP#llm0xRRJ7`$;Jk2Btp*Uc2}&wUd*fM*OH>}2&# zC-TS|_24G1?xX><5ChQmrkekYCIoCvfQo0YP6huCToW0XME{X;gaYzlWMRkAkY$j% zL6f2q{Fpe%i&O%Kc+5T+F>b)I6``Z~g(w?5{)%$qBq1iYiVq`%l?tlm980WuqAh1C zah@q>a^fuI+{8BhmYRRs#Zxf!BV?$}MVT3~QsK={gEAN)_!J4e18+Km>q_xG+%Ol*N7;bFahLObU;{A)If-5QO{YBFC_04x}7+FbA)cY5o=bc$3-9Uc15R z@5$m!h6GJG4uqK89e=SC4Dub?^`KOgPVKN5IE z{iC+|b?=0+Niy|L&S^2{i8U^tiW4_9hm=O z52S#A{FI+``#%kCdYOm#As~zu7ZCAV0r^UofJors1%&I5b?{r|gON$Gkq>5U<%4d@ z2Me=IJ|q((ACkz(2i=elmU0F8V0pUbgD}f8Eh&8~$%mwVF!@*!T}(a%3FJfK37sa> zxO_7h zs_?%FMZ4n}m1pAnRhj2sE%W?|0Zh7oAG7oJ1B~o8Uq$yXnId`RixCCa1Nt#Xza)&( zuc5QA4X5xB2g!o-+Q+X-B)(?mLg9V>^gifvMv;@dd3PPwC4PyB39J&_8KQB$eU0+I zDR1A-iwd3$#(Jvq;bIPp0&6hAh1}B--)I6zcF9xQ+x`@|GzX`}a$v^IPk0EFc_VdI zdK{<4;!WVxD8cGO0s=b{;5~9V$P1Gic$*ws=(2`_5iSmSdpPc0&k?Ho_I_%>K3vf_ z1anp1Ili}{A8R)pXvoWOmU!A)q5YH0_On30VGGZMx)+VV(JTs&CQ`7`^a>15+J%0Q zSX-l>9(c^y=4_Ohe@=@r@FvyXMRiJ!c$sRi&ek95NA@JW zh21?U$5`H-bV_-RftirLgvNPw(O^@1|$xkTWbfOBm_&V?oNzIOx<`bl0 zacQh9t}_u*me!Lg@x0r-0UKa@__q%UnZ=!VyX-~ zKTp7#B+NEOwdA{~B#8)%jd8Hl9tOc~MMM=9_(`s3+8QjTUD$YBVm^LqDOR4MpvwAlC{U%0Hu>={RqL{rfQ#s}o7Pcq=sZQxUy;+N}9rOJyl zc~nL*#WmS`P;MiJ3}z`e8HIV{FEE+QmV4( z%@k4ti`W^08C(>2QwtLgDa7JTpmd4pWbB?UE_Fg~)kj z7vknsT!^GM;6faobnu^S^GdZ_Dh2-=-dM$IJ2iaNSl3=unSxqkF)g4xwI~Vo@F8c= zWe~h;PXXe)&*o8rEHF{yXuBpzVSG)f_D)2Q%RW4VC$&EVY*ryo?c4iM=gHUhWnTD=WyN8lkI$9KP<{%Mk9lb64Xt*UQpb z<7|}8R|D5KiK}^~pqW_jRxUuFlbm}F?7_l(bnp>;(W#_^%SIEh^Er8spB|t8NH2Hp zn^zzWt|;}+{}uch7_(fW5m~1>3;5Uu?|7$q8C(*(`7Q+YWSi&XgB)BBRshpzr&OPc z+MWEq@4+-v^Kzs~YSQM*5Itf-QJ6rwBOVYI@<2RC!-Hw8^Y#M+hSh8NNw-TTkF?nC zyj}P#FtR77gS+u0E6XXPm0?=cPUB{vRc~IZ$r=DL40LAuw1#I z2dmGUpOmSj=FTP8ys~glSlN6YAUJtYZe9+@#5atMq*?dQ(tW4#j@KMxbm7i{&t`H!ON70EUZ6?~{;J}T^E z(Q)$Ab_^x>uDwWXPI@8(nH{GfwpU4&n^%B>6F1Sf0t;;TYJ?{%Tl&0YWy-?-3yb*Z z?jV=h?LUOO^15DJj2kPtP$k|$=!9f~=fmz#f#;DB=EUiyMEsg>K;}3UMJ9O4`-|Y- z@C`4lyHCvj8H%`{9GU+BU1XE+s{a9(ES!5S7J!5OsFMBUKg@@t4|&;kyUv3a{2aF6 zL57j0r-7z#iChuXd_4pO7oXmNkJA!}2yj^%adQb^w|N78y`UoP`H$&#=iZHvwrAqx zI?UP9jf6)sDkb))NbtEF$9+pq=SVU`&(4A`6_(``% z@v-!VWCK_~4Z>9S}L~<0mwc@mx-RKP<2{-;f!$^7=!9>a=5?jYiq&peOrr;zI zDIQps0u+R3O{CKoBeZ0$OsPk35Sn?zKIByl{~pJDy!J}IZ19}sC~L)O9>)*r!b_~i zJ3?-JcbL4n%(3`P_q(;%7hYUBuuUMgkY&QRbyJd>)iLs4UqGCDHj%LEiB%Qc}? zahg#@KoyN0C;WzR8Jcv;#>9IwazU%;VLgaMOcyhhjR17d|0T3{5r=;;4MdMuR_>!qC$HhK0SV~*C_TP-In*78n!%$;&zDw z>OHCu6$MCPTof+z!O2Jzu*5^%R+^lIDgJ`*V%|!Bb3LV^&+nV466*KCp1Ow2eK3>{ zhe1Y)9>e&)#N|^CD@c)_+5Sa}Py;`_bDS2*QyECh+~C`gfmB3(TIFeW$D47P@k`jp z%J*UhissTNH!6B{ztGE9TM3pHgUvM)MZ6i@jsz^ptTL^a-j~l9{YZ zh^hoPB@)VksmkykipL~o+~ao4F^Z{0V)7sQ$M-q1ST@;*Zwh@t#;NT%zx2Q=JXCrf zp3=h{PuCIcO@EvV(1@;R$#I!Hq3~==HMAwZRgKwWc`8?5g9rBbKsBByy58H6!FV|b zOUU=5_nhr{`Krr;aj+Us07&o2QNitJ+LddJJk%aJz|BkkPx@POp4E#tRec zlUMKyGi}XV7;_9U;r=4s>*3x;Tbnv1>&cG(5T4D|tjwS`PGm>#gr8r+H%`=_k(E+x z9M6vKXYYh>7G1{1@%rZZ)x)%Fuyy{GHGb>!YsCMU`L*&Lod11+7w2Ck-~8-3sN}hy z4`6-53pS&G1zgA$;xf%=o{md}&1cE)Z^-Yn<@ap-&i$>W+)gTwXNg+;-H>;JH$aoL z=}|dLiLLv4MAxvm&A&%-C>$N%6x5-qWv*g}Zz>3PdTvu(o4%?;p3LOuo#qBm>$C*h z-vOJ~+>Ws$R1~S7z@(Lxfwn{M+`k}#LNLG=$i0B*h%1aqRCC{t#}vptVa)2>X~+WG z#=+mH$lr#L0R#DH2&o##--VDtLPQ3iiYmRAd})3e`Q#)A7!4v7&HM1% zd=j$6EbJ?APZXWzKjVur1HDoz3>*aH3gw(#=^X#DD2(#nNem#5`-Jy|&ix2!!TX=# z(=~|A-ubSiC9FB3X&=k9%>cjs zCAaxFeCI9!DHwc7tZ2e#=e(1Mf-&+2#_X8$a=teX%ou^z`AN4YOy0@F37}i&dlI81 z5n2rIp6?0F%=hNTkV!aMu;cshp$y3@FJU|G!32Pg_6Fvpyq>@3IB@1vq|9jxcAMMb zX+E7ll<@M@@yFwox+EdaKLc&|3ttG5`ohA2xS25THJ^b`p|+!lQ7;;}D{TVv?Sn)% z`TcChG>5ygb|U{ZPqHhfz2;d+izQ%HsgQcXy92<9$FbK4y>sU>VjS?B04Rrj*PD9| zgT1-4L!g~mknI~mIM}|4pLBaOnL(YJPJ9T8==&%e4HgmHd`UIojwbfQVke)8n*3gJ zl&XC5U3hJ!J3<+|H|P<+gHkifxA30V#BC{=+2%$+d=@TObB2~L%ghj$p_Af2SQevQ zc>bX`wS?Jo3Cq4u$AGll5O^_@E%})e06Mrs-=y&gM2LZBVGO^4pX(|+m*~U_JAXZb zYW}7_V@D+UvX}BxvoD6;#C_POfS`j2gV}tJOx}aq!fg4vTHnrMjyZ7-Tg+!le!gUL z!dzQ1;Z-GcqJG{`z;VomjNe;s;OBtXd_7R>j9xm$T3_=zKxE{3kg+gA>GgAypVyI4 zp;TO5ANC6wfAxV{pTBxK8meGCyc*G0W3ZE(yggIG^GpX0N_S6E&5#U*!sgcj>H7V` z@&Mq>8Jrw#$|3&>GUfu7A*lX)D8|$nYGYHGBm2^UgK3OI%Lfi-N(W>SB^+J* zsqMcBQbM@O?_XK@#nHP^ek;@a34}QP<@7QkX3(!dh}SI_;{8V!;?aj+h({l~5RX0rLOl9wAjDTfTvifd zfr}1?or}c3Je)dM*Wz;h`TjzGk1AwzO`%4{U1FU0kgh6tGDc;6tK8Coyu#3&B3Pkz znrcMM^Q`vvGNS2*re4#eaTcAqf~ za{WA&U_Pz{vC4_~n5QBBH<3LqHSaKLaP*hSL_+kX@2LXJ#6dEDP<0xqKCFs{sxKQU zGOU*3#XT*Nod8PuQ--n5;TNKqsl-E-f;~Gi1mwHl??k)8erhav54hEnZ|F-(}8*X#ZQ z8pBUs*NQxt0Un74EFl>hD78WZLBW|Z8e|D~MH&!w?s*{%cEmhym)z&JV=KA61uVck zjjL_#Nx1DCnP;MZ0KO)#oa0&nSK+^mBF8S7yUh!Ld;)9J#DVH!R?6L0w9o&0OZm-jJ#^N*9rSK91$VyZ~wPrd+v~fu%oY<>8SNnT3GwjGt*pEx4Jf zoW6c?q7ys|)uo9wDONOuO1vB%PntDJvq+l7yvZ$X{>L=TAfQ|QAq#N4Q#P*mTj6Uz z0a-%s?xx&5k#48OkJkPva7le66K_Sie<*VJJ1U3y*~(jN({oxx<2Ek~Mei=)gKW)G zwzd(i+nl5OM2U9=oUmtQR+HOL0#KBWCq1fCAVXs5Gz&10$tiQKwmFk&HsQjB0Q8){ z+dheyOYM{K!315182>H(UinRsIkq8reE+zqJsr#cby$B)<+O+*JvBKxdoD`jyp&#j zO}cnm!n`IeLdNRU6l9O7PC8@NNp{7nlkriV{B%_3^Q1bNIIK=4#o@%TI++I5DM>r4 z6ZG@^#=qh_C346($}+YOr$ugFdl9yqr_O*9m5b?P_^!r(47?KcXK9oYEhqC1GvnJ- z15>oT+zrJ;?IX*8cme*<^-#b|lmbzH%6O;6`bInB2kne!!fgn%LWok>5kk2@2tQqE zgfq?ifaUn{ABAFjDk563-A=dD;zz~yjBdc002n!@edd1?IlgKoIVN7Wc~!R@w4H8L$RV}Tcn(KICuIXsu7B1}_?dVr^rG}8k- zjc%vKk2a-e!zE`L{swILcWE~-S*hI+iQC){xz(QE=>Kad$WNEBSHKz5`DY36-Hb6uw8`=g~g%8pO7$s~2^f)a|6}5plG;9NL z;7V;k>18aGMKPK-pdl1FgzyuVBvwY#2KI&B$=M{$jJ|&pZrz9BzWR?u=3i=M{R66kDbzXU576j31eeXRcUC5j11HsHgcRQ+g=^oEI{&9Ob@PgNq^eJy|b!8Ad6(bX?SNAuo*->t#Qc^^47C;4qdhrlkBLmT!uF zG26Trz{<|a+pVkEkLc;F-ye&f_WKdN-|yE*li%X(P%*h!e3CKdRBiLrSOxNpN#s6| zQ}p7VOlcq1xAEw?q|##_SaQeVhByGH3-CZyv8PtCNO)YXSE58~JzRFmnDc*Mh@$2Y zHP@6QYWMRjLU$@GAlXzmNmN-6bAy5dP5Mfl94SbOaCzO`&cihElL&UfNe-yUQoPS% z&0(~dE3PdS%k^hw{j%__Siu|g6RbJ_{+i~Q zK<1ah?y_H2c5i*3L@KT+6|41A5UJXHJ#!q)!o@l*4gujH*6D_Hy-hJoX^;y~hZNWN zRiLc;Ra1k1T`HDJ#o_uUqy#gDwX(Ucv5c;Xbh%in_xZy>FzgQ-1fTXxC&47^FPXm5 zym6(3C*94x34~(_Z$-=}eZoe_9VWt!HiB76?B>}?`)!2GVKD4Mgc6A8Gh#Y-Bic*M zpY{E@cLSt5b;o0wRnDA3>G18Hr$gubgzd3e!zt;xb@<6S&Gk53jiphqIR-}|={Dbh zN+>s)8}PyN)S2c29A2{@o!y4p?#Ou)*iMJltlT#-6UKIj+@UWf#!pG);Dv5W%AMf< zU_NtiL{h$(Q*!5@LmhRU_UM!f?4aRohOPHSTdI3%7s~G2*e$;io{(oM8F#!7xH%TA z?T`V<*e#f;n#1+aMV!(72$zuk=fOL>@vY8f`=1BbToa`3Z@@LrpV<=-(7X(=}&%v)8AD?{}Tzc`NcJkbLh%;7O-+Use9dnvbl5Yq|*K(8D)kztJ z)cx5P{czP~#wWIMyQ8OqE1|~}%G+IDlQ*uP?LB}>Y0_JC*SI>$I~g!N;jc?gr6wvgS6jce&+<38$(lg4-Yl`8(DXz1id62Er`(G+>kXSXF!z z+SAn@_~wCcLlSSzDkc0Jq={WkVAZ@5Aia6)K$y+|dJZC_z_r>`kCVy5oC1jrUd)~k zcLaQlE+`FR7`FKe1dX%ZClU*Zz8e$kpc+cueBl_-u zUjLlD)@$OdRW3LeJ#Ep0IX!F)QCP}TkVc$?^>F|-Bm5f&hsYPo*v~}9E}%Zd(SQN! zTncgt{DIBc%ixaqb2b1mVdLi44(xipNp`5N-n<6;R2wI8LZEiymgDey+NPn|%aCME zUf3&5Y4#HUsOa%b0r$d?#pGY(g5S%@V4Ua0J{LgBV4T|l0oZ0M)Gt(>=NI0zP_WK& z<4p@a)>DB8<1x{Ueqn&Vw3n&u$yP7eeMotoC7Uau*G~JatWIIL*`K5h%;%_ZGZDmW246r6*k z6355m;H_|62gGEcE2^7H&%Gbr|M)q0H?waQjvf+KDJiDe#}9-YJYO`tZZiCqy7{nE zdl%dwiZXiv@?(FS#!2e(l(Bk;euF1HtrurLVRKwTG~G6`HwRB;DqnxBoA$yqUc)#W zjP5l|R4h#5(&DDCuzX%(57s3AH`_>yL}+#CC(pCx1=r6cqci1T`qqDSTCAN3OuVF) zCZ4TnjTe(BVgj}5*f}jop)|7fAvrnAR6Ieeb$tH9_4aHvL~!fvb+4oeR6LlCh)I-< zO3ljI;N)?xK7=8>rn28_FJd;PLGoaHm`x8d1wf~d#S|I>PSKjCfTKt$4Hg;G9EG@I z8bZt(lw#7clN(xeXt-A{=C^V9;O#DYdc=eklX zd!`;%_rT4qU&NW~`dXaS;p_@Uyh8_+PCL%+`;x){D8cpEg z3eGvEj+rew|F4Tlywv0k+V8lDH=sN}7o8iH7~Kw3iMunem(B~Gt7?T8j4NWCo#8?X=cz*QIInJ;tx%z7PPVedtZ`Jrle*w%)JkkxQ6}li= zcTVC$p=M=z7HX@5MNsYxhpt*aa;^@Y)dk5}Bag&AwL;8?$AOYB*oEhnUmOZOXSZ^d zn&-gLCK69}csAi0Ss$IKVGaH-F%G!`7W16;g%A_~{tAHh3*fGoaZox6(fa@la9WJo zTm#l4vqMp4z|VYs1K%9ZuV_3p!qO5Nq1khkq+dcF=IQ`P7fy?4gA_*UIL;zRLFKN% z+hCGQshlM`Q{aQ4cndHM>3fW zkEF$Xy6E^K2IR4hme*A%v&vdPw_a$r7>F~xLPqA)EEmmsuq4Kd$h;SYMN4t6W_Cf2 zbEG~D>T&$$7L=O~#6b|wC$IMV=Dr4?E>=k$CljZ?k*B&;Y3Ke?1_ZdT(&Implg$s*)!5CMtwIpkv&SwDqS!>L%Qn=EgXX5u~3 zjbKAl17H3Pn&qh%g=QK^mk=jE94TM7|F;R_kFIs`#KL|SLzX@Fll7yka7tK0_fxo_ z@7%-}fM3qJK&+oZvBsuD?rKigUlsaN76jiQl5mZId{t`p>c+Th>RQ6YZB7U(=0>k20{kRy3)b1>TGdG zqQf9VZ@P?ZPDVU)XBHCzc@_BvQJq+KOv`%!|BX6LOZL;S5{Z1LgBuY8r$#YQ!yWmo zn7}A-;pQ!fzw704>dExV&;zi%Ihu#x5i2y3Y$3?yc^(e`ndV1Yni3Q?5h%3(nur@ z%zY8l&pD11#vEM9=WH`IGTp4>8$HZDjxu_QbD;wspxg!{kogoAuy$RLEAz7_k5lJ` z^i)r8sx~@Z#M3fTR9@Iu2f9FYc;4%9L~jsp<|Mg@pid2QBGE*YX^n$Jao7&XAH-Ch zW9?@!A{P5iWI8yzhH~-oICbz&vIA57ugw*7R5e0?Q zlHGBG8r72gu#hY3TAvHZU#1Rr}8-I(#U1YQ@A9u z`AfL2g$|(PG_S)-jXdNi^I^T~^h@e^E@u~9??CV_@Ky#`n2b*T0Eenz?qL^i8`_26 z@W#}oY~(_>;z@Yy!w8-GIrI`bti*(1S{_{U(mFxBrRcc1tdb+HOtdtvat!g zz*@;=OicZ94H<_&r!@-@^vAVqveI|rneV>YPYon4o4x!exV0#TZ>V@a$rz%BdYRys z(2FJ>*~?~Q@7Th;9Qa{O+311hhrlK-)`(VU{yBZ@%Q^p=<=iLOf%XC;wuRYOv{qf_ z2HWAqh0RGPC}12Ae3%K|kClw~!!bVx+`$Ln+EAFk!n!e5xFamg__tANlHjDSxR|{- zi|e$=2Q7N@Nnkmu$@or$PoI6IT)Bxil1IX0CTyOB;8EV`*z&A&nM0u78?B~8k%C>& z1lY!mVP^t^`eZnDwfUbwG43RBKED$wa&=s2JA4`<{v=Z1hN&=DWHl{v_z2PWv7@H% z@0ZhOYwT|jG(Nwq#=yIml6N9M_*V8*H`3czx|f!x)&qIjkU&`$wccKYP^t zx@kGT9zl8g$D^k26D!k~5j)gLwuf?3`}ISJ*_+O}W^@an7u*F{4_%FnxG`76r4Ou$ zLeqa7VZojFVV!5V-jV3Q_oYsJ-s@h8zPTqOpCYveo@&S=46JKWrikr5qQlJ%+ z-CPp={pWx>?+rIrv=uGcH(d)+=PR`sFBMn8?qCZ*t8 zj4AVC^nROe>~)bE*KpkNC7j4bzMzy$1=S-P-MJ@XSN3YFFN3ckK;)SAZMa6+OqWIJF+}-`dgX3)EoNN&U!mqIrtP%J)&~J3?2?SNb|a8hZuN= z6$96b0k0uNVu0yWuLlvxLlnBjK5YgUSzbPE(kBy}AurJH=-)XWhKVbf4#COoT^Hck%Eo?AZBixm#vi9hI1E60bD%wy zcpTF1Mp|=+#Kn+NA|7x9l^@X7i ziRw+wU?M@B)Zb2Dri7IKIW&c1iQX&2O(XcQ%o~*$Ms`2~w5GjLwR;7^P~) zpuv|Zq@A%i!0U?j6~;P8GIg5oMtQ7mrjAkhUQhW(#}a%MadBK^7>`dh-h_!Z1R{yK z_@~05h^)N!rY7%zd>{Vyy@xFKn4lXT~tAg3);4; z@4!s{yNT<8(*teVm**fc5ES<0`QCH^9XI6zEr*}sp88ZeGga`)+48Qb1I1Jo-|F-f z7U5j1V6&UQ1qrbyZkn1|gToq>bn70v^^A1unLZp4#Lyt4ow`mMH@&knyGXBdyuo>)wJrnTu(8#`e=R|Zo*livvS4ELgTLeBsy?fLu(+tv+W zXD(7fIB;8kjChl(-KMdGw1!F-b2+1J#ALEN`E_h2vfrVdzrARaNio7v+GMa}E9W?v zw|pU}%A>|p4R^{btD%7}z1=Gvz*T5oWJ_(iO%qzGqz@bgPT3oTp@0T&%S{! z&Z=vD``KNHRzaj4qB!=yI1VyljUA$Y_EXDX>4X-6_rls`a*FF-yK%&R^mp;MtGve< z-H*Rr!+UFk(=)1HQi)L@e+_l$=yv?=svy?K@wY1nd!&13FvZwA;|}hf5q@ky+;`%< zG_F3Px4s)P4C~Nda1R|hT*188K)ALep-6A@Vg2xL{wk1(8#Zw6T2PO*tG<8-h!qlA zU|*>9g|Lb-zrIkbpjfQ)C#GS|gKZ~#+c!_^(0HG8baUDp=2F3};HN57uW|t{uMIWY zFDC&l{=&`&*ZRaJc^_sJYS#InwP~f1w%nJgPMv|A68t}#N+dpHzTd`iiES`8gMLTe zc`v53r30{OvE)|Ok6!&i*>{(~tVQ8`F=)u8hi(A zoWC0M;Vi_|!VJz0!4xQLjpO99py#^Y42BHw5<8b#XJ`l|E6CCNdovg>z)RHjJVhDYdnwerA5i5Rv2}`BO^L!xu_s#r)}AG7PV~$ad*Lhl zy-PF%H&a{y?2u@62RdR`hR7PtKzb@E_&`b8j)GKIGs%N1T0*JcavKrCOL zZkNK^tOdZrK6tK38857)jF;ChR){;EHvsWcON@BI1;DcVMI4F4sry=C`+xJ`@^ZEt zhSv%mg@-p@czEN5hnI!l_BM>ktZ?^>y!40ivR33}Z744=)&m@+NM6b!FXc#HDk3kH zNL~g+UIqZDr~qM5031~Sh7>R;qA>`G8m<&aw*4V%c`zC}u zW{Lv}DEk9Te$^kCDOMFgT5(LiI>VFmOfujPF8L!^$sh4YYQ5Xf=FZ%5dJUzu-VRFP zjZ+G5oKkp6DK2?d-)UFyQX5Vx7i;x;)~_8Xj+Tn+aO`K)U&sH8!F|voOa3}+>96zG zRVVNBYjD>5nrgMl&t=0ka_K+M25Ye(;-}=1QrvAX&7O&jQo%FO?Z(EX4i@u+?*e!G z71X22_BHa^;LX1Tj(S=YxLAd3{~iE%EcFjxA&;fv6?u*scOf7CIW4BglJ=X!;^5)= zzGzJwy*0eCYdOpa7S?12p{au(fQ~wb|6HOnpdSLn7wg$4pW|ZW$^IcF(u9L=)rPq8 z$<&Ct@K_{YyQenpg)t(WwZ~fs#gv!DQwha%CR@SB%?{uf49$o z=$UVUCO@UU*`oT=wmVydR7)?Ya8PAButnQFYCsO}j~EQ5QxzIKgRTQPj`taVCvu!; z4FgQytI1Q#k+WnOLsJLx++mQT9#%%4UXGmn%9?U@AWt0zdHQh3p%iScmMu~Z%h>OA zNa~A{uxyYYj{O+YSVnXTPG&iB z5!aR2?;VD6JTS8|^6YZt!hRaem9f**_8tS4J=z!^`NjY>#4)vR+JO$L zF_<*)O~l@CtLN&+MO24yJyM5%d3f*;2iNxfo($U|zB z1Qid1F%`2CAC?ok+ln`#N*9WgdPvF6U zo`TK{oaid(go|AToicwr`u;^*&JjBq+qgRVnkngPUfR*uh%e+{)Yk}nrM^Z2R?yh& z@4R^pwQW~?D`;$iM|B=`WD>A~#@4<@LpzYKpcpjM74(tGby;_yeT{~8AYY-ElvpeE zH73_($YXs?jQwF>qq*8P*2Vs)opo_2lH*0&r^#?b*&p^bf;^PuF64)0eeg1EA#I1>Cjrf%3zn&Bdzc-$E;_z{3ytR&vf^+!gR^+$=q^~WC@ zh)qAU()#1?qeS8Qqj+LcvU^@29YSJZwu@kW{!i8)Urz#B{C&{%M@IUUu0N8e!B6os z|7v9Y|M~i3SeReo`s0!EeB{<2kDTkTc>S@9m|w^B$Eb)8+xjCL-6OXC*hTNJWc~3! zP@t99AF0)rtv@~i{lG(BfBaA2ZodI&u>QD!Psb{wp6QF$85!?r))|Q~T4!YH;J=Yy zw9Y7?2LQ6`j6cQB(8IgV_^GaSM)pc}opC|>C_kJ*$=q2YM8jSkfC;c`Y!mzhgqsCM z35WScupWDHiXzq-&M2sB5T?LrT~UTNidAQOil+l>I8TjejTRMSv_{j@7*?I;X<0h3 zh7;5XYc#@*VU6acF|4uqoi61{2H_ft_`+FIltK3xSIG+3%3>Ml#7^;aklitcJOYcm zMvxJU)0)W4=uE|}z~X2!7h^`(cwDjSR8Gs%L2Eb(jX2OX+%>eU!~rvhGh>~p?e+ie zTrZUT%!KYyp*9?Nc!V0WHsS8WBG0lSsQAJKLBW2ElFYr8{KF{2+@vWeKD!!dO21s0 zJrwEXcF$3-1^(nH`v7ef53&|0lLRb3ZHxK?U5t8FM}Hu`P6Z&K?*2f4-8z>2wCN8t z+$sR^b&8{ay88nGcI#Nds{Mh6cVO-8yELPYIoHg*$tphSnh6A}_6Hi?fi+zA6J;e4 zy88o@)e2Z+{XvYyVf)eSI^;<}-Ti?8yLD{H;*=IEY()~gQ^#sXD>0*M*+ZG3D||{y z2QxbRF3o5qtlh(dkj1X4f)1>mEncv8wU;_x9v(8K(B*Pv_u zbn}YmH(*@wb-1_{ifBN}pU;vax(HO~aL7>>Eq$h+*3lOvwdFh-~(6Z7mBML94Si8-~gVVnenqTII@Ow zYOvW}#wGh1p3hfsVFm}*0IuLJip0*%;N%;QyD4~pC;Av$!4^r?MQQ}#7m<5SL7Z?-@#68S!m~oXj%}$^rE-wz)43V4AR?8BM>=0igOG#?5r)9y&B8;6U zSdqYi3^g{+rwaxpMeu-*8V=T2Fv3B%5K`+U7)X#S98yTeUQHwl*4QKvFNG$hk{zjd zC-6}HF`y5x0ZP5QiDzOKUEuj2+(pA!Xc9j-80vN%40Q)CfVr&sH&HGR6!cqe44e-Q zDzFOOa%G%nfQuMX$WnX}x0rD}-UwR~&l%w+1a3jg{S6xj%1a}omsAJTtvYb36BeCp zdS2VkgdRNTOqs_~PFG4yn{3%IG@A|BPGB6u(@V5n%|;%Sw4rxM(uUq) zNgH}cB<+)J+8Um#M#hm}tY5_Zc=;Y#N8!9ypQ+85^!Zw0%b9+m)+1Ofv{3668mvEG z>)kSnSbR^|3)2=*W5jid;&L_|=NEzk*ipun{yv;c?KvP4W}Y0zL(r4=`vp07d^zN+ z2XbYqGM5^-AfFoERT#n5j3k72%G@F0TZifT>3ApCo5C$43&piWUf$n>M}>JAYd7E3 zcKoyn#%{daa)hBZ*nPy)cn#hig7khQJWJ2JS-sR}*Ioqs$S``y^YSGpa{zWD9o!E& zaFD;*gXW0*eXu8b9i-dtdceEb>3wsj_YdM;t3&bSAMWLs7J?q|2}%yiH0Tv4?_ve3 z#mNgmG;q$wuR#&#ZHgN7aUo0t zwcHb?fp+c<)2ybd)?Dv@ig!0R1 z{IUpkoby49b5=tyq^^8AtS%HX6K76dG<{PccH}p8@*n$5-&ofd14R(&5R4P+CJSUjHHi>Ml-=&kY8#NBHE3wFawt1?Z;tvxW%s9ZQ4 zs&@%0@+m^~@lI5@MijJgj;sAn#KPN0>UkGE!r)rp)*&Zr;@n7Z*2FmoPyIiR$a*;&t98A#iR{4hHV0~4&{U?lH4z@z@wf>Tc(;=ekIqnV~ z3#ij#oja`$;5T_aE-}lF2Wv8J*{pr45XglGm5YxShH#BAmc~2ds3ip0K|mS2SV`sf<7TNiW|D*5o1_>w za2o-^D;3?;Vk2jHaPzh^(TJUvw0|YDkj|WZ5VUtvu~L zwaN>T9*rcW4{5@i6jE0p;Gyt|Px?`SG}XzM@=^&2dHCHl}m;SIcL=t7(_rV`A115;<u5!M z`M*9{0wblony$W%fEfFx3_NSWxS*)Acm9;XdGo)7Zw3EtxoLB)KH&@pk3+pepA3kj z3}aAbbQ)6G4=H!FF)wiwkj3+^Z1mir9WqLbbuOd(5e;wOu|7v8p9TSc4_vnUQ27&x z)B3|OK)d6$`)b=v!l{8hQ!o_2XV*-dv7j-QbJg8`)qp!dS#v^XM|Y16&!l^Rvy z8dcm*OPiZUrMv!HAxaK5YNLCc78BG)_wMe(uV92TwT3T4iuBs>>vm{@@rq%AX06nS z+v*0TM$|#b64bSmxE96WUvgyfS;<;q>t;uca_kEx$kfrh5MOes|1G zdm1+F??>+1RR&nQ$U3I2<1BSZeWemtpuV05+o)C|SS`5yK#t(U^35dS(ArYrGhr+! z!gldSL`Q%mQl`YxN~#EzMB2r>4~n`IsjXnsDdpm8Nn1frNC)% z#BRGQ4%oG8vXUD|N^UzXMJjH~#3PC|LcD+(yLQ8AQ>cf@?U%Q5bT=pxkuGMxs1M1xQ93p5<1Q zHOnz8oX13YdA=(bjdc-HU<(SviYpdWfH5LE3(ox4pzS>iZ7)5t{UAsQ%c_-*p@r5x ze`NqIWzmA8Xnjs^8o%lEh>j4+-E?pQREjFR^oj2mr0L+L7^mb7m7dXtLLITDgDW{0 z(=x2xmmb)Iu43;l%Q0d#Y3ehED>fHl$HZBfUPu-N6P)QLJj{X#Vp&M>w6HeKNHC59 zn7-t%Z2gQ(US@E2$yx_%9tuw;_L!1dqE}`Irm;#_XHmyODzwChDQMLcg6Fc|f-JN1STM;HPVt;{+Sn z!L+!hF|ii3(*dje{J_*>jhsGn9We55zoRuTymu1MQ>a~6n*RvWh=teo-@xU>{D0f! z|7e&0@5KMt_|^1(4;SYdj@ZLuHhQlbx2jWrxYDiaWcy=8k%~;(wm(jQ)8Z#MnZ>Bi z{ua_2x#4|n{%Hg(wp@lV9S=fH#HRbu5Rs~JTFi##wWW=p0Ie)3xZdSaj@enA?Pln6R+$NxM~7c zitAfdF4Q;T=w@&da#V0;ab~8^!E-8cSh+#ys!XK;!{L94#FIBccM^;E7|bbnYAFrV zD#@iJJ&S#-ybA5COkS&Zz<&?==T_Kd!5&a`(!+0U8=eL~NM@am5%ksbA0ky+;*!+3 z%*E#aT)`}T9Q+pwCJW-=4=b3Ah=V_Z)P6@6v;SgAH1 z-BMbK7y2X~%WUJ(9mNZM5|2!<@#v1?g+7T#u1GwHhV-qJ4Gi{pFQ02?n7QJ%C0%(V z4v>-@gaqoujSB?C0WrKdNMs}z6Oo%?ZM;K7^1RKd7UkeTMzm*F&Rd z3S@$!DHxmGe2bw51v)Gp$;8{i5B}lhExnm@Y54XYB}kXiH3a>bS$e13#zQ_FF3MAM z<013mJs(PMJmfv$Js%2it#3bDnMBx6T_}aFH1<=1mR$mB7|0%LYiKW6a zJDFN^xYv~|ry7g3QHMr}_U>LvmwQtUfoW)^Gql1`!ZdWCGjxEVglT9hhF3J%;u0}S z7H|Shn)u*EnluwHF6kOjNT=j?7)m8KQokalDB)r0lrkQcPD2wZRhop--jtz}i7lOf zDAFNgIK#71G43Q;*LHL=Rmm|CgmX~@DW_6oB7k{OuoRVSFo9BMVerxMHl$hdRQ|0@ zwd8svs=?#K(=1Fr63q{p&*{Vy(O3Kn?OT-=A-6NR`OiY|-9wA$em+On$lgOZX_}e8 zi5_%`>idlND&lj>i&8^4-%7A|Xt5_NSa8S>vEd;i9%)h^G4Y%?eNM&bcpR*lIoBD8 zd@u~P1L~d7%A!>IG0`@#D11C7+DZ{MprRc4BHC1^?liI*#!+VODhH>r2K8cW_9wu3JcnJX2A88haI5a1JLC?R#)|1BKOO&Fs&8k!A&ocWRu^Be zQvXLa&kja!wtPK%kJxPaku^2Db=be+~e5s_P3Tv8|Ovs!1x ztyt%PJFvJ^TMdh@txqC;H8>p^*G8-#Q)vMp0`|7()OfWuIAsUKOj9f3gxpdMCtjS^ zry?BUm+*?>hybH8OfcbR5tTimkZCGjh z4C9v57&5V>NkX2Dbon*h;6XvcvOXqaVr?|yVr^X~8*vw0h!m!1tyw=RI!5F2ijMeL zP#hs2tHBu^Y}C3L=W~H;VWSfVY#{O(6?qI?`3@CCJ|iMOB4V68@$#dU-3_6v_GvAO zFs~?xDtEQx!W0uWMO8Uz#D6u6632yWg#NfF9~T1F8w^&IjChV%zmYXXH==bz+G<&? z8*%4YHyWjeXX_xVTf+2#WT4r@6tS)6awcjknW(K~*j7d|rmfi48v%w?^0zXE$hKfV zIzf*ZKBV;>W4OprD746503pLmHi6}5V5HD8+=zL^aMN@`hL0EwA%E5@hKS)R*vidH z#PETL-=&D%uu&Z6whFMR>x~h&Ynvi=*Crx<*EV!9eAG}A)&NeQvPpytFDb*#*ch+Q zCN^R{OuHyfjR##1FLG9=bqb`F1hu~;_Cl+ZcNx{89fa{{4`4jnj^LqWm4c@tcN2oz zV6S3*eI34j>(ho4Pgb!((gKD$e52~CU_vS4v?%M9VzGO`zy<|ordm*?8godXSgw?~ zWdj`+C=KfnY|reviL-xA_+->TN3E>yU}(_cn|OrT`8j14@l&1oe}>Q} z7s|o&*bqbiu=-DzU?+JR<|P*nXy$ zGvlBEKMg-)s?F?px9|{eR_`t(%ZsUU8WX6zgm{M*da^jbhcv_o9P|nf;v^0MAiW|5 z4p=%5&Ou-grr8DK&dl^+9{Zbs0+zJ1Q5@Bmm>EpGlXg0acuWmQWieGDDgTq=3@oMw z9_ixP33o?v!Wq0Rj$NCz^Y^5Up_PW&w<)$oC-s2(!2CVrm=q={^Y@Tp!Xz*&#Gv!{ zKlvZd-@gFCco^pIMAJ2Y7tm46-yLuM5U`Yjosf$Mo4}jMRf+7hm{-RP{uW}=*(aT5 z22ZD%!P6O=!G96nYVaK7tTTA}G3pb5PCn^$S^_r{J8{Qm@L!5>$T(iYD~clmj0!+8 znHfAIni)KuW(H4ZYz9y7h#`nWc;;3L{g`>i2sE=}CaJ|{@Z#^+G&1uPz)89rh#Sbo>mD~5>SD%pz8OyP;yOyTJ?Q+PUKQ+U#+5^S24nZgU-%oJYSW(qIv z*c4v;qlOwz;RVpAfRN!OWjM$2>mPJp${LE#-yNelw1cod(*u|=ZAT1~q6W{WzOfpo zpq+*F_H}yuLobt7JCjzzKq=Qyr*~AmSk*K-vui5Pf-IlP6S{mVPw29#Jo&Zj*N$Jd zmPR^Ry}pyTL8o`(5oYzFa9Yobw$pmnN;s{jc$gbhrS|9HiO<&`e4>9oMY603Wi3nJ z|K&8?X%XGg%j+y!|bN(ZgM#_MP<1e-pp$yXlnod3D^Il6TNp7PH{7ZuYBKt8khZ0qJbtAB9JD zqU%UF5_}i2#1=!^-zLCm@zcDRC8^H7l~?P5muKO2<12R#B*q53a4hIw|LTr|+fMUE zNGiwq>pKwA#<73wq{nX~w3xHyU-J^?q-!P3OXLtG^@66wh zWKwqAT%89?V9G_lc@A6qvG0U8ugx`7JS&6cdV;RK2>VW@;7VZjs`A162qWr9d+03~ zQgr8U1#+*}bKT<1{MW^~n$FwAS)lV9;_MBfBXHq$kt>kgPfK#Q#6YHYD>j;2ri*Fa z0qgQ=IXDX$JhUUJHFI~E;gWiYE@tfx+B9yJE+sMxCm!XIv?R$0*?)-ohQmJ~q9A_-DFtp77*B_hBil}uNnJ}jF_I$)B5y(X!! zUy_E*im_5kX3|zoTGWS08`f8hi5Fx_c}|&on6Qy6K6DaSp8mD{a#r8UReE7I^Jf45HodF zQ%3^eMFQYO0^o%LAaaPOXk7|_Bmskp2UH3_V?_|w8S$425-B7;Qx`*;6h+mf!UwX* zt4VVq-@KaC580}%%%mL-3$JOTvJ1hQG?ZvzqDeWTDZwI5DIC&V*}M$FVGHoc-=#-2 zfg1{fN=LUQsHs3vN`$ZR_800wR~4n`f_ST}<)l(5qnbDr1ve4}HxdOXp-7Bsc}RJB57_NYd{!7%V4R#S0}z!B&%j192x4ryTGV;LSqsgG%d5G6mRK?0LM zI!;(oGq6IBWtvW1VCz*DaRa52@(+UI#`3 zgIUh}N{jY(2aan+_d7MaRzsx~kX#aX~ z{wv}SFHFtMf0cmnzSPwG*XWO3my$9^dEn8Kqs?QgE00D;9^qThQ65uApGTuBkC~1< zXl6myxOlm{#JbC+^2tJ~r(8^J`xC%2xoqAm;9k?z8CaE)yM8r%Sw+0>ceL*MbMd;i zET8x|{4WC~KXqNuL|xAG)VHk|c$o&`yxj^ZFPA`j@5*U$h49p;!iGt@l*M(G3`BFq z@@dTf-wHX!)sxaG&y1Jvj$~?}VQj=LB~z7#V`Ub;asw8x%pE{@olxGF#&2_y*Qqq) zh=!9aH>?k*FPvn>dT}_zNe)=wZk3m0;#;VE{^{DTUUJ$RuI;jI#Y5)916yom@sOv6 zY1rK2A+<1s4KE(@wlIWKuz1M9@c0)eSn-hiLJBx7iid0pFKuyhRO{Q%mF8MuKNnr8 z*PO(&w5YIAwpo{_??VthvON7AdPYBvI2!of$S>jFqu-p@yPxjhg^*b+F~8edZ>7zR z>r#nC8SQWMtuJ}`bZ+#b3Zje-}1BN|3ez)9}XHGhkfxELm8HG^?A2Cod==HZ4WJ!73vxN z(uaGOCPb8+goQc|m*#Cc(Pt7Gs0ME800NFi74#1L7b54cy{K zutJl;0!KPB{{DC5rF-cAh;Z(qOK#nJ=&g?-bi#&yD5S+)K8V@F6#Fodg^YTD7A2Nw zUWGhUqx+E`Ej@|@MreA~MVmAN+H_00wqvqp6TO;=YCD*swnL)RlTMemoPdJ?p8!$K zFQNEO^Je^_?N^zV+eo^p#)5{s3$S%A?J_<2ZNrrUxcz|BI%0`yJA$tO=R|(?epc5o zxC&#VV&;QH+4l1sl!t#0f=$YHM1;VNqEWx@Xat%A+vg78UN5o~)t?qjDlshh08+Z> zf5;Md0mc>^VY$K2Z_}c&i0W~$6Y^=4YkYe4ocG~G#!HDdI2U715iXHmmhJHJyT*!E zDAwm90ckCFrLz%BW4R^Q$oD?P;a_s%Jj!?9#^Fve-wmQ3f|yrDtdoLbO7j^}SSb3v zl_Q8g=uIZxhtcx@`_9s04kxekI8|N1Y3rV3b+MRA4lWkc$)QEqr5|1_ddZQ+VkTM3 zE*7)NT5hqJOV;v>#XL;2Efx#OTF+t;rpkL4i@h*dzKC}WihbK&fQdLd*GA4SxHCBW zemrc36$_r9Zsh$QFNE~KDt)8i_hv#!FD%hFdi>Sd5V9Hu*$cewiSfh?cF6@5-V^nE z8pW)ihOvJOOv5O9Z=;y=UAzilfv%tTSJPy@hc^K%5N6R0Z6n>J89+pt0YrouKnGpJ zE0ASi9aI@u2T=w_Q|;i_>WE*fanT$6Dn$G$WWXJmlaKh-gOLpQ)f@4v7hFJr@g_iw z3u$lxOw5Bd5f}I<0GJ4zG0uA6ESLziG0vt!+HB!N(hMM?%m5<744{LqkT)^1Lf*uv zvb>2CWnd}4uQFBa^ZQDtyb+H8p&M9i;C$)ppsYMdbOYxX>CQm18#q=!&gzChh_lZkoq!c5Acnu4^1LV?|Sv3ZRF?;)Qd)vr*}fV zXe|YLH>(%Trib1W)Qk4hOYb)Iu4Z$B_hj*ko=GSE9{K}zh+vny1w-0nFamWDS|b%a z0}U*uJLFxKoC_^e*|>(EzKytsfSAT;c(VNijG6RZ=e&zE_^_(2hGUan@Gj{1_V>eZ zoXp9TGGTb>fmNCR!`zzyNLE#Mgw*QW@&g-^=v&X%{s%rDvJYF z%{cTj%?N`~&;tq-)r?DwK|zO1kA{qcqA@PfXxxp6+1x_RgqRRF)I>=>jfpX?F@f** zJLkTomVxm@|M@pey}O=s@44rmyPu;;{uN0C#fGC4QXihlBy-TJZ9pY8-S?4>>Vyfh zC7$Bq=U<7tY`9#;J=#y8PGYxNQecvB>U+FN!ZYZf>fX_{nyl0$_9TdD5c=ZW-$PZb zCJP$!vIxGny4q^C1#~5E1184Y_`H;=fXuVh5=WSK=)g$X`c>4Y2-%8jeg6qm)~j%u+Bm`9{tTb-@ z5COKk2RHewCf$u`E0O|rH913ey%(@r{{dG;^{w|MC!ZGuU0+JNo=sI%R#M`vq-b3i zm~c}ayS&FVxC!jO>q~0}79T~<#g}hHc4(7L-HBPBG@$NdQengh>nnu?o*(+{7nj`~ zUO@CTC)@7tf*NV?4Op%w(HR@}+7TdJF;3g7;mYMK!f7u-_w8r9Boe zR!V}EV%EfIdJT=czYBsW^_PBxVhHhJ07#3LwWV0pVSqg?)&*i2_I$DlQyT1!QISCw zy3IB`k&Vl`_IA*7)TbwEyu+_CTX8Ras=zs*4m7QAo6IkM0da$oU{2$uBXb1BgU&}5 zCFe-tw2d-m-;EJbP^$*2c@(pKihU!Y^$qbN2wAEN(ffGmojd~tCg*@EiBghsqoj6h z4#A?PD4{ll{P5c_;k6-+yI=y~Q4J*`)zEAkzQJEpf}yw;3^(f7Tse(0?WZ2RpwZI7 zObSfm><2S~rq+8sno1kQBd4JckILNh$0SG&v#rKoN}r?OYW{@PY&?pPV-+QMK*Bum zcI8w|H_7WU_ao3PESHtuJ>O4=OxFDwt)>A@%>6AOi3242+EC;H1k_WQM1G%A8bxk(4 z^xp&*-&5no%kt>jR3e+gd}h!7YFb_8(fXeK*%U4B9eEmwHMGRg{Jc{xFU+Q4Tx|T^`12xxB#(1cM5S_l(^U81 zlt-}<*WBuS0e3WE8KX}xKO>Xyt6FzM6tyBvI0#V1N=Ob?kvfWqoasK;)+^rQz8G7Kt z#mVqIW+H?&I-hY&5gbzle6MO`i zAW91dF4RhcU<3)8PW)n0X%yq#-^bb^1hWqi7@lgZf#_!HF(m(AkN7wX0+wUc*7ECoK1Us1C+=lQwD6I>~)=SBZ@2N{e;W8I5DS(kMKd-dw{ z;yWk;%$(Y4{3)`vDbzMWe{u%4e(#p`1?A*qQ)^n-z(YCgG`Jr?#_7jVC?1-T=~a;A z+>fwRrIg@cxe0JJ<=L7f^c{Z;an*05_zaPhnwJ3IDso6@wI4>C;FFERA#Bhja^B6&M%gIxxk*3yMhg#ubj_5fy(xqWraEc5&7h#@WdZ1;}= zCgsQ{ZQnSAYj@)dHx9*}8;9@~i%w@Lhm=|0qIh0>-aRH*_FXDSh<}#}W`383HK?!v zYrok9?kT|{DQD3zGChyEqJs1-!3)U1X8TZDL2_1v2^40^uoJ8xsn`|fg0pFYgt<}t z_6bPL&~C~)H^BhQY)YY<>wtTf1iF7&5N3uII%nb@^c4!7R}S}JE481{R|xb0fo?0v zxJj=d;{gq_3KAwoeUO6+GAy+b{CNC?Jgh>uiXW@-|hmvt=$EBd%FwtXJ~f;>9@OpY-@J`+1~B~^03<7 zc0aDYj~}itCSZj|q}CyL;RQY~g!{DDkUbd!-G}d_d582U$cS8Onp@{8>g;_JH>TOi zGfS*zTe}3!(znD)h8rce#5!u5#Gz?_mI{IKi%QSWrUjO7)1D;53e)Rpwg7|aWXx?}dR{dtYdA9mdhWYiQLF7aT+%Zf`uCNKAY38k=)d$@EB@55$N z43^9TkZ3O+Mq=Xtn@e+;<;7tx9nr)Gsf}oC@Xp~~Fb~!wcU+^HgwZCqNrf$;_GklD zbDJ_^eqzd)jT=`XYa7>a)(KSxlQty;EY5qCF~mV-3>}L5V0~U-bW`g!cCwNjd75rA-n^0NX)u?G;X z6a&e!2tYVO`62GK=2nvVxD61TD@$(1N^XPO0Koxrdu~%?gS+40_H&ye`?ymCuK%%U zrrrhzjx5`wO__Y$Bugl_!EJD?P}~7ogcnYtEi?KZPV2tc((xpzPYlvCcSjO?DjUaSYbKxQ~X@R}MpF z7`6Awq%S;=nR-I=d>eTlfn6mFd&dSC_QuBpr&koLw&&7b!7{MdDMjD_KZHG9EjqPv z;Bqf(h7s7Ski}8jF1y*d#8`7$c&zngfX0@M$pI25UX0Q>0X3ls%!v#+5_7f(`SZ)Kmuf-0?P?B4; zN8$k6OJ94v99vxgS(UtU4yS*5F~aiQgKSS30AFJP4ri8}y+tR5k~uMiE} z`E_LuysAr+Ij{^gZ0IpZDw`JOFgx>KHR@&_A#u|`;o36 zcacGW|C4pVLO&x5$we!<|ENW+yHD~qPjRvE0f;)tD=UI+^T;`vPsQX~U&@BWv*ljj zt=Hi;U#kT9f&gCdYn9=yjzJ&>G11l0mDRZW25kOIG=%3kb68DgY#fPfjpO*UZ3HQf zL|AkZ=S_^f?FPVKee|M>qL@=!4d2P|^x~WR@X9N!CZSG(0SF>rFKw*=3f*J1($+10 zg1)xUP-@*zdllvZ$4Nh90e~a*N1kj}4674X<1ZnZs@P{xOoWf?X)u*}A=CagDtc#U z0-uKhXf~)sq^s=<-{WQHSqIy3^FUzIggY>d6>V1MKcGk@OnYVRa4NpPa|G6K(bh3Yfiro$-%2k& za*tQhlHA1M@_4{@Xrsto#;ipx6O}gnt9Q@gJ%EFY7`U9zhvcL$G}O)DJrkW+n-#ai zTia8dubH56yQD!SKWH5^*=ZPVvU%w?;rzn{4)}qW+vSxeaHStOi)D!kEck&pwG|Sq_j&Ke8W7wsDZEw~q6^r9?I9-rzB*^6Ra7*H7-hO~G<|rO3 zJ`U>Ph9mERnHyG9-@#ldlNg@*@)(}i=b&1`j%MSU5{Yo{fD_4hZryjPbzV9!DeNGH zxpGjN`VKGit{ubW0qC?ZY`}K&Bvn-aULr`H06Z*mb8Elm-8~qdg2`Jvq%djHwme?r zV+n^Y)QE1(x1y0FQD5GV)ow8uY5WfQ1=#2wveDVRFk}m3lRric4-Ye=zb??p;SjMjAr^{+Q zXTTLCE!8>_6S>^}DNVgz_`fvuN~)K*jV!)kir^fg2#2nn7bEp;lEB%xd9;U98Qc}c zuE%EYj!t=eYM*zlr!e(u-qr{Dq>moYD|(XGL()xLFGkU`O|uCPJvN98dNbV>^k$s| z^w5lb5;Bl~qmfUbMXEC2A`+@x(C_d9#G^XP=>-TG?f#*!>2F3x;4TRW8)$q}UA-uL!ce#IIXvp`VDl2`@%7vj-z6aRR zb4(s!$40F202=1C#)G0@Z_;?m3qv?`re}D0XawhVINizS=dvSnc6MN9&PmM7;Vst_{DMwzx~-2_|0Zl;TLQBnXCiHV0LIGyAqCEwlI?&p2-$x&?U^i5cRcuR`!X> z5^P8Pj3CYw3>Lk!Irar)t z624$Ec+eQ&3wi)-h46*(pk=@q#skp2kTzpzj#r?dE$+3WQL>2{HV#htY>yn-_#Dn? zagqwU{1ERoa^NyN1H`T5S=jJ~3tpC;-U3|k4jb<pFOFCI!)w8DUd7oT zEBAShe$TO@eD{7#ctq64RPc*HG)(Be38STvTR?pGnN7lU`{GQL{PlNmb!(KHQ51JlS95R3aCF-BhRexE}@zQtsEi>yEHK8dh+ z~YWCT zTo3fvP|W6*(H&t1TA~8ID~!sUC@q-#J=W&A$aNjHHm<~0g}%IZyvOz%|3fYkl13Xr z3C=&Bxh?a$V#`JMr_`CY>>Ny!NOL*4JzSl$J+LXU`Xk;HMH?*I_#)b?a?ha?#Nv%R znB+6P>2)Y#&25G{uBOn`e9%eMWxAE|cwy*K(uFElTS0wtdCZ9aNmxdBh6Z|9gJ+D! zgF#qele(wyK=85sSRI@ccYlcFc;Z~zF{Uvhfb7_4p;|*_Ofu5*S z4EAKl)bIO*@=4d@0XJv?suiLNCn+DK&lyu zLAjCR%}_eLXTLcSEnXPkuQ9fgidF`rrCQ|yqZiKFVC5@t^TrI^1##mt2i(JzB9_>@ z{taB)qC7K8>{4+z=hNV?F&Ae9HZJqacg`AKn5$d^;~XY<$OL29nXBx?i)``7%XtmQ zYj9?-X%zJdFpA$fMDbgzKwr^mz!eoyKZD^~eE#(u(LoyEB8}uLr@o)>#P0h^5qu zL+KqK=74+zgXasX90Ly=ibdIwwweT#vyIja96FaX8dDxfS0`=9b_(@n_a`Ww?Tpoz zYc{l(BjCrZ9oz#5a%#GDF&+WFlOMH(A(WCiX)(csV7Z{is)vHHz{1>?O_s3SrvUYU ztb8ONuG;r8mRXV}R)n01HTA@HPX41<&7 zHF^&knaT3C;TdTjPyTF0S&zZKfz>25>fDJyBTHy*W3I{{dRBy0Z$ZafnQmCF1ptAb zn{RVllm)I-qF!9(ijh%UD}i1QR$n>g#V3rIN_t`nJ6GobEUqpZur+jG zlPlxp0xC)5%PH}B_f9=AWfFzN5@ZU*mls|zw^D^&C0vS%&MJqLnk=Xvmy*PTE5~4O zuP9tQNT^>!@dY)+@xDr8Tmvwo7$qPkocqPwYNS`wkA^XsYJ?uu%7kD8i@?y3qd zl}l{ad~N(`2Lif9kE48RM;-wmVCw~Za2|qXcN42iv@0a40Pvf?&yG}Oz1 z!voK0p5!N5OaYN{k$#k|Q-K7Pg78T@HES6xc1gYEfxhb1b`qiz>ryK&yEnd`524snL|1OeS)Z4&}diB#@hgM!A^;l;HA52Pi{&LRBl52%JbYO%rns3;?T$2NM{Z{up|wgZ;SzR z>k?MDxcGf4q3t@SU?fJiYXR(V&MBB&&nXH3U8>ASV|z;T;}K3bse{jK!m{ z`n*3D&)1@l?XNrq9UDu}L+)68;E3w+z$v~ceIwD>mWicH5jitJCC1QF7_G;mPsM5j zM?^JTc*}`g0C{l%&I2qge^#LYDL4u>ai-zKRysp7aE_`I5Ag8t)R}%a#}jPS$0l$b z4K>txW`;2} zOedgf$^(qu;#3l*t+92A%k&V|nP9o`d@L`l&LVS!c5^6u2xpCfAc6!3*d!PPkzlET z)|#n790u!t!ykh*>mh^7C+Jy{Xh2&#W}a=Q@op8H@CdBqpU4Sk;0R2$vUUOoWrZz5 zah++EroLC|tKN_tn;BhOvXc}|d&_svl#}yzaJg2+5cQzG&S+t+6|Lk`%3oUzpKa@_u8k(QW}?Y$14rO~;+C40 z1!~$hSau~D-I_^8xAi%;AA|Fw4S$t)>YA=n$wfVj{X7)?=Ybm1=`neWwW4GRlv$odNOv5=e^%-pd|E)}|OjB(uBN{Kp@?zJk@q@{W1Y8d{=Ey8( zqVeY_AEuKmt&|lrsW(&b%Wa%+1oUEBO_pZ$xmE&ivbNUTV?dRmxY0btIYAybJ+LPP zwUFR6(TVBJ4&ocWosBcsu6uwzU4=SDmgUylQ2xdZ2*POyZUL9J27qxQLUh>j$uUAQ zHZDhTH5@}u<3;e{^{2_FpiL#OBTSHh6>}Qjl5BJ{+jo2)l^0=-849tQY^Pk~Nu)V8 z7KcvIXbfN&2+dV;B}sjvYY4;SPkm}}f5DF2fi^hd&~1b8aDNnz@mZ(JZEd9jDkyqnD9eteOwQ2bQ}4zU z_F7P=auO2FqI8A~vj4q=et07KAw(ymtY)3XAF!5c`06~HAxf9_a8inDel-sop8+;> zTg5TZ9uVXL1*$eIi@~jJNRW-w^&)Y#w&H`O5UGec68;~a(QeQe#Z_o;+5{vxQx)ii z?MYiN>exjnJK)XX?<~C#*`=tMv8JkhxR*Nq@jFgFV2Z%lsqY~oW8odfYP56PMWeDGULuG-jjAU{1kIcWtw5iwVA|FM7~)^US0Nm}Q;P zX{sk3`EpiEf==xf+=Z#v7=FvQ$SyKRx8=RVNo)(NN&4VJg7FTHP%cpuvf()EN5Z&7 znOsgKhi9lU>O8E2xHydz@ozo;U5J0x7XgS6S$@lX^K(M;>bY&&{R63!{a3SKxBmDCN=!^sZ@-ONgyYY-QJwLZO>KVGA9@foK@92%%SKEAr1*G-t zXi!h<=BQ`yg4$Wzd{7*-&w0ivLMG*ImtLOO!;XhQbP zSx^sb2oE&Bw)u(#>{nMn`DmaQHiUF+SxW5JT_E{bgf1apIZ42Nl?P_a;$ z*QuI_AzfTQzqFhEOqhLSem8j?Qs)**9m?!jgu2a7Ryi||NdIbbLT@GyU=DffZqCxS zBi3p%&8eS89;?ZKTVIZ-wA1)~2=R=SXxxWOSohwC+gGgx#G*(17Lq2Cjf)Y^&d|7t zH(@b8G8#&mcioSOhm7|Dy{7vhf|fplo3ukV76a8yuzB2g2jDke$h?@M0n83MF~brt z4>*m75TC`J+qW|vZ}m0^#hY7X<5QoIwUrid;|=7~PibW)v4zf>G~R}+yn&prS4@Zi z<7H|D*8KF4c#^(7wi^g4&bLx~(GR9cU&)0D*S(;wbKpeF)GdU_4;?1HRGK}Py5)0dQ%;f8@Z*Gvj2xs57q zB7{r^WaC*mX_}2c!23PMDa{(>VH>V_+33AEEIwi)xA%p_u$Tg&gpq6tML}plUEW~ZAdTv2C|yV_jqde0k-}5 zYo)M~sFsnR@4ibpO-JL$Q5cdL3+FNqHY`jUh&9#QQlL)t9~)g{B49N zm!`D1=^PH$5pSbQI9miN4%Dwqz?{H+*RREF1#;sAl$v`RRdx7GCW^#LS zh#fiEfZDNCG7Iw6`l{*0pZla>wyAfTc{|nx)Y6*NnRmQmsh(9Lf*3>Dqmh^6dn(E< zk|QOO1ZAxbI=K2w=_8a%%RC_}9D%#9ebuv~o@~MeTj>lF_%=aA@{Z)8#DU6VAI1=M zcZn=YiA*ujJ_0F4;`^8FcVf$v6PMPD(Vxt+IoG-l9thy0ABF(#FApf)e~Pm6?8t0{ zjx26KbniXn&{pV#IrzL6iFKrw?2Db66fq5p2vI1fOFZ zg@kboCl6pwdp(JgS8@pNao?JMR+A*+JpDAZZudOBJP=%if?6MVLn^TdRaY=jT(ybZ z!4(YL8--Ii-cjl2Y2@)PJC8kW0h#LHG3?(OP`{R0uY(HN#H{IX6q|_Sum*c=~kkw9NEn%R4RCoX|C~&2J7c4tUp`AXqni2S1sF zq+fe7CI?WZRo|Oh+6kaGc$1sG@x5lvVDdv57J@Ulf*{$Z9>aUE;q4j8qq;Q{b+y7c z7Q1!^H=ef7_&FsEZtK%LzI{`Jce%h>!ZC1R&qukA{{VJ4`QVX)|9*v;0 zIEAdUWv~1~$u8b<_+abQ53QGwqTW;FketOm6rLB9wuF)`KGH6sRsI$iANLh!dvgnwX@rfr--=H` zRGW(nhkiTLWPvfYQ_*!H%2ve|yU4MN;3-vhn%htV5hz>51$CS-MeC%vyfKMI-ihVr z?L0R?6B)Xe=A*Gu&r^d2yb;gCn4;&Y;cDiv=V1&KP3w}ch8M5?VHjh)aGpn1Z%-!d z9Wyn@p26yom4#u{THd_?#qCQtNkrMnAPNmvD@bGprxS=wCW1()H?9p!WZF(bc-0`R ze4tQRTpN+dR2HS?s8t?V{|c;RiPVOUg1xmqtPAK%iZ6p{EPYhzytkHy$fnN`U#h$k z4+y%;4%R0CN!+#U2WKQp+r?Gjg-e7C*AgbASi^l+gpAZ26Ea$}C1k9IH(5AI zWnX#t|5KhVpAM$I%Pyydm&j(FTn0Umwb#T0IN@^sq33v(ophPV|-LYxes>Ju)nLqP?2_fS<3W;gOq?SNv618UY%*zCT)VvIFV#OmPBkIg|X=M=SOpeV>g2cEOvIA49m47ya#fp{$ zixoN?y9WK8-^8Q8Ov1tSZzx~jwQpT&!;C|{FPU49jU1LHfyBvrvhlJaR>JVm?_Lh# zT1^sGOvG?DPY-Tp2}x)X1~(w6iHO0tw}AhR2@$M4MOI_2JoQaP$h+LYw2ZNH;(=rB zmu|(6RNmNhu>-((n~e)YVqXRe0THAMm=adyewwi=sHTiMtpiE`)WvF^+yTO~xe)x5`Ud6F_A7EX@w<%Wh4&>q? z8$xDU^dRhZek5j()w~lPFP`qUX!PqKvFB#0J^RBxdp|bXG+~e&u+<~!y|u~QV-To3 zxBfUvv*tO(w&6l~Rv~ zgL3E;*ZpW70tRh@I>luF@GM=!NTcPH;<;8pSf2d73}96z69G28@P3l61fRa}luA-e zeK&_k_1N>eBx;+vyCzHS!6kJqlc^X@>X+Kae1#^*h3?bN4Ma6HtH9 zgDJFrbNyVybBmwt#k?3*cN66+tR}zyp3rvkyQOQXdAfynj)9ClhrEMx&RTJ+Q9yvX ziHiwBajUVhgWEpdVhW+#ws7TD^PdKDRsA;>WEG2^uWb<^trqLiR zSOQmNhusOB@T+a33^wiXzl6i4SSSV)Q(D|sz}q0uF%bm5Z4k8JB5t=kQ*V)do|mEB zsCxl>Xw+Pwd2)NXti<)aASjOoS3Xl+hf9HuAkvhu%z?CyJ!iKEa(SXVv`WSGpOq~r zQ7eE68bH$tO%m)I9c2hmgNNA!Kx^m=bbws9-Qz$6+)6a%Whv^81meyj_CGR-{MtwT z%G7~T@EIuiy$tcoaP|21E57y7-E31i)85VY3v>L-=`Sui4}#;l(Mx{c1iGCY;&&Kj z9C@UN=mq!w2C{pdfuOw3u#-#+u4cT?)h`Kg^$o(+DidAs^?AzIJlVSURhYLy!IND* z9(-Ne`g-~&E)Dp4X9>YzY-sv}DFyU%PLRpi2g%qh1{s^VTCZT4n*7Zzy%N6<8pdV_ z8C$>k`o5)CAq0jWFkhpsEsgORo5}9GzKyXp2#mcmliL|$?H3uK1})5cC~Kdfto>j% zX;@pr!P?imL6qTbg?$L#eo%P(PP7qhwUf7LA;8peA9>pt4bY(cKH=>t^0s2qaIh5; zles%W^EKoUajc|V{};W~k@HAE_`9PV0ebK^TOw1PGQ<@A-d6{{lY9neMOl(M%Er)u z!@uBHNjO~8<9jt?h|!aea=i4&8!F$wXP#`%7Cv&-T^HZY>l?e-qQ1Z{F0S%eTx9wQ zpkH|20UmE0g2!JAI38c`BHMDVaTW3(1NvLd-$XQKZI2+g?wMJQn~@5XXt#`Ehfe?q z)9)9zh5E&VgT6es3bEY`8RQfkh~+Ju#gE7^FceP~ zk`e@(n;;yu;e-i(NkJPbgjC4JIDp9U3D#*62m5V)9LJFiZe%*CL^_H4=hFGPK#`_8 zlk0sCk~SWqevmUf^J^jnk>Ynn--Rw6yin5>RD9S!DvYU0GH5Wb)v7)*u@;Zh=HgFu>fv?#+ zxevkqUWunp?>5=^dbp`hFBB8rKw-4iWQNrPi$8|IS*(5<+nybTv77f(i{T7qYJ;*p=AJQ;br&*DI4UBTS4&mWhG^}1L*w{~E$iQvUY ze8>tHdk{n{TLlIat=OipMQj(gtWD!bCigIr#2{!i&3ckhd6nsxM#;>`@uIO@*Q?Y`Nfg}ic(lfC*c^=sXV?s}jNX3a%P(i$F?`t3tu{3bqI!IatVVaUTG{>W2x^K*Mu8t!+Dg zZWi$_vyarlyvFQHV6Ta@97USjgKx3^lm)ZO{o<6mq93QAs=>IOqPfH^HI@FIsFs|3 zHc>;<;1n5#>yZY&x!GVQKD^=U!F62RjCd-VoCVGQlDs8k&Bpj{JmAEU+$3P#sXnYe#|CE)-s4P1^fg z3EXx3pMVT%x}Q$ zFOmnu&o$nGOvg^Y5yUYsi>T4$>K1EaLjOf5R6byFAyr-ZGkLlx?BHyp1yp_tQ9 zi+X*v#L1FZk?Hq?WbE!5!RZ8qfH4L=G8CJB4dQVhr+PcJ%W5K29a`?TBn?rqmAWMx zM}Oxkq?9aW9iNJ}P!U#+APjU3Vy}KeivKS`qN%u9VSlY>`E(1Kv!z&`wTFIh&Bc`(u zIUp){S`3eNk$xTH@K#10 z=>)())pQnmnao{2sIApLByhM(A9SUx5@`q*7PA@rZ)OA8jMpZgN84PAHi57Z1S$W8 zCj@jLO+a(cw7k$WOk@lY*!QVtM*6C^Bp|l8+Yr`}ERK;ZiLiKv)Oi;1tPK{74o=Cy zGvdUXNUwbpOxoelYVB*C9IO*)tl_nIWCQATJ=ehZU7rGgZS~SNn6lQ+k+C2`ZRuIg zov4{&;*xbkZ6x&w|DJW@MAGcTCG?U_`-M1rQ ztj>L{S*!Vb@XW|w!zLC0f7I`!NcXt+qiYz%9%d7$*gfMjgh|{9TD0J)Pc2afi z(RM7#J0{y@`oDALdPDA=44j`$IVHS2(Q;vn*Q*0{|M8FI5u&(zSsEpaDWV1`WlRAz zsI7P!6sFIKkYCS3U&-y9=$pt)q_GbeoBl5%6I8fI6Dlol=o3XpD5NE^bcCWFMAQ+Q zuq~`FUyJfvSiZjwU+Ub0+C)8R*eS_L2}Wx&$0UPl#dt?D+=PIQ6-|~f$wZG|J2YI8 zz#VW+7B|TRZ+kLq2$W|;IE5@FaQxDl!&kguPoZ0nkC_7iF;Y>URktlHA~#fVtZMfURgTZ`KrTeXD4!QG!l6} z`btzDC#GQk6T|72p;&QgKNx-2TlxARzVy3r!y5Pt@%ECc;u6&?ZnP(gvOoTicVuEx zO??~oVc&w@wr(XHyWv>lnvgX|SJ%V&X@-r$@l87LnnDiJ)(lox7P0alfw8F3baiMf zU0qSa0np;ZoLZdC{Rs}#zmK9!r7dT))_Mn=RlM2}nOE9sLa<(?o?1+8v&rH%{M!l+ z!FA3dXOH_3UrS)szyTW-o%$+{lwcv`N?L+AlR6O$sI3oV)8@zkU^sBU4LJ=fBuZN} z-8K<$zTxKL)Wd2{ggf8alymxH=q>=sV)C?!k{MP7rtY8qI3j4Y~6fU5v(Rdy8{a3!28r2$tuYghJYz2)w(jqA|avNqN< z!tU+^kQ+MAMI$djQ}a((KT+)%tD@R3HmHBimgzAhi%Q2hZ8uU!rE8q_PI2;n)oBOe zoc<|(tnzq^oMf%Ksm0;lT6ze-xd&SB6VKb!^N09M|E(ls%MOx1ilOpm}FCi2ce-CM~dnA3cuoaEQ`5N}L z<tI{b_N0`i!I>Ka<)DgzXODR3gO)hWV)dpOr zY;BX%Uhz65)5&VDc%7f=gv@opQ(=6C1w*Q0HtN zK}6dTB$zne!%lE858d$lpm9|8){o)ZBbs}_-Gdf@Vf%_Da@bvFgIG;=D)+bPcK7i& zYHLcl&7z`x>aPx!1+#I=e$65Ei$<kPGN|0Yn*(`y({7IKGU&>hYU1EO``b5MFp2g`6BF@YVQ@ti&a=y3 zb#dSd$L#g3qOewFUHDHNJj)N}pfz&um6%OT6+{OAB(+AAO)a(ZA+4bu7OeltktqkE%A!H0HJ1Y<1N^)TAW{XP6t zPWAV`-p{z=;tyVms}dq>_$KpB&||SOrQ2P&^|Qi}gPfEj_L4ZiVXkPb@k8wT;ED2U zdCTj@tMUZzIc@Q9A8cXUc8kC?FBH@+$f=&1UgBAe+VT{RTFT1 zm@Z9dE9tfUa9(F%5MwHefriCsJcdFTC#ft{6x#4aSyD(apKs#jE@4H{#XVl|69_;; z7W(7WJMBi<`s!2iT!K7p_PzWA5}Z7eLnl^ej$K^+=NLGDG7RHZt&aj2#@&zcV>Rg$ zHc$&*ABVSczvX@swY8c|L!(>_6{B408SilO>L7{l4gsYvXdjy!XVl_U7Vn;=_4EjPW@-Xm9UofdzS?Qzb z5n~t>r1IKO0UgNh=pkQ0qoId<6`zdV_!>TacH`^#K%Zpe34Y^Ir|~8vHpa8?=9fMw z{gAdsZ%~V0=1H%<)J;hHA%=PdCbS=FHOGgJs&|@T!v~fIgy4S|I z0$tpSUFW5TnPK?=pKz%=U!$*JdBa{1C)<)o3!aHzcx;>yzn?H>d zzUNQGvq{Ft)}PWNha1t{`gk-ygB|lr$oR5m1OKj8F#G2w9n*9%L#N)nm8}-iGN{AY3kPaB68wbQt`)#4|S9KwtB7YeV*+lwO?9&xjE7lZVUKChk|8^D*5;P|uLF99s5_$c!4!tO3@ z<2}MU3_^5}+>O5>-o~T+l>m)5+L4$vU&E}gMVGD<$Y1wxuGeHkmW-Yq%&YCqHo!_~ zIq3&)*7mX^Z$&`s_v!J%(rTXFaAv^oQy@u~x@KNlamN}h@(VJxZm9=TPm zDwf)7Clk^5)E}Y0=A^oo`?o@yzs9e$haJha*WIgZZX$KSJ|Jo05vm|#J)@^A!-b1s zu;nzy&-faz@n_%N*7%Z!eNN%!ypE^dk)ZDxd?^iK|1%*gG=zOHih~L^OkVYpM-{k9 zm)1ZY(J{?vK{W4H_9>sXsc(%V06$KOsgj{cm!giULG4UF)vRV(2`!B450OkCp%E6bAKkc z?q2A1rv{<}u{s#3|j1cyI$$k*2A4%}gu^)w=PPPU zfS~XwT0ll?id;PQ2?5x&BgJt-U=c*HF!yf2${gcdz~x^wdWm&O{F2(3Dl^y|i(SHy zS~+O~;_*uuRKrV4v8n*O14ti3z@Q1J+=-iF9EcV_1SWLrm|Nhq>;W+GZjNb{9T-Z! z2!}Bp?S2jX)A}pM6c;yo7gIU^7y_?fNnWSQi0UT5YGIy?ZH9IY63mL>{U_vjBgNb% zuwf9z%~-lH;u7KHRk$?;dBqHgy_nOB16L1u9xZO%D&(wO+_+8%y&*V|cHFO{gjDE& z{d-$I=|mBCtzg{i1J!NiQQTm_G=sUa&R*&`QYRtf=AC*78X`tLgq?<#h-^9lp1@YF z0iJd6&=E(sxwY?dxT|t2>cD!VI&|{lBI)xa+Uo_NkJY#j6Dl!Ux8}26x3^>?97@Fb z6hAeQlQZJizWw^SXzcpA)Py}JrAa$lDqMn7M_)2g+gTo; zPwvF2;;hVRHz7a&mOh7i;!QrBGs{b#r>CSRe>2N-OJATLHYH8`+|pl4K*102XOQfZ zy9p3~x_9tO(0k(GZ+YwO-9!|Q`J%rQL9F4JOZ+^c_)T&y~kt6}~FIDQC^UjpPe1kn!QnFkQbsXu{A z!5lK?@vne?!+YKT09g=dHNOO6QMHEE{5y5wTHxQS3pY_7rwh4@$Tfn0qxd(r*Zp_N z4F!!RHxyr)+@pT(u^@K|xyniYr1+EG>waGV;K`r?P=;tQ5Xuk?rT}HgDz60P472v} zr=LG7_PXB_AZ5H?^PoVCc~J0%%!2|;Yd(|#=FReFkUv9v-JdHIm>}?=U_R+V!6edy z0=s-Zl$Ff8ia#W4IltHasY1a;Qx6Jm+IdhwW)BMJ>_cHwl!y64@|H*Tx<6JZXaWxk z+Q5SXa(hrfZyySqq&&tSA#-`J`=1I0P2oX7TX;}Fa}P>61ZA86j^xi#{5jg=kn(r{ z<;VcaQ2~^rLr{)k-UkaL1Z9$WSMz5Lf7bf^G8sTw9Y9$VKv^4tvW|Jz^Jj`b8+?9Q7eHAbK*5UG zqwa@V z5R}uI_YD4=$)AlrznmUGIU|5_W&mYl2+AhrJ&QkQ^XD9&Up56$&I+KM9Y8rJ1m#@j zJ&!-<^XCGeU(OAnoEJbjKY(&U2+D=bdl7#w=FcTQzg!qVxhQ~wNsmXDOF~dCW!~rU z=lT4(%;%R&11Qf6pgcc-a#;w<<;;5pf3D=u3w(aLJb-dV00lE8uPiSJLD|f_SMld+ z{=CrVm(2l`s{$xj2T)!Zf>L8%4jAPv{MqXB3(l>*epC;jYzd&?8Ky5^Uc|iH_;U?^ zW_*6ZgEkM!wgAdC0hE~#lv(DT&ka7mTo*vOK7g__fO10!%1fB{M*iHypN7vbFA1RB z7(l@*46mgdAt*O9?=AdU(~5>o^})7kic64p_%`z^Zh>s`{|H`D?NR*6I#eYdT=9^pNggb->!-!|LX*6FOj>*a7RL4p=Aqu)6tcx&ziJ9k5RAfOVP=tDC=0?|^ki2dpzY zU~Tkab@SJz4p?V(z&g7F);T_`ZvHyA1J-#Ru+Hy*b%777o4+pXfOSy^tcyEfUE;&) z=C4aTU_Gw`*7G}HUFO5;=C8{;U|rDx>&gyTFYsY?^VjAMSXXtxy1E0_3w>DK{8j6K zRquecr32PhA67Siy{H4$whma=bikVNVRiG@YzM5l4p{RYu(tcKy7_CN1J<=2uy%C7 zda)0yo4>B>fOUNbteqXOZt!7s^Vdr{VBOdO>!uD^4IfrFf8E>x>y{2!iyg3b`LMeA z>(&lfw{^gJX$P#A`LMeA>-G*sJUOHVgLoT9>x)T#Ab4WA(>IQNsr%Wu(z>Y{x^cah|Vm$JL6=tqz0PTi% zubDT_>BKRN-=9Rz=`1pg-k(v<>6FsM3HD%mIp@tUWl}0L2rai3(ETqC@Jay%P`+!g zUl5tO3Z*a&_lqDCSPfl5sCX5cfp9lMWmfC{cNn@t)7*@9e-@7AgtvTqPuzF(#C>N^ z+|9$p{gW=-xAw$+TTk3C?TPzkhl%?~UAS-ViTjqGxEFik-gTI`f7pflB|UN9*c10n zJ#jY<6Za3ga9`IG_w_w-@9c^DhQq}DZ(X<-dg8veC+;0RaliO5asO)Z+VdC!Y-!JNkds|Q3*Yw0abC|fh`!}>(dqOTGTX6K?iMNn!+0qmD*2Bcz-M=^Y z#C=sy+*kL+{lde<-QB-0?}_`0p17~3&6L)w2KD{UI zGkW4avnTG2hl#tpe^2+seM(Q8<+$Z(Keez-A?(W~~dg5N+ z6Zce4+#3!PcX$7u?1_7IPuy#I;$C~0xV!te+Y|TkJ#klh;;tSh?(Y75Oi$bsJ#in~ z6Zdh4iMzXhkN3oVWKY~j^~8PjVdC!Y-=jTokM+b|>WRC2n7F(9ccCZl;hwmQJ#mj5 zChqS3y|O3nRXuU%dg9I>ChqS3J`QP;s{d6(>oI@J?(`Y=&(8J8%ie zYWxe*Ee^cPf20^(*~SIG$53I~A*5AVxrx^JA0%&)4!Gv`O#rAAm;RbQ-nEWLUQ7h| z)7tTpw}9`upDcOWE(2;v@L;h?)SfAp##6xRnPO=?4Ua*}`*YOY0E2khXn$-DZz<-| zF=r08bo*m732hYUet{t-+Mb>@O(&-yF#QBGp_a1)#aic#0D!rp;v zh;wONg?47=GVuh!A-nK_-5-NZCji2QK{ix2He9z%}bs01)Ki~bZ z{ITc`ls{_2N)!xqI|@Apl)#8xBEkk*yhQ?hLUU+440|bb`&DoN9PwmjuMXD$KJ_93 z)ExET%s>$@DTG=G9Mp;A5jaWQ*|QuUn(W`~;9W{0fvGI2mVnMe?JCqqgTe_FO#3eY zSBq`2BSm!SxIL4xP*BN$w&?>XmJKB*p#T|b)(mmbV%fMkR8P*L62kV-aY5WT;-Xim zq2!f&fIETjGtm7Zb+dP*qj9tj(8Q8#T3k<5%uxp#+z1{ufX>BPSXjg>e8rpG!K-kTgDnqR*y*01f5L^a&9KH;asxq)s0X z_l$>IF-jc;`wWUfH3kMRYBUSQ&p^xSdu@4xk9Rf(^^j=HbQEn#hU||e*m$@br*(!S zs5HooRwFWeXNs~RgXgD2#Q0u={C}?HyeDQ=`0?9%}Ad{f9i{a32DFH zW6>zMlX@6fRG0ATZRBgVbn)gx6IJH?rcm=fR99u4!HN$6LI*N}s1x0UOU{ zCLB~Yfy%N*d}FOOi%qBtjf~rdtT-W}zcPB3uK`g4=tg7#U_leerQWQ7LGJZuZR2=k z&Z|565Nb|QkYUwhfDuDOqJ4$l+A+|^(QMqWI+2qbhaBA63~;hA78^W?9ujm}xqmzZ ze4<}=2kQ!)kPoveJTvaODGS8v5J!QlKQY0gRP7NM9&h0$sM@;Y^0U)kykg! z2g*}2QQV#d?@F^upCKYRARBN~8~qg=%l-=5nhp!nv;MPwe-T_{O6@PCuh(C8p>84{ z&{-ghJYur904nvAQ3m-J%kVEzR3Oya7B$J!QnoqQ%GG86@0(F@ig z4sZU>MVDwB@Hh4^Ny~$nyUKfaXFPx>^YEMK{#{v@I1%JyrfpqmmE`% z)GfeF@0_X$)@BYwG@*{o&y{nUQCot2Dku}f5Kk4iirRDoWJ4KQs}U1vDk|tuVt&U| zZLStSFCLT`^+4gZ*C$aEBSGz9NnnN%(1AFm#FeAU1G{C`!Z!L4=!Lgl#s=sF)Mozx z^O`P|4XTj_bD}3u5U9OSrPE&td%ha>d@bzxIy|_qjSGCt;Mh)aOF6Y|&jG8+JmSX_ zF%4lx2`P9X%q=0q8Uk&KX*~Rx`T*u&bw0H)Z{6Y%JYttWg^TAD7k@5zPVo$O?^q{j zg;4Vh_6BIzgjNXkIrQmUIvw{ozQ?><_9;Bi2gaH7(wA9CtzUHS-2IT45`-?rDl!P1 zE-){#M+!blipIIGD($O6c8ye5d(yngH);f=R@XaUbRheze`FqFqI-*_G^>A|cX z!+cYFw4nzhd<^pq^VN)sdsw^2MfKy7Lk^WYL(tf69Cc1ZcHxS8z9w3jskF-6N-s@) zi8_alhAwDyWJwXqMU~A=45ckJ+)#pY7+7;ah2n*&H)1#wyKDGDi?FfU=GkT&HrCpT zzWuflW6^EYxA8VsgVi@Q2K!Xv*FuwWS!`Atq9ib8XY4)@#c>lL3baz2DyLCTs;2HB zN+Jovf=ZN7E-&$~l_;diwctgWODk8Vd_ML0(_^v_e|pRq;!lrd+xU}RSNW4KU26W~ z>Lz{T@f&zSF>&zy{$mby2tMXG<0L%h=mYU8C#fgu_R}X?g2i>UuaMyz$YaG?-(*(m z+dhE$P#*)t&bJWOu9o2=crG*mn=I6`ox&n*8y0D;e#(82{i&-7_q7H8;ZmCs{W8{j0V}`P}yNW z!U6S%4ybdC1*P2(1L^Za4lGgr^R}UX7Ms{0tjS zGzue$5Nw9+{G{KwtUkt5lB@AKumomcRM4%8Jh2Yc8m&Y-KrJjRedM6KRdifd2Q(}Q zG1POCN_7Q_H~#IklJsF}I*p#xP$2H(0WzEEz@n9D}hAucHTG zxG5F~{xw#v0`yRA3Ebb!3;%u?>0XHTiwZSUMcdtiSMSn>c%>D+4f-sszLl74P96pR z4-p3*pAN}J9wy%6yqg6+kne+EzsZv^)k*T8~G>^&(B zD0%+0`b}&GuM_b^a(>n+;kDu{o|#+T3s$SF_PFONQ&G2oj_eNO2NfJzmx!EgMIxWY zdL8Y~hmuSG2k3LlSnTLSO7Z8Hv2~;mD#c$|#tPTGtdxM^<+;A-DWdhP`CS6AxwVm; zTlyD;<`#i)4s$w2<{d5;)eTIN1{m7X6cQHZLIP^pBt;TQ3gm4`hm+`Qn$p1cav-JT zK}GMaG~zO;dpL9Wn(@pGnDhaUs-TeSr9xJm34g$ zazgiCp?qazaq#nP3yqMn4K@=GAyqK$o#ML#G&_zRs48 zU?@%Q8Dm#~;qoMwV_q<+s}bo$l8a9-f=!?inIxvv2|ohOWFnwY)EJ!}pSqvsUqsB& zBn%37Ey}?c)+imF^0pke;ONF9YtJ+++ko!8MHPwlOw|ly>c2g)BicTE-H-93> z?p+i*PaM3#52XkZ`F|N?3+J3h>B@OAz>nkKrTF*T`1d&eP3JJkt3JloPm$3$e+K9H zZp5dhKq+x&tg+V5nWmg-{TIKBpGJeV{uR#YuOMw3mn)3^^GRA0#IVweidX$}42UtH5DJJhU^oKC8*f7q|2!ZP912<|r-qB2@$#OSp^c z{t{XsW_{g2R+IE8LeH<-ux-ntig^}0)CsA4#Pehl*nJZP06ZkFcxa?~qh2Wa0^1l5(gbluQ)y44s^?+xcfg9<^?_(x zd8?zCCrZgpeV9s-BB^n$`>F@g*g~-4QlHAzR$e%7A{X7C%VV`RNrItnd~bFn`Z3PI zpuZzjN6X!SB)QZnA4DoGb?(gH5x25730B7j@490=oThHFlrR)E70q?j)R8nXhjnm5 zHKkEm_he}}B1u)Ns5Aqi$AZDVs*Xlx+FHt^!K2fsqw`oz>2*T;IySoxub5f|g}DAs zuK}c3C$U)J_&Cy6?3+EfcBCstx;E_edUAl_*<=VIChYg$)Z}(QL zJ^>ly5s+yy^>D(*2LA(LFg7S~a%Y|a4z-#hdqo!K>qc{yhltdj0+lJRtEfE%`P^m| zJ0g~0Kd5?CB3an(XJpq*?nw^M49|0bnB6SaeH7r---Q96{%Wwjz4{srV}_v^aIx4S zcv?CQT*m9t^2~z#0sbLlGc;s34T0^(%}DF{5_6lq?NqOZ8&^QD3{|X`qcLbE9^lDLVf%8{Yr2KU z_Io zt}q0vDU+2Z!*lNaoR&^vgc2`15OYeyR-xj69upVt$}WwOW0~*|7&=?kR|II6)wEjB`<*|3tQ;59Xq>83CJ6R+B?v+8M5&XCty|{-Nh^o)+Q4 zo&T!SSk+2->XH+5VF39SZ||jBn(!5VxCVGqWm{dw#%QDgaG^4EmH^u#qUzHtK^A`g ztDe|if{KqPjK9)es2cq&U9EDIl^2454r;IPMY8r`?J6_5Z!Ke)V--kLlPo(nl#}v}xBurH772 zTw8_4b1u?2atAYK83^}R`@>h}m1tRa(tFT)mTZ^&HjVFXQ2;~Xaf{xp3d z=mMHA0kw;Y3e77T7Px86;v_sja7nL`>mVLBf^&jCkzfnpQb9NxL%iQ9Q}&k1|!ARuBefn3MqpC^|1rQZ`v{8qEp zh{%d+$tt$V!w$)(D=!RhtT#TateN0)^fVC~uA`@k)i(C4WHq_EoLq(0qXYT)!SO={ z;!Eori@X9Q;~#V!#x5zBT^C#0HPvv4{WtpNHdFth{31oRErt5pQh1paR9ln*=%pBF z17A5G%H<>k8t8BeJ~Jm{&J@Z9IV?8B?9!TyUCyjoLA`#1FkBjS&tO&XJ=1(Q^1Evt z7Q-++&Nb@Kk$;B$0UAFoX#BLG@za9FPm3BqEo%IDU-sTSK(4CFAFsSu^=j$r<)xCU>aAWU(BaYD**XXjl|*nw+)%JPfJrAI zkXC5u0E%Tb?XEbW!KB3)6F~v@eIG~t>Wn*1jH0NB;=-uop!1y>bw-8X=X1`z@4c#Y zIxyew_xH#5n}*c8_uO;OJ@?#m_j}J3KzT3BdoBu3M-t+%>=m<}k-xeZ#y>N0c=z7L zu;nR!FEGP~oWScdc-&{Av=}zzvp9@U%H{DeP#Po7 zOsUQ9fM~n?c8J!8yXY^58&448{Dj{LCzf~2i#+azj)y5|2R2j>KxjV7fu>XO3#+M- z{O?2)F!8@8e@86OVv7crXC1LT>xku9Jm<5%4Dv>8yG*%sVfz@6WF_i zG?qeBMHpo$tH@$0^i@PrrqYTimQrs~QvmV&3aSF4p&99NH~j3c53jK}iVj2Z{37%j z_I~jTQ@{9ymEQw=B9#tHhskT_yvw?kv^6_uo{O= zkf)HcaX*>vAM)vG?#ndV$bhT+n~kD)baS)OE*`V9+30`|{Z=~lB5<~qnWpiT_+1~9=o%F{h#tK=FVN77mLzu@fCV;RuSwIN})_qbQ zthakMX8XpRX~K@oQFs^_IPG^|7Cw%i#yAePAC0&1g7QCJd^mi(6nwA`51u8*{APQR zhZ+9dxKjart$_YkKn(y)t5(er)$;=Xq60z!&ODsN_q<-Xi0FCU7afpkf=Olct0)sU z^;yvtjLLd%*ax+3EC;SVEL6Lo5zy<@`GIdhef%i)G6Jejoe&-?5fUfBS%C z6<)O*-!#6X!%qP1%-;Z?djI?|Q6b5dB+He8-Q>znh`w)WnLC{;g047r z>G7##?5fDI9Z&vBM90OmFz2xDO{4Y?LRe`G#M9x^0m{IY37^40j%hM{CpGm1oU_33 z5oKc435H>E)P}K|@Ov6KNbtLCdCyp!L*hvfqVdvUm1oQ-mWq_58kS)YST{!57olrlyY=&Ge@c)WxO2= z>Qt2qo{cq5C>$Z>ITJEBVFF+gRs)P5IgzMy2SwL=P5RSUBQ2xUn=Ave_`)SF>bmZM zZ2~_3xSB?2VgrUN1Okfg6A_8kL5ob?268TnXN9`iq}~E$u*smzT)7XxbdvI$!=Bgc z!KT?pk4iIn{0(h|DdRR-so1p)l%ac6kr7z-Wxc`?ppT-?;B{#0{TRF&ZRi<}%O@T2 zE}hF4bhmFwPuQ{XQt-n!cO4wq_lULKGgw=cRYxcB4_gJ>yB+(d;f@ zJ9@k=TVC@U*qAp~qqQG7aNKINwbcoI{Vv7|usPkUC(z-7sp9V`4C@L!qE4IBV$S%u z)o6$(sKinUj$4h6lqJA!gVh$8C4``kTa9ML3QkMN<+#=8SYMu;kjrta(Xm{fmypYG ztI@GsE=kDcxYg)bE-y;R<^LJ0(VTlU+l`hL0(L-HJHQNFyd1~CYmRv86+kuYC&3*0 zU>5itd}yPXUhH1bTY>kk*h{O#q-8>Bl$a?Uw;PSuJDLQAhdi1jg*QE{sE)LYdE9O^ zANXjN*KxbiP4y73eKhOWal6sYw#1Lyjph>}&2{Lw-RR~zblh$<-xaa_&~dxb7`fnO zk>+;w|9895*s@C5GT|B}H&1lzC9j3pDvcnsoniuCrtxHOhCp*a#sto4pbX>?NW0Ob zi*>^z?M4d&W@E@$T1ciJ4I+(olVtMIh|-v4NTwbSFO7W*t&1vcZKqTdkK2vro(u=u z!Ew9MGTAWFIBqvu=2}J?$L&VTRMSY~xZUW5?cpD3ck;O1=w#b++-`KTZ8>f?I@z`y zw;R21+j68`+2eMjlWog!yV1$E<+$DGWZQDwZuG)!%m1@>qh-S;VeZ+i1IWgWfYm&7 zvIDfyL=fCB!pqe{;8qO>w{pYm!pWfns z2L0h`9J;Ft0rP27U$I<{{|L{P zCEUYD!Ynuw*0GD@G^?G9EV^zu16Q6C>Jrk|n{gsOed1if;pWBBoYM{QH76NkxNzfDh&IjW5N9pkWw+_4A2d%|r<^nl-iI|euqt{9^q$5n*k?S9vQgTGBMukD3f zfrg7V`GH0F8_TL6)-!5i)D#ESVJ=o~YLgwmtGWqSSdM^I#&DP=FEI}IUA)%dcLhsE z=b#8ML0#E{`yJIirChFi4{XWG>08a>vm}fD`sFUPz#lBN50;99r8Zn>ksNhzE-m&K zi;0;-C=!~P`2woxQ}A#7uW^*usgr>3S2wAOn~GIPR4f>o4tFAp6MLZM7MtfmsqzND ztp$Iw6<^K`BMz&9sf5LrI4?LN&LS{~IIJ2bPJ2t77iygKHDDT)X4dn{dZ{okqxn!6 z#f=GVPnucEhr&{+IB(+lP!cb-&zo+pDcy?SETG(i>E@c!E&I)x&e?S1!bldK$w{*m zvdT@1@QzWaCST2*XO;67N)MyB4PVU?&T39us67ltFuXNOCaaQZp=MOX_fe;C6-rys z!+${w$}{$l$2pLxx=cU&MG1FG|y!0r0(mRTf6AyV9Ave9@5$o6yLT-7v zgJ*TJsjr{`q}_7d6J@oA300}6ZTPY};rfqMj&>Zy!AlD0PBLk9Cte!e3GakPodxdp zNoVn9(^o1Ycf}zmUvy`!dAX-?16 z=5pTjJZ=0mJ=_h6hqCN(C7(FuA4_~bxo75S*yk$?l~XfsnR%M>w8Hi@m69ghEUe5t zEq7XBj2xGrrreyn%sdTsT48%ovVM+ZJSVjVI!HTG=>jaF7txk(r)JAV_ny(;d+r2p zw?uHc&qBD|4S{`jwLt}wp*j~po48d6xtmwI<>Ac0)1f~T8itCz-K!Wl8*Uo9cWYa7 z7<^Blb=*3Ut8Im0!lAR>$~M;`evF7i*SOVfZq>vUkU?;QO%p>yE^6Km@XfezUQA~M z!aXXPy8zAFFCALt2G+JI-IjeB?|UhGhebR8s)^5cuQ@WSR3kc@##jyn%FwGCQ%!_$ zfQ1m721AaLPK*;LrMxG|@ow0>9GG-$z9gOLs&vJ>Zov$0@X$zPCXpjC%qISo*D2Yy zxv85V=fk)IrN-u92eWPZYR)LmX24`vO%AC|Cx$@InL3BR4K!5 zLKleoA}U+Y^wNLC47($!ir!^XC!zs+KJ?DzwcxMwRj!kOpjL;ZZs6ipFtEx7UfmqX z6b~U%8h7NrfOVf*HGB&OZR)|T)mNlaiweOK^?TuApsKs#m0=5)QObMAF(ss{>h&5} zZv^Z1U|2ze;joxJ9xn77XlDcs+Pppk?Tdj$uipUsV_>^iGr(F5?7$saOEnL0I)Dv1 zGS;WLq7c7~91I?sqel=zjOBNfqDG0b!Ly>S;PsD8Z=DlMFvU;5xww@@V zC@g5(Cc#qAVfFd%k*{=WGtxt6IPcx?;Z?kKjcec&Sxj-5P;Rfe3Xc2V6Pz9~1p?co zR|nNQ7q|m{^t6JYIJ}D2zgy_)z3&zdaN)Z$Ud)wOe{24lSaEO0dY?mABe1L0gi(`jxo9+?E~b$WHMYhO1r=Z(%r_R)0+?~*~=b=ry6!~@PWVU~-c7gPO;E5#g z3{3|1kr))Tj|E*9z!$mk?0RZyJY9{|y=sliCZ$cEezypPku?l>D5w+&$ZO_iQzgpmX!?E$D@Pj3)iu zemf5at#zLvm1=I&_#4F9^OLiOCk~%WSVj1%jDt!a;zBCSM`gTr5hjo@q=;*rt46)D zi6&`cNiW#E?eJz0h~Tr(3e_qD2A`KfmT zL|eM^p<+yRA5}tLh=85`cLTSe^4JYA_N{80o;+m3o8+Wo|8~!|nQhDo#X7#*l*Kkk zkp)ne0`V#q9+eS5rjQ_7Xo(a6UeUQ&Blx)^V*n;mN_o7BhjM~>5KV1l$j_|og-l!E zZA60twoTt2<-pHX$2kQdw@`xBZ-kbifT(@rRaIurmcbG?EX$=&oh+E3OHJw?%0^>hs&54T&j?I$ z4n5zIhayJu&eYeS8kO-O*pzzcIv6H#cfJ2nDQ`(Q$9l=mz&KaIiHiGXg4sqtPuWRD z?b=Szg@xZq`Gju*UgHIH>$4v^lzkxp>8U%BVh5J$VNAE&fkBeI*nq($L(TgUY#>Qc z8_4fMz|oa_f*}-+?N0{}e6J?)P)*i8Py>_9ONKr349#VTgSPK-euS=REDfDO(2+5? zrfJ$iVvlq}HdSTq#+{qS(I5hsX}82NZKA}HHnAxd8b5}t)W+{1Whl+y0lG*pm-+$d zPM~r-;adTO&E^N4=?9qamqJ=>Vk0K>!4}%6g|1}A)t0lmE8`W6QbmsFefgzT0R0*p zXcPsFS1WMpMLisuvCyPgoH!^VfK)8{c=&cO=gb=v$5bSdWwwmiruET$UWWE(lI>5p zAEYqgMe8mlO$myYpd})}Hcfcv5j6Oq$~dsM6pTk7B0;-ZX4cWh`6>tih#T(5{H`fu zHV?qI&hb{BWB{8Gy_vK)e`eGS=z&7rz1l{!9=!6qQ_!u zamM?>qJ~_>mux5#eXAxn{DeGKyxVi(cA01}a;}^+t%pX5UY~(bYS8x}Fb+wn7Wwg8;)4np3A zpA|CxqVd{kv<=fEDEM$AGJ|4kpMFJs(G^~}0bbaimpHSeH}rC6Y2!t3L{0N9@!X0M zYg|Kw1IIwXn_!GT^u=T2y_lp?-!uYRZKkEI_T^;u@u-Fz0}_n9xGCCPdWx*am@#dS z8Pne7LL1HdVvT8_4-cfTQlLAM^QIO;6$z3~AjzKo{%B9mq4R zA;C^|_+G%)c1>Ro*-U@_3#i601WXe_A~rH1^`#&KmdrSsLS|>A$17L%SFm2`>2P$v z%xtowzaL)X)F~@(YQ2*xc;)K;N;^h{*c2di>RPn!7$@a-kNj5F5VE8H1`Xb?BVyZi zXyW>Ik{u_yTpN(LmCIN^b;@XjE-r%fqn!Ni!SY0fO>s#dV>F&jHt+4&%;%unuw=2N zfDv?N%Z@E-|H{Fj<{xs|AV0pP2zO;XJ1%XU>3{i7%C(S>Nvo=91xPDOs)f4bhE#tv zA64qvsi4XAU+@lk1xeNA)QwCOM3xTPR=BM!(XZ{k7)>p*CVXX5zW|LFO1s8Vz*zQU zQN!!hSwqmnumYf%_`wS~S{UIZF z5n#Nt=BLrk^W@H1s5LND>D_x0o*?5JYppp%%4$+vGkA~Ban_#P&qEFW+H2{8ql=_wi@o=acvunwxnEa^tvD zSA&O{m%{BjQ*UzNuG5V<4qNh?)J`y*I4VOJCOwg|S8uvZXv zAc8%fuvZdxFoK;&*bRieJ%a5d>{W!_62TgTy_&FFBiL@jUPRbCBG_$&-ALFwBiN@2 zdktZ?MX);wdo5wNN3hp2zgH4=M+DnX*fe49ieMiitWMawBiP>)_EN&$6T$xFBBXIS zVegG#i=GeI6k+d+U`q*mF=6kIU?&sSAnXGX>~Vz65ca_c_H4qgBkV&F>|(-Z3Hxva zyOyvk2>VC`yMeH43HxXSyM?fq5caVM_Hn{qN7%{;oz#=HDM&drohrXlqVp^x1~UzK+!4>M ztSSzdU~RN91HkYSJai?Fw&}k?{>-{t_*HT!G?#XroFuw zTc0kxoV-%3nJ5Tq8SNHXVp} z)tvBa6ohbtt&^)DCv530#~s-&oCHjWDv5!aWWnnm{h=d8iW1_u=Ic?JipZ4_JKt47 zJ9Sgs&+#f7Zw9`z?Dp{DzwBA`On%tD^dvUAp7^TLyV{?%2D zmx;G=+PT*~{kA`Rukmv6zUGF{op9@!kG-Yw3V4T{Wsm#)B|m!4&2M_Z&~myuzr42k zk=GkqTYq@h%|E*7`i~h}zxw%~KDF#E)w2w(jrR@S^|jmHaEj83?T}}q^{r$<4$Oji z>RoFgE}ab4YvFfqrLn@Tn~m1N%q%s|)T!$coqujnT(`y<_&uDH;WtI@icV^U+L6Um@hw1_A51GECuYhQ~LN&>Ju&; zQaC2>CX7XZu+4nEUgIVsIdv0m<4RymN+@H1-V~gOB;_qMHcw#idL(lsMd%LY(nQ@n zfw7%3D=8STB_KR0!e-cFc3_gW6rP4z>x38dsY6vMn+;AQlW2@tY59KDhVIfK7L zwJV>;qnHwqhuwW~sGi|~Y`R|oSS>%<1d0C6*Vf*EEJc&$SHsiP*1iS+X=}yNHr?^v zxUIcQl#Mjm)=o)VO9F|ub_>vlrmy1fd~NMk#M8CMHy|E{zSooduhH&fRw#_sZzM;o z26RaCONVKGUkj%q+U8dvPMzF_->0&d8^jmg%cSl{dPlUsPW_0)ZIjHXag!V~6gA02 zK$9#-+~mv@35$@ z!31pjTaa*g9p>6HOaCrK6iPv|e=4GGa<8)qF@x$x39WpL6Mhf)nc67K%PND?m`g*R zso?=Myt*I(S|;HxE>>v)sAy`duR=#h=JdU~rb5O7yKt`vMygm%NGd|Q^}V{`6QCM# zY0uAArndSAnetk8*e$3@MkSpxvaW1oM2K9&aHZc?{T4B6thnD{hLBGR$*G$fkjH4_ z+3U3fDu4H!mp$gBnU6Q57?%y4_<^m1>1Pi&UdIT_s>^?J!!>ujV@Kn9@fI(B&dXj> zc>kux>&5$tH|=@OXvZ1PZ2S$p=zvCVu7C2TBCqCx`FTCMSL+j*aXP$r3 zvXB4K(7IrG`@Q$Q>e3SptqpH}>M1|J@Eh+^TBZZy+9>;N{v~A{^*_S23P%PA+W#=m zasMMY=zpZj)kJdGzNbJSKPNSBUvxKh&-6v=*1kyH+83!?`yzE~U!-pBi`1=sk+|6x zsRMnHI?xxX1AP%4NAyLSNfroNc@_jdECA_?q@0Xmn=qD!?Ta*SGLlm_;Z9xSN+3HB z^$KO_ixR5G^% zXTH8j_?o{j5>*~`2CyEH3V)=mW>m9Sgi&%p07CS3@8GXX-1E&%2t09^pQ4W9;o z;66Y%f8ahyH-Dg3$cL^UN_{$dKHS=9yDJh{lW}Voq3=uI6?Lsr_jD>G*(#eH!ScL_ zCA`<_a7NxeK1N#ljB!lQ9Xa2@2`2?mG%gm$_$>n}tsl=n@!a2AW1avKE{IE}B3z}e z1!D7S7-XN)g+ApN{G}_SnY5($oj5&HP%Wa!jI6)IY|%+pHlmHE_ri-Q zuxPv}rGU#bZ)A4n98q6KG#vNb22d{+ZJDIScj8tAODx0FTNQ7WhT`e%7H_xp_K3H~ zdV9s&OD`oig6Rb#^}Kl90MPX z{oy7)3tndRrCdjd3F@Hrzsr7En}d0BF)XVaaXQhUkBni^C(Oek5*sh#Fm2oL>5qU7 zoXn#Dvl4%-Et?ztqKid%X|X^ys|TY%kO>LEq{+7Ge?l%hW&Ep^EwP5NbW&P)!-+WE`v5R#KVop z@NEdmVGygakX;=xo4B?z=mCN7=b~vOZ{|l+RABsJFv~1CWs_-qrQ5?-RCS5=ne!B{ z_9G)JOn+houRc>dll?{1Ph?ZKqw#C-9!~fZuw^=aTy*l}9SJ-Z_*Axz-pp<=hJBBB zV4Gmh_$ulv_tY>1{UtMH0;)}D-Ri6K2Kk`939qLKuUCE5zMv3vG~x9%;q|MpyFX|P zI-BrnO?bGRD%_TPL9iJSQ}|>?0=+UgSLp-E=B&WmgsB(!=BoX*mb`%FCd5wlcK0CH znj5(X5)?Ol5SO8G4)Er=*NbBQAY`bhnAj%K`ZXv6lOU{2#P_MxfU{cvJGH`8RBRg z^(Xut1g`7j1Mu<>N4XpC1PXeUckAap{GbB9f+Oz_(_=^8AA+aB9}Idwh>s4J=>p(3 z2J#2)cSV780l*YA8MjW7;deRm7UFLGBN2cuV1Ja@{DJ#pbn^%9kJHT`xNpZt)29o7 zJ0bvG05Ch6C%4W#H6&wh{k?Sa2RQGen?G>BpKkuZ{QDu~s+SsSxk0Ns1h08&CUoZ*OWlfFgj5u~2ICbeGi_U=94j$DZO zObt`5;@y$5m>oLSInD(6jc0Fy<0$zXaSDgF6L)Z0lU&I#XAIjZU2kHNNtB7j6_Ltb!L z4O-kXhhX5N_khQUKTaUFanu=8izX0{lSJ^c@nD*Bi2neCW(Xs5h?|kScn)z32%9;?UT;QxI)~V+bBI4PdnQ2V z5Ef165PQuWB8jJS2#cq4h`nYGk;KzEgvHZ2#9lLpNaE=n!s3}Z#9lLpNFtg!M1-kx zh`nYG(S)gUhzL{X5PNkF(S)dThzL>V5PQuWf|xpoC?w_()DO%d}C$3iWJQu{*#J>KgGm2oG~+oLPvTp<`AY$69i9oz?42;Lf?Es|y(5b6;rgA4 zh1N-8r!8$pFUq6h24$Ya(^2$!60apAa|$8!A8y+H8XGf)kD!O+j6v#CN3fC81?v6? zrVD1=VXA^o7mln!oG!2{9>H`$x@7&B7GVg}1rFv154|gzE^rWn>8PD9tXqL`jm#rh z8#rCyGwY#y3Pz{6{lbw|#<sN?`kzp=#%JKtb#*5dt^FN6EsWhsR4Kc4#@_XS zrGvn~1wf|_bOAti6(Eeeb@HvuyY0o=KQQ17& zGi6V=3r;6&CxC79I`rQsnmyg1Om(_Xt``mqp!Ie{f&>fP#u1u!t$Hhp6hR4%N)6{2uNz_^&Y!wG%h1OukD z{|)I3nnd-3`wbM2BV}Zdy0VlkQ)MkDCBTJL(IH28pqCQE3KAHVvXzu^tz22aC`VLO za|0hUCC8338=i*?VVR-1M6TDlIDQ$d!f}A>KS`Pc*Spk6%CUk~OoU%mGG4_O& zN&TM?3Hxuqr>2^-C#1uxQV-xez7iJe4CUn{Y20%1y4)a&f)rgw(F8)H_|fwBQr|*) z*RUR9A)brz&XMVIeV$Q9F6O|Ru5k>kM`guav_)XisFw&B^-3aa2x63I?5*+;u9KXq zFcCQ#DYrWq1QX8=f`5k-f9)Iy3yE}oO!rTeEqG13dd8%Rh+#i|OcPY2m{jsIM*hCd zI}V^E+8_!Z_wSWvIEYA@@+f++Ujbg899^23R5yB!@(*nblPU1Z0W%$JTzv)J;x&?z z%81EWECn}f&ci51PFco7>TOv28(6;$uhN zbv7?k??*%9sq+JHwhedV1?>;Q1(6dWCo6hW9|8mcAI4|pz4vx3Ui4u31B+6 z-s7o+jb!v=Q}LuoO^wmJ3SUaC_;sp-)wJg9S_%OV?Lz}r4%blDn9H2NZ+H@aMRqIU zJCSIovo za4V&n*-=_HDvjyC=&_cl-JCXA15^oZaA)R!eQXyKyQ~5h7w~0ztWHP zjsDicP6kZZ6D~3NaLPK@8e*J^nNW$RYGzI1={8d%@${JKk9c~`3`abDI*Ac?znQ1d zqiJ4`G-WMN+uTjy7JJ<6TRpQdWPF^bV7*F0YGY9t%gjTFL(jT6c#w zaaofsq%*yH z;ff-gsLL=b6+e0=`mx>{fjV3ph}Z397$hwqZk~;$X`^jX74uf*!X48-MG(aN?P%2r z*Rj?q`A$>P)h*AJ`J*)V(yZ7^ohOW88o9=R|mv_Qjs&z1v{2JVH(A6RFO zIfUmo>dgMPiO-P^Gra>CQu`0l_R!yE`>#!RSAvxO+@ zOFG5AfNRT^GR>AT7)kMivB7smPu1gH1S>$Nd)e{gaKAJ2JL1W&LCYBrED;&knjaW6 z3{J?-rs-#480oz9F~~-UGp_}`q5VZo%^0X?7lG9u3mnLxTn+Cc3pf@$*}XOVKHQn1 z^PTV?Bfdt^Q%Yt}4Yo@m&5|=16EPFAa3hn}_Tg@;D{lQifYjg*Wb+5Q`2+W#=;n`8 z|090OYtK}NE8cadxb^=e*s&iX$w0)dKd1rbLwXW0{49_qT7y4ulYpLlqzfV^Tu+_- zS1aJp1Sn}OJyHkYFZeY01NUF)<`3$yoBF)Xqb~gnaNBuRmpd@aFP4FL=yM&ct74+c z?8Ai2^P{mnLV>k*FHs}yp40qzTmjVhZ9A&zDtBNg|1<8|@4uvpz@L^=pkwVZV z5M~jx)~|ytuF8>1eo0HpF`arE@+e~;kvYgS_b`(=CSvd~1r&(JS;r&lQYceTrPh=M z2{4L}O&q_hkC|wmj(ct{O&oVQyQiK=m8;-zz0w?6n<=lkZ+I98LJoRD#(;`0oi1WF z(>4LCzd0{ikoz^VCFvi8^kGjB^^qCEG)qcbeeJk*E}Z8FW`T|@GPR&Mj_!)sxwT`% zxEC;QjB=AeMt=~?AePH9Bj-l%)qE|ze6uD#xhZ@3SohY!%S^Juhiqh+BI!|%gMsZD zQygS_a#QzKtO^OU8jA;~n#;-R5bFf$8`aNF3<(O)M#11!g<-4-QRn=0bZC*metK@= zJp1^7#vc8iDpTzO4E9Kl)2El%@TGnD#{) zyD|=wb`!Wkw{_XS{a*f3TbO2FoaWeh*=2~wzpimLqZgIJu~ObgIlHO7i2HQjaa(qs z`P*FC>MWZzf1CTa2mOD*#&*#`5sxw0xpudEu9S9r=1Lj2 zcdq2QeRHL(+do&z!Q|(J2pVgx4{a{xv#>hW2AdA~37UWKB#c2RgyjG&!rBAEyu6=f zm;^BlhPNl$VGjafup;5-5U61c1ORYA`N?JT8k4bYiV{8NrzIK#;`B`1j7`u)Gd$WP zVS}P6u3^(ADS@en_@7Nuas$>FEeg?WL`gFLaiC37B|zgB&A}%3noGBwJ?K_)2iDQ= z><0O{QppVpbEPG2&^A|Eifg5FrLr5e&y^}}&@oqnHRR5@Qnz357lSwbLymg@=$R-1 z%FklI%`Y_vw-F8~KPA8DFR{Taq8Kp z$+uyl&F`ETfK(ouBxRFnlYY_fvI$d3qD-6g+kM|8jOYwDnKtP!PGnjVHj*}(E+Nx# z!bZ#{$4lZID`g{QljEgPj!nWw%u$Yu$~I+2zD$lyk+u2l!Zt&NFOzA*eTU7ojco*F zGTmu19mO`XiZU&F0?Hk80auKbPC&Xa>+!(;$UCIhE7_|p6^i8BjzavXNcrara)%4KOJq?GwaEaD~B-flKpzh zAsO<4$k-(%k_>row01zrz@)g&Q1vKKBTd+=?X0yJ{6XH+GS1UElaO-+Rpv}YmpPNfGbyiIg5jkkF?At_NM)t!2vyFS zgfJw=v!3t5m+J`SjM#PRa=DX6Y9mh?#{N(uLA)0;CX{m67v_2@%#SK+&${w_DGsNx z1NYfX_jntYAi0+;?07w}PLUmbV0hUvsU!N2K$z@iEZfwDb>La|BN)_;1z?$1h0jB7 zhqF#u9Ln0te}!S=iztPu=d%oM;^3aKtW;D`%8Ho|$kr?MDnL3Pg=~6c6^9Q(z^tJ; zKXI#!?R!QakoBN+>feCNzvwJSj)~aA7Wt&7ko2_C6K)4@jbaksPS37yuLZm-313W) zIZ0ZQfUX-bH(AtJlJqQ1Mkyyfm87Sd^mNmsdOdybM{w2s2Ta^2v`!W(*j}V>*JoiT zdg`6L2OsXDOrb~V)bBL=|99ly)dib}mv<-i&Huadek}{NXL{q0u!utgKT^Kf12XG2 zqM1&;1F9fx5FOG6(c#n?3~Qv^_@^_x3e=3d99}_BxgnTNy%XVVkNCCgD7P6|%7V-8 za1s%48~{r+x{kw`IEc=5oQt?s$t2LAYNB?HVg7U@-gLo7qv(t2w3#sgQ`c5Vl@PPp^)ZdCvhh-nF=c>x zC-ThNkICn57mEy{m}*pY9;Skl2X+eod;R_{SY$hRAk}rps15{~Se-IJ}7HNc+0mN51 zuhow%OTzdD4|78qobIA8C~2B#QhrggLBu;h-*)^pPtP@zaJgI6%TKdV$LK z=ix3}gP~ON2ZwN|eb9zY7%ahH`S4<-%Upz0VAU>z1;x={(e^M>W*99D>lZ0gaIi(p zl%YMea;quFRPd-=6cbBaOdMv24T-G81QYU{8+gW}UM);u%s|8D!Q)1MrIDKdOtMQeuhbIh ztJs<`t#s;>k`mIwctEPZX_d76Bkpv0kX(RqexfBqQA!nI1fw=o_5o8JP4I3D@7@=| zg=b|wg0a;B1#Ns}d2*%)Af;K|ry8znWgCzO8N0FFS*dig#zZ}u9v$wFd3YH{q?64Z z-k+;&)5olAs_VW=TAsXnW!AJ#aB+gWx(VjxYg-2}qe*S1*O1$C>DzRj3#RHX7`;oz zrze3A6Sv2en2vW-ZQJ4f^uTG_CftZYlzXL5!O;E^6p^NV3hMebGPZxdIordY_hAe@ zyku#W)dxYco$#czOrasjl${18$__#FMX?e^f*|^$pqA1_a8%4nmR^)lR8)#kWqHR4 z2|X|rL?IL*QTPZnI=~>DQ)1VrJ*Ouc0;9u> zeMwJ$(o;)%f~034=~jf<)+L)lGJGu?21g(s zTG7|3SF)}M*DHh(X9n@3xHet_%s5G~Q>QS(*E7C2ahDufmC_4e1}F}6@=zz+pG@jZ z+y3Ajgvd)+vL|&(+qx-0=oC{fTY8ddbFNeA-2r+pk~*}Rb;uLBBbvlE(L7Bjs>17Z!QsVJ!wp%kT&uhxf5iiYC(m)Ot?UxC6g<4dDfQyAX; zWOyeBA1u@;3+go-NJ;)+>=W7uHXJJ`hR zqvJ5m0hwLf3M8{9w*txRvQ{9O?QR8EgjOKA?QI2; z+fpl#+_tp>!EK^s2D@9heQYZ}xjm&7NNz`3f#mkYRv@|UYXy?qC9ObmTWke_+eFC> z_Ox*OxK?~}`>0kRxn0=`B)5aDKyusP3M98nTY==Zy%h*<6D2d)+rsS`t@z~j)K(z5 zUDXOCw<}tK?P!e-ZWARl*w@1ChE{xXds-`y+^%i~lG~G7f#f!5 z1(MrDRgbytY{dk(iIN%Y_YMx0=L&AW7p&ioac!R$tl_uU3qpSJz~BJCcz&?TZxwsG z{8qdgKV`h8FjztSpwkct@OYq52(bMtB!Z%$foBPoNPss5gi4TCGBuK^l1#;tDO)m2 zEtw^jOv#d2Y{_8to!kUnq|;40fu&QkbdW=32U(OX1<0f1DL^Kboo+*?N;*BHbCRXA z!qOSEbWXH%POx;ATRO`OodMG6C7sol&MHf1rKK}s=?q&scx^-ZIoZ$&NvDr=PP24Q zwR9e3>6~Keth01REuFQ7&Kl6^Cz%bF%o&!<<1CrSS~8EZWKOqa9&O012brOvV55uM zS};c~K64^^cyFHuDo8LemS*&1B$<~5 zZKGdK#0=UNjym)kGLjtQG&AD3nea7W2+7IlmOKK)5A)-uc!!bWcL>YWA${Is^O(qw z>Si0FbZwiRlGe5{_zVarcwF0XC7P^y5|9NPFPXJv%cE)B8oAHTWmcNB%7tJ<+wig| z79L-1A{@tG{UeQ=mUml(&l`8jLO!mm;WZI1(8(0T8-0NJ@lv>5Ab&y89NjcFFa{6( z(wc?A@y!T$kKGu;QO*cBX?K`z9qf*PciRoATL*h0AcUxUQMV5EM!;Xin0*oOw{hzI z5%7mGuoeN&i)q4cky`K=JoKM2QQTd%(5Kott`<2i8HV-#yq#GdvadcobSHXt5}x?q4ltUY!u{_z4AW5bykj!5UX{@hlh;H`({=l z6Xkv32f+p?B3K|53yE;5782ofTSzv%6|uc?){yJ50hO#F*J~jWxjqYt$n{%DE4i8t zh{y$b3ySCk1q+G@vM11NwNeaLXvLl6VL9^-=xK6`DX zBEFy)1=d`kM-l0ZBL+oyMfc;4~wo*onGT%h4qot z=_;a2EsyGSH%JI1s?(9m#+BK&4N+x|=tPw{qLZl1?`>)u7OKqEtl=xJ%-vbTSX`MS z&JwlQvKBh7HAXU9y30~mB!h45fR?13Dw4$>)$xEX4`7Cqj6(h-6vk#=vLHg8{Ka8+D6x5 zVsU%TWRYBr%g+a0_YErvy*EVr-!450byR=ih>17&S^STXz%$T`?GR@a0$@ms|a z2PfC92OTVO6CN-m@`A`3Xz@TU;p7ntOWCl&G{H)eq((5i{XH8tVCx-+WmKX=U2Cd>@^l8yE1h1l_oV`x3Zu5x*co zH!kCUP4Eyzxi}}0w}A-y^+amRB4{;%yF7xvIKjgS5%k6c^u!4I4+$OyBj~FWbXP>s z2NKYeBIq|G=q#sk0d&Gx^u=pB=EyJGV4iKm7+B`@g&OTktpU`Xf|;4Z&@>{~bTPaAqD;ffnRn;t$<^?tcMWgA)+^gb+coUUw@ zC#Qa?g5kN`S80=%vQMUciEX)grbgZROpUs~M6N=0^SHWo-5xPUUH0jAZHHj1qSWzPn5svy!jU+YSJ2gUf zo_A_QU*~(LM)ENKJ2lKF%zN#60NT-@0-LT8*6{|g!Ww>=Mf3qD=+?{Arx9b|3n*b} zK^yC%7+4DX_9MXkDsef!yacrP=hjIZYuxftpE-}?)|m`~$ zf8c%$-TZ-@H{Bcj!TtE>qDeX8e*6kY@VmDSEk{PaL+132Z@E-IjZ^W8Fe%sLovD8W zhBNhb;=KnCtE|mCFzIKn(H|A{R8(Ygq>@6KWE*MnjKwAuL&}wv&^0}{O%BMaA$Q=X z{4Q7cs-EG{m6AE)zIm53u#xu?D_h6I!(?oFJ90Yx_gqE5BdCR!uuXJRTS1L~Zh38o zD=}7kH17`UV2`aWvev;|m1wd|AaSlHm{QP3nae8dvdGq?KVI1;Vn@YhdOMJou4%Gd z`kBt%WXV_|W1#?)d=yV~N{X_3n^+oS#M;3DJk}ZkB~|B57t)pF^7H z2e6L%aML87JVc=-(w6nci<#VNtQKp2y=b+Dm>D#*!pu;)-7BwM6Je<_u6%B+9VH!e zC)X-qUW}aPvt#X<{PrBl4$OZma9c{<&Xr{%*R zqg1!x$389p@bXx}dh+J2IJBebsc0ZzZ}VYV3nY=W$rRGfq|k+>uEqLoGx{6EM8}iy z42Y&mnuoU>KG#r?#|s%^ONlSQB;;_l#_QXLd-p_x-r=M!Z#x3c4?er7YD^O0st^rkZ2x6R-!Lk{^j3k;*k z6>TIq54sK^eSE7!&1B#X#rIHpnf^&?@Fc}A8jSNY!{=*eXq)Q?)J4hKWX2m0UjbWM zG+a)GRBttK9D+}K-P?AKWxKcO8n*^y$5qL*lem178NHqBrRmh;zzg@pltkY)&OLD@ zpSiM`#zLUJT_jiQ%~shxiWB}2T)_apL=cZU(V|k_`?lFK;+rcdFaoU#N*&&pPNiN# zT?ndkD;(^vJC(UMr#e@3y64&*T&PVuxKNvMaG}<7aG^Hq;6iN<6WX~F=B~Ii+k06+XSv&;-X-wH;0?eesKa< z4snOH)R)J#Vq9Lt1zy}4E%nRYY20eXja}TAEY_Tn5kK7bmvc`Dw?^V|*6l zmM=aPWaoVY=ZI|>5C4Qv>U7N_rL z4Q88PFzjXh-V$!M?Z|DWw&Zr?@$Jp!cla2J6>(*KhljzNpZ0cS5s>zK3Vtu|?0Y+W z0(ww(*&RM3_wthZ4xjPi@Ie8s4ay|%XLk4?0_pWY3wj~Yg5S2oF9O7S(ZwBpJDj~J z-S!=R2b^&D9XtF2oN)Ms9ex{}xHSvy>4k!H`dvGGA0S@X_ILP;;p~N0EZ*Ul;Dp03 z?eII{gv0OL;djA_`?AowUMLo>%Vk~Z>_m-A=GpC|K@Vjy5&hN%Gfo{J$sBU=mUmki0tNjO^O7@^r%^h^Q^9P+C z+{AZ!aTDL^!%cjrA2;!x8gAmFDew`DtFi_Bw&8v)ngYLw-*()vMN@z)wrC1)#}-Wi zF4>|fz%5%e1-NF5rU2J$(G=jCEt&#cvqe*YYqn?#aLpD?0j}9{^YtriC9!lGYFab1*v^zJ~X#Z40~@>u-)*d zM3;W^E9_G6r)X$miGe$$8cExFx4r?u27fR(-AFfo;C?*a{DJ#Sy7>e5S#^9vNefJXY?J zp={Ga5M8NCQ!j|ATQ7)-7)_{22%`hVSpE5V(=Z2jIaYl<@`+gw>^$iZM?Yo+u4S^u(v}i*>MSkDr;aY1#-Qkx(HI@rj$LP2`!CJbGOUO4FOVmLg(=By z@VM<`%3}-OW-YZjB^`9EH3XHlpaT_OI0-skycWo@tCxB)dgnUF4I)I>kuKR$;cvATm!1pW&GV@MZ*5BgZ(K;xPZp1Jlzj?@#B5o-sATC5nj zN5eV+;j%hlbRzYE<3v^ij8UW>l(M)6RW23QpXp$11-mNYZ;=!_Pqg*nubJx1IY9Sw z5komFewX0IJt|me!H_iTh5rmh4_0`8!w9@!<>iGA&9Y>UMaIKL`!O1Q$qF6N%UVJh z)#QXw4wQ~XCj&z>W70_}9T4;^p^T3grJ1vAE`KaKX=Hz_Ae=K_lmu8aSxe_Vp~KuR zEFK6ZFEUCiJ1SBo(~bd|kde$TOb3$5;kFz2CO0ZkrBZ~Pm5O9_VKNX*7M7;KHpz%Q zl}a0Os8lqg3zLCdys=UmMMN@`rcyzt2DQL%P}uPCzoJ)EmGs86@m9^uj%o9)3JN>T z(txX;!v3-};i|4E0d2)qYf-S;kgM`SU$iY(9flT4bKcxsU{_r>{#Q8eW@iV6H8?v3 z7aT4m-J_AO3OgJb#jEfZMbf$Jo=7(5Dh2ZL9^u~>p(+$lJj!JA+%X{%FV z==!Zrje-5!Cty)BSP7I8;H308!utR>ufuIM9q!Ft#goE$Nx?qWf3R zvy-0Z(4#lr!@olIL*IL3k=*c4j3xcE-IsIgOb;EPeAE|rfX_l$gFkMaoB&Ne>Wk6N zB^rO+I(Y<|eAE}CJ(p=e z)6bf1MvI4TLY-miQRsV&dy%@yNhof~qEnabV6|PxARGOUfw1`OBFHtUW zzeFEyi?d%MOM3T9crtgtEKh>Mykhwq4KTULmG@(Jt_V}H^pOfW2+nKILb^JxASLim z(lV}~L&g~$FcI^~Bn$d5g)sx9n*EoW*IDNZ*Bx#4*XgSl4dvC3dGWWYZFX`lt;*yn*r zgFkSO(aj&Y$LZz|+?(j;k6UM&sGRaqUtBpaBpQEEIsb>PoT{Jk7)zLT>O>R$q=Row z?Zt5GfWl>rrN&D>VIUkN2}F|)I$YRw+zEV=!$1iK_jlqsG^q<`YZfte8D|zHhe&`= zRymSIH{CO4yv!g^au~1V5oRilU!;nfO}cLCe9)CT7E;*Gz@ihLc!^4G;C1i4^LHw^ zfd}vT%*`sdfoq;S+9hH`RcAJ}H6X`c`G8(27XGCkuRw?5?#OK3GBipT9BF!b-1J0d zLe}?ZuqLf*u`DjMXe=p=)0hX0)5v?f=dd^JKzU=JCxx_UtUDaU0z@H?k##N^RM|7u zFG1}&Cy%~35v#gq46n;FS~2V7Fj`53bt9}KVQojG)^qgKdXJu3-vX)iFOXX8=&1$y zqbC;>7Dz8>TOh%pxIl{S3#8bwP>P+cDRwp$-@se0efg&5LQEh);)>9lEXo7|gro>~ z6f9Ao2@K}SptL8;HGu+ES(pu!?!s)aK#s)+BzO!ypqC5t0aZE%A1M4|@qyAh1|O&c z3-bZBJq91pvt#iA#h8zeu4eVP<)425yS7qrrj92NSo(rsO5arV1O^pIF`6oT0z>pe zQ96_Qkw9U&EXW7z+%Xto(H(;k7RKC`l(-V^y0~I0)3*|sB6ghWv3ax$XG+hD6ilc<#N(KVvG_8MPrn*5aoW^kz4rzKm}ExOJw9F^YWD7mrbp+C-I*^W0IR3^s)r`LxiZz`cjSzIMLq|<|K_{LxFhdyP$chyM>{uY^7=6H? zcDQ`IL1S@fa~X#=^NwvOlxS>2sU*fW6ii}lBeHo!V;hmUj%`HXI<^sc>)3`Qp*YNr zj%||p(G2L=#>}3#46VS_*-h`@z{YVW(i1~9*GccdT-fCAPx(~pw=U;zOdO(P!WiQ) zQH*hzV7n|B^lKx?1j~EJ244Km$?uQNTmfIW0;87fSOFuNT$sSB0PFlue&ttVGgn$H zZ>$|-n`{!R3alSpF>vD8%(TVIGeW_DRDz7xfvR|2aCSRd?s z>3=NDS1+)JzWkaeFU(gTuu4x_^R9*Y>Ic@My-)h{85L;jVC@fMGgsL{w`!KaVnJ>E=*BOM&0KA$lF z^!ycLGuK!&t7i!`YW91+@`+!M&AiBBSv^Z&QG+h(TK&C+8Iyu~&XOlM$KZ^@c-j58 z+`TYo)a|eS`8)r#FlVAe_uqNnvSYBua{6BOv@b2p8f)Ea+VA@Jg*l^k|I3n1Pdo-^ z)S%3-fBx=;Ib(D1r+57BFAFoqYWD+Y>XL;S6WzZ4oS_@$;Vg*NjHP+zqyG!_>BY9p zt(qmUn9G;_;mX&H&0K4-tePdTM9r2@yzR2FnU`2Jt7b_w%D=e&m#9v4i)QsKfyN5( zgx`GboeOhD&3N}e-utwLIitw7Jne5$nHFS>jms6E{ru+^W-MS~etG7t;liA;HvQL0 zxBO;d&ZuRtTJyW_F3cILd8Yrh>0>a)HuTYlzWalP857O;8-)S ztWjq#KL34xKM!jIv6@jBPgs63D$`4Cky|xOV6m9q{Ls)@V>1nlWz{T!CGG4TC+vCe z*vyPYvuc(=qh>$)?QcS5y3S%*ElXgDFupW?!Nm)6#$vkZo5eRAgEJ~n8+NzieL2nw@Q_@Hx^ zY7;D-rN+|8A}_nRE8C{u$75?aOjdY_<2Gg)h_{^io%#yS&_|xH(sQbYxKm%J?+oL^ z0sC*zk7MPG2b+!GOvb!3>G>KxJ=0%AqCl>S?)X68OvXncxCJ}oW-X41< z%p7~>8H0>l-4vdx<(ZDk%X7 z-9Luspk6H?-Qs$!U{yEOE>QpL=$|@^34>S8Q{P0U@n_SnGr?Vtj+3r4B?V}jb*8Ba zyLBe4sk?P1tf{+oCai{&m4!M}R)Fx})~_cve=tRTJ>C3)`)}yx&(QSrH&L#Ra5VKb zaMU;T4HhhpP!y<0 zQ=(%9oa|KR>U;2NSCqiiV~CU){TEy@S7tDsXL1r7GfukEMx;}CYfkYB>)=Ja)wV+A*%~B~94R2=mPG_1lRnSu@a<_$p0Br2go-&Cz5x^qZVeYP z=)yFWWd*AWNKDzFU!I&+e_1qP=vGJ6VuX(wQ=7sKLX*g72JU1ARC#Y-2r(lWGMFzh zm^1~Sq?VbI7wRN0Peoq(ZoMRUXR zG2xzNT^5;TQzzpPi0qT-Rfsnr#OZ15<_9me-6Y?E#+%G{55K*Qz2Yr4=J>5O-po&T z<7R&P8*h=ojg5W$_B7tAf&2NX*-M5+srrM5o)%xVs=%U+ZlS%`ZlTd;jRn5ZUXkJ@ zLkqkpj@}ai{~AZ{jer;0-S=Au`y$}kF=l@Rd{RWS@wOz-dQ5Kc(EH*@6$@J#Q?FXs z3u3%(3)>md9_+EOFT@$^wXi#4Sf7RcF^=1BVMTkq@7Bfv@)}(=d!0pY93))N5IA)f zqc*xX5bV@ha#ALZ8{xvT5Kg$$1qG8J76|N&;83;#Mt_Q)?SA%bXx^v{Lp{6hbrECvS_0H9aPDPtl2*Y zF6=-#byk?M3ff0HE}$9i&#b#BsHT~b=OJz(x995qWC;i8dw@@4rHu6RCxmEIhd&>S>{fPZNP1Z`aLR23#w z2h1?TX7e6-k`!%PHgzT1n(MfyUl@LDq-Sh3P<-3=LB-f~-~m9UhsU*z(X1Y?vUw9R za4+3hYIf>uTt@#EHz?MB93>8d%MHPOWg`ox&g$#!6U*i2CUy8Ops&HXg1|;W`AIcV z)!&!JgTiu{!07zl$;e-+6oygEQb}NQK+94QjVy%w1O_V-?nL0w2U=_Cr^uN#N(hw-IPRM4rjnI8Q+oP5ht%oB(4P@A4zA^W4RGKJ!2 z?14>!H*CjAN(d8TP&s?uhOn%u#O$}>?H@w$q(gsu$y8*fMIjyuJ1|E{OH7}(wd z09!T&pk>3m8HJZ%P)G38IY8jhot}8;E;=T6z zj3JYrIvXJc4&KiP_B|K@+p+tqRXTRJ$v>UR$EQi|02fE@)W|^-35s@V#+h&MH2+@B zJ3ywv9}IHeNjHDszKw4FzZ?BTc`(W`rKtw!K! zN2s7FCA@o2P!xEviF(hT-g$`k?y1d3yKhhbe5CvLVBeofHj}Q+&p6I#1nu)t$L_t& zQZwr~Eo1W3oB)mt^a-J6$pr0|&1T629hOfBJ1Lr=(`2`qd_TzSI703{NR!YOu_Mu% z!kcNMnA!#2Oq+Ran}>QIS)7l2r(vdrb(Cu7eB_Js@(xuh&PyFdqGVdg_gZ$ExrdI; z$2XrQ%tyYH znH4h4dIK&^vPTqc=REWwocYN2LqFyrjv|?tc%R8q3-9fyuB6hU@epuvKI*;yA9Zg6 zCP!8Ejd!ZLtGC%YGxTi9^hB6g%uGTO6e9uw1Vj+T4JSbe35XJf!tS_0)8ojdq69$_ z1tBP+2q-SNpdug-#NFcZf`TK@BjZj&o|FARd+e(+;h)8 z_nf;{8woTB1|3Jq)KiD9=KWVUQ-{F1B)w7YD7Hs4>LtUL>XnT80#I3nuV)_uR_I3h zU2G9D)WH7;WXa@u>ga+7yq-MN$#7ZUu1g_=7Q|rlPsEYvA2?*a)gQ@Bc6ik-F4K};5O42 zPtk(Bn%fri=_^{0r@3vWPG8ZAIBjhU+TtvliHnzOCa$ic1$psuEy#f+~GP#0IxOdXvJeRWHdT`P>uIZ>XeQ3?(ba;yxCxkLY`L?b)e6ChFG-ZP#^cfuu&>xA48)P#7Dx`IaYvy724LIzF&;b-OI5df64*mk1ri6zkXV5CVgP~_8=VV{= zG_$WMGwOII{&5g$GZUArXeM4ZG*j|z6HLjidmYz-NR1E7x36cI4}?F;FdqnijA1?y zzJXyr+$syfoLpWSi|6E@AQ&H*lYdj^gVL)H-5r99;|3ICudEqIXNq9&B<9`%#Fi49#V0{@oiKt8!s&jOasm{?wq~nkO?C!6LL@`}w1$Da4%EZ%k#{To^IwtTbJv_`O2D%GVW5DCHZ5C->FX>g_ByOcTo&jblfCsn@ z%?l@80*x!IEIeccrN{)kP)8gG+D8SNzXFJlau}{rN^>1?)O$?%=%9)?puSXr7p@_% ztz;FrgBs$LKF0SHUPm>AL7c(dgzz2I5%w@@nht6Srw}#H!u5m;omtXxJz?RE2s^7M zug$0wJEW~%|0PJ7ukJRGM zL)DRuickh^42XpytSOFRt(^t2kf=L~qs`OafLKWE*@C2EYiBF;=&Y6R}W4aRg_9>6nGXPHk7GGa+zXZ8O(_5ZJG6l(vr4Vvbf< za7^FnKm_d7g3?9=;VP6hDxglwsg(ssp%#+pfnsT!wlknM4)s8Bj7Ml^KWrq*T9LHN zHH|}!4YhD6>$-9pEl3L~P~Yrg+8a@;AkjF*ntwIN!eOHdis{>($bhYC7t?_ZXm%9E z$hrd=(Ci3~DoVtWf>A(1&jQc1V79~K7HMlg)b+IbNRg&kWtYcmKnj?_Y-)+mb6 zns#o)!jWNtjnt6@WMUy$sy7{ofZf_Ir>zOKn4{iUrL`Vmg)^S)zKy)M4KAcay9k( z$p3lCwF|9>TPV~6MKQd!Gocm^^+0fp2-*xoEew&g7Gt_kI}2(dQPP65&`8|E5rq>Z z6Ocb0q>5xlVI9bTEh;!hY#oSzR>v-;y%(`6M7v{Gr~?_$?kdUx zg9)n`F;$9W+H_$V&{PYGF}xj!Kx~`fsMtCZfvg+}Qfo&V?TuL5jZ__FtDWhvf#Io5 zGq!UcHZ;av;gV?^dtpJN7cVsPvY??NhIEsOXjMz}UFExjN|=ZqTC5;Zx`B2xA%UNH5%s=!S;vwp&>?Biyake$-hl zG5$C34K1n1R=B5cj;(N4A7eZ?H$C0q$cYXtJK-RU97k|ui(d}1$e9GE%A)~#kcDSG zutTV4M9c|={u~doaQDBE!$B7FHD*l-53;-ti*eEY{I|$Cme-kcEOH)Yr2K%M3mLaA zmwbVRHGKw5E|*AFbfo1xQrEjB7xF+0{^D1s zWtb0yKgTd12;amo9|(V*VLsd{Il;LXd1)*@_wq%8@xe1Z$wYkCMc8+$%#yP%3|MDf z7_C3+A}Q8c7ZQlix=5`4tjqZ-hUj~y6hL&=g%YsNx=;w#Sr^K|KI=kRSZ7^CD2NY) z-#?{H5FZNTB9~-oN!{5P{46A7;3m!jUrpabmDbU-G154CHb&Y;&&Ej8jQ6d1=^M0V z#^3ve2cOi28UN#VuYbF?%lNAf-gS{Q3l7gvVtROn(uohxF!rAxo{4UqypyZTde`M0 z)V}Ci$pt1uH-&JSr9OLGm@TVtP1zTQ*)mrOv;U1!Wq~5Zin*5bPzZO~f<8t{a2w49V8pRRlItY{2WHsBYK*xV#HzwqVO-FPKF?$;`S4Ua z;VFdoKw$AX)R+p>(N5v1bY_SYBt1SY%5-TzrpKp6g^q}!nZC4N3-T(27WCPIEyz;{ z&D7a~t%y?sEoe&pb}Emh^g^C!hHi)a5jMEAl*1F6s5;QLWxq#NP>@#4L%R)=4dmr4e+%+xTjeLdi$T2-flk2r! zgwTRI8?Xg&5kU*$+JepGMF=g(vjJO>7ZJ1~&lYT^PVH+!T-vV%bcR<~Qy8x@~N`U!7c5Mm?pP&||CHHVhBrxkg*j38v{bqDe>#nvl6 zYS!C=J}TCVJZ~ekAP!Y%Nu1XaTJX;E{7i0;4Aio#6?I;ukV5M@hs9Ac^-7OPGF-!U zkgwNzQekR?&P)9zy~`Iug>^b|ldZRqNfR>WEV7PPhTnu*gdbhRKaZ6|c<-IPe6 z1$}9}7UV_sm|ia#X}uQYsd+7^!$OZrI)13GfXluZ>)2x)2sH1%8-+BqE)=#)(i`<2 zmG04uhV})!y^>L{`e0Y#>kEL86uObV+^Qf$O&x2j1$ER8>eI|W^ucgh->-P5q6Kki z;gVL=d84A4IPZf9k$T#^%F=>1npY)RulOQ>OPX4jM3~y3_q-1y$@OY4t;h6wwU^pA z)271qw4yH6Z$Vz#u9-aR--5cdoKUIPdl5hj>e_bAqymg@bOc zLhI&lQ}t7%PL)#we7#SmaXLf5_xfZerxOMKrjpF&biRPq2c2x|v;pf5GTqkM1C}LZ z!mX1CUY?OTx6UQ_drO>Cg!4PSLqiu)e}{%C;nKsoXvz2v%~`VcP&R81vT!g`{+~iAB}4gQZ1*- zELoFaz*>`Fw0=!OQmi!z5{TC%Bv!vBaa|}M^BXG^OtdCJ$yjR=6ppnfLHXEg5|oj> zCP6vbYZ8=|y(U3J1f{+Ex)$AUHj&>6W9bXW#fVnW-tH9Al0)gQp!$S!c zG}mLh1UlYA5Js#ELKZky;YS5J+CCBB_=1oHi$(P@xn@f*6cg2CtRKpVQ2Nk z8o!ZZC-ub6PMgLIFUMR9i5czYR;a9@8I)+BgmsY%IeJ0JLV-gKZ(o?EjrJ0V6kias zaJUt!7d8w^v~5BZjBLXTLKY5pb`?i;BHmUI-mD8k77}&BV5w$An+t*zUl6izxPht# znaVdf-^O3moP<8Zb#Qd(r zLJ`)&D;Uz_+F20`iMpdWsypqCh=s(SElBg*Q452rqs15rwDTkuj&yB7GPbvk9ko!X zIa_VyHRFn~~eXGLVjK7t?_j(C`Y1 z8s3hSKzg?FrmEA&Se!LUz?^Tk3K2`C*Dw~aqZSf5GuJTaeA?R( z3x{f><;-tFEEM)@MKS%loe8mUs5XK#zaz0QWMm*HW+>Xuh*~&Q8w;f)0Z6ab>S(WS zXGE=XL~j&l0cUCio4F2zzwGELqjT%Wa2XcPpc z-70Og$YssVs5s%(iuwJCg+%7e6%OLGvm+J;)y7=LAVym=VxiD!DQ7JQq|s-9EEH;u zq8P1d=T0mfG4nQ3ds}K_QEvoGTeJfauv^>ZbRq-lja5o~Fq}e+7dmam*r&Ca1;!K> z5;aG0)YEk)0(NV=qz+^tcGoVc6Cto)3yyIPZTyKs5~VEvbb?r7;ZPsURSxM!ZS07JArjYO7MP$}NR+tXEHpB= zaAX)GNGiT|zQjVI+9_*hu+)JBq-hn&jLJHY0b5pZj0UxHCKhkBK6Wwfy@`cH`(szA z0~yf%D#`+b3ac0~S&C%ZbYU6LWDAP%+Z~8NrYC~4kVCb3qg4c{l^x|;0u${rMprIm$bfc+=KOJX*qwgx;9aIup z5$@u4P)T%GGp<;;iqHe20Ugv39n|b^c2q;IB+l;+Epc$_*Lgd_ScW^*3)d0d(6HTh zi_2z2FAmY3c2-Mn%xb@DY)PleqX0OoBS#(dL;%k9$Y}>X7=Uq{o?_AS0X)|urylf} z0KB1|Wzo|DIJzT;AN0@w^5p!3o*m#ZA2|b|M+kV#N6ta$Ndl~N)t!aV3r9RnP=Ddb z9EkV@e`jYW1c2ikINPA#&OKYs{Z!1kA33yxZ|5F$!P)p_vD)`Qs`0RoofRGS`M6N! zS)bR0XIY}NKHmnB8Xq`|a|gqGAp9MM`9S!)4D*5ToecBgR#`sI>&Q!E@p+xQ2*wA# zoqPLx<(yLUw{xWeRYKuB%_+Mlyq!3e#VCI=O|bFHUwh&g0*~WQLSCnNCF|@C3E=Eb zY5c@fb}A#KUiXxpdPb=6ySc0+%6TQqaipqpZbzf6sGQr;DC>%IHu!F?#-+aU-CT*+ ze>WGuEh&GFZ+$mcB&z3ZM4)=kM&zmIY($)T&W6-+V1^Yp($p8Ec{zU=bGhSgbWTd* zH)}>5cT;i`ekl_Ph`UUab1(;`@Y{{hl~Nymn(76LjvuO8zda0&t>3;W43~QBNFO3R z{6blBcr~~gpVBOyc>GTMz!g3;gs;jTwdMw=$_n9p4J(5KG^_}Y*{~8gN5cyAC;2m5 z{h75n{Gu3sd~I~IR~kL>_;e=ge-`S8eNrhPH36b2{GP7t5#bxR>C)Kd^w5dN=d*?6 z=w^I*7f4<@Gapj72OJMLxlDF+bCwt=Fg+hi7YL7mI*?FgW^{9gSSZq)4=bMqQWiMf zUV3zMnkX6I%!iW803`#IuJF^?BkLaHFLd>1TS(?}tt2Dsm-9$w9;9}XnFnPa$;^Y* zPBQbLSdzhyruAnF*-$dM7LvKCm1JaMJ&$DOL24(Nc~It&%sg1_Br^|+B^h%IHIz)g zg=AWa_};H>{x&Ykw(U*xAhnasJSg)>W*)3|l9>m^l1v^tlP}~|GH?^LM{wL?XVomh z`--Vm>jdX3)>e%a{I(cewN)GdG#g#~CZ(8LxZxY12g3I=%m=~`Fw6(S4>HUL z!are{54TDl;iu)LvDi=llwf?oPv4_{dWoDLweS5(C98z8{4}HDrx|GQ(@e7cGzr9h zT4J%EJ|ps<>*e>1DTv5VQxcY+rZ6l&O?lXUnleEg-?vx4O}QYBJHHyIY!JttU(1hc zeceRNhx+j!?M!`DTV*_b&zXB^ql^#!l2f8HX^0Y^{inC8$|=;%4_^Ae}KiNty8%d=C5DFi;zE$vsx4z1tr z-vM0;<4f0Xhi|1w@!9NNWPV#Mr%8c#pV?V32Dh|s^(UMv;lez5qU^e;Z4keHdVcOvDgE`22Mi zu1jmYRkhiCUn<|*ca)cs;KrkIr!l7JPhfE=r5lZ5t z+Y?HqF>cQb*W`#4y^vDMP)eqS5=SrX2_<=+H79HfA;^)FHB*Tyoo*$Q0b!9gYu%O*q=O|A4@{FE6{wOiqY?b1sgjX8x1|H&V1kzd&!i*b zR5E$x#zR91H(KMibRZinig0K~Iuxdo$r&jdZ#TB?hW~l$DcE#sCTzM`LRC=Mbg_l1 zr?BZ_4OLxX)5RXD*3zbnNmS+K)J&|R>M-YcVi;AGIa8xb*LTLy0fgi2XrR3b1MN+d zZ*YcAbTGO*-YIn>(7*ZkO-`wZP!6HVva+kvpPn8`idM1$1u_Sbo^Dn%5Kqi96$GlSA9{NalHJOQ}@wU9N+tI zFZ_@;;`p0i-18!B!|`{%I`uwn!tv{We0E7&aD4c?4<929s5#x9;Z5 z6n;xg|NSU#_EBW3zmj6>{T0mL1LVnaRATGfV#Xc{7Ee)Q&qV9q7BhB9^hJsqyCT@l zwwSRG66d3+k?%nj+F`~{N5mnBv9m!1+95_xMuZ`Fv3DW+?GPicLQx1_>`sLFc8HN1 zfgl90$$u=z3>*G~8UjNvmP!=`c3y0i>I?jbSSwW<_z$sHsy*}{VzN|;=s(12sV>of zh~ZL|qW_?3)%U&7e_T9|D}d=sZrZqMW^(6z-URDA0Nk|k_?iFAb=(ELjpi?J6Tk5W z<2S_2xAGh83vfIBLtJZn8a_nyQwB!Le^aZw3iOik`l0`G8LJ-%B-p~OvZnT2iHyyd z=s^=8b3tixXh;U=V{zG@!%IIp|px~CMWUc$ObdjS;n#Xu~&TYjv5(o zmT`POx%*|CYh=M$#xeSaMe8rBkp*WN$LT+MbdOzXdgy1KS9?C9^vaJ=w8B#_czV0Y_Kili4^v_?$2%OR z=6vKOOY3HqoRD*!Z6|x{pLEJo`#Y!kXW|#rds0hhhEK@j_ovh4ukMS)HzP5*bmMfs zyf=U6{BizxdQWOR0Ya+=5O^4+t@KUG%F@(g1m}>sl9>sN$ngv58zRi{{k=q@CpEZv zeOG453EidP3ogToaes1ZN!D=-$)bP8l7#1GXZjW;Gwb_s)6BmgyeBhzJe*u|LVpTu zZ^Y7HDu!LM1!mBjF2R0!$lQ!r8o8;N9lLDN6#S+(5@B2~WKk0@D5N*NtdPtOt~c>z zrU~bmEXDPtP!@h$t60eJcet;D67{4?;~S^@K_;`~6eJch!<+STqsshHL&QZl?+lKlsz41vNz$s6w)l83F-I;F!c2YA>sWf;WdirKgw3!3rhbx1pM3B zj_d;8WMD)224Fg8J_i~gl!VCSAFU1n z7eJu=I9o?bO%3+Q50($E-#Db9<8wX9p_zPoY)zlz-R=JXOT7a5gk zKYq?N-#$l1Cfbjup19;m8J%c9{?lGdH_8Y_`|fk2)SA9>=BH@SSc}}TW2gJd zlMRWYhI*X>Q*JVb!i>nK zn~d==BQowL!w)ke?J*gv!i-3bOvYN1A#&0*wF$3^>L!A}YoPU$87&!7`d=}_}Nz`R$O z_cP6VwRx{G?>*%0mQGy#Ob5?n&~NV!laB3MgtVNUmeaHlXlga`S1(%#Di44V#ZeRF zRM{dmwha`YEmC9KAj}r2v274$i`3XQ2(v|MY#W5xA~m)R!fcTm+Xi8_NR4eXd2y<^ zy5w*t5I+>@J5cM?$m$*kv5}e7$a+c9{xE@dqkSGp`NzNqj| zs*F-=PnA(>?Wr0bu1BBAX&2XHh zUJb%?rbb$O&UA4#e$Fl3#DTJzV%ug|_(P7r2Nc0wkuQJrCuqTaIWkDP!M89ZTb(cO z194OiWSZmF{tFdzlC^(Q<-jj}?G)a$$4-@*!8d_b?%h8D(@dzb4WVf+_ao6iei3$b zVV{Mr#u$6zt?#=k@xFP;!4fzf0{4oC= zt>~ltF#pPM8PU{__#8)v`TNx2PaZc*F*#$$U`*pb9TG_fpM~%y$J|oiW=GB@xCt0p z`Ev$EZEL+mksg=CxM{HbX{bdnmMAIJ1Mo0?Es0@LhM;ccusxxlX@+6)`3{akO6BmQ zklcePKVurGO1YvW7*kQFF_xNS>$0D^iE~3fs>E^S8N2{AFxJYFxIFzIEdo`EyVaZ| z^Qp-acdL0x=F`8A&=Pm6U6RbFKZRmz5_hZJlFX+jGlV6)70$qoe<;fG?sOx8zHxfC( z>=1%oD3YR%pXDOY-VJ)4M6PSrmv0%V=^0n+7JRe-R)p~ zT=eZ36z+(&vA%|z5vJs`-NcXK{!m!-*)nv zd(o+=@5%~QeDY#PR3C*h!@R;p`LJm(M)zVHJJOSqMSRsq;e#VlkmK)#JQ`@}ZNU*p zgXfo+0XvFGFV-aZ-kTzQg8!+~N)+A8!JC8zeKFf`TF;7#gfX`Ygbnu+pMd-?g8UT> zy$HRUDnA2#{U_1*tD6;x}B^DD;o<7+jw30auD-HSkgvlMCofY zgX_Ix@5jUCff?4H_xtS6Zr z3t2+ZV|(_cvpbS0B+f_yQ^Sax!n~qpV1?- zsU`EmXfZu&?l(|P{FKH|;OWrT%;K;p#|9(d>w_-ZrIg+5mIiUO&@B}Z>czoC*V~ld zB<*Lz^&+<*aDxbCBe)T)@29YHj4fj~wek2Q{wGb^a;BZc1jsR+cpT%Z?a&3Bs+byb zs?=nBSw{H8p43jWN1lNMAHlKC?9^5Rlf|*l{#+r4ZDTyZLqE2|1Up;mMpjp$Yw6}f zS7{hI*c#Iu2-bjLXDte6arU#2-{e1^rl}hTL=|QUlH)@o2dz2J^o8v(dsK_T?I@9f zq3zg$V_AC6iiTHM}0S{Mx@!uAqj z`*3A@WTvnrV*3cg_V7$$bf$l4XKXJG*)D?ZVxd^tU)WZ$7ca!%X3@mq-lmso!k|e= zhY3R_ArmGHn}lqbFk%u4$uMO!Oi6_)OTv_>prv67OEMOQ`#BedHV#IebJ^M`v_mHa z?zHC)u3R}aRC?t_!xGT!j2i(E(6=A zu=#Qspl0CeREe*yWCS73%vm7cED$EV4_#mE9+vKn?HWf84yKQwQ;~1@19IiR91Vv` zGFLN@&xqJY20Zt2u_LL(r$B24b`V1t-4m)|T#_2xW6@-(Pm$K=Qr@avIi(lT2%(hD z>=ghTl%zRXL|``8a2!=%M2(5-B~#qXr!awu z(~2DWv0ie9L!<{w_>(C#Ok1)J$SMsq2^B<}L=cKF!v4iFo??@u`3?;!i3rwVMyUye z9atb?Pirj*l7Xl;PKZ5c2?|Jfb7X#6KXYYaPni z-;Q!}a?FGwtW;u?m7y+hic1O%c6*sk{tODiA>gbD!Tyr{sQVXEn<#juMHT@4IZTU; zfCU%X0ShiN1=8_JEiwgS9E^NdYe|t&h@c~*Akf55`4s=8BBXrE5c_M`PxrBy>Wn}2 zR3d=`SUWJZ6Js$26ofIGT2x!Xu&gFH{>iBRIz_HEMkoA?C?oMe-P^_Jx3dP{O$y-gePg?jVVqTb{M^|o2kdNZY=-b`vyZ%J;hwP`AacTsP~wBD)2?eJ&((=J`Skfvqg1sYqrW7&ty-=_61)3m`l zh~kb-CVi+beQ?K&Ngu9DAKGE4>^KSI&xq(q9nf$@bhIvgBqF+`E`8Jx9Rbn76CwuO-L>ucEa@tt4PzH6JHdZsNeq%L5qK!3EqC@qZHdZqv+E~qyXk*Qm@h7$TTUw~Q zwyT?X0`0m3b1XGjn7&XcFkqs>bwL$qz>7OZxA>dXCKQX3S+_AK-ltIQVu7()5xEgMNA8CLeZGd0W0KYVXujitatt)152NSH? z4@rW>*svs6oQ*W1j5eYyX+&9Sqp)9kbp|)R-Zn!x#V7WnSffwI5*NmeY`1K2%ayWV zrBQg)Ur%C4!K|h3mAcN2eZItLvi?A)I=ngO42lz-N;^dfs1IH7C#=k##KsZFKKYpA zzlt$&d+@8eE=OIGp_oR=l#U!FE~62QE-+qp6E&2}y-g=lQgtZH_~UT$t8gUdEPLzE zKJ~Rve`v?!bv4X|HS|zJSx}2>XvQ2^@Wlk2qla|yaHk+rC|=vtK2PWkerqo~w9=`F&MryWL%FKR4Y|MFz4ByHm9IK~WTG!}z#^dkc$ z?2fY)?D0)_-ex?3jw=JaiM#qIF_7n;+Z_Fmo#TbOQF9!2*f~StKG+;b9(K+LqZ|$_ zuodDa-JuQssT9m@W+3U#?t#2yWi8g`YO4^$#_wvraA*&^Qfn9tPA3N2_F%}7e<5;} z7?o#7xQ-;q?zvKNsw6hFAtdEi_e8wL$7H#D8-}Hd65I|NOM*L4bv@RDeH+pa?m{|e zsANRfXb`d2{m~W>a8uz-`JeE>!xszbne*z(;Tjy&+T0weNKIJpZe&&_0j$6 zqwDIU``1SgK(sV|!MQB-K!j)cD3!*pDCXAw64IR|W8NkCw0|aQ+m(@5cQ(DLKbe&< zw!Tt`d9!5MOQ*9~sVXFK0m`od;mTREj*ClChDl}>cnCdAH_5CDR=1)IlgujNjAxW# zl36t`EDw}SGOOrCe5BJRnN{^-{!FqMDUtlTJ}jPL_kD1)v-E^ljxR-fVJ3I0v!%3l zv%96VYV)S#mXg1DQ))|TeDkIrM27G}tPd}5%NU;NEt}BRUSc_L#fFyhxxHFt_%DXtyyO3)+Cu1CzCF z(mR$RoqtbllvaW54U?WiO=sC8>7g)9=CQMEkMyuflX=soSvE#`WM;=WNu3HrHbZ(; z5}rzIK>`~fy@UxsVFE>;UWx>gK!Uo4Qf|MOQRM8)PU3aQ;vX(0{a=!q>tDbgJW~Gt zj7+chpFGR)-;N|4;sV2Po^Qeo+VRh3lK&RMf4hv@fD@9#gWEWl*X1v8edTwH2`URnY78fqml{f&q@`}WfekZ zn;Fx3r4u*1iNt%k&Yzp|l`3RGGTshl(1^y{F_FJAZJKdx%?}5=)*cGfV)xpkq4~;p zRxE;|WtVQ;QCfRqI2FU91Z$Qx$E`&h4dCEpsZ4BO(s8 zdA-^^S5!{Yj8wJnXE2Mxp6}YTqISVYF$K4f++o_GkPI7wHeB1ifCK(^lZEXN!_u&a zulVrXqVg>Kh+F|;ePQV@0m}7h%fO zt_`lwPhq8$k@Us7iYCK_^u;r{?wsKT0L{#3=Buwr10`MD{d$!9kjD=izQZ1>nC&eS5T$oox!E!LjGJb~j z@@#{FEaPr8%W^l!GG0fssANd9UQt<=@&bY}lOfdEJU+@xK=U#}8=UtA;^?;#+ zk&jJj&2KW3$Bk|Ax1mk+T;D{|x!ivYyBL$V;4E8m&psS|)qF>j2R5Mwa zew@xs)SG?d0=(Zx{ehC&dsieA+=U~$=q?g|1V|_6i(SK*UcV5TLpZ57P%IYx_kkBE zEz8%_K1um4&8&YpGK$?e*fP_FqiN`>E53m_W~r~Vzu+h~fh2XN)Tgv?ROAt-%6g*N z`9ibHkY}jIaxu<0imy^%jYTSV#VrA*6RcGKs%&CcoF}*(b#khNoZdyrl=d*4y(fA1 zrn_HU>XOY)R8NYMDWZQVhBGFkdqnd4lje<2I(cg^g`Yz&^>J>=bJ94L1n##ZG^n9X z2o9OxPFZ{ydDagBzYOrhGs7FVcVRY@8TFTA>XyML_4wWt0zLr1B?dl(7yR!GIf%Kt@^H&d?x^O;G?RF@oES64|(Er(yMW!|Inn=173d%c@~?Psb$4)0&k zF-f|QE;m!mY&$u)4Qa~ROf7CWPZ#v^id-N@GMeeYg`SLVrYbDaPO`wf&3STPx%?RN; z7jcTzF*p{5ag~lb;3lp{IloKNCK3@e-x=}iinJ$;@JOwd@FZP?_aDnnSY}JBR3_TU zFOtw+gO2}w7wRsme3;{Z1lGotXJrd@<7rqlyB=w#&y1pumn>qRAz;p%y$Mb04F(^8 zB;3kti4h5({o;)d5b#(2ud@@U&7FXX&+_442kf~M5h|Z~E;3%p3_djDBxbz%t;l#4 zLgU{8fw@-`gwa`sgr~Wa5h|Z`KH#<>H2yQd$qfX51H9_oYY5DYS%wtjwFs5Z-3E+P z5E}1?vyr7E#Wz^#dtkJz^ z#drroYMZ$a^MN+~f1_Ay^wl5Q{r{cB-QZOD4t@xw4sPb-#-*koIEh<8^D5HZdORp# zgcE`0)1IOj>$vupq0urxzXGYQv{3YdKOo{?3A)|c;H_g~<=zz`q=PRbn@70gZoFby zI`|4w)!wCpTM!nb*TpYgTp^S)!4)7CoP)wlU?(WweXl@LyF zBa+a>c;bk;|H+mmysKpi7fA{9VRdDELdy`GDq)=9<8c|_g?5*1d376|uZ22)7i$yx zyo*`dsZN#e;Jp~GG&Lyaa%X|=hon7cRqrG4d|D7Dyx{T(*>r(5zX=-!-dzwb1&9e= zhM?kj!4(MmUqWT-b@%EB-)c6M%lE<0hry2{9U$5I-R*$bw+TigmHo|#yYdc zsqBlGe=8oitf>1DZf$!+%az}4zee=wEVTRdZthmTgX_^?TepcOn3U^~QriK{seaew z6m?q7L<~rOPk<{l{3yfk(ePsoU(T>oWs+!9?J~{$12f;N;Xg8bsRA-vZS%(MB=*Zx;ysYVN5FIP^}=&0Y4Lm^c{T}TIX~5t+$xF6Z5~{({!UfcuCi?ZJCF|# zaxeo{{!hSD9b!f|>UJC6Jd)1pVIqXxuB;n;lZh#8ndACNCX5zRo_+RW@Vfz{mB7df}B3Wh|ZN7`FEmvQOX$@ zG-!(8$u&H=KCX09#S|vTyiSh#3}O<^k92!$u^*03IGyKBiC_rHQwK#9DYgH!3@jH+ z^m|zIknh!LuPlyO{G`L%>X@@<%{{qvx`=N~re$eDJ{Foq=DK6Ci>8XxfK!7`k3 z@4_8LP!3~J35dbjL{eic$}>e?<_>qn8Rx7xHXwiC%l|H$8V7r0qxy9ym$pt_@;922 zzr<0R#g!o<$q{>wTURTqQuVUJIuZ913J?db?YrLCiFZqVT+h5?y~8?n61QUYtVuGM zfLwsUi;5yhes*H(yJ6~UJJF((BeUl$#tW-SjJjJdK@Qex7p<`TEirZw<4VPNyTM@8 z-TEHI=(jOKPt7kjydO~bV#5dcO4Y9Ct0!6eDBhDtPiltSo@kKFG`}pHMpbzv%l_j#BjhLK;^q zjkqMmajMMlA1BC%1;qae!`CoeF& zYh_Fx>me2qhaI$LQsua2I@;1o=u}6U%>?Y2RyszP6rG4eWZ=p7u_#`xMo%FP|lpI;{ z{|54IAS+$?Y+CJFqzz6MQ4Pp7nRs4Q4uM&Zup9!S9{(MeQ!jfV{&(_z9Uz@5pV8>@ zGL-Z&CMRn*@Rh24oUfi#?Gy4&);@_hz6)5dyBIq^0*Xe@0`MmDpDKZ!st~M_o@sL@ z_!Y~)k-2Cu7Gc_+pwgs{z&lle5em|Zq8Cb0z-pf+jVhlie@n+n%T*o!6rr3tb|gs< zRmVRgApWn(*k>88@<|Z4IP_0`sb2yF_Ya@KB=IG?PCt&`-N z^$6(`rco#DiV)rxo%}pPPL4wxtl?$d@Z6@+LxL3(cjml-O4ns1C~Gj#}M}$Oyf$QmG&*BaV^hEyMt+5tt)>f z!o3q|H~@^zM*j)$!5|E{1VvV_0Ixg`>Bx{whF>`+9xOPez13yc4Fkfj?KrW+b7JR) zpni;J(5&9l{yqcc90mI;i<+*?hKG*PPfy8M28YUIV51`)F`)T$%1>cbHo48kfk{|Y ztc%ZnmMivV*?sg6p_;gIgcToB^8dj~@;qx4Q9H#m2Oq`wQ;X(MQ)`^l(I-v}r?J=Q zqBv|;At-KIKQV-FM}!p$cUF{U`9GnVYFQG#fcft+Ks`U^|GVnfR$vC!90?#HME7VQ03evMt$O6~wWe!8zL9#qN6^e}dqaT2MtQO3?X4V;ebD(wV(z2}D;r5NBi{E>dd{mk1rLNuo}bMR)?qvyb3e`!77$1LVzm zWD-lD+Mk4cHF5UmfLK#3=O5!W39_Oe4U-iu3^1jfDk=J_Sk|8rs!v|cb8@ zEsCgKy0R-sL!Z+LuDe@u`USEfrDivEdpaf1nl|E}TX4F3aRH+VXNI5Xhm z1@lc&E1JI!Dfm1p68z^#u(@8OhPe|<(JJ}-wt*y>bTi~E`B!y7y{HF2#rspS9{dvd zsNYvx)=%_DNbCVs`C}_pF^!?Jl*((3_5z=1Ds5V7wf_vHKmeM;rb_$ILJ~fG64%?y z_-m1d)jKOKi}{0emPS>;LgOs?w^|h&50%|Pfj%UixC?c?F|O;cw5~f_SA=~ftZVM^ zdFraQjJ3zty1Ky>3a&i?GHx)9VAR@^h?v%-v`gS+TtU*fI(9d$Zqsm zvae0a{i>T#HysxV#f zK!IKI?(av=2h?T^sErs9xj3_AGps{Kej~-I!*7s@I=tjhErlOd6Lq+vHHd17bjNDk zKKL9l2rHvl?V~;M4+M)jXO<;e+T&GLAf39CP22|g^3T}I-zslw{=6V>J6nTj$6pEh zb*dlaQYjrWM{FJI75$|si*j)jkEmE8e;9ebWO;IvaeS%r zamR-~FuC_mV7RmSVK{s)Yw5DcY>six!{8U6^_OtCm6BsOa(Zf)l>;dYP0|MU%$KV% zFeJefQcByt#dp{=a3%cN_00^kBqGcukaXmBPQMy=-n7j>dI&S~5t15@g`<71b?`7^=os zF`D!dlJy~IbA1RK^T!zY(?rAH(cxITmO(Dc%#siYGRn8ab$mLEdeDYShjQX>mct5@ zEPf!L{WgMQM{EVhRjOBD`9crOoj2%8!11|oG7(&*9q9?kLl>2~10nGKp0o}4Dw`vn zaq`8OesV7`rHh8L@G`l#Nz`?i$>orlLW4IifeqBeOH-x}R-ey#J!z>#9-j~u_^u>= zXAxtJ%@V)SnIkBKSeQkSw88->~^Kp=<&Gd z;GPFS0u1f4=d)5+o{H@l6IbjSRyc+870k1QsfSB(YV>`*5n~_ba+Oy9B0y{P6%%`G z(i|GYJwlvc#e&6jvHV-$JLQ-3!4lx>*g4a#j~Rd!t4A-uVgE%F#qw_e0uj5|elG7* zV0hTMYGS1PVLx%&RsQ)nl4x*OIxEty9wQ3%{v^nR>w`UP%jZJWxt?@o-5$^>ukvxZ(g)pswg>S=OARPnE^cId*dE zz)KgS@eKY{EGyI`ZDe-OUDc#WRBVNl*avUeQ>^KJ^aT*Y(O_!DSouARp)QgVev94y z&~$J;LH~3(TH@ub#Hr5kDts4WWv?^5GC!4f##W~AlYM(ST*jXb?OX77z}LEIk+2WL za1S}X0UwIRd-jJb5;zAmxP0~p2t4V%7@>kSp^6yUNX3($oGTp4A%-$Q; z?H;My(X88-q;Aad--^1``hdV&{6utvOCZ+Z7JxX~x17Xk@cLPq-w-l~g~!1?oPiwN z2{D2%16DXN@vks0jRQQc-88iGrq0{>oackx&1~mcXJ}Q{nI3S4R`yNxIZJV~r<7$| zcWYZmS{IxLoLYfZLi_SP`=d|>_lXSFQwCoZ88F9x8`~FaMTD)K3v39tax3y{i;-ef z|1f-u5ivJ-4aM^{CuP18hHK|t%_-!+92t>!8-vH zya$ik5EAHknZZWqTeG*5PF>0lrc63j;tV|LatQ6e4)hsTxt|L8e~#^@892DV35Okk zmY;nlx??(6wV!BgP(_;mV>~K13WIB!b6<>O74r8J7CJ@_ZM#xogshx ze7bTPdRcu+VDMU)gdW_daAzfg}qVWH>C_rWCTDXf?b zN+6->A&VfKfu#H<+86)RGP+F}88D49PLIm4%FZV%-$!Zbl;fX* zcklt$@@|Btu^VnU!EQL~%d@SpW!NJK5}+6?-ksAW)+Xnzz(Pr+bvYz2qTC*UF=H2z zoO`h3IeAiXt{<`;oTG$?=61nzc&>=&r8&isXh(S71+ex zm2WI2?S?m6>-MO%P{XB2b9i@fVPGR9xAG6LR3khMGWGp%v>*REpyCd#>~-N8%cBF( zOWAz_7b5+nO&IKxHJg2w2=?zKqORAvHCD;N&mkx<>evh z7}Juql4QXoRt|eERO6zoxeYD1J;mlMqe4OJOovvpP`0mlX5>pnSMV*gR&n*KwDBY+g+SpX`*vjG`|wX&obb_<1;G{rv(jEBh%@sHa;i7XvA;LYsbLI-upo z7mU5&{Rm8F5nyLsvA^a>r1C=)#@!f7Wx9`>ehdZoAbI)J7~>+&Dy13RE2)`ueS$Rk(kj_!8}Aa1knlMV#?nB7Kapa5p21t5jVTd2!IS&<;8k`-4RNWk@;eZ$Jd8(;kPbeBT5Uo>2WbQY{ct?><#y#vu%d56 z#r>;T`>#U|Eb-BX63@fuMU}}@BPLNI_ZDl+a3;6m18NLZSwH1ZaVjpyx9>u8aDej< zw5$C5;u8!=kN>YI^iPJ?xWSvTspfx$$2r5uO3bB*4qud$!yW_v2T2iMv}Y3R!4!+d zx5xNO)mGqzF>1IN@DafJ*AM|Rq0OLA`wC@(@zk*pwI4y`9vaG*uRt!Zsj=MYjuMU9wPmhtOQ@i{AlZb|7f*(~wesY5>P@ajKTT(4Gu7fSv$W z?m*-9rym;a&y?>+1qzwC28E1Pp^y&iP)clhdt!9J)Zt;&;a1c^F6m>AK~BBkt0)rp zOm4vozvY?H86sxixrgwpz|(0IfU}^v2T@|S{3J`vS|w(+!~u*D>Lmf=*=gPTlc1f` z+T<+SIi;hGq6<~VUw;DoSfgyqMB= zsWLGl^NEN|Y}586)`$$TX*47XB76rAkqFfOCYA?clK4|3`d`B%xDAiulZ&9#qp&B( zqc&fYe%=5^<0joZH+vBV_HHJ(FrYgjk-H_nB>PTr)u;%CXe1fHveT9AE-yJ(eC9lZq)h+oNenGdbhFJctYU^6yZ|r`KJq6~r%F25 z@1k*QVc{wdpmd$QLDzN9x+n1OSlqF zwBTp#v6t|u@^#MaHE=MMGhxTZf8*k#aNg4}v5XE3R6c9`3S_u~#8t$8{Ggv%MU-){ zoX*gtJ;s9Hw!03c)I@IcZEP3u5SQe92uu>ZM);^BUD zOoIA%fyLTEk_x^#U#;T(EzuVOPvaLwn2qt(qts-bn^Z}6IzIqc$54I>7FVD#fR!SI zc*Vn*D2K~x7zRrbnwr*BH|_rzRp^3y_-81ZOy$XX(Hsowjcc5eHZsd)6Iy(U!pI*D zYb8UYmop$kqnRkQo*^y_^*$S-`#BqzvA!s#b?0wG-_E*+X5k2w@9F?=FJU{_2d_L1 zLG6oo?MP&M!8!yL=1?fGXmRC46bRQ%fJ|iz*1)GDmUIm#z(T?%5TG6|AQCRI&l zNRv?rvBZBJU@@b5An`lq@{fzc#6ju*ev+rQ` zD05Ga9VO+voN@L}?N{gx?qskJL!Ca%<*p@o1&Sea%7@1K3*|qPUcr!GTzO zY&h=RiqC`nUa;S*?9W~Z?aauOs6REk9gzVXC|rhRJ#V0p8psY*)}lFd$|TwdXN<|0 zlQ=4o+YpYe5{a|UO7#BO(J{hV7bSXs9fseZ=+!aTj}wEBhv^7EmFWE?V4TFAiNl8% z$rx=Ew*A@A>pRePxZ;4}F6a2HC2r!OB`d?bqps&71gn70PN8q)AL{NZZ$&LLT1kw0 z!JiqsPfjMf$rFy-6)Oj!?*5AjfR8PUH)EB%HmEO9; zOE{H-so*bRW|hz2P{e~DLO%XgbOJz;s|li$ErLDS#`dpeRiE36_?= z)`jm)X}G!plPzk09sLcbTm~+FN!0NZEgvGN~R-bgax$Ceu$dHGBckbNGZ66Fqz$*z-%! zy&7Q^gRp`4Nd_M&nl$+C#iUxlklr~gC~Xk0O8k%{otU);TLLu;#|{*4f`JI=2kuhyMph* zuxADZC=u3W@hlEjZbU{tJ>VY%c~&lm!IA8=WW%6SOe->($||Vpl$WV|72y5Yfes#n zw)Cf`7URzRk+gI8q%bCUeISYyF%iFniAbXoX*2EX!p~erRZ1rXHY1F6XT^R<$=Q&x zOIZ?*B|BBNS@1qYRh4~?qIvf=C1w^s0UBA?LJA)s#_72Ya6tW8Sk9bU&YZDjd9|E* z6YWyVY3yntJGp3o+NAY~(wkI4T7u%;No`~(!6X`{feR`s}NbD2Gm295Nuz%ix z?fD#>C4@jN_XcXy3*D82b2QAx`7tUnrf( z3tozM?Kq@+!OIZLd%@9o@#jJ>W)9XMu-JZFomYPa({NXs`NwN`24S@fn>1lPGdCm5 zz9?%k;Dt7bcQ*JpyYDhIEgkyY2>_ctB#NHM=nJFhD;eD=F}ku|FE!8Na~@1hAloc+eX4!%5xL)Nfe`G*F& z%D2Sc1IvTx5D(Ic=-hWIk$r~FKOOu+c=3Qlnsw(K9PZ@bF4OhP;Y;#d@6{cx5^+!I(%9N1YNT` zfCJldI%KRv1ZgGkM)`3EF1KIU2X{-XJ3+?+2hOr6RUWd>8JBU;{b2{8Q>H&*U6025 z2hzSx&iT!3;j{3VX!NzFp@Vl2f_myzQla$UkLp+hhH$*_yyu1TSCyx;c`f(B>BVS4`(Cdbdv^Nc^2m?yno2{A#}=apOmaLTI~~=cnY(Xi7#5#T|_6XBvx3h zy_n*zOj16yCloTaE$+tmC0Gx!#6}Cau2|U&yD(|D2AQ1{{eBtvzlSmxQSi%kk5i%K z6Fm^isQ`2x9~oXw+@!hhqP;KrH4lFTfM+YtI~9ppRpv8IonnH*Jz@&x5^wX zohU*6ur-UI>Pso!y`8~e3<1Y~3f*#YKX)2qCzdSTG`165$&V*V5|c6BDJbXOs1D%z zG;*!>fs5eSs=XF3*v#wjifrcV0F?>ejih*jgr!U-XDg@DKP4UX8XdL;NQDHh2Xw8B zFb83Tbz}VnE)N3I3Z_IFR9HwI} zWC!{4q}Q^v&rG9+*K=GQPIm?{-`R%wPDfLn|K;ok5{-vny;J)}lbh`SVs7(=7IPbz zrwh?K@)-`?(nigv@msWfSH6eYRPZodd9WG~{!JXWq-5!IAS+WKt|?cpMxGisRV_3B zIVO>NWoX-+#Oz!+zCifGaD0dG&T#&7Z=(2Un2zwj!g1dniNkiOpa1*_ZQH>ZWD;uM z(fo%x!_Vvem~?|^4BpS#vW@Xdo;LUQ@ zwAR{{NsAJ64{#!|pblUK7p8b1BRarQX_E>V9e5hNZm0u^1pG%d2Y%ksVOt~pm7xw? zktp66rXzf7r~?-#4tr4Pa1Q(o*sW$AXdF|vZvN1hO-wY-efOZ16uZc^qx)$Wqq*-J z(HmOLeQ%&y+se!;pTQ>)x97f^`YEL5@ui;k5c>6iOOFKaMR;rd#23xZ%#7GjeAUS; zGF6a9o5(y`O};Iwd7Me%YKSh6tspufq0qWf$mc7&qdQQR9S?Nji`Z<`ryqdju z8Y*TlwoGF7BFn^i#Qq!yzcf**X#eIZoxR=?+l(iljBUn~-SNEi$tC~MymW82Yv=P) z0birvP4;C@y3?uL1+Q46fsV(1yhj?1{U|MVSejAk91@UzsCxEGuhjReA$_Bva)3g6wf{BrB#NZgDV7cU2zHq2+ zGhl;PL{!sCmArU7qn`LF=nht%#M$5p^bMX7hR~8J59iDeBiEc)oD4YrC4IedXJj{#cZ*73oP<)}Z_eS7!>(V7Moxk!LcJN>+L}6xa)qkUj91 zN}Qv`{@dl^_&*s98R}UYG8!_3;`eM~$(xs@y%g6C8Kj=91b+`heyQn!lsE`nfZ!tW zbFkv{qABc$;|@2Hk}uM#YXdvu7eBb8+{SlfmzPRc@dbOm$;3k_i63C^`3#yZj;1H}%U-;w3Lj zxLlLw`p#LhqmEA!lbrm}*u-6f(J$MP`0NLo;#>G+w`XJEQUX&H0S}HsbjmjQ%c) zzLU}4N6`xyy(Z#n8>4=ddm*FGiE`h?=s{8RB1Vshq8BszXoUN2M*k2+FJbhtD7u}| zk3_gT7`+$U_A1!-FnV7UeJ`WKOT*kt86AnDmoYjTMc?;-hGxS~_d9S1QQ<3+x^j;cyH`9AX0U{?bA~z96+XWy$`DiU(D$O(|j{zC*I-E!fSHVj(0fbd3kyQ zH(d}gJnmM@VG3XS z6DZ#XUuMQTby z)gxfzI*W^MxDC3+3Jopbb*?=6B6ZnBT$tDyw4m}v4 zoBbVrxQ=kyC=^r{4H+>j6&<=u)w(PD&T`JcBRfa(lkI>;nI0jCv@*CA39zU6Dn5|{ z1`N_El!4^#`^)RZxhD=JR(cuJNXNHO38_W<} zCwlz{&@sA-P|hZ_`XIZB%6R)wr>EEs8t);F5Etb!o_8|D*5f$p#Mzgeszr2YvRbEG zStPaU2Ls<^c{|zu8NdeiV1kV{q=r7p zaUi47Cn*MmjlIuQT>p!BCjNo&`4cGGKhwRmS<3aX*iUqqR=Y!jfo^K zMrk*3g73kLwi9$Gj|+NnKDm~}$G<4$n^ru|PbJ1P?jqcJt% z{|3PWQ;`T77qJ9x73XZ(b>=MgJn3uZBo+$W-(aPPuPr?XL&5aFIo8;9+W*4Zo`nLv zf3k-(K>ihn1dOwVkONJ%2ywiB6NJ1uHulH&8KINZdmSQFe@i+U;&do+uz-2%SiMIO z)@RUvGLF7Wwg0l5ciC3af*BzhyCHVcGe}U*KO`f+>`zoJlKv_uVVIQYar}p<_JH0# z_LDYD5*G@_oJ;=+e4i!0{<(l*sQd#T2>X9WdfRrtT^$7=Cv|8_`;P+L?v^{Jy52aQ zsdPT2X|yJQ{~qwc-{VGyyQT26=tc*OXiHVo-gGnydV!FK${1@eM@Qyz=3mZE%j+tX zHRN$g|1tp7`W7ru^8!j|d%6tk-4HdWV}ZCGN=ZN_*dhJ_4n%6Nm-!)QQ!U z&L?A}O4lhh4Iz^fN>dj{hQ~=X5*C90MvA#^onr0;im|inJU(a*jHrADHzyMv9o{EM zF}h)NPTbtDLsQavzD~Q2Tt|~L=p+YrtNwv~D1p0bd0h6NMyfmpNRs}f*Ae<5;GY3E zdxda%g@sN&nM(}9lEuItnunV}8eBpvglQ1el7|L|R}3LgY?EB5szH8{xunee#2dpL zsu`I_aop!~KC6!6M`rtNQv+9B9|zjO_c_yekB|h1#6gbtJHpNcS)8Qz2Hb`{@_0s? z^e$uQ1ze|If=fjVkUrdxz-lkh2VJk*W(HS88N9sB49VoDoua9FqGV z487yKLLu{q;O$}xnd9^th0ONWN>2&iR-<&(RymyBHua(iIe5AZh{e=FJFpHsMpXWY z@&{(y-gBVNpaIHgg*5_+;mt%{^+f9ND8U@td%+|iFiIdECXjQ?BTh;h7fP-we;R;J)2zeN}Wn;lAH${j2DB2G$ubhG|cmk{wmq z_ELN7SEwWAh|TOlC)*v4e?6)wOkP2N{@Dzv>xbM(@9r2hh9~|4{e4L2iA(nqd!P@# z$hkS8x}8q&BNWqkzU6(G^WYB%_dbAKO!XNwSMX!{t3Ld}Pw1>Mf`2ZeX3IyUgu9Vd zom?EJZtpKBAJ25um=NpYv&dv{Ug*W_{5ORAc)JcApBZAZ=k(~GffD*uHt2r?^ETdo z{j4eMqbRH?9n)sPMtNbnuEvs4P0}R00<;uav%##LPe~oiTnI7Z8kxojJ;<=U9!YWzPVm-aM%BC*5=bi8wcC(F8TUqzaPAT4cE%Fc9Y!uRAwiA2 z`#V5fJi7o|{}f>ZQ~IIlOcy$cP~<$2!TTmk4OnOhJwJjn$MNQD<#zNE*K`c>NtV@S z4ecK4O9_?a{ugCKFG<@<&lLL((gV}zffxnSp09ws{;T-OmGjtZ{{=2=9y+QwLH8sJ zz-s^uK8;44vZZiu)QGXJth!uWbChSBXU$r!^yFCu^?qLF8uTqvv&uOm+w1tx;OqKd z!!NXEw>V9`I7|8}d@>}{{x9KdP7mM(Iii{oDFGhc`VwRO0Y7kmneNB&FLlY)W+mm>&3Zx1I_EFT%(%)D7UbWJKSws6BV0HF~T)S(v+12BIDMT4KQg{x~h zyZxNb*u?jXx}O6txI_HH_Wk4knA&iNJU#yQPCpy;snW&-<)0mH>T0N6$;9N{ zP2#}Dbc!}Uo+drH_gskY{yhKlS7ZLjmfNA60teI$>Z#0zXk_#>%{F;7$#o2gw`PRJlGx8! zY8*yh*oxcfm6N!d*7cmrO@D94LV9U~u+kNz6!B%h5`xX6WGbL_m=ajVqYsjl8o0^7J z55#SvY3TTmp||3cvjZT7i#QuQp)MSe#sl|fA_>F{cPH?SG!@9zOrog(<2c|TbxT4H zGG(W|dw`upwQC$9{@+m3&<=zE{tiHUraXOW#`_1I*$f!h=(?vcnFx9VL4&`;{FT{K zxIdb|RHaOnBK-Twj*60|C~uOhNK46hEDE>2lLI9TW~1mYN(|sO>>3A>C^1lxAMC{g zz?WPAhp+!nl!^zK?jtO1=hLt&Ec$Ef&~`@Va=qoBa0?_1~G8`f3f1YCd;fLH+?QjZXy_lXbYkt zL+%&Y=g`#9MdC=5?vZUt4y0R>MhmS`>}$q!lhx9#W14dWk5B)CzEi-QQfT7iTV%io zPBe`8zfl?&eOTtOl2&%~8p>us&VhciW0)d~!--;3d4}&^}L>%m=~^Gn7pvj>DFA+gVRN5 zZ)_p8V1?!m%DWfDA4qj+q@sS5x0q=iaI&_sZGF-zeNEk@6Wi`JjxpJPWy`ZwkRy97 z;fMSyu`%ok;eq=RL04c zwuM^_*cx^fmix28lXOP<(hWK@e?*Yzk5K32b70yJ{F68bGB<@iWF z{rfrtxGpT}sob<-H9uLqb)nPopSQp>i^2XRa-am4I68GHQv?z-BYtD7NH)b-tdZlN zhYX`w0;)@&`qwyK4<>w#TY!PihLSaQB$)f0KSl8Kdcp z-P#$!@{^(|-$jjZJo+cJxao!C@m^T(FHKh8qOcD-xLsvge@EW+O#~(|ax?T`_O*U;PfjdhUnCq<|_T+(mD1q^fW?y>lY(|I)i% z)}Cq7e_h*57E1Go2zc>7Ntdlnz? zBA*0TIQ9(P32_UJU8^cLcEEF9itzOeR~J)NQ|M#spQDRR@%rcKN)z@1Tw{lWSe056 zo+J9(nRrIw74*chBMAF=BkV}Ru55(8hp_7#VP62XD<5cteTJ}~Ts7s4%F9t1pPdAD z9bu<3Z9U#iQGRvU6QEAzzZ>&=lCTM7JVn@q{GKLE@T=$d5@AAvI_w3)ga&oki-ZXc z>aaf%CN!wSUMB3B4>r{I&xBe3)c{*X`aN-V1MGOhUTcJ{CMuQ9t18Ut; zpAOsdFN8_m>af=cle*Pme6UzMsVH4Wzjwru6?eC1@oj4n2X{A5^GKyD+Jx16|>}yY47t-qS2v(W`>(r9= z3v9OohY_z1yNR&V8(|!DV+9h2|c|>rI z!8n-+elZwll!-7-D-&Rqb;w?JnG*xP5CxDzv7BFFw_uM@7$=!Ao5PetG6sXfgbv9V zjFYhp$r$X$aEQlXTn?oB#9&+#r2E8RTo|PL#9&+FRhY7PG1=L}}aY!MAsm>a07I43)%QQ619>zL6V7vXc@B{r7 zLEZMb;e^OaZZ`IVujg}1(>I5f!*T3WxDSm|YibkPNpWYQxj9 za2#g+6L`U3&FF<75|q z2k^>H5n}e}N?_BJUh+}E)BgQsJVznR0|=2F$O&_oT}))t@(}&#(TI*Cjy*E|9f(Ug zhzwXY{hJo2zDw;r#2;$-^aNVLF$sqHsMr1I70GSY%Lyl|2<=@05hw%kE zOoB^u9TzmP>o+0ho8a;!Xj6?MO^yq0qHO~gD-B$zPC;Cpfn!3_>@i8Rl+#6RmN{sa z9kIu1b~65Ae6`CoK~BGfK5l^xF8|#~my2S!v|9%mx?nt`an+I~sBbJ;LdtJk*n)H8 zJ(j~tEi)RPy}+JNRrHR9aKdxxJw4Kodl4fDk7HMNMp*DCc&Ow=Aiv^ApPa(8G?j}9 z40~7xc_$)K<1Sd+F=aJ(XghM=*#kqH|8!UL5)!KG<;eACxW*Wus_nX_h+nkVV}4Ok zkNHIhJ?0lR^O#?B<70kNe~>Ce$ls&`9-BZ<`=E`m|qm*#SfVgDWXdgJS;x)FmK~gSy310jZ@ux4_+=jdrj8W z??V0d;{ij2vt4K4I|cpPt`;zfzz#(o-6I!o(@=LSWo6pJv8ijIr1M&kpjhpC)70TS z(`;o5VS3xz8Rgn6*Y80}?;|+k&yLTVa7X`I%#7yrx4A-(ImzUjN5K{TwX}ry_2Hhb z_w@ztS$W zbf#lst+x7Th4cqHw?+>#lxG*LzFq5e+!JLaSVmsU*a>A!h0_NUf->4u{xmosj8th@ zdp&a;9mO+8K02-nR%yq^{FBVTqn`ivU}tjBQdz9_1j zMNOBYwlj;5ldkT>xik|Nnb9I=AeNa$A8agA>DFb<-#;wRv0!CLMUf-I+pNF z`*RV;9lIb(H|!KO-C>v`ngUY)#R0yY&B zx9+P7s8_Spi$JXx--PgxI~$IBuUiqP1#uEu-(J>t$SHu)?t^n+I{~{=^Lmx>UY6jT z^5-E^V(dazS(45ZH!#O|*^HT`o(4~A88T@aA(NzeOJt(1wILJJ3zg<*9{dFk9H+g;PmLaLJ`x14Gn^3_Aa`!5|I`W^IAXOMycna@HTk_Ou;J^)ofhU{ z%&K(2QlY*mKMgKvRGe(EXic4}K7`h*t^f(RPqwXA@N8{ti^@+v0-@V}HhOTIT!*Y5 zFZbJ(Uq^vc$F>K{Sz0Pbf4W_^D>p{L2RZ%**ms$A6j+hT%Mi|%j1pAW|X%&Dlen?R<4d9(g%57lqYQ*|F@u8!dr`WLIeGcv!*hy8dLj^gprV}70D*FYA%J-Iku(kjUtnj zHfaF>F*OL?6>lBj&9J?hP|Ez%< z3c}inSJgxJ4gM}w`w*Cm;2(7AR3(f|wmjqtBZCgbXem8pdVw|B9%=DxB%%H>+@GyO z4rV>OvtjkrxqLfo>9M*#Y}r=9T98vckt6oS#)1-3h76cDP_8)s89M$&htPd*+p;=P zzi13G$Zv&Q1Jw|B8{ADPZ!zdu&6DXE?}1sFAEGPd(_3G?!%%Y9i8U9N>NO+PlFxyL zcr25CFzaq_YV9iQ{Bh5w3`$&!?ETN+henX3U@5Aj`ZHGQS*#fCiS$|uFKB@R@j^Fo zl#HrZVn8*GFN0~qre0u`eG{x;Bw`iAb8ixkcRYcj4(lvdiFPP<5NYV=QtCx@Qnst# zkH*xiBdJ1r)8gMr?HB@;<}$+Cq3FP-hW9Kqb30Tyn2qd0$F89ZVwUW{w0w)8Z$8y) zP>V3GqT+pl)PgFEScJL`_06~VoAEKhSoPdlN4K};TfNhXAl|ZJ4f1W?PMG=HF_!3+ za?M`3C}?sO|zZ<>sso*rgu? z3F2cGmiXMt^Xyr$K92?$ogCHqXtFN;l#XLm$GZM{)y=*X6XxGgIP_}FjJXBvglm1A zAaM=$-|%r>Y@0}!Ac+kAOI_+()Aj{J)y##3BLmo zn^T#IHBEWDGrJ1jJnKSD`FI9+8pN$8(Ino96q^oJ_U^;#L&MnXfLFr$H1H~NUaU{g ziEJ|0JF%C)%dW9<{_7+^Zwh(~`Ext6$7LhjuJV5Yr^gDlCv`mMlRB(9AhIbL?)qE3 z0+1Y3%7JEY%IAPUlj7I$bSPj8#(@A4lZ*hRVM5+{ITvrC+BH&Eu*f>z<>U?upi0CN zaQ(=c6-Cw&WWYT#Bv4joy{^1g?0pQi(3?=+J?K*4M3L-xuop175RQ zJ1_>x!$2}^b#W|>K8SaDVZXnv(lQTvd$V7$jA|Zi*Mf|p)si|KwuJYTszApcU*3e( zhjD&^S|+@G2&-Q#p2F^%2rPVXu~-><^F)mu3zB}l4{+)xP5u3l3CQQ~F9p?@2`-z< zw|WTrMpGFR5Yt!RNnhGcDKGvnpin&$Al{;6ne}H#U>H&$x~SEkbCLFRtjkdLbe0Vz zB&3faqILhdy^J4qVXAu>KyOFPq&TGcHF~emC22i?a%Pe)HB!R2QC%G@u|X~+ zsqA)_7}z*DG@R-QF!K|RBjwD-(Yxe99c9nPeem~W>%0Te=(097XBs%V%tVe3cY^-Y z0BP~+0|a*P4y4!E!Ati~*#VWa^43SM2*(TDn2*&SI^5r|S|7vckT|zlt^b6ZW2X(N zUgF&0cn3*Gb#)|M>kQ0s?4cQepM&LMfDt}SM;eaxaNs>tzG;eG;pp>tqs2cMsrka` z*<4&a*o45OXIt*^w)eisO&z&onf8isTTCfCROG>FtB5>EfNFT8A&C#wbvtlh?{MZ! z)!oodyva|`Vq3~EgasX5f}E>Td#J8q%AAH|<_csJE(Ugl-VSryu}>oB641PFyzAHu z3t{!Ej<%~fr4%QYO;VSn9}!zV4#=-utAawWT_f^@@*6_4up*EC2Kl_1Hv$)-9NPT) z(>e6Q3(^9aL5SS4fP7x!#y68GCgx@_9Xc{}a7|5Ep8E6 zr+dBq(L95&IbWWl7KgYKrm-Qsw~hGe>X(tYIaSQWSSqs-iT0MfPs_|jTOqCUma>*( zW$dzIoFx(FYf{zSQLpLlP>u__8w+vp~=(H!=_YPTGsd8dLZqdYMj*au>Ic!a$WrZ-ofS(h#H3VUqL z>8YzlJ~Dn(T~Mxh2$x)s-CTsOH98nr`UDvGjXnPc7As3@k$60pMK2;E8`(2HR-v3b+ie2qFY!Ozw|Lw2`ud{QX$1KL)B749Iu;5+DBwTNo15f^o z`ZjNpqi^$CIkuBs{M7h6&{Y#C93H(pcLoR1NnNQqH3c3N7hi8N7k!yyyv<+vYUVUi8 zM19{OdfV7BxZdR~G~r*2PuS+u!uYz_QmZ?Ps&Nu{fkOY`5n7ZqkV!bjst^-z&;r1> zfQiBYE)0u5XUIfP0{c9{RH9TbV$rzLbOFB{pBQS^JodSejp;)oyZUj22Q%qR1U>S> zEvKLtF2}!;IJ*7@e7MWF1mAF%!4#A2G9-@KWiU>|E<*uZ?=r?QIfO6FiP4*29x-wJ zt59OFy}CEi-LLLz==RioHQch=t5z3JMuHN9lEEx~&@*P^Yv!2kty4CJ`xo2#Wk_#B zlNxCf2w|KWQ)?DZjg(RH04Y+81cE6h)<~&`H(lY5VQ&ESDi}b$9tKdai~+o9jhq^5 zBGp0J+tJM*xaZQ%A1(>Rjb9?oFOlRDAqYknb{Il3GlbvR@`5=kzII6HR%^$lcn28C zpky|6doXsQ@megB4d4+zR&)@LvI;;B`n2v6z=x#S zH5idpkcb3mM1nMch_G|SP$X~q3-kj|#>VH-xO}_3P~8c$rPxmDmuovYckCh_N)2HL zEmnw|$~Cn?ycVafP?7+=S4^rD`}#y1thu~zCMFe>>*G9hYP>!!7yfPUdX($^po_95 z+`;RhMd2#|5`$32p2gS3vzY;CCAEXnzf)t!OV^3SFFk;>LII}~W95ksu@1W~kQX>9XyLjHN3yW z)s7Pb&6IcZWJGfym2nS{Qx0DxL7J}YvW!eBr zHm@Jqa>`n%0_1%K{ir>c&kau3sbTQln%lrZm5UK>a+6sDN8m>dhM;_Il*iM(Aa2)C z2-I06q}p|LxEw}Trp^xwgmiw;pgpixWdYz?P`*iQwzW6so00LqBDhW+`DU5QJu=`3 z4i<#TZ0~cdMoYd0_gq_IL-7UdJPHeQl{aEhIf!}qFmPzTH5OH#v1%*N&9}+0tdCnA zws_FAx-cvcaXK*02JeThmNpoXY18qAuk`_R_(J7%okOdRu%ZbEM|?S7zsvD1DCHy_nN%T6%RVl6_uPTiV!o`M`; zr|?E*)Q*FX_Cmhhlm;ARM)7Uz5!>_QOWHds9<%8%hmF&;w@fqMLggHW7W`GHvskLk z7c?lJCvU>{`z-53>w(2nM0ShwlPQONO>MH+*6-|v%}x_zSk{?N#|2K0+H>i4zJ0G_ z`nGiAq<8~>`Q%nz z0*i*8CA*b-7$dZ>e%1M#scTeuTOV z3F}6r8;GsH1HDl6l9g76ccId<1B|LxI`JDLhE58(6d%M%X#%t#lR*2~%@l_c<=s2_ z(*^%|fa=5DEYm}UN=0)0uf|*pVqsTSr~b+V661qAHk1o_mLvAVVS_eri-Jn!Vn*Rq z3+@5vpqkgB-Z=`!a;m82`nEFQ z*Q{2z5->KHP^()>yaW+5v<0ae=g7w}N5Z@aA}MPkQ?A-B5Pc>&vfXwS^x>2@t%vF! zEVZ^fR&7FY54|_)wSe`i?hH;X7y#kS)}|=DTL8%bregj&ayQuVuV(Y$&3S-8E{6Dv zxhIzPq`Y9^r1?43B`iU6q9HPo9ncIJWCzR>vIFKx*}+o89PCgGAV?N4)XD(`y?H}< zGTDKV7tFPmpQ=1sC?i-T?Z!+fBVgXV{_XDq2kv!4Mj&-K37CW)K{b*QOp-q3Y7}K~ z!JvEJMGYt;_%^=M;xSS8Ac`gvFW*JBT3q`f6JXrxz3jP|EMR;39q92tLcrjx%9T@L zPg)@1tJyK*$F_enqdtrt>s0@TIdzvhZOr}<%W3uk#I%y|#8sD94gneLQs*0-=!H4K z8o&M{906P0pCZHRt{{(hF_rUS_>-}n>fJ0T#QzucP$VOcAKKO3kRVuyh<0^%Iv0s^ z4?2hG40p^6;1NCj>a}1by*ruD$(Yr62vXe+)pE#KKrH@ChFopen5W|}Lq>LunL;8i zZswZw_o7?c>MZoVVQFio@b^Z5Du=@4&0d;K%HJo-Cgm>|H}UZvMp@{9%rfQg3wSf^ z!&aMtjp~uoK#pc1`3~Reih6{!fL&u2+6+Q*p;MjWJ0KJk-w_{nFn;~1$T{-l>A{{# zLg?`~gEF*7 zwEp+V^$)tv1fA1dN(V9FPTvU=?t>@~Yt*mMAk5+!Vp>1LfgXy!8~GbV^L|U*P(ecd zKQU(Ld5NCgO~}jezzP!wgrqkTDaRj2dYn<8k8&$NXW_a)I{xnw5bVsdqPWc7zXLx= z@aV(@8W)8#O@xVf1QV1jxg9mOd@h>Ce;WDgrkeX0wnUUr$^gMh?_ELfZ{ePf3H~!m z$1;Wca9Qv&{q9(1$V&7M7W>B1aBOkUf&G{2e&|ZQ>lxy0;8&hw(s$tT+zH>5-@1_BvXDxwX2wZ^RC#8F zW*xg5a*+q$JCEa z8MATx132PfTw9}8-vCG9KR6-HME`Flg#Uhm|JM`zkE$OdG1!F&_do7<$Acz=Gb%@e zcCAyq!_n6(AAnoAAm~qk4+G^+M6BvfNM28Pzw z5riU<9l2~4-c39PePTzM*H|BPfzyzYb^*u#2ZrnD6kHEOSIkwu%n+>)&QqnIv0?l_ zqEhH1c8xhmb9=u*hiI}>X>SW#oBey6r!NWd*2w&ivb8jwEz6zE|U=eI*x%L*l`FCb#|F;+wgC=o8+4Rys)|jyCNB91b+R!4n}{y?f?|i0J)ZsqvCNk0pf37 z1~BFt$>Km*Cbnz4As5I05<)Rb;-H%pbeu!ekrGQ*NB_pHqu#6_l*f4ft?*Wt0kP`8 zS$`N1{wg2=!*r~Iezx^t^dbJkdVTX!%ny#g7`^_!U@u0i@wYfMy^>r4p5rX~yhCt8 z6K6OyL9jRDhl%%zdzRB<*9i0SDB@z=2%Qq_3ol5^kG~ku{yXgs#h|EVJDO!8kj#?z z1(FXm5bnXoPm+^_M2TM(&o3kM%gFpP|Nd9(;>dD`mBe2Xe`)-cGo`lvQfq&yslT!t zs@H05DWtGluJ7h_ydWz3+4=Zlo1PbCYN^8d{gC$G+XS_lo;ANhd3?l_m0EPO^1 zmCB{vH>qoWX^&0h50e(fez+P3)7%iQ9F00u(@;}bmniKvJS=hTjam6R8)5Qrm7u;T zSNRHKV$I5~Ye{svovTybC#2l819D4suTFO|w_^XB zWhC(A#%9SdGijEESeC^oOJtv(u{wjy3*=FX-ZXX__Tghe2mXVhxdYK0c9(XsgTx)Y zgP-auL=Fz7vlzTnAW*-_28Yw>HREC&hV%jSBqSIcF54?0O&`v@m}$5UZ-g9VXU;br z!Jz=tz?ogGXH|PQfCDpNzRYt=gTr{WJ)0_JhmxsMvlum-N*ywmBg3pWTbTcFdWnrZ zD;<~EXH>63t29FlQh?TkWVPfr)GJdO^l{%N+qd|e@G+~jTgu`Z1|+*sr~zPzfL*?b zPypa=-sM`j@f||(%|k~p`k@)=+G1mM3hM4(hb)VW{>!eB2!019K+Lsyx3tv%975Qq zei`)?jRcFj@4)n%DfkEDgG`6oSw>67y;l(g>gB0Xu_2a;iIv`UF$C){_y)#0qD5Z= z9#b)65UqlL2uc!C+m+XVj$I?!y^jFH;4rw*obSRX&B0DTzek!cV`{r4<$dtQvQ9*` zEiMpisCECy#UH}d#D!rR7X}7khzbVLf-wA)_PHHFU)h`0YXE}u487!*GDFX~rKX{$ z+){4n4|?09VbmF(E@@FjG$MR@(mNepu#hbmaFq&G!##&Zt#~f27t_az6sD2nqNiD| zYrP+H80R6o#$E|~);0F#dT*_QTKQcyPchs{eXkp=;B}m06G7ZWK^VuXM&$NCUtUK%5@H=Uhs9PZuorD(r z)ksyItK`jieP(L2E_{0tva z17N*$jJeiN(L76CE8i{iZx+(#oLd-Mff%XI6H0rm+_))W7Gf^KJ@`8qe~ZttA3P`&c&<>`B1OE6D)@5H><>sn2Y z;-;H_1RUr|%pPW?kAzRw&R%%l0}n54Wc?Lz89ClvaUCEd$D3pAJW`kAEl2sgpnS2M z%6uJv1nK)QpNJ2Fy`$jAqUaxuZyCbFWAKGEM1o!aSonYT^|K)Um8Ez@fmLKB0ALj2>1Q9Rg{N{4IJT}WTyx`5G=g`04)u8Z)8vj(xh!!Q3B zJ{5o6_$x0Kn@QR7!D4$VTV5{qrtqS-*qF+ecNDu<+47*+waS)z#kNznJhQSRdQvN_ zd(Uj-fQ3ucg?H=(_JTc8qPGBzYF9pqB(6X;XcZ`^LB(Jd;-Y&U&rcyZ4j)Vw1p%Wn zJPY3$71~v3#JB_Rvuo^rj(;}_uxm^R#X&4j@Gv%ct{%EDowVM<4|hIhwUSzWi&& zAKRfk&#rtHwGUP!n{uyRxh?W^+Lb>>o}68IBlOr~CnB<4W0J9x_#^bJ6HWS~_LV8x zWvA-!OBMVCcr$N4sV=enmFPdsq%l^ncBkYIoYc2@)JpTID; z+>VEnPNXvhja(QeW`+Qr1km7eDrRh+UIurN((i*BxypTOi(SR-h0tGS--5@zFPx0y^{QQtL{*ahQXwW2@a-k6 zM|9@X4)#B)gRy2x&jnLj&?IP=rBhD*oVuZeH$y2s&|!N+EVe`1DxC4m`o*z&JJKp2 z$Gi&*cL5WIHL_k2SJz0{$SE$1{t&O0!n_`qBFI;AyF#2(A1IQSPZq+^VU;izGTX)o z6RY^x$v{Pil2$|AWABZ?vB)xR{5#O|DHpC3=2~Y=>|2~axQFe;cLwJbZv-mt7+J(2 zw&al>#~}{?Ra#Mt^_>1o%Vat4y{E$iRyc&suIG~OHDkQ5XD6)E?2 zdl1HYC`?eF0~)O7VgiJy1BLS}Yd6jTqd81FIM1XRvg$BmQIu>pWosD!0=sq^zPKM$ z2x!$JKj<}OwKT2^8gdi?xdB}HVfX6>a0EMfeMCAZbCj+qJSiMKn84$mLVZ z6NtfiHiy5N_}d$Q<>gv~rt*I3n6+BJtD4F?s*7j{bxOoQTr6B1h}8-!;PIM-e=jCI>4NQv)N-7^%A{Y!w}iiW_=5(k81LBw-~I5%!)11e zmH2xv{(Ss>5P#?6?+fPdYR)KS zKQQpu%=cI3Ynkw)jDNoIf6e&6Y`%*P{9yB4X1))a@CC;I4f=7;T)lyg%DtczAN-g% z2}{87%NGM-PY->Pd$FZR(`S22@afHBKlCC5pR#L^j_`8~q;S`8oV=9OTip{;ZT|@T z%Bx5mf}ky#uyAfzCUZk`Y;Is0a=8U>FS@P`c~?1?nzkFp01gYY>w@=VY}pAkaBw?&R@`#ljkIe`d(ClhDM`f7?oxoOb%col!)X)b7DRwSqHPuwCOvX3fASpwt$8 zfZsw;!KdHGGY*BMmiV8l5XKANe7^u)W?Oj0QA%p*HR7=0RBpVb%#EGF`J$6Ryw%jj7cq@eGqEd6zm*Xoy{EYxK`*^cqyyu8#G4;5q{O z$*hTamvn*f-8&FX4-SD|CzmR=jv5>EfBYQyH=UpT2$h@X2De3 zHORCiVqyPgtxL9BGzp+29ibIPwB62naJ^P7xOf&;VX|bH-czeqWy6B;GDFm>ARFJ0 zo%nG>x<3GYZN0D{X&$XN^yk<~W$zj0@GW!-cF;AFkLD7^Wukh!2~eVJuM z&3qWriUWWVWSKH9B=eC4+j0GsaUp=DhxwiTkqMJ4%n*sE$TlW$B|*Yn(Pk&zkv1Hh zvXJsn7|T>mV`g^7!p?NY-eb;Go26t z+xg{$cnw7!1>e%h)}u+xNptb~~%gIU8sA*51gg2djg1 z@fhXHb!V>bZ0fPB^_E=LTauZzx>P!$=Z$xqH)UmeGE-cgPacm`0qo##j}QD<2CCAR z9e-^eJ&w}+d}7fbahGp3Z)Qq{i+)&aPnF*Rb?VEwZ-VJVuqU%tyVzs3>CC(Gvg2uX zr5=LAuzE-qPp;)N*Wwk~eD>Nrnx-89&G}|`{aK(_YJGs;^!hQpP;B0te zYj{HT%rZCFHHM*x`?2R*x|^R7MhOA92MV9>`|dW6a-4Y@C?>vN0A9QPc_( zG%L*cvg0L3C(#)OnIgj?cGA3P4$@kQzE(%Qf86%=VDE9N-@zwXk0L_%hhy$@V(yQ? zjrlfA_uo;vL{;39E~#!whsUiXKoWLW&c;GZ{kSPs8|urwneFX{OtRz2zK-lT`jGAg z#GY{y?C6xAIo%gGfg|e0ARvaDY3Pc@Z0_x}Yb;}PFRp1Y|IPl9$cp2MVE?D_SOdy*-g+H&!oe?-3-d?(#3T60I{mJY7P!_2r1ey} z3j+A_$(M+ViyrZ1q5rxVF(9|y7szsH6NJ`Mnld&me$c;63UI5uTm-h0*{i5WuQe*Y+;k8|D}M7{Qv zdNF%ZO-E}Np`TtHQ8uJTlw>qf$%*?2(p~VxSCB#;%nRpbl;g+t&Sq;5s|_#+i_qa* zi*6ZvUWm=y)(7p)Xou~l7LiU1`hCDoZ`nuhK!aANRJ7lMPMN{ z-tJgo;%KahN5%?uEZ?+c>*Whh7fync!2Yi|F>!>Gcm$_g!VaZ#=tYPX&Y>5_+#85a zJ+4fN64y+R64%U|5;sKiZOt>7S(s@wzcJUy!)(>F+3AX=RK}Mnm2qWCWjr&JPN3-} z2$|T<0yD^>czU&vx7+5DQaUvT-Ert|7*d}=Zs4nS;@I2B!qLwOqtX%mv1@mSU`mnb z8t`t84#?EVmLQ`WOk(*epL;Gwft)KhGDkNerp~|Ea_Els3jpDWe6E6<(o__9BN{Ar zjp3=Z(Qa{vA2wP+kj`idN@vss>5Mv7o!;||gH6*>!A%A;mdomiAB1G4-cgWRa=8!> z)qwiHXpXje?|FS5BHRGZI`|@@D#<_tdQ`t?cbo|rzfc2HAIa)Kc(&Uf82TbOuuC0U z$(xZ5$@$UbSJ$KW9tlkijqcP*9xyIjLefCAlU)nh-SmExi{oDbO1Uy8>AQWp-gDY) z#3H1QA@*y2^%;KP^B96Zq5eoC$D(?nN19k^aP-oOs zpsl376t$$oKNj&%@!v^g4CE$2l!+Id)h3SM48tL_z@!*7z~?5c*Aa*R!pf1v^~QrxM8U6Y;#QWm`me+QZ`{pI8A;>8^3*pSPb z=aYZm96X8i{R+wICx<<~Mszh!Z!r~*T+c_2B1{&Qk8n&2Ujkl6?Y47I74Lk|Ac6BND1;ClH|bt2+LN2*FblEptS@>RbKsXm;Fo#uD^=iKtkY4?{n5@7 z_QR4;`wu!3Q->XkIcNZ0Z<&ffW|h8LCKJ(XR0t4wqM_#%<2?_Ipy_)D92uVXC#+Y2 z%K=;Uxu=l-TUuv=rx~-x_+1O8nIyB!%Csu@V?CeX8HveXh<3nL&vV%iQYasDpd1VE zOn4{bKNMfcv8*yqkg5-(3k1(1CG;h#C&fKi#XW1|o*?eIB=W@C|A(f{y923i!h010 zAze6+)_OULbW+Pk6EY&3lC|KM^#jXNBNfb&>-~jdC4>pSm~U)p@2%$>%`IwgW^t;2 zC=d+OB0vFozbnPL0)qXeCb$nX9NK4ZP< zE;_Ngw#4I%HS(X&q0o^3d>(FES}}Wk;W;JT@SGA3?t*BDhW#H%HtbL`tK0Dhk9_f_}_ zUfwDyGm=yjqyAZ=Mp@yZlQ6261^*Si0x#Aj;(itI*13o@g=BLz3d7Olq7Gg;c$5>@ zUgS9v$u=8eT1Q16Qa3|$shi>RrEbZDl%g@EhE8H4ic%XVIT0+FL)u!$vJxXntRDU) zK$CoAL^^t+8q*WinFE96@yi1E6$*qKlDE?z;<6i-E&YMz%0`1_itL8fHa$e?e53Rb z^pR=7zYvDc7Jf`m)>Zc(gqRiV4Z3TDlujCYr@97CF%sb5pMZnK;|DS%em0ycC*Gka zJowgxUBfUjsAaJ5es$yVhQ(;w@Ds)$Ui}*;#j_A7iUF|$<33qkit#wm#3pk6Kbtga z*CbJ4lSWWP8?h|5l@x{sWln0ms#1cMCZj>$WQDrAPnfZS2V85ILaIQz+t#R|JE+C{ zbSi^~nW{$vK;^Q!a3fpGlBG^zUBklwvP4bO@09ja5m4z6^F1OI39)b!3uP0B@v!Zc z#~Ou5--a55Q+*IaF0iMI=B-oxRp<*_B7qRtU&OBnIEHA9k)AEc59(8RG(*R z7j9_rYP>)~YJ?5Ugq=lv{g)7{bkZB`%sL{{OdJC0Nkfoge$6=h z)EyPQpaVZDvvYJK>i{70iEZuQSXW1?hi<@(c~B=c30MuBI2Xi$CMPmDVbgmd%-(KE z29oR3A!8cP2q}a%;Kns&GM>`al;{U6l>LE`IVCV>?Lh!CB?C!+ezg)@J9#EL+1kmN zx|Z=+1dAi{4LcFtlQdHoq7e;^29MC4LIFhw7CH?R1sctY_A_u>0zG$5Ka%0!JhLOA)mKM1BUV z6mao8qy|=6AL@NFt%9?ZRSvsjr$D2mU*wj8SbARWuL64+a8#oOjl zaaiBOZ`Tl%?CI!SCa(zU6daD_sf?%NdQBv3SyPv`tck=er>_ddEzmF`57B{7n38*M z`X_+7SDOUPz1k#T?$ss%bFVfDn0vLY!Q88D1;t)XU}pZBR8XJi#ir!Smc zSt9hIcT2$o#y*s`&HYK))Z_G;=woZcXWD}4tV;WWtThdlRitC0-~$e>8-fWmSp;{t0Z2NKguZ*EBi z(ob5_(vWsbM(}NM2XLr1hsa?0V}ATHLw-3#xn{D>bBlpDcPOL0vYV>^H&(+iORmx*`IDj<}~Y?&+#~{3k$jTtyKs z%Y1Ba1|l?Zl(xb4*wwHO41d_k(;H45e%6(Asgce#Bi{Qll$sUboenoHm^5G_tGDKl zyCj__8U*2PtGj2_ohCfxKgsni6RHQf850ny#7RdYN!cj;N?%7~QZ5tV`wpQeyA3ZC z@LLy2hcr_K22I;kXf6+@Ggm{s5wiw)2|VkPtOV$$lbAfJ{tf7@%M@2MXF!rgjnAkW z?Fa1JDG-4qB)d%C{NPKw`@S!f7s`A{)Y*^tWt+=eAN!&|91Q#?%9Yriln$n+;A)z# zd&9>~Z%T!#F=-KbYEKa+2i#DsKdEp6F!n~3Oob_Re5%Nlgf-&Paz%^j9zGyNa19>xJl z^$lJWS%jWfZ3VgdFI9;$4kl(GH?aFN`GI*+elRVO@ng4PD8CHmm*G5bVR)?112@h! z(ET<#+6)ILwgR&_eVW2Mg+A)9{uXp%Z)Vf9Ekah0-ocBczIfCr#ZqSyl%+Rs=n9-ngQ;r zU^_-q9JQjfP@{Oa<0$N?722Kf&q>TDd|VM^q@FhbCMR`O|af2ZBM9hphg*;8s~;OxbO?5Zq+NFW#|Ka z?OK$?sFdf*?=Cg@57L2PxMOj@Jdj7vUK)JDqP`)-cTj)b}eYiyPZSR;2PQ& z4`B12?*M+=hC0go+nMfsqVi?r?hT<&H<$OcGqV7Bv<{iCah|V3W-4UfUQsIO@x!C| zqOg(WeP~VILgFrGXZ#(3zfa?j+7Z9OAGJUDU5UT18aMs?gY`4{hW<~&&v^U`VQ0fl zDaUE}`#An6Y1$Ki-@{)k{(g!-#{FOO4bw4?#Sh>opM*7}uYhCi7uRUyx;S!uJ#yU~ zxz3DS>mt|3BiE0`g+(8EUl&0)My^ef>vNIody(t1$aO>H%5tKS{1TDtTT$q_k!!uU zv}D=7UYbk{6)TsZRaz32>)BE}VoYcYVf*Mk%~NO!rfal|oDK=*H-s}01})DI&p<)*q1<{#HIB70<> z8S2Vk#@jw-?h_jI<_hy)v^A&T-6#h)o0N4e3Xh1Rw)-||0Oo&2h|T{v5x`U;z<0Av zX1x{rR$Mb|x%YV(=Qb(KsUC#BlCAn+_qgdx`Defd3kZPr=48|N0RV!-i6ACRJp)xQ z+Cjj~*O4&(d$_eXBBJSqr$=Mzo|b{6VX#2FGg+H&^WN;D-Of#g^! zkK`vAyoV^DOhWk;zpjHfqwVMNyikB<9b~fqpVmRFZp1QL>WC_sEPkS!&>@fU?$|oE zEVhm@16Q))%EhFv{DvzR)6Yh|50`a{v0*81OJOf=zreeI5nviwHjj@jb1MMEe#x<( zL)s%GEkSQ{jf;|Oy~f$KC{R|ox=&WP$SW&cR%%Ak zsikg2?-;-^5}>{!TTXiYf5IjeOb6EJf%z^qD9jlCT@2X?w@(@3-$~!@#)tE(U(yeJ zB@?JG%X7ZRVdNRm6oOd&2;_)CS-v|aX&kxQSuaV)AoT>mnqv$yMC?kV4>EyXzNBSZ z<*(G1+mUTy4Kqy#ZomsgYjlH$uS<^96!EwlxHQ?7jn)6@i`4T-k z&wT*+tdKH}M;Tc+x^S04KmmVbH_Ah{w;I9y`YCu{;GQA$d@=61fu72}z|8iTV~*Fx zS~=S4!j6vvz78s=)4Y?Ism!+rqZt0vS&k(-sM?z@fbxmO|QH6rtk;80_A zbb`c9b+}_KM5fHcAFEeB42UvCK$QB8$F7kKMejy5%A^4|O$;b{*GHf#)VQ;hym1bt z^o{=v4RPg@1!FylaU zqg$r(!u(h4+DY_Xhzp-E{`*DP1Mf{0I%W9F=RMB{m@p?laoeI zN_310mIo!li3n}05U><$mu9t`vV>h4 zY2h7#^q{}ctHzq3bI1qk=m#RKx&h7LS3y(uh@y8E3%VKx47EM;%h8@X&?OHiQN1)u zz%moE8ydmBQ3!=C?qy0)L+|B6 zZ$o2m3~0;4UmEIPglxvALE?7}aDzp2j=yy}>g`$`A1y#`Je6UDUpk;R=Oo#qL5Z?| zCNRduqn)Ovybsce8EfMD-yjg`*|3$L>1QsV!TMD*qO;?N8 zTD-Rx>V+i`iY26~jBw0M?A-$`sY+&FV{{=`Zc0_=MLx9L*Sjz*PeDAp#$W$S7^X73 z+tDovDY|&rfv@^0uKH547_T2Z!IZFr?Ubg*Ggz~tbIpq4niZwOnibXMoZyvs{4I)1 z32o(+GFzp=)p$gy?h_^{?_i3U$s&@vSZtM7EAwgyjH<%NC(I3ncIIP1L?JWU&bH0VoSPNB4o!+}Z^LCtAHFxA& zt2arsw%{~IYvU6WEC<=^eT;mwESPu?p%u?6yc;cmH>IU}@s)!;Pej+ozc1nsYJJvs z@yEri1Jf)%BCpKjy9mAz4pEa;fPZg%NAMLxA8_;EIq+YJzb=f$Fg_-uM2B}adNiNK zEUZbW8)H`Aew-sb7;CwN!VnmYfOB9^{g&EG9c!c|<>sLR0QdMi0gs#$p$&N^;+;K;1`a9SUz(-b5 z-b$3`RL@0j?@ClZ%1^R-9zRf+4L3R54dUs9taE6}KObS`E_dx7fHgUXrv1HuR&^of zBMC|zs)+g@L%bwBpF*(zad;G2?;7|AuLnN~b5KdUl&3Vj0Q}k)Bg zNj+O_$#|beD)N*+}Z zAK@n%1pMGkV@$ru;KTBDgLCAY3eJ^pI{2u3Gr@WM)|jC=;w^-zRu5t8T!`|BZ<9!Z z1&4_df$McKTuXv!Sa(!C1uY)3cv`QPocCEngC^_4VcfS_FW1y7W$KkS^~#ufWwl;7 zYCd~n{aV~YNaE^YQ4Jwi#U)XROUiiH6Ge=+I1smg2t8^7H0j+)FxG=avQwjAJ;+#J zLfuvg567nQFGnQ*pZLKT{W;2oO~+2$qGiw(4EhCNuo8Cyd}Q$CQW(5Z299R) z5JGj_Xv;|s8rG4jzKHDXnsk5~n;+5=Y5f}8x==lkiP@~*Sfrw|WvnAIv|(XOO4YvuMkD`sq52o_^Mnca<=8awt{L0?Lfb zy~Bs_UCp`hBG4TCAS3t}!;NJcbj|jq{0;Cmp^7WnG?&0r-qipVR{ay2v^kTJ{*&=v zg)dY44D-1Ub8z(``~>vkaXF^edZ1RShXZ4&l7rb?)*N0zmxID8wJae@>cU5g!}u(P59^GcEy0!eE1gcgdd%U z%R5?_pjcFOGlwbJmhstrt4Ve72cab$h1P#NKe-U0p@em`rbpxj#a8^)eLPo%2i(7$)#zz=E4%Fn zmg`OBzLG*{+&%OIydiZ3t8sswY_u1c zsSQC9lH+B?L|;caIk7$m8ZO_v4M=!DB{9~7eLA=d9SGq+!mJlu&aN$c$75N0mQe_< z1kmxfAQf$xFuMO^L^jT!!0CF2VJQr*K+t$IR1-kwlrB%$a1vT@Y>rw5PYd;~$>9SA9JNAsss z?HZ8<`wJVG0)}vaI zPcX{ncAZdM(X|>+(q&2>)WIQs2U9t6UsFn!!e+aWDX76Ki1Es~Og@p`ghRX)+>fUg zz056_tc4%K_kAW$SV0-tm`j0Y8EMWGN8~wy0t9o=NXe&0U|I;0xg?ChrZyg*Pp@2s z9X;BNTFUOCNApQ{rht+q&t+T&!~9ugI{K}3BMnb@Ll#wH=AA;Y4x+q`$8m9I4vo1B}RRFPw%o)2b&sJfXB5vl14c_OM1JGdAaL`=e}=R*=3@EpR3 z**Kxlo6mNLs4dWx{Doq79-T{WY0&}2rj=|CQEZAGZSd6y#+P#rNMMpUIzt;ZL`NS$ z4xJ+#^U3>$+ti2dl6UVDi{1I;U&jdtW8M08fTUkTyY0nXJQV-)sY5~3qfk$Ht=lrx^eRONdpxSXu` zAF|KLI@+|;_n(4yJMTq+w~C5{lFk5T#6zlZH>QZL1Qy&tjKO{?cpJ(z4PIf+Jp+Hfj04tXpsp2#W9FXvMb zz$7JWEc2@{O{oCJ$njGmpEUc#qBVQ5)k*q7W6#?2Qfqoh>voi=bMQ^mIS1?XD~I1Q z$A1(ld>MbyE{2_x9`BE+X>cQ4>NjNRSF*rDj?lv&MZUoejAa1F67U6585wDE{-VAk zUJZxl64@OkZ3q4zZ(jl*S5^H#d2i;uS&}r9q%%p{bW%#vM>GYv{ZLKQ(l0Sh8h3$iFGiYzXOih>BZAy5<*r0i~hsJQ&x;Q#%ebKf#a+JgG| z_tVU~>$&Id=bm%!xkXE%DX1u?Guj+P)8U_rb`-M}3fLL#2%_omH%yLhLnzjX(X{^o zL^Vu&V;iVQ=v&M4C+0?;j)^-WPs2n5-X+&$c(0_3t*PZrd^4Wv>!F9K?g~@k@}gAR z87-iAuG)Ql;GGczZ_z>w1g@?vFv#s6xhJY~Q)k#tCL{Wa{Ob?nw0}DLMCje|YuUEK zP8+{YoMfquUoTFw)y6+Xr|f5VQxM~+uc>k?Cq?wYY;yENkc@)g1~UW?L-^tP8}VEH z1u)AlejXJ_jt(OMTC6wiuPe*?cQrm4|C9224Sunt;2Tsgga}3`$Ejz_?wy=+^-g>HA_nEjCrE>jc>I zX!hjjNrb?QIRg_d0HFI`HlN973x1I~IRuIj9vzI~ZE{lKDmnU5=7eCTZ~x-N`BB-B z#uQj~E?-~t-F`s03jNfggRGdZ7eIKW`(n&8d@jMKdL|JnADtY%f-w0+_mYVZh1kr< zH|A#){Pqx5(*%biZm=kO)`fN+yo(5?xLsA&K-XD;M=T9qf5R>8G8y~w^xVh?J;l75S)cB`mSQ8!W zHH1xF7XJHBV9Sa&T>-W%QM%08V_e5{F|)@wmvus;OrEWIGTfH?)*GIemK`hK9^;Iz zuIJR1xowYeUROWC0GZ+T7-x5NLpaavG0t)SDg;!oW+=U|_R(cYgho65CmBz;!W|dO zAF-T+8|^W+DK3wwLs5`seqF7=KH$ln|NfIUnAH4IcQDAM%3u)7iiW#Lx}3%O{eLm0 zq>$89*cWJZ{-gTT^eMNvU=)QT2MryKsBmC!sp6!iWhW*L5_%q{2gf(g48Do2E`Mjt zjZoK4`J4OOq@_a$?F%9J72iVuv8rqBp>;651=X&9B5PkShERu9oVXfbosW8dMHpVA zS~Y07;N0LbJBfgr>MzT9(E}o4CIEXF3DUg?DAfcMR`Gnod(qS^KLh*M%vIT$t8zFi zqlilPui4T4vyvM{|BV&}k8dEriWRfAaYM*jGPzOeA)MzU|h*qWv8JH3h6EaljC+|Kv z>01^N)pgo8qh*i_{q2_b4|WP3HINP?s3K05lw}2jf?=dxIlz=(@g$&EBKnO9*oHJ> zQ|65{wPJGwQh}A1xCnoz;E(?E@V6TK=;F0-m@>)47NR!`&IPGmN9f$@L)ZXYQcqMT zr6g$v-ub(b&=nHR_1`J(RrsxLAi3~){2=E}lXA`^eP&hMtLjcqd#|C|NHcR%npP4~ zBn(JEwYWm58iiCdzP|lRbCK+PB=ew3;>fnYuCxSJL_f#PZ%_2|n!H;*8&nqv zfjqUmsLWq=_ePErJEU|f7|3=S0o|zvs2myv=o3?b>KSq>&^-avs`P)L4h+4Zn!KqM zZ*G#REx3cL)yxU03XKlFLfqdd^osHIy|p;eH3PH_(nfV9nW1%USQ^$fr**9dnK4~m zNw)|+6F@$oII;T_$WZz)koTGb8E-l?Oapn-5rKM<3}G2gB+zl3z8bxg5kfuVUj=uy zn}p$@46IF^l~ERP0S3Yv%6b|r_rsrWG>6R;bV{}d&gS0_Fi`k6`K-yX2k;l2HT8X_ zD&~hF!xw@KPfF`${kLK`I9?qFjQR zOTsgGu53y5Y1v`yA@ZLoQ!o{Oy0+pUt!>+RI|TP#?8FL#KbxQ!R`Y*Z8&68-czIp* zxgC}K59`A+4n}>>LYrAaHsWUCZdOFPf+Ht1>F3Z=_2tI2eL<~|7{W-yP7eh1@OU+SOJ2;RfPD!T@_ zE9;jmMOl%^{7v%JL{d_ivWAdiup+VZuTY3$VTMP&4iCq_8_>;|criXE3iXRWDVd^y zDr*9?H8WW92pbQF#fI@}&M@36$D*`7Xq)ODoXv_E2zrviY5((R&Aw(J6?r8|&A=x~ z1$MkaFzJ6M!tjMShLb19l0!fQfrupO|9Wf5bqm_)7Sz$(%$s~x;+aJIKeZ2GNx!ab z_qr?^LikFn@Et?@7jR|RP@8Wl&(emKr%wGZ0C}GywA2NK9cX**irSJi_HRPW;7`hZ zis0nbo~CXz9cEIyluXK6lk}Q73@{*!OU`akT1uSFu`Ck&Y%3R+Zrh3ViY_hM>T%3`?NH2C&lF*KF98Axf#Y5x{+>l%40+>N^FO8$ z#Vd^{F5<-XU@}hq)8I7IjW=S_zYPein9`Pm_XaC8>F7fNdxG`#%T2_!Ow}>b82cLiKdLLX%HfqHO&sH zpv&5?YnCHc7qvxa6KxsX$8qvmtS z2BxDnsw_^sf8y2GUQ^D_PMr%`6nq%Camek^l5v3u)uy{u>EZPb!?S+;O z)FPGd&~Y&meuAS*PQ+wJ3jnK6prdxc0g;Z{0dG~AKiAZq zfWrv-O762X@ji=Ks#qSs)iU%Oz16ags!>Drdaq?2g?P#uV1@kefy|(_RIeMeR?L)v z4oW4?z{GnW>!=YB0C{tf02JyvRHXWOG}{^^$M%cm<^%!~r7u?ma19x*H4GIYT^~e< zk+>Bi>PGm(>lb7@NR!p8z%W(010@94P7)b;(4k$$tSyu#KvHoLRlr)@G^EQopYvLV z2x=iW3Q35QAvl=TcpB}9xSlu%>|P1Q$ZB|IV}{3KRF6HE9h0?LP) zu3OHo`w6*l*eU&Q!jTbn2P8YcV`y{r))>Su(v2R3h}OKTE76d_c;2wH{TGfg9{T)P;;V=6LtL>f1q=LcRTk$ zgHTQ_`U>`<8CJ3#&h`;s24b%*O_)u#Ik2uMX-4E%uQ;htTT?~~ilUu|OQu%<2MCvL z#0o$em7BO0Wxb#COQS>`SfXuF?vj1zGBl?39 z=L4sjGrYe5ZD-E#tRFZ- ziStpvTBTkEWNKTlwY7ac{!1-!AcvJYLvYI4D?GN2MBAX&k$NY*>->SR?W*^)?ey*3 zavx^J_u%iA2DG&qamCch*#7qsuBQ^KSTFX`9RCLh8~UN!Ki}4vwlvhHMld+^pyFU5 z{vnb;57HJzaH9aO@{yB(#|J|^{2w8qmYA(R05^`NvHCWY=VYXJxxWft&QKCoBGS+r z5zra!=f^f>GpvOOHPSqkxT^mk99_mHS927to_#38tl(~EVudn=0NXjsxLn5T&g znO6qBUe1BG=9L&CImfNOh%YITLc!Ysoy&2XO4MT)_z?2gelNl-`St&g(|{uj9N_ik*0eN11M0^C?mvhWJu1+_bLKH=j?85oqGEaX*1T#uQ*xJqf#_n) zMH}W*Fbvy?5bY<^#{NSrA!=k()|caDIsU^)gkx0^QmBn|x~`3*5Wx#D1V4h{{-;2^ zkh=r-BZ+PQruX5Z%VD3#bb^5V3;4t|u@|F2yv%1; zzlFB(?&i-<3?d(l)c%L1uOR=)TGgKbp>Y9I{VAOUW&4l9nJpjT_&L&}Ikh^K^MYbu9q6CV&%RdJMJSszv{|mUyGW4lQ6I9or zd5IP0YJIU*FpvKjvOwJB?TZR{ALGKIBWX8Q2+m}|6X&I+SF2Y-tFGG`?;1!XyY2C9 z`ALp%!%u3wgb&w&V9mTp+JI2~J~V`^nPoU^=`Lu-yur1NZTnWhznE~P?sjv+e;nA$ z%jSZ=l$0yCVo0E*`m~nSmrOTo0%0pbu2F$yBMxa=J-z`9hUKP zG0Fog1dOOYjc`S*`V5@8ZY=80!r?mpbNp0yM4imqc=%S}W2MU;vQ_y0^MKm}_`i~` z7Jy|yb(Y6(0#;ZCzKD;G@pNJW6xF5Dmy$*!VI2o^lTX{U?bs_%+du-9pjmh>{|Dgj z665F9Yl`g`=ixAvdO${0u835{~ACSs)x)9Q-RYT6d*Kg1fUE?AdfktE18>0 zbu;nAy#F_rAlg}%v6%7^Ce_=SiV9~)<@gP(LM+ulWgYuN7Bo{IIqeKE-L(QJu0xd& z!(RHLxg!SC#C%r^u2w?JjOWUCU@(Az29-1K_uwJ5xT*fV8`FJVIFYi$L=KT^Z3Pwh zS3sA(TzoU?IyJryb<}uvlVTUWnA!H5s60pYi$kml=n}}IZ5@DiR=3jHwo89y`-A`|Bh^c>&%$} zt_LLMoE+#>XQ%XU^-?sLj;{hQjN@YUTeN?(Vw;ZjHe)swORGs6>oqabaKGFIU)3k# zVj+D7eWHQ+utYk~Ddk7@aR>~5fksi>ty)DOr6ex}DY6H*Avw?1cs-6V|II}W{CS+D zi?1f3UxW-2@qdV*M{<*@3q2&%!(@Lq$t2G|>4A`zdBWrg);<7a(Sz~mCh*Ea;-lQ7 zqT;ZxhB)P5x)xS?&;pYF9|!`?Deiy4ExPF)yI+NxI>Mq%XY}QT6Hq#t zn39s1%M$o%9k&e<|NUuYIg~{SztlRS>$k%@H2d(Zz7DCtYRIFtM)L0Px`FK~XJDA) zWPb!!xXM)=@`xjMK&_SWrh;xK!Brc^4}EFzepn8%L0lv}PZ;dutCo1`J5y+>Ukv*d z2?$yu78oTh#`6k16ok4mi*n_xV^kU5IkH7dH6okJ8;lUn6z588}UgQD7 z{I+FTG(v!eTE0FW6N$LhX0z~b2fs6X!yD_I+||eJCu3I2JDC$9b{k6kqPHL}uG#6t z-+LnUc_i{t50Hb2#?`uI5ML5hqe3}c<^~vKhNpsdHwuKKflJO`ZJJA z^^1!URO$w{q}=LhOq3GsKzyhkh)<{=*aB-{405WU2K83I_)*{%V+=$3SiJ!87$inN zu3ovc^)*~t_4V~bsa|yaa{k50B&uEB6*xmyf4u}KZrB%L%Q{$$k7AVl`i8S^kF2WE zr==Lgs?Q7lq@xY~at8$MD)={wsv|^+L=5snsx+D`&!T%w7ODce)k_iAGDDftO{xyq z6jXOE^!*_z6sB}PMm?)LF&Clu@-_H#JEl4+#YXL-k9;5unBdIQ5za;dDjvW~MiZaZpw z;EI1VP5C2?rf2XMpSwZ$^9YaTZYpsI(tnKerf*xlW7iE!x{;N??56F(VnSJl%qj3~ zglyW*tQ%IAUUKM9f#rfUjmLEt@vJe_$H=E32E~p*W;d0G>8)bTBr}*AKDtq=jGp$=xNVBhDEEk;Lj9fpiDFH>D8;C*&+Ab}x-rTbX(TmdL??+8 zyf4ccO>D|NnNiV2;_0ZBs4E4@KVhrIdr8tlVg>lf`ZY=d!5J{y(FnL zQ57-vk|nB~6gI`{)=*M@+F^;dRRIs}wM1J{b&{;afpu+fSNBQ5nha)=SUkjO!aEIp z2s^Uot}2Xb*2X|zCs_#7=~`VzKYyHK&X=`1SNSalm~K$GYU6XE4(JM5&BbP=dv$$)05s54?(hCiXdFRqfhm@6t zL03aq;Q@0*Z*UPVaiFgN0MbNKQy(GU70Ufyz;`|8kaq?OwJf>L z?41U8<#DvVe=bPXoxaU3oSMKr!X)VPrpETfVL>`k0Z42}Gmytj(WwA?kGHf$T*5Gz z_{i3Sd~c5BBPc8fjyHx%=2CtRpF(SE(N(?@nOySNvoLs4I%q4)LjW<7a1C22MiPo& zNJ4R$HuA8}d}3>D^slY8(Y{-2Bd2l^aP;;Aryq}9sOK6qzyCx0{C6#wt7*Yfi?Lvw7HzRj*gEoEHw?Hcql z$6JqSAMbQ{a0gmPBKsc-tkUJNAksEBg+1CC98ViMD^DXd1C175GLbWvkm}ts*lK>e zqoLko+$>BqLusJO8|UBv)AtIliT3!-SOLNGy#*ghBTwtlWDVAebOJKz#8pyXNv>Ah zB#Vl-nQfAId)B}_w-eIs*MRXBq}ysyZ~-Vkyo}(8ugA!!-#qa07Ny=SA2t_wK(Cd3 zOP7i1v&A1lM=7|@Aqb$vC?nPd+{N8&X)PE!6gJ{%xG@%iqbv+^p%7tVz8DM%fnOdn zFnPzS#R%zJ0x3PTVAwEVe>Oljg*js)_|9EU#L^sy<99ilWXV!3vEct2JtS7baNwkj zhO(fm+5Wco0rZLRS2SLW5ZSfJf+(r4x6>+Oe#5e$SUDej9pymgq>}qLDsv(|{Yliv z*h=%>kHSnH=DH$`r98`FD2Y9hX33S2nAx+R#l;P_EjO|I}8?-7Yi$C{%t1s*5bFoOjZ~-}g?PRQ`k049u!5=4kr>j~xzE zJSdY~i||i}cvN3x)nj@oFTvj}b+&Z~8oiX?BitWG;eC-_%F75ddMOe!jb6$hkVN!S zSPKy6972j7`Oln-H%x;|c`oWrOyY=Spdgd>|A7*0TF} z?!eiMK%v9%3JS2s2=M$%) z9fpBPIt-tcHXu|V@>|tm!0TX`d)#vV6CD86VfdW%H|tIvhCie7s>2YLB>NUi;+oI0 z-k1&pbpozLuFdKMT!qNk8nqYtyGAEKT6MBc0P#)xe*v~Iq6z@7TPJ`S{l6j~h8NBt zah-s0ETH?D3+^8VWCdIM}QIvpuSY36EHn|=mZ>c z)K=m9{{Z-yPJk9b_}if#H?j{$Y1SFLh6SU4d^~2WcJZzSlHT>i6o-yh?^AR=8d%8V zeZ0z(L`<%0j(-RBhESLvzZLUk{2m4*$G?jYcT0Y_P#(w6u=}~dp98_w6#aX)YhH^Lng%{LtA@{rm+SFhPL*^Gs)K8&8FTQ zgM+||_W-OhMW@!*9ubS#+LPj@x3y<^Uq@ODO{R(4+N0m{B3pZZwe%5Qd0?%(2(A&I z#f1G;y6`IIhHfh03M;e&j*T0JU?vXNscT~m96)n1&{B3Xq z`5lv=GGFTAe1UzP^lk$HeKO4!9we{l>D#3Hqkm<0frM-DC9CUUWYV?5yA_!>H8fOS z!5B3rw=C}#LC^HFn|RZ1iG~uoeZmxCb$uBC^u>1pU~<61v>*{AQ#fC>WQ3Nw24)(U z2foyTqcJCbi{)@lkV#VqbAXd0oC7oI+1Cu#Iq-xgc4fX484Xiw;+YLkKkPqf0^d^$`U6U|Tz-B_5VbMhhR1K+7$4nUM04MsyLY1^6_Ief8=kQ6BMBvwNT|f6C*&7`{IPog5gO zHa#+H7w)pEXO>9h>gEsDi7ljQ_1x}8JDNRB7&^S0EBk>ZidRd}FUh`HmPe~6{yY?m zH`u*915bZlZzMgC-Xo)3ZZ31Vm7B|S2>)IC+Y&qWEs^~f+MdhOF6rk`l|a3NX>|?6 z8yk%}HwJL^X{3DzF-cO1)z#L4$ZD&>6XYeY;Zyj2k>kpii(%Nl9_NhMQ)YxObr?W} z?8up|{u7&5j^gel^qKR?^$-n$8~rbY_Cu2y5<$2ETbGbR4?YfqA_%ItIoV4YCmLZBQM|ri0KeDfFvL^0FJB>fcG& z2yP+MTD|WBuJTh)N1bn$wQBqaOlyxnfKO8N&}cAE;EWNB*^GWjCm0lrC=~|MZpz!O z1LMaUV+?E#@TIy*hGDHSmMst0VgLIv+(!P?pO|-u?pG&SV=P$rs~-|Q^uh!D{@9p> z{SiFnImRq3VPLDmOKKAAn#4d~pzn((6C}4)$|@AVCw^u#6MW3ud4Yx42c=E8XDvaS z=$@r0)$UnDF1BY0RI_JkJ#l){SNM*z>^=~d?8|a0>Lb`?+;nx4Jl zKZkyY8Z-ue^S{7TvEvZ2E3Q@^Al$lRKdfWqLY_Lr`n+-i$6s60Bw3uHp;^WjHE=ktvG;ZbL)kGg_B)4L&LH(SIal4 zOANaj`s8s2c#q&$bopcS5|h4RdWqns&`Sb6*b^dpiS&@*W6)A8aJfoD)rV3?{{+s^8ptzKnA(i&~FQ3HDNu^(Qlk+{V|&`>G16b7UvyvG{jR}Dzhr3R$M zMFY}Ht?DV*@WwYHN_QvVYl}YMtz5e%$J%{8x@3Y?RBpD$n9p(7%SI~8uI=|@Tn;^t zM@%IWkLi5@0g@mV_U>uI#=@|}g1o8f-CYxFl@<`#(IAz0?Wd#!n-z?hiPIxn8VysU}Qq2ynitpHhd|pYb`K` zT3aei6g3~WH_cd6$n9Y;)`g_-u{~Vilk<$gOp3O8Qto+G&kIxIxHC2Sy=M|n6N z=P<*q%rIcJhp7#&9YGlyqeZM`j<1*^v`!H&nHVnbX2MH?gsgiAuQW53Zj9-!V>&Ve zYSZOjXS%}cOgH~^rkfQ@_a5SRnbcznezRlg-p6!zPOit-q>r=#SD%d}XhrZ{KOMnK zij~VdkPdUFzFgE1j#1l#9_o{+BSRn<`9qtWixvY=PNtffIhbmayww#GRGQYzzpHCz z-q4TDShVGyhK`|9P>(i8;5hz#6z<5xseU!8Q#{X{E#+A=eV(~n%Jcr|^UT{)p0!b) zl$`OtF6})pnwP7ksp`wyaDu^hi_|Y8^~-4e2t%hT*>*ptK9im34#s}RmWn?!C?4as zE`f`YiEprcZySVbN56+Din$U5k|E9Os-Qn zY^Uq)8-n6dr2jTt@{$Ssx zPxozIUm+z=9L_c?dV@iuu09VXqE=*Jfwb?`aCg0igTfyIg~$v-%no(79)3#%)ghA)#%j* zoC(Lj4k-h6h8yz%e8{_G==#t@g>`+ekP5I94fdN}Dg; zUHIc%?ZY2wit@tB8TEj1Key<-n-y8_+yQN z@FLv&i~Gx+r6p)nIbotryY50f+Ek9kY||~FL48Pb9qUbCpUWDDGlSk|^!7q~^LCu@ zE3>xaFXp>(j<+|$A%g8I*!`~M9SQkRuXhg$wfwvBizOMF$$|2}Xi`gn(fQW^)zhGs zHO@p%B8N)eBXgQ%CbMCwJZ&{Beh?>od#3~W>;D0aNPSH`3C^oqIRUK<-rt4TNY=pa zBCVzs9Bb53ti?twnivs6OgUgE1} zurVCAo4K8cc8dc?V>eX3!p1h&(ZQPiCQSJ8vWjq6(j$xVqKl8+FRhm;0F=u4k!n6& zs+ylhs@*-9R7~|XB>S?-0`_}v0|$|-S^04R#244qrjn zlSA?mY?(=U-ZSZ63b+3?L}Vc`IuK(juI{HYMk`TDWV}dUZPE1b2r1>?p`d8g?4T*o zbl>15~ zh0KvMT2ioF7_%goPkFbY!C=WwPz75TG7Q{poXPp;Zfb;Sdw&H`ukMN;yhwm{uVbSp zzMts&EuK4qSKKcqa@eEmU;oaxyTe+!PHRd;Aw97LwzS$wwu-PcR!7_Ev8h zz_3k8ubqUik?lUt)b{u$d?MMdKpG<;BisEYow-P&OaJC1x-2u0=spQI`<#sdj#vA> zp-h(rs!aE7L<}8r9KV4~_gHvBnJ%HIOjm*_(`90yE$~IAD*=${3KXz-iD4kqB}S8E zx+_?i(C04!-XYfPJvtvij3mO2H`pxk@0 z7I=Tb1Pb1W4m6Y$7KM@KvFJAQSaiES7Ttlzq9M;EI>JOZLQNv1er}Z-FTzLgKp7{y zLqi~Xgi%KnkfGoaXcOUfECS-dGc4u0Vt+alBPNto>oLz!VXi1$y{r z;p3fzXvbT{flImGg(&@d$R|S4SS~as?^^q!*wgJ6v8fkFL^j@S$;EjiHE&lrdQm^x znkLGT0*z0}3^<3*=>CkB$x-=yzlEjRgi z*OFBCJAqe^tRuGA^ZdT_=SeXebw*jFp+QtW;VK^;9RbuFuNk zgk0+wdL9E+Qor?kuAFkq}+Zo21;c<(|j4D$295Q4pEH5VtD@ zu3V8d>^5!HpLmt`eZZZ=au&rjbklT!LJO3MGGRz2Sk4Nt+Kas1ZXpv} zuJ+Vw8`>3Ay0#5<%atBr#XDf}Mbph%H=LcLNSgL?{Ox&D~50yQr;(HgCZDn zS3*xf>jZ!kX7QZ~fHec~>TnIlvJG_G!!d{P+|Jr;W_g;hW(P@A*iP{9GcGcm!WB_S z0 z?3=u#n#HxaE?8V4i`Qi!TwKGJhV-E%%_>8zX3v;9s`d>Ku!w*SgChf0e7WLkmGZtF zw#aU#gN*$G3}(S2Fqf`#Pw(`Q4!}4q4$P|DjxL8Y+nk88S5$qyu#SQRtFW#}X+)*G z94QNza6k7B0t-MaAgQMNp5b2SWw1+c(6TJi#V}=cpse!=NLU0;QU~3k+$f4XLunrhZ)LnY#&aIT{hFB7Kx z8j=vUXGqF%l~t=SkN^==N`i(-$5OrQT5AacoKF8SU|{=?5RaXWFX{h;-hI_u z`QzCTPAq^xAxbVj;7VP*nfMBAR_V49r+begYj#trr!~7tnmUvpK=(UEe#4-G^>-me z7E>I~xz4N52xnm{a4Ey}{>b7al_s&Tvs^)m zyOER^6szrz{!9Fj?avKn29)~ht+XTyc7nkddIxNP4yq8jlL$3cP*~dpu=?f*xMqUG z<9?GN{-$?ns;?0R`iw6sg1!Or+EAQLSU0oLJc*DO{T$RVZkBy$Eh=HCn}S4i5$M zIv}$zQ=$v1uoaqZ7f%@WmO*!X^H^C)sNX$@+|S_g|bx0_0dr-8@QAT17Tb(Bnl&M zCp;ZIdKfapEO7tCZqUNZ_;tI3^}(zy$xYk0q$*dTA8{vS1&YxvNC=jSeLT8XERjoS zr5(aBl@jJf_fGk0J0@C}Cfd2SEfw%!HCO?B-sQ06{>_8L+1nNCmN_c!1>okW_%C)% zVf^3x*q)0tf|a zMJWQ>3EsC+Q`^6b_5BA&B}OSz@XK7_1GW#rO^o^rl3F7Kn4AQ9O(?Tzz@B)q8O6N@ zc-23m1A2E$rfU3>pj{LEMiSUbD6B~a@pH}VLfszXJU3k{^nxaG+I^;k8nlNny>D-HOcLKxICD(9hTy#qj6?(|)hX-V3g z3&yj^9W8`W7D*-;za8e=gD=IoP+gGqnezRW_&W`M<@Z>X3kWsG?a8WKA9LRwb3Yq% zXI4h(7scGi#M~c1COjz*tMpD+5fJB#S?&VMl0AK zbMF^(zdh#uK+OI5nET)IL&MqlW*kMx~P?40npTkB+&|in%`-bKf6x z|1sw7JUN2-mY91ibZd?DSN3eqnQeJ4D3xt*FU&NI#RXZb)F1eS9;wIFgyL@lK9Rp^ zlK&9(bAH=07s2Pu&q&B(hl57tCSpNIUL)b08E9JxS1!>NF14?5b=%h(yTDKo4gFedz%^Ll%?m@0Q z2C6uTfZee+V;6#@%4YyMEfU5N@6Rj+_Gn)_2gulJT@@6(*TzwZ^MUQngiF1Y9~HcI z#vpAnRcgA3+nLwFkjJpXlK*J?Xb_4tpFl%Y_FfZgR1ioJ5i2zg!C%s9OpaT`YXbG5 z!y037Um8$^F^TW~WKin<{{!^jM%Hr-JUuMtQm5)4u(@UJuBQ;WyuUFCN|TVh9-?D+ z-Nj(+!Yy{Gi|oaE%T1+fv)m!a?Ze>xb8MEFImbDl*{{D;OLf+ z%$v)!p>G@UnW&qjm5t}?aSvnstC_Yk>m86HSW2Po0Jo^dC>vMD57+QVP_I;f9;W5~ zg8JYdi0l1cO2V0(SY&B$?L~$#k0ok6KVe&Vcqsz|x!)~R<#-^64Uph+xX{?{Z2!xY zwNlo%Bg*7s>`6!)=HOEwKbAyqC(TX3q|t`JR4z6L-Ijt&Af{Ru4)*Z(GhdtLOti{5uT2HML* z?wkS7Meoma(fh^nyo~pLv`=rwyGB2^>*qfGtb~SIZ)Lkvg1Y7LLVG|1?VV^kA~EAF z@N#W98sR^a_U6gpBCvEVo@%y-7>88h#g%SZ+2oW%8;`kxX+NRl!jKTTmlqI zX+eiuz$=3LtwM98p@6ieat@s$sBlZ?AbtOvE02dkkYSz(PTZSM4bnKBD~*T-ui(`H za+n>g=Pnk%y#HRFF_Los`b@6lW~gzlExXbX7*xL&sE+{@Pdsnh#kC{`<19f zfTiM+DYhZQ9QHed98Rf740m9F)WVswGBZrg(e8S8@Q02)*puv^SGfrBhN&b+FLMl2 zJKdU3@O=}I|AOe1=$ymKI+#sbGSd$&^x6(y|LbqC9epRSyZ*X#&T{=fqX&}ip+?V} zhM|AKIW~J?7CL^jt=R*+l+OZFU|<_+mK+T6%g`#@o6nJLC!6cMZn%0}ua6@Y0eJor z0ri=*w~vI#^!J{E8<8oA!~}s!c<-55`~>nK-qm=%@ZorCr3gVjYqgE+GY_?+-X3M3 zd*g2b{yvUBFbCv&cI!%5^UOR^YE5f84ckq7Y4ZjKD-39^I4w z7GH)y$7^6U9M2)PjyI~l59;UR`ng3veVXLQ>f29!hv~=WBy+r_`dO);4lV+Y_a60~ zr=QFCsl0?C0W#nSY<`tVYrXCR)5md5PbD(wkWgK);X!a>+}%ti0RvVwuD z>NgGLGptNuj=bbKhxTHbRE9k6BorWCfG}SM|MC@(WMcGY+87vc3nj2^I78t?W>!39 z`*4QRiA-k{nC=*^cjk;_ojD6Wml4or8@Z)S3J!4aQJ1XCK0-B~rSr?=G{>`QlX7j{4 z?5%Q-xexXECD&_VW){7pkqh!;|2Ta8?ePO~Jx0?sdS?xZ*15sHtM>o}4y-l5ei)fV zYi9t%l5^t|#KjvnSP%j1{*M9!l_;i*V#3q*jzUZWZjdQ*vDfy{QEVRzmY6c;y+II- z10du4#mK{Z734eYk9u!nKPrIW*2^tm;~*^#S+<>}S?v1;RVBwrCPdc|zG1tE5>?VUwUf5QB=$0=FTNDMWC) zx)d(d?zRc4HOKOIV)fARk7J)k-zq=o$!M%zviY&fs3~K0NirZ|MiU3*TD}W z_+ZQues4v`EV-Cxp%BB-wa0CaRd8J7yPJOPLZ`n2>l^r?>m+^%@R<(LZ(_9ZLf=!o z5NhAQ)I`CfPdpc@cp>w8R2bAKO z#B19lYFow!yte;U+Y)@xwuB#V+keK}HpR9@tMIEcp%jQczgP0*zr zCDQzCw|4U1M@`k6HHD+;ZmyGdTUKc}9ehH8=G}+WlQ0VbU&%QzIw{{;nDJy>l@UaS%&Z|HPHJ^4Y$`>9N4ia<@aIWPYDyu{4X-u89M08)pC2 z;i>RxJr(AGVdeO@S;3W?CdJw#gBlTe9){$z9z{rYLPD>Ui zh0%L86Q3F--dsAiEvXFBw}mXIvs@X}*m{K}b{O2jmwU6Y$)LI!2P5qa_Jfd|3o@3i zss+8OuSazSh{cN|-8qOf?Kvkj)!BuGzelJ$Qm^5XF}-5r1|D~c!y+kD*vkPM@L|NG zv>=D$^$G)8O_IbwCqcqOwYQk;JfR6tSQ>)_G62K9@HvA-siY+d24PuAIYU0fc?jW+ zbJ4RHv%D=e(@opBDN0G4CA*Qi^Edcwo*_wpQzznC&O^!fb(61^M%*=@zazRZ$o8A? zZfFVls1rdw6XdOjY{$ZVF77nux%c$oOVY-XT?|Xx7}D9cY*{X@T60e5Zb}puzC!!z z-YF=__TG-)VS6|TXdi|-OhSzXcqcNAtUnPSsI(vsxHANd6&-gkRRF-vTLl1@lv$?Ui($NVa;0_) z2xyiW`vmfUJ!5MWf$RB^I(pdhKwsr5=e#eFWhrm7>ostU7Is?&?|s0iZ?@&VA79bP zp-%I{9L(=mA%t*nP_bKangOhUKOr!MCbGSAki-u2dOTP_G}DMl4#~g_PJRd?Nj||V zp-Bzpc-zC^R|CZwV=}$9q2MII5e){*8wIG!4IURQk%AFL*qJ+jCdqI+wK-WxL5bJft7bPrZC*^S8A7^6W$dD}MN! zv%L4B8NvJ;LySN&2t_MZ2azn-)XvqL*CJr>ihr8Jy zJSiRvy*R8N#RbsaD3Y7f1w#cE6xP-va)=5Y#tnvFozYvn_>{rNc%WcAo`__>MxA;5 zzXpiq$~)n_eJxz|>C(VZ2hIN%@dP_~E)rT=NT9IL(_C7ZUYJXlc3KE~d&Ai>LUoy6 z@a^gI@J_0oxC`a|2IWIA1Elz5m!XlDybVafggjEo+Iav_{w?eBd)CFOK96NK zS1!B$ui>aqOM8}@7dApioZ773c^C<>sGY9v1;F0<@VWl4khK2+V;m6hc=la_FZaDt zM#vNB);X?yitceUZR~M%W-~h_*x_{dUA5pD?0%(;t4htlF_}0Xuzm*|Z84rWd${u$ zaG=tb>KXRhIdiUYiX#o>*?1_=KMlAAVBJptT6m#YIYPG3Q)wgRn!RjmUq=lVXoefMTd`=I!RH=fLYe-#wFFJC{;rx zApEE*Q3`!lTbKuyTunaRhsc@wCHFSnhq8|MZ>&Bzm@i^(RkX0ZKY`fM(oQmHCi`u> zj2$bAIX}YV15wGi?il99dGJ!S7dV?Z$0r=7uP-Jl>r(yVRU0~YDn02b77%AIakU0ouv zECmW*5dr0(QIc6<)wyE|K0@VMHjI28GR&$%|zlMxH*M{X$aCHYCkw-RJws$pm+Jh|YzJDW&xdpZIKN*JYj}oe_47-M5N=v;L zIK3RrsVg==rx-P5PVF1bDaM;Qbv^pC@mr}wsK2u(lW zX0%H#`0rspB6L?Ey|Swwo{v`82hnvm= z!oob@CpHfZf;takp3Thz!CB@3)a*_GtZvH#^-+#VT5m^atteJPs7xKmTPfS>-Ds2n zHz#hj$6HWQNL^d`NsZ6Mr}hjq#sq=PZUNuhbiA({N&{?qV`rKT8Tj-HPD0qzqtr z7rkI)!z~bRP{xs}{s#qJ1uv(ROILtG|l5K3Q1n-GlUTQLT(ckn@Re z$Q~3%zZAgb-phYBq>(nHkv3(~L-w2j2&^HCwpAKwRnK9~t?F_zW31{`>f4cMTGcuH zR9?n{Hlr@~TH1t7}6ZT^?w#t^3>;Qcj(H~Kt^5<^I~{|mN0 zc-+aeK%nKjWAVC^9h9s(d-mC4WqD6FNpCx zAaN(p!+RM&*rF+-xdkmE91YCK%XntpiUwpr?}Fda3SLv)j+XZzpxIsz2QAiG@g==& z>7@aHhgjfF8krQI?Y)WK-K>;)ahs0*Ww3mR#~?)E^3CjF@B7HwJ3$6x{h8=DIi_o( z_t7tjs6Hge3S45?$n;As9D&<#l~(RaJ;EUN#XHb${HJ?79>v5>KdN>L5Sm_41%~9n z?8*iJH@YOA4ASqiG##Xk$Bb(MI5ve^^r5Wnor&VPFrbfq+@#2Ky*vj*tfG0mf$4Vr0Tn4*r5VTL zE=YibQnnnOtOUQT8U+*dOPAn+*GVz&5H>TWu@PRtpqGSp5So-wv;ng-iPB~*D4J`C zwheS?2CxmX!r+3jUwN-T9^XDvR3q~T!I~R}6$qjbWg2 zm*WkVpfujlk&vXb`hLtF|20C#!#7fj*6T>%_7wFuQ153Eoi&PM%}^ZcDDO(8u@aTW zdddr()wdH!j{B9fnqnINtpw<-=5fluOx+|Y{hP&&e)pcD3*V>tg(L<#73 z75{DorEh5qC>hh+pe_E%H9lB3_|{JPJvEByI%rTK?i^fq%?|Pwj&F=}`4%irBqYL# zHH#T%dw)lzEnertcl@nPyWaBt0gvtDEpZsivb7{TrXMB4bD**i2xzGTm5I>JC0_Ov z>C405?*a15%L=^{hs*D=@_U&4E|cGb_-o!?Q91a(I8KxnxE3RRGkh>c zO*yl(KyxpG@#f6_5W4n%a%Ly^;LJ|=s-JK$nsrO}A`Zlu1a*7%A^3W;FqFm*Vn|{9 zV0^Z4J3{G_*@;PBzwMtFaRj8-m42(+R=3ybMwF)SFi--QKGn+tQjgcKmp zYBGCsf^4AJi0-6I7HLFtCJJDDnVEZ*Ep}u7&3=`gHqv2!I~-mkaT>I` z>eA3B6Aus*+_{&6t`-}3xCk;|@%FF5!U5GcTt;OHQsu1N0&!|?gs_g`jgTT1fo2pG zzY!AoaU-ONdmIw*8?K(a$&HZ6ulqq4aCwz`)u;|@b0>t!a3_SH=uU|E%$<;+xvM!= zm9UaB?_UEfQ5L5Kw+^(0G{_M~I3ZU+Toh^H50suPL;Q9bKbN5Lu2!YzEZog08O?O7 zvZU5~oOq2MyH!~e_o|Nk&NejH*p(xt6JMo5E5`%ZxI|`p{OqMN_+})uELC-1YP>P+ zcP{Q6N7&NeHXq_ekZh1Y?Wm$%J0Qoq)}kb+DY9)a4CqtBJ@CwD4BL$NUHW-<9OTDi zSjzi*Fb%jrW-V_3pIl`&Q(kPE zx0st{hk5-!AgetP-Y$&tHP|bR8!EFZb66oGms%t`u4(%Ns zMzHOX&hTe((m}Q4es~0JdLE5?h?vHF%PK7yXx6>}(Jyr5KqGAMT#iC|D>upS;cugv z%sc5`s$ebudj!EAVGw~^{?Fn;ECPCeVG#SpN*sfB<-d5TvrMVmmWL`2qYV4Q6F3n_ zReLLcnH-QGo0Ot|QUKJ_ly;TIh{oU|;tJM<8Bk<{X$sDPcHneqY`kStPQTp>Nv0$@ zlxImZc!vmPXX8tH+tW)bga=Sx$h!yu@djgCPMskbIdkH$sKZN;k<`G0?bH_HBMD$Y z&!EFZKxa}tByQR6$3zNo*CqF4Hx^%v{ z6JBjwAFE}?`un&kSp0bs7ROJIg(REY-&1j5vRWKYn1aKJ42gR9FGL`m!6!3P=3&?f zzNDBu52fsIJz)}*8-h7S%P=+fRw)tzQwdjtuA|t1^;v>(Qipxo#z>-=ZUK%>#*+!w;% zelq-dA^i5nRHGA=@Ls~zVv4&D<+7K?guYV*jH=ADAmT2=~yhDudBk`3Pg5V+>4 z4wyGM0;quRdL%!|@uTqJkiVF??Py!x3?x@GC$&DLVH7mgv(>wt=wQluAA!SGFZ*~M!|4!wIY)mDJ|DXG zXh4^Cnli_JC-S;6Ccqqn=#Yq*Io5&M!HBKTu@~Y{IL9I=GRLMs!3yA=2W+jC6BpOs$?_Ez!Ebl^~>|ICq zVMtgUga%mkMtU_RVb%#P?Qpz}elB8Bi8703h51EZ+$;{l^6}@Q%$TQNaa$PoCA!WxOYfKIDZqH3D$&H&*b9+D7uK&ZjdZW*%|y%apyho6Rnu(} zS`XJVAp$orP=@A?u*geOOJOSD-=}sYxMMrp{{$0!~mTs z?>@SFWu8fJUdDPk6IGDHZ{>R_{Y6rq;K1GL_^ ziHCqW&I?=BR|x*YebCX7$Wbm?!9KqdK=JMxHcF6TE9gm%ACC_!e*(YKC#l43_)-Ue zBjq%tPKoxiHTHI->0fM(5svT?AqD~h<0ryzjqz7P3tEtd_zz)48fQ+va>Fl-0}Q8} z5&wH2XP|4dDC@YxStm90G+M~y*~mP^$eLP02E-}BM5)6P%3v>%=fF#~N^8_&JQD>> z#TcQR!C3JPR4E3j9PkDZWNFeUQ4^GJ68u&JKUZiyc7hB6DE;=JOdy&=1{M=8~D~y^QG$1t02A z6cm{29|7RwOv(w&Z+rs74hAN0I-I})p&`@HS<1ASGMNZM`CBsnPefnlIQ|4&gaKxZ zUGnf&!5*9c3s#!1aLlFm%4LNrL$X4wN&xj+4lB zR`a`=8S|)Rr@UvdD;y70U=G0YS1ulE(&3UvCgSC9j zy8i~0qEsAF>~(Lh0`Fltv#}VC3Xt=H2)N(4P$30kG?=BlSc5;z+pB4TE#R-v+>!2j zDK_1YEsbp{dS8yb#l(F`lR;Y;m#z0gYZwhCL8TwY3&nsC&>?Eh;C!)l5ocu~b=^aq#OKg<$B4~fX**}40_6T< zRHbTi*2!U0Aw)%6pCNu1_UNz;YtVi@onTr#xN9qTlIE~P-e1pos&a8^i)=8kttF()0ShrcZyzftLA&tAat^x#4Uzl&+KeT$X z-Vz5{f3&Uoj$##~%b--l$Eub)AW?GxuCNYfB^15qi> zd!zEzz|hQ9wmS#SqXJD~wm}t&X)xDdUP;^E5nF(|K5ATX$T3_LNeo0ZS8e8QVQZS} zPF=5|jxhDJeC0;mdqNMJoU;F|LfjrWzZ9~wg5qf|8=aOb)|cC$s>E{B>*l13BeYn* za)o(#aqzjKyKs>^YXN98CMm1~MNfw@IG>R&*w~VU0{0x;t}~gnUMV>@eS`P^OlC}; zA$5}>3#(EJsyjk{`NsP6z>I*unXdjBW4kx-Dk}zc;hw@V`=7uLxSj@f;QgP#4iHUO zU#!7f)D!+oym_jVUQ*bBw;I7t%FBCEn{eDxp)+XwX}Bo?qpolwV=*g%1lpL*DXX#lG}L;cc>*yqNTJidhH;(A)%U6;lm;{rZ@J~{Xs z7ySE_iyM`K1@1v|I&Z}5RC~`0n5vfQwM7fvQ8ZhIE`HEJ@W3c6ruT+4OfrCHFB8Enx|JevG{D~q}MgL6Qihg zrsKrRG~D=0GnShGz4iqQ`wzQ%9aF~aaVSIjhw>3FIhSK~h%G9xZO#MXjxJVnhl|d` zlj0#eSy!Jd@a#cejodCMzp7W zZS#8?qo(M3QE|-ZdNJN?Y^k+xXk!cZwdk%R!3O&p;j&Mh1lO%5=@ag8Qxa=x{9v2PRUYdO zFW1|#6gbfY$i!1~O_eX9{L24h?MvX}s;d8|@6DE#PLs|g>5@>I^2kim zG=WkI6sWX7fl`sBfeHmGo50{qgV-=Jpdz4Dmda8Pp&%e2A}S)dprSt)R0ITNsfvh* z8$W;UOZ|Vp=iGbWn@LjYAM^QS?mOph=bn4+cJ96B9=e~KHTl^qD;t>7$r_AT0r(CI z2U+z%|HlA)17}H=2i1CAm8t3C2n_#r^_NQ0b86ymDvh!-14*}vtSC!$JvtYuCX4(< z7YLb}BJY{X8H0dWE|NZqlSI({3EZ13{R3s`DWxvNFVp2d;?bOcX-PA{M;Z%y9CVS> zlTxxisLkgfQNu;$DCcnKS2YbObA7vyD+iKYK_k}N_ZE^j_MDI{?}9YOSRFb2lsXCT zrcil-RU(xa5GvA0n-=`@du6<00AF+*RIG#$6)TM8%P;*x2!4LCpTr{_E2q;#IYqZS z+1#C8jcR^_zb+z0spO9 zrqFo0#t@QyvlJ;ZL$)`yX;nhY=vXU|;<@R1A_eW;@!yCuR4 zfu{%eBcAG`@AnHhY&y;()qdBNTcSv-TOW_!L`n=Y(z>~ay>|3_pcGcFl4^3HmV z6ZXqWrmaN4@zIZ?Nl4ZLAl4bAtrCz5Yy*5F4^u2R-lU6{;A`Ic0Jb4 zGKj3gQdBZH0p1O%;cp;Lq0LLJ=SO7>rG_=#)!lf1QvJVX{rOS3aGd`QK=(2}4Wd;p zs`2;bL=O_gu=0PbobLD7VI?J}dr)b60u{xtXOeg~L(g3v4>Bt6L9%%E;;sCCFTSx~ z0_`OZ5gi6h&+oKUSa6bjbf%;dYE=@0n`5=BcxGFZb>d@9)@{|CgMFb!(7fdnSIn5b{bue!G1np|S|_QT zcyhm^L)xSf^C3+yDdh=HLfFD`O*bheQxw**Og^cZNGRy#nM+cB2Qk%SW=YMmp9*Ro zq>}J-$t}8!LRTsoE}4f>L9FfJEWZdD?qq%i5(2w9>*=bMz0&iPqTSzXrR!o3ni{Wd zL9Vn0IAhl|koAr^P<<6_(I&k%913n5<@FQ1u}ZQ=sZ=uGzRVeh&~{`kWp2KSJk=-N zJDkn%=lP!eOmj||x9Op;4h~^@aR176NNFSJ&_murM(OY&YeNp^+Ia5G@1I(_3xOhS z0S$W-d}LphhZkt85n}#6as?eDUAJXOAUK)r!R+>yw@`?Hp|8uK(jj8`J;M-^?9;hl0L-|L2r{JK16- z7(z>^TuhG10DcxZbW^x&2^C*n)o?5^z+<|>u}Fgq7l5NRM?h$g2uDW15R7<-BcOQe z2*~*Z*F!Uu-c13MYCa^pi;m&ss5jVSyrX-b(QeVkD_CjV-}p&J}!q*VYbtvDn} zI3lef2x+z6p|py(fHqwbL0zLzb~{CO4I3f5jF40cjOwJd)^f&#;{X4|}?err|*7BX{>UO{f$@f2my+o+oh!@&$z!lz@xinsk-dent$VA!K z&gKKG4C!n(?qKyv46vE5>udzD)rW~b;Yjs`Aga%Lht((E0@}W2XQ?m4cGMR`BsI&h zuOZUSwY}IJ)bnD#Sx;Ma85lsj1qDUBgz{3&`WJ5oV<*hL>zvii72@CzJ`!F(+x0^cp7&>X2tVP{=a|=V-^`@>#jzHbZysA2r`ea9cd$MB?6UZ<*yM}Hk z&3S*gE)D%fVnNspm_Sue%{Vj-OZ` zW1ZKb4_M(@hAU5tpV$aXiFgM%b>-SdwBPQAzC7|(tM^J?0$l+7<4 z;*+WQ&0Z3buwZ1gzE{P#Ko8~nBA?qhBUg*cwPoN%u_)#?BR;VWH zr}rFJegJfgU5SK@F=?h5?gh*Z`=YNS)+Buqy~g!LyoPT2BKn)Y=rst^8|sUgtnG`K zu8vP%p&U*=K0OBS$;YRwS!C&HzQ$yBU@|N*Ai8wj_#}WGpD>;fjr!zLl# z0^0GZTgE5Db`v3~S%%}2WU3Pp#^br_d8{+FrRq$fzPrCE-PzSwpAMeKmK9r6bhvs3 zR{eDzLt6;zu_p@)sX0g<_O=iy)!9GPS+@|8XeyP~{Re)o1Dx<{yz%50$woKF+%=RT z>Qp9?A$pCIA(}ogGDLqf$2{_q9g(5eonx|%uRwW!ioI{YcMq@l0@@YM85Vy87q&}^ z;St`XsD*m*WD2i4sEzjTvyft(!HOeE1S6U5fk<~I`y7mLW|*G>>dnRsGwYT0Bk394>d)ZsyNL4m3#|EF2Z_US_7xy1W~Cu<_vK+F*g8R6XlpAFdQ)QCl0p^n!4~+PrEeLJ$tD}L~Z6;C&lEqor zKq44ve-Y66hRsiW1w7L0GFF=(f}H708D;R@KE$MTig7ZGqSfn6HpeclHQ6k3ax=;n zuzg@LC|no%X{@H4=1g?atxwXBg3kqg0X(61PS%~EXk2vr7~=ODv~>bKfp zGpnyLk{k>+e+&sm3^qp+rNL&(;Cj%y8)+?q>iYJ92E1%{3#Y$*VmB%)08C5w4`3NQ zz$vlUCZ@*O-2fx@qcm<`gV%22hS&~ZJ1n-*Ytzr&x5Z_QM7K31HnHvHmq6G!+skhs zZ+rR3y24(m&fv`Xuxi+^Tl4vOsm@hr&d($}7nr?z_z&@^;?4n!SOrA3X+nd|r-{Ml zlXk>l^X_$pc9&=xwjE^Vd>f{t*oY1je|;;?(?ToG+Sl+rcMs}CR20^Zv$f(JJPXEk z2FEp7gj4{`I%kq~XR2Z6)oR)KKx5gtPAChG1!ZF7`HfhFUfLCl$6}XLV#g@FqI6q< zZi!lXF>Dw7ie1DC&zFZMihSfdkN}ITuULuY^+edBo$o7D>dB=5@fGk?FWVufg+08y zE1e+p#P5QrMlL*UCOOckiYsGPFZif;42ctakYX}v;tNlFK_F_A5OvX_cMUOPHm`hpjP!hyjKs%QzfcO5e zVLQeThDd6b;ao~G4d+rH@RYhgo@9?WcoTxO3fmQo3{T9;-058d`?oO&&JO zz@6y7#rV^ue-yUgNDcb;`gG^MeW`Tke(YBx^R)fWN7L7dmoS%!m^t5Ej;%Q6a3k5z z1&dy==3P2P;Vxa0EydiWOYl3qOV>nDLo!XhOLrgbqz!*36ji-TCsCtU#AI(qBhU7N z53qGp9s5CkNL#qvqBh!(E1chl_;n2K&q3ifl@f3=5apM-LvUm~F#!@mO~_Li@MrNypu?le=s4kcBQF`^sr>W=vSP(vuFh z@)1yp;+;qFKBfwX`vdPJ_DHX@-y2~#)eoOf^BJ@7xq5Env2%PJgLf$G8Ss%HnNws@iBf#yS&|e;p~^g-QjTI*PX<1-a}#K z-4fj5^MYO^UvDZ%drHSZ=p z?RtCb9rpI(Euc+G1`d%Qt{vt}-)~qtO;b;5o}ZioLS^kBBuL*+U6K?)?}h$=RcG#n zise`AqCk`cZFk)TZ-FPZ-Na=L$hBoVEW*8k>!AN2Gr4Z-M_DFMZ^8?pCaxdb&%~X8 zkQVG_MXsgpN%EtZq&xvt{tL+fPgO5QIwnY~4*OiX4bOWo3z3w@!yVK*#y_&=4(bs^ z+WAp`DF07QF8}lzC;v2CXyl*%M*bTwdxP^MCTr!N>8f-Ok?0+e?%UWNz<1W&3b+ec zz7b+EKDW0e<*BRW>f=^J9}7xTDHY!`)XbcnL?EO$bK3!)7Di0t$_~|2iOT#jqc3g@KZqQP@_9)O9x7im>AvZ}e7R z{f<+@Q_cnzBBW1Gi2cl+xSsDCL(cR0LO9E3pIn{@W$jqdkUqm(VBWg1+vj0B~e#+KxEeG|UD!s`)eUb{R++4oRi=wU$`9U(emNVQOYTa?EMGeyx*sFYAYpWaQn!!bL!jYN@LDY=(4r@ldwPx%Zb&=GI zVJYf`L6Vwh*sh2S>qQD6J=xz(pOy`1hDv)kxx>fa?Vt2Se_#wxg&|~FbS6%0vlI5Y zOtF`gv?L{X)ysS*PS5J=P@%PrJ3$n>(QI%F^Pc17^aIo7kJATjma-G-R~TOOD?t;I zomjtO3^^gLo^`@FYV<4QR|R-k^NvVb`WSl-FTdXiSu!{3JoQ@yy-$U7C^gvy2jKEd zgWTA&SMTUM3))-W*i)SE>=r4YYgoYRxw=Le`)=KJsg|1_9<_kJoU*}GgwN9m8K(W zmJ(u)X9b!Zjx#@w{thSq?$Ih45%=4)HxoRGES;&J2H*q!;ZEPMQZ|&XKHZl9zM$sZ6EYu z8#GN^ddOAoW!7lUFQPb}7rX$c@(Ce_zqAwZEP1;DyMGdNguew`iK+6wSYzTI@_`?8 znM~J&%x!)4S;wGE{PQH0-gqL{uk}}lgNi+xW|j|lT8js{)rp)Mu<6eBC(BQecruKc z<1TiD-rQZ5XXw4QD))|_B-H{H@(70CW3lkE0(yS;=>1{ao@hhNUzQmcRVJz&xrc)#O z)z{!YhkCF8o$HB59Smt|5PT)@23MjF7rHZvA>8F%-Bgzz_))g!S!g#$kX*IZ8BNxj zf+2+SQU)p8F=)iRq-Bw+bUiszu+PvJkKKG0uC76nu_DU2ceISCISDtyP)Yet%6IS@ zE6f(Wu%|7k7(|h9c(*PZSp(=VZo6Np36%SBlX!!S-v=UZ{!Lh160z|NTU3-N5zz!4W9>)9VGm7bP|;Z;YeKxdW2YOw1-|H@q|FULE5^KR18wdy~<_udBi|Q z579|f9;SnO`vP38w}j}!4DTfH2*cX!86nJ`5VBWf$UCOycqciTAWBRrA~;_}Q0)E< zLi8(N!Z(N@UpWTtE3)%jX&+%@V4&m(e|dE;Rn^U7E8ZL(q@WW_+1Ve~awF_K^W zA!Qv|NlGL1wp_7UeGNEd^(dW0aktKQ{d3tE_qL~p+dm{{|sgF0ZBuPn#;57EE z*wqa##Yf;R1q5D`=LfLT@#huN-tcQ+J1?%##l;mqrNfIWbjxu@DqW1R$P4XqMV8%2 zgXy{PNUneI-uF%a=bqRCbi!I(B-t0--H-s(bCQL?>~Ekf;K2XTNmRZ`2SWNSaTAqq z!!>g9N#wPJ?N9TN`zY-Lcsi38Q)VpKrSu4>RLB~U!Hfjwv>N{yt*}-Mk>Axj9$%R#ovGu22EP= zz!`7S?c|jN$VZG887;H=WT~w$Dnizn zRjmkN@gVn|#7dAR?B^6C;v{*X2AL&mQeboeLV`f$diDxzH&f*N(iA*p<^}yIDC~5k zFb`U8zpg&Wem%d;ew7ZlUn>r=Uk4n_FKyeJ=6=#n7PvHkPd z_wl}!ODE&fxz?5Or!K|wT#74Qip#3X$TWGT%?tK$X-;!#Ugy&MnM?DXF3lrcn#&_; z`l44yva#}6n67jO3z2YfXUsIagBGN0KCeg8=JQ3QZ9dOO;^uP|Qa7ImA$j?PY{?kH znX|ca_dS*J?~9a&W9$BrKJzMcm9R6&4rv}@_c?*k;Bq|s#@xmW`FIf8T|5E5!I@oX)38i~OAYELmm01aeG>7ZZvEJ6Qz$7od-x6nEyW;-hbOxa)}Xi95^T?jiCczMA^JhU+&a_0pwsYXBqGQgTN-?LPJ;wU67ND-kr;gcbQOW97%5kD=>!xM4@0kI+MI-C;^j>|omaXM z9_6LLY^Xzisz)xwnit1rpi`Q1Bn-rFOYVup%X4__c()MsNwW>S~G2n{d^m;o0 zF7ZYfO3o_tl7)pYM{x+FE09#I3k^kUu#_d_oas0=O8kX0jSIKgxh?v;XOOa|d%l$) z;Tsn1XQ7vB^hRF=RQWM}di>yOgZs6^m7aH;VEFacAho zqtQ#I((y$3MNlmTT$BcJCMOm_a@y+^aK&WCBWZZwLvqoa??BF23v2o^$Yfg*Sw8fU z8vU(JqAeW${P~7Rb-`Q%mmKYbZCI(8j6X+GVkol6040iY!Tnmwgq{chwcjs#i;Fui@f2#fov}Q z==N~-A6$Zxl@F%u^RY?l8h-+M4-Dl}t3y`{t!6STHJ&TpitE@{DD$UjgT z^h$MJy7D|cpqlYU7$POpg7O;1pa)_vD#`ke5&&;o$t6`dft6P9!V|bk>tvpg+fZ+ z(k0P!g%g_p8^c-gEHyfogt`_L>Qp>Bn2YO zT>{Fug!OAo?jMDLF;2H6Hx_nEX0L4POX0U2`fpxyM7;^6;jsA&5)QPu(l70T^khve zxZo_1it3<$u@U7>2Il~yXLC200_za>GJ?RM4{N^iOD5Zg@@BIEg;M&Eq`bFFS+VhC z7-T%Zd?TXC3FImQF8CK)n^9D$5{|9ly*cq?~tOQ&-|C^GwJr|BuRUu(^y`x zlu_wQ1T-c|N+4Y;;n3+kFcd+GpGM_^Mbgh63U>?bz7;=?dmgwJyYYRO3rqlB)`B%pV21E7?`5TXLNT`DEcN_U92w!SbQt;kBnmtB3Md}s zPieutDd!e{NZhZ8<4**~8I9uhaN$^$qx>nIj`TF{ZyfGihvS&9;m&tBR-5L#uS@Uu z#PKJR?`tkxnD74*$Dc^JKM*Hz_j2j|A94JNg!`kzh3Wmt;lj9ocDRtfzYr(93d8-C zIR2F0{(H;6zY)iuNWQX45?q+?KU}yly?;7fnD4(F?f_S|Z4Ni)cq*$T$>Y5aC#xjE zUE*+4xC#;62OZ9HxGNk^R#Ou08i$kBl;Cc3I9W{z?z6v-899*El;F0wa4DC^oer0F zxO*K=R$UVJGY%)KF2OzIaI)$W+#?Prt1iKP!{KDrC3O_acr)|mPo$1yRV}!%j$~CW zxUi07RV}!%j$~CWxUi07RV}!%j$~CWxUi16sxCbSa-*YaMPjar}w!s?*^@GP)ctBx7fX3wbri z;X+>R;&36a<~m%+t8Rx2<#`@(J%9IxV=0}|(yi4dV^)gYjMGu3iFY%$=}4gS(>B=d zznN?@Q`zX~up2UisYRJ^1_!+f<3$g%4|Q=8Ag)*p1q3-HNGJ>m;w_L43FnkvB~gMe zK7;PJ)Uto<++YuM^gW}cj;n3G?}~Iv`#UgTveXaELVw;&J+iLZr=aX+&8|b!C=2e? z@EZt)Q-ORn=>##YEoj<)>|~@}t1HjheV4Wcvm^U1RZ8=T_$Fx^_96*oV17!g<^~1{ zEpR3G^AuLqoZ^bVz$`%3aJJmbLS2OyDK`sQexEJ(2tI;~2rc(UPnwNM-+c#KOvs5| zmRVN+?`O~>?ZhBnt^lrxH;5A#Q)#*~H*j^ZJfy{s(83o5%9k@q)&Xs;1(z9GPIj~i zCZwg0v^dp=EZ!=D0|8Yo*Oco~M+Y(Q;dq$s@DhAu%L?k6P0Y<3o&f|FMyYi9YDG}7 zipOlQat}@iKWsR&1sJS`WNXmvG#b^}T!{qB-PGCK&DnG~C7zFRU|LH@=TZGE7|ywv zDYN)zp?Bqn*cYrw2o#eTwi**_Am zZ)IP_%`iCXuqsP{OU_)~m_N`@OEyh)@)$x(JQFX}L(^B;4g2#fZ@O-H8QD6` zGr@yTqeHChk1%3mU0E~mX;vkRM3=>&OVa6*T)HHYF3F-xQs^>6x=d8h9#|oHhLgYk4$Rm{HU2QO!q}c@(`)i~g+=Pn210jqJ z6E^@-#Q@>NJdH2$2A`64I4;e5P+jBFyc@{ZNt5xA=0eTX5AIPlj*uWlL6=j!9#g3G z2g6#s8>0^m`<43Ie&uVRn=^!C-KhK(V!jt`Cp^-ADyLv(UWg5g5(1TXLurej01cI; zyxF;psk2a3+(VpGV+XFD!sWWS^CPsSe@<-ue#z8ev7LaO2iw= zg@NacfUeev zjB&pvyWj*nZs~&!*dRz&mZQjdboyp>Cqv9Zz-fPD~Tc^4q3d&L$8krBPg&D9~=*rU((AZ*PL~paLD|Lm6lo%V@WU#=w zFGf&gp@o)Sz%7(yNMAbwg^ghZ+u>BJ|Xv9 zgSC9c$JO9VtOw+0=WQ#){m|=D6mfy*q17Lw+bF zM}wp@Zd4=py*qj7T<~F(vvMekl3%Fk++c`5jQH85Px-NP)pG)MKuQloespVns(=}S z>4xPTfEQaDd)pn_4~HuzW`@3#Y4>a~g3nVr#B1l2Lsvy_fwXf%mC~P8D-rEHUJMmJy#G$O8322qdHw_tKL2Zz(?o!ngfG9v5#ex1b`(|-~I4_7^D&WeO{ z1xAg|75vy4NE=gwOsVz5n7~|zL9kv^D;%Nj#=`PsXP27U!i142WH-xXVP38w-Yx2g`33;?bl;kYhQLe0oZ z5GA})TO=|sGTH=F+KL1@*&Cq?&UcaX1#%vTi1QSSNE)0!twOu8^QR?XjhR0MX+->w z=TE%p!p$(`6Wl`gywY(<(+e7rT2)61bw<5hj9F+{R9u3OKp@D1C-zP+;rJTR1Fstn zG$%Xj`|C!BP_X3Ye4uk9ET{u?Loh@xf2=qv$`^9Z5 zeNWu>(ktS2l$x)h|BTXHac7oRiaV=xj<~Z+qvCefl{V`~ed!T?jFFJAa$8t;SfRF> z%B5S|olR$$?R00h?Z+#}A#I$nIi6W)+hASsFFGG@!xrwj8)Qq`;3-FvV*V3NY6Q zV})%|dy{R{I+!%fd+CU7~sbqNu>&P3Pj_QK~5w8|+r@mDGHxn_+y=Y>=eQaXr z*Om+>&LAl_Tt7>6EmjnykmzHaCy z$NRlhF32OD$|)c#zcUvK)WepaB5oaVO>QmmHh^Z`%8BFP4ZOU=1psQ5(J>w|29!$- zNf{!RF41%aZ)esVt{5PkSuo|U4`jm`l0q?ae)uSJ0qLApyIPz8A&Tn1kR?fn=@=WI+wR=hJ z|Jl6@S#C*Qb{QZT$|jKcsJ%mECcKxFF9;4*b*P?@?jSgk5{3E2YG zNV{PjU9fJvYD5A?m#zCVCZ;z>x#^7r(X+QlK&FmnyC`Evr@nGFa<)vaoCCmIV9K`} zZ3(sUP`WW(s(79@+g3QeKbc6+(HZMLQ0LcnU*VXE^Rm+@$&&M*>C_)-K6RRp(;EGP zbxT_kYv3L`QF`SU&~-vrf&ldm`5CDqT26gqGA(*dO*E~En$^Jz(ba)HszX|OcwU%R zk#=3C07J#`4RPu*-2N`?`Gq1xBoQk#QCqpAhRSHm+eoBFEZ~ z!<{K}Pc!r}Js-QR7`pM#((#|)oY+_#qR6$T{3<)i5dr`_!^NWkf+JD#?%vG2(js&% zr7s?b<;W7$;RYP(RHd1X#npqxDy>7%t}&$V2mRbb{Se;KEZqsDMnxr)L{_#$K1bmW zK4B)j5Okyu_hMHgF$`OFoYine+z!L|SIW+{OC7~G;Jxf+NyM;4-yPpG@K2Yx`~b#Z zW*uA#qSd0H43PP}_4qz`AR~c}iuq~ABZ9L7FY6$IeVQT2svrCXeQ$5t5B?0daGu{e zKR5tiSP{(6;q~AVO!6E2;21C!6Pvft>FMkebfce5%A{0$aW`E3W%uPfC@A+sIu}c7 z*#$VKufg(o)j2p8=^#SqR7o+7^QwO`RQ!=toNcMV4fI5Iert#1*OK%zFmQDg;%2Y} zQe4WB(hq%-NxK0`nwPF|-vBt*VmzmH~0DLIZT;3DqixVm1exP3wvf_$?WGgodqU;7<6E~e8$9(_dRY*&K{~y z&Yu5ks2iL8cvtM-F?r-26GIa`p$T85@-p0an@Mz0dV>SdQQnk44E17@?bBpik|tmhmSP9m>UaZQ9naompBpy;2478K-zFh< z#GgXlY1!+Q^&FTo$uvsV*!6Gl`KDCo)FC{NFm?W`c$0f-60dFp+c`Bg#CNxDQvHg~ zJrk4zJuAaA5w`!A!|x|R-omxVY75DE5rM2WX%)3ep)!aAi710MuoDWELB}J7ir}Kk zAQ@V4s=U7VX>=Uwk0FcU0>;pFQmD(JM^u-i`##4*j_exY0Z5bG_hIP&SZ@KrVV|=I z^0|icX_h=@DJ`d*Mv$&56tS^4B-=A_1P421_+RuheAsDO)rzc;Oyk*RJU?hRXf8lX zJr1f663wA*{WMMUBL<$9LPR!1lch|_s9WePJ%M5ysOs9c*`CY9&5jT=wDUufChg-H zeB}B%OH;2Gb7ur6OeA3vQ938NXs$Wp)VW`X{T8H$b zE8srNb|C5K5p0vz&gy1$9~yUdunJ=0k>@|TK_rR3g2`QEPe|qRt(2ePSz*(XO?Ewt zbT*6U$;+yG7U?V^wxeJWdi~fzaAn>l?1kYpR2CX;5*3(1uZf3`N0Crv6m()E5TLB- z-c7Kfv=S9;BqCe65IEH*z^rlX1Sjh%+3i)QA12UYW29K;r*y-Cl4LaGk>@ruNVW_A z*sjEI8w2BU_m4JHs#ZD`7cW~%HN zQ0HRQk1!E4Gvgj{2A#@@tQT23R4#@u%<#OQj?_M02i9&$#0YYQOTmV-??WIC{`pR` z^D&rpyL;@uD>i`99O?0ohSCNnA;v=tU`(B&ORmN|_B2+a9}ZpSLZTf&Tix*g2#^ENP`ckGikB@xSQe&Laq@(p8SLEA#VOO+W+$8@ENY#sjMBPQ za>>^EGxR3e#+eO2BA%+KOgI}=^ckgTWY7R)VHw_xs=NnrN&Tqc18?u~5xu&gRz8dr9F zgkff5+OF|1Q@3M$XV?+xdDwTeaadJISs)qvu!OGaW2pXX>)X|I3_E;SOyukm5bD&< zqLH{17?Tt(cGOL zgDXLdj(L9U8I&i~A7gP$&usmo2Y@%tzUBmqUGK3=rzV-Jh{N%J68`gLr&T6j@gAK<-nT> zp69?@2;S(xEd;;nz*`Bv;=tPoHs543`WV689e6vz;~aPg!7&HkN$_?Dew^TA4!n!t z-yL{2!I>Ym8Qnv0g#+&;c%}nCN$@fU-be5*2Y!m+w;gyt!B-vlX@d1P+l)Q~aFn$= z_E~4;WV_)PKOF8x> zezAaKU&fF2b5rCD!71$Hhv$UdyJ)8|S87b@sl83?=yenjYewa3iGXT)sZ&i$5Q?>! zD_)6v%rVHfUz~#9Kx`T7TH^Q^&Yp0*6@PgXWN_!wLU00?P(?9vBHCi<=#`NF6QP}z zM$n@Hawfp+lU>-g69jf+*Sc`*+9fQUj_WtD1a8o~o{*f4f$BLn{>W)T4&tNHD9nwH{MD$A+^xTqhL%`IuhM`Hp5(Km zm%Qu=8ZPMQ5zgD`9hs?v9_sYp4);{@yEqMcA9F^z2o)3P+CNqN5I!2y!_Bz3UT99Q zrz`tEP5GJWVQQ&>{TN+2@79!0miL6pc0*DpxTYizR+Yq_$!v)ocA}Pnhhu|Vz`o#C z)+fMn2R`D#Z3NGBtL^Wg1XybGbYkT(78{3vxkjgJ8#g1KyG{p{MqT@1L@GOWB1WK|bG#TTx?=P7Kfj=0DYCrt83Op^VUA3Fzx>)DkJ{oo6z}*_g}kHGZ7OaaOFs{)R{!3-NLSZPhD=~IcT$z(EIq<;mfn$ZcDr+{0WbKQ4;fqsLBxJ`*OWAN zuukV8s@cZ)A8M!1Fq3Y?gwOM2m(lfla*s!NFYO^-=#CdxBEZbSnepNg_*l=QUvfUH z^v4<4^?VOHZrC9I5d~DV?^r7T3Y0@AUuF5`;m$(vV`g!(H`6cpK29PsV)=03f@kR= zeG50EYW1r--nT$UwPHd=t5f<~bt-9t(ibO`4@TZXBUZ}ATP*oeb?PTrON>BQN&`JP z-5O5V`NIh)!&Px6ek=9UqXWBXwiHlopfuP`pXPf`mRu4bF_&4h0{4}9}dG(=bja{ z@cQebh5X_cSWJPL`qIj?y>m4fX4Lf?u;pPqnzrDSfy59Gi1V+(8Hn?5;Ha!}jWE6s zjZSGM*b8t2I_Gq9mPiw}LtluNj39B$my#!N$qRl7F36UShK9GfISydv09 zGdB6fI-c#&xN;{-{n1UmMtV?O36HTs!U$vwX?2okhh!71R&cB!|I0hhar7 zyz)Yf?UmnyNP9SHmN^{7X$KBR(JLia35$CosOi$5tLoCVKW~fS)sK24A9K66IW#m?c?}ejIJG_X52oOX z8hETj34yZERkL*%g|;D}oXSB>DupNq_vH_OtM}s=8AlUFcr=05ie^V`6=gp}eD^_k zckYQJQx4jHEugYcVbU8g>5aQ-=WYL{9fiZn(K~Q%*66m$(WU@lm!YU`+ren z^ay_-kHTf8V$1*H#C&2eStE&;jCucs^`rD zh7=dAPSO&rwmSB&i@@t5+tKjuA)D-e8f->PC9k%#tmGK93Td3{+G5q7EI0eHT`A-7 z|B+w^{gIFyUegE@tQ5KC7qh#8P=OLjB)F2)RqA3R5u+tBGCuZjv}8Dia1CJ(wk#~n z>DG=u+1DvvxUTpU>BZ`uE>`a|5V5=%Ex(9egNSufc}}w;fziUsvk)w3VZ)IZT&arAyY)CBNvBM|8;-x}=;gDW*%ZNv|45K(bQb zLbCLcvglXR3(fFm83c!uYVxwWY6Y}V`pq*jESjwEQdwM@W&st-L55jOg+d+E#P*fi ztbL`|J&b}@=Ofvwd1xgrn5g6?fFY;pLs#G@ypbf}Ug*G>hTg!1RaOPtRPaS^7)%2* zG6S;@P@NNwHQVuauiCz2PxcnB@8Aty#!JLjpzkQVzQgQ^+cM9pI)F35c`WH`QXs+} z;h-U4Y;0idUP%K@#bLhD_A|twOW*-WEs>0_b+uS&%=NWcY4#0FQ;D3vzs>U&sZ zLYEP0w$NgB^VzCQB_Y_+bP3J1foX%Ow>q#W%$g#ByP|;$(5xA2DpEPk2*^gtf<_uC zLDm*aN|$A$%hJ##F}lW$iw%kOtd9}q$~EpH8KDFexex?Tjm+Yy5r#4+ph#=EA2sWa zjtsnz?7`VJsH)qhN zIWJ$dFU4kbyf}uX0XgUN-E{p6#2{b4gdQ4W4U2~V7(NTbGYMyf9JUTW=HZv6D++GoCq4p!Dg*bA&&W@V%k$e z1jBvew5o7rbQ1L&!a4R-P>nfuhJ2Pu9=CFW6|6+k@_EoQZVf)z?RvM{$tg+0PK;*~ zBgq-P^WZ^d1|rtR^fjZ72@+`4SYE41&kX&w=|NFwLYN5KH+Khx+}>>bm~_&uQkNda zQiAiOpRQ0H8gr0h2{K~L;Z!fHc9di`T$x7p>AN4$99CMG5tk%|>hIhpz2DOo73lH^OWR{oV+3DFogKGbwa=BTTh`QI5$Jrg|ex1rJJ% zFcCDKYuR{y#pP#cyNF527|gU}3}#|tyNIc2S~C8M86&1BW{jAm-oXlF1`4hCe5CB< zWM{F`!MJ)>uoIuclNtOt&l@4a7iE6D%m+ZrH!n4(iNGKAmEl_=&9<>;*^im_V~+jU z#eQ_zkDd9!Iq{FcuZP(ej4&bcjOmbPOolvTD&!dxAd1lROdm}8I z@Qh)FXG~0Z#?*yT%ub(!s>gX8W};b?H`~Gj%{*l0A?5W(NPmG=ruboSEQ;XxAvoP! zR^wxCC_jU;3}L5MbO-@cW`o|1AFlcL5W?w=UcWV;G+jd81{F`n5zIv} zYYCs;2CuvX9ess>qWt21?D>~)Y0ge15nRBE?E{=>4J!PGel8LF8tUs^s4u)Kg>kUF zKO!|G3Wp@h&FHEdlcNVC#Uc)i)TgrP;tx^#dd>b$Bv3p6fw#_XD&2@?P&dw0g6b=A zEO1$JbVZm7-eZ_3nJzp-@)**|z=%%7c#VvVvZs+Oo`S&%jnI!LQ{~Sg|6r7*5J{D< z*c9Goi)CYT7sqB}jf`3AnppRRu`Ygd?1wAu9W3Di%=!c_&nL7cnP#(rDU!Pd5Zv>$ zH^M5y60Pxe+t6Y4l_>N9tO@i17ldBHMU4I>6ARr%P0&Bq1PyeGE$j1(LE8q@!z?|v zfo4u&(I%}4im(CCL@k~)*l!Aigi!$~%u~0x6t$FH(1w_lj^dkG!znec{vx=5^PVu) zN+0fFn6oJu$l9c@)`6@K-VwQ-4^X(&Z$B3B!wK+!Mazdn4_dVQ*xh0l*^j;K#~%Cu zPgrlfLpH)b#jh*`+AqxZM-RkD@$=v}W?MTYiyy}qdbV6;PZw{B>@OlJ#zYc|YCQdp zuo_ticKIZoT|Q}Lmrvr@<&(BvR5KG~b(0`e_tRk#!Bwnop>ayMUpgWQFEV+)@eFhX z6&ZoKI+5Q_bG8Rn%9^nPz>MuT3v303CA4^!Qp*@!HnXglvudFwXucsCE}P-O(rX&* z-&kELE?hRV&J^OZnKcv@CG0A2?C)7xT6rh9&u#j##Rx-jQC-J1WTRc5Uch?RHN{el zQf5t|=f$pb{PtqYS+V#FXa8%Nyy(waPv<&_Z&+UBAjr_($Q^^%K6Db5eMt-DY;qe{ zmm&5<_DVRn(bVkxRu-$0aMgKn$pb?ObLLfNy2V{s8Vlg6cL?kn1%fEBTNLPt0(k<+ zf|=8mERj0haRyA8zsyk>TIMJW<_unOZ~nXB?N9Lzcf5ErYOl}_W!o+I=-dVK`*<$+ z9kQ!`;VuN4S9%)kfo>QTs|%UGA{;jJBwnkHw`Q;o$~n0;>G`AYBUSwv!+V?j(Mv5* zNU5Qf9)EO06!&*@Ou(IPalO0xqvtrF!yi500Z8s53oyAqgQ_02*n$@pK(4N>T4K#R z$8sDOAfEqA)|vNYxr}W7xD_O9z09SaatV@z#Ow!$duuY6^{cSpI0KRM5(>%E)}rcN z+L5oWlkNhpxI6e$?*;6#di~_7N*DXNeuQH`=gZkoyf4Wo!2`xg79}P*R@h0SYM+M} zL$Y!vk?O{sxR;_K4@XGZU#Q>OgvU2fBAAuNp}-Aff?Z#>w8WZP0WQ=A=LhUh(JrO= z`N2a~(8CIuHcM3o+N>viY$a^7w90Dr0JKZ3dVtR`w7W!>O$T$ZFfaZDG|IA1jJIx2 zEr7D|ZFoLwHOjoo>o66loWfc$+lrjMRMr7uwiOrA%ujS@F(ormXzKK%eL<%z4Harh zOiR?NRq=q~CNhyHZ5mCaLFgsMkNjA+nLkC47}@j#5Xg;}Ll zsDkt)QyUW$_jAfJuR0l}gJ~Fv&Z(Dff-dfbdUt|-&|PBB{!8~k<($U@yGUqesf&k!7GD@3pMC6Y6g(V0_4!>|MRd(gI9b-E;;wWdkped zx|qGc%u@CTlS!`4qQg?pAKA3aOzz)qk{;}bs(2q zr9}=rgy3Ng^4G;lPst!d`$5HE#ppi8xkHC0_g21&|nwyRh8w z440EzXE&5?L;Tca-8@#V!Plh+3CG~Ny4Y-v!Q5wg2R>e zxM8h~-+1ouml%S=@uFw+GzoJ7o8_F+hPK$bew2>V6QE2Fo0;Xb%qV1@a0v#ihJ?(q z8{01Du-%PlkQRWB{N3lUGUsj+T??XBK;)5uy zSQ0{Sk}T4aPy>z#a+=ZTrP4BGN=GIvWR5xU^Mk69PWk1<8e@a|=~%Z!nIcMo+_pH6 z<&#NDYFP(C&gn@D|4bQlINkyNOvG~;@SvoeP$tYW=XYWYs$18m7=Y{Nb&oCsVQMm| zdz77u{buXb?=e{CYpX=rMK)eo7rK(Mt)ga^PhEvuBP`H1tjB58`W-KLI+7{MhBrVym1m$kPX_aUyb8 zRO3X@uBpa}JUzO}i{>Fhbz^ln5yLIjI5MyFg=(BI?~!U8IavBeHBO}bd(}9R-sh`v zBEr9^#)%xq?yKU92=laR9Ba4~RO49xrB&59k^E9MP6T`pz5o#NUZi|ebvO~oJF9Ub zvyWEeM9QD9#)+sWKUGx@X&i-WoV1_C)i`NlM^)pbjhU} zl2>6{5L^w-Y`UdK!A{kXjAERS=3M$FaU`f1;vOT84K{*%oH!C0!97hJiHzW0ImtVB zlte~w&odl}jNo1*?wo4eKZzsZk#P0*1INxUf}2YmD2H%c5EaRhfeacsm9+&771BaYx+B~J8GA@5%!j*U1H zF8*oY*oY&zF5=jTBe+Gxu@OgbClkj;9Kl^e92;>2cMWlD#1Y(m#EF(Ir0<)=u@OhY zZ6%J4ID(6P1~@ii!G&jwF6P`-b+-qFdLY%^KEZjfRr!_$+G1wZn~;)MK8q@yYvl77 zj5d{P`6=2R=(qgXV(5O}&-8T`{(5ns=DiLHSZ&ZNF{cgM!pUy7anZMcN~Mc;Q^#`& ze!5jb1OqQ!D4uR!nb*ukN!6s9ANi(shZNL_c1Y&snYi2^;n~5$z5t%WEV_4hE)6h) z<~hMq)4Awq&qCuYG?&Π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