From bcaf8d26c7b66cf293a7bc644a0d7ec682fbc2e6 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 17 Sep 2019 17:43:32 +0200 Subject: [PATCH] =?UTF-8?q?Readd=20Mirror=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Code/NetMessage.cs | 2 +- Assets/Packages.meta | 8 + Assets/Packages/Mirror.meta | 8 + Assets/Packages/Mirror/Components.meta | 8 + .../Components/Mirror.Components.asmdef | 14 + .../Components/Mirror.Components.asmdef.meta | 7 + .../Mirror/Components/NetworkAnimator.cs | 427 +++++ .../Mirror/Components/NetworkAnimator.cs.meta | 11 + .../Mirror/Components/NetworkLobbyManager.cs | 659 ++++++++ .../Components/NetworkLobbyManager.cs.meta | 11 + .../Mirror/Components/NetworkLobbyPlayer.cs | 155 ++ .../Components/NetworkLobbyPlayer.cs.meta | 11 + .../Components/NetworkProximityChecker.cs | 171 ++ .../NetworkProximityChecker.cs.meta | 11 + .../Mirror/Components/NetworkTransform.cs | 12 + .../Components/NetworkTransform.cs.meta | 11 + .../Mirror/Components/NetworkTransformBase.cs | 447 ++++++ .../Components/NetworkTransformBase.cs.meta | 11 + .../Components/NetworkTransformChild.cs | 16 + .../Components/NetworkTransformChild.cs.meta | 11 + Assets/Packages/Mirror/Editor.meta | 8 + .../Mirror/Editor/Mirror.Editor.asmdef | 16 + .../Mirror/Editor/Mirror.Editor.asmdef.meta | 7 + .../Mirror/Editor/NetworkAnimatorEditor.cs | 5 + .../Editor/NetworkAnimatorEditor.cs.meta | 11 + .../Editor/NetworkBehaviourInspector.cs | 197 +++ .../Editor/NetworkBehaviourInspector.cs.meta | 11 + .../Mirror/Editor/NetworkIdentityEditor.cs | 106 ++ .../Editor/NetworkIdentityEditor.cs.meta | 11 + .../Editor/NetworkInformationPreview.cs | 280 ++++ .../Editor/NetworkInformationPreview.cs.meta | 11 + .../Mirror/Editor/NetworkManagerEditor.cs | 112 ++ .../Editor/NetworkManagerEditor.cs.meta | 11 + .../Mirror/Editor/NetworkScenePostProcess.cs | 90 ++ .../Editor/NetworkScenePostProcess.cs.meta | 11 + .../Mirror/Editor/PreprocessorDefine.cs | 24 + .../Mirror/Editor/PreprocessorDefine.cs.meta | 11 + Assets/Packages/Mirror/Editor/SceneDrawer.cs | 56 + .../Mirror/Editor/SceneDrawer.cs.meta | 11 + Assets/Packages/Mirror/Editor/Weaver.meta | 8 + .../Editor/Weaver/CompilationFinishedHook.cs | 141 ++ .../Weaver/CompilationFinishedHook.cs.meta | 11 + .../Mirror/Editor/Weaver/Extensions.cs | 151 ++ .../Mirror/Editor/Weaver/Extensions.cs.meta | 11 + .../Packages/Mirror/Editor/Weaver/Helpers.cs | 124 ++ .../Mirror/Editor/Weaver/Helpers.cs.meta | 11 + .../Mirror/Editor/Weaver/Mirror.Weaver.asmdef | 14 + .../Editor/Weaver/Mirror.Weaver.asmdef.meta | 7 + .../Mirror/Editor/Weaver/Processors.meta | 8 + .../Weaver/Processors/CommandProcessor.cs | 153 ++ .../Processors/CommandProcessor.cs.meta | 11 + .../Processors/MessageClassProcessor.cs | 135 ++ .../Processors/MessageClassProcessor.cs.meta | 11 + .../Processors/MonoBehaviourProcessor.cs | 77 + .../Processors/MonoBehaviourProcessor.cs.meta | 11 + .../Processors/NetworkBehaviourProcessor.cs | 858 ++++++++++ .../NetworkBehaviourProcessor.cs.meta | 11 + .../Processors/PropertySiteProcessor.cs | 356 +++++ .../Processors/PropertySiteProcessor.cs.meta | 11 + .../Processors/ReaderWriterProcessor.cs | 98 ++ .../Processors/ReaderWriterProcessor.cs.meta | 11 + .../Editor/Weaver/Processors/RpcProcessor.cs | 110 ++ .../Weaver/Processors/RpcProcessor.cs.meta | 11 + .../Processors/SyncDictionaryProcessor.cs | 19 + .../SyncDictionaryProcessor.cs.meta | 11 + .../Weaver/Processors/SyncEventProcessor.cs | 152 ++ .../Processors/SyncEventProcessor.cs.meta | 11 + .../Weaver/Processors/SyncListInitializer.cs | 5 + .../Processors/SyncListInitializer.cs.meta | 11 + .../Weaver/Processors/SyncListProcessor.cs | 18 + .../Processors/SyncListProcessor.cs.meta | 11 + .../Processors/SyncListStructProcessor.cs | 5 + .../SyncListStructProcessor.cs.meta | 11 + .../Processors/SyncObjectInitializer.cs | 89 ++ .../Processors/SyncObjectInitializer.cs.meta | 11 + .../Weaver/Processors/SyncObjectProcessor.cs | 123 ++ .../Processors/SyncObjectProcessor.cs.meta | 11 + .../Weaver/Processors/SyncVarProcessor.cs | 335 ++++ .../Processors/SyncVarProcessor.cs.meta | 11 + .../Weaver/Processors/TargetRpcProcessor.cs | 151 ++ .../Processors/TargetRpcProcessor.cs.meta | 11 + .../Packages/Mirror/Editor/Weaver/Program.cs | 62 + .../Mirror/Editor/Weaver/Program.cs.meta | 11 + .../Packages/Mirror/Editor/Weaver/Readers.cs | 371 +++++ .../Mirror/Editor/Weaver/Readers.cs.meta | 11 + .../Mirror/Editor/Weaver/Resolvers.cs | 141 ++ .../Mirror/Editor/Weaver/Resolvers.cs.meta | 11 + .../Packages/Mirror/Editor/Weaver/Weaver.cs | 598 +++++++ .../Mirror/Editor/Weaver/Weaver.cs.meta | 11 + .../Packages/Mirror/Editor/Weaver/Writers.cs | 350 ++++ .../Mirror/Editor/Weaver/Writers.cs.meta | 11 + Assets/Packages/Mirror/License.txt | 3 + Assets/Packages/Mirror/License.txt.meta | 7 + Assets/Packages/Mirror/Plugins.meta | 8 + .../Packages/Mirror/Plugins/Mono.Cecil.meta | 8 + .../Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll | Bin 0 -> 43520 bytes .../Mono.Cecil/Mono.CecilX.Mdb.dll.meta} | 3 +- .../Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll | Bin 0 -> 87552 bytes .../Mono.Cecil/Mono.CecilX.Pdb.dll.meta | 32 + .../Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll | Bin 0 -> 27648 bytes .../Mono.Cecil/Mono.CecilX.Rocks.dll.meta | 32 + .../Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll | Bin 0 -> 340992 bytes .../Plugins/Mono.Cecil/Mono.CecilX.dll.meta | 32 + Assets/Packages/Mirror/Readme.txt | 15 + Assets/Packages/Mirror/Readme.txt.meta | 7 + Assets/Packages/Mirror/Runtime.meta | 8 + .../Packages/Mirror/Runtime/AssemblyInfo.cs | 3 + .../Mirror/Runtime/AssemblyInfo.cs.meta | 11 + Assets/Packages/Mirror/Runtime/ClientScene.cs | 760 +++++++++ .../Mirror/Runtime/ClientScene.cs.meta | 11 + .../Mirror/Runtime/CustomAttributes.cs | 58 + .../Mirror/Runtime/CustomAttributes.cs.meta | 11 + .../Mirror/Runtime/DotNetCompatibility.cs | 16 + .../Runtime/DotNetCompatibility.cs.meta | 11 + .../Runtime/ExponentialMovingAverage.cs | 38 + .../Runtime/ExponentialMovingAverage.cs.meta | 11 + .../Mirror/Runtime/FloatBytePacker.cs | 60 + .../Mirror/Runtime/FloatBytePacker.cs.meta | 11 + Assets/Packages/Mirror/Runtime/LocalClient.cs | 5 + .../Mirror/Runtime/LocalClient.cs.meta | 11 + .../Mirror/Runtime/LocalConnections.cs | 47 + .../Mirror/Runtime/LocalConnections.cs.meta | 11 + Assets/Packages/Mirror/Runtime/LogFilter.cs | 7 + .../Packages/Mirror/Runtime/LogFilter.cs.meta | 11 + .../Packages/Mirror/Runtime/MessagePacker.cs | 136 ++ .../Mirror/Runtime/MessagePacker.cs.meta | 11 + Assets/Packages/Mirror/Runtime/Messages.cs | 492 ++++++ .../Packages/Mirror/Runtime/Messages.cs.meta | 11 + Assets/Packages/Mirror/Runtime/Mirror.asmdef | 8 + .../Mirror/Runtime/Mirror.asmdef.meta | 7 + .../Mirror/Runtime/NetworkBehaviour.cs | 775 +++++++++ .../Mirror/Runtime/NetworkBehaviour.cs.meta | 11 + .../Packages/Mirror/Runtime/NetworkClient.cs | 464 ++++++ .../Mirror/Runtime/NetworkClient.cs.meta | 11 + .../Mirror/Runtime/NetworkConnection.cs | 375 +++++ .../Mirror/Runtime/NetworkConnection.cs.meta | 11 + .../Mirror/Runtime/NetworkIdentity.cs | 1253 +++++++++++++++ .../Mirror/Runtime/NetworkIdentity.cs.meta | 11 + .../Packages/Mirror/Runtime/NetworkManager.cs | 1102 +++++++++++++ .../Mirror/Runtime/NetworkManager.cs.meta | 11 + .../Mirror/Runtime/NetworkManagerHUD.cs | 128 ++ .../Mirror/Runtime/NetworkManagerHUD.cs.meta | 11 + .../Packages/Mirror/Runtime/NetworkMessage.cs | 25 + .../Mirror/Runtime/NetworkMessage.cs.meta | 11 + .../Packages/Mirror/Runtime/NetworkReader.cs | 355 +++++ .../Mirror/Runtime/NetworkReader.cs.meta | 11 + .../Packages/Mirror/Runtime/NetworkServer.cs | 1414 +++++++++++++++++ .../Mirror/Runtime/NetworkServer.cs.meta | 11 + .../Mirror/Runtime/NetworkStartPosition.cs | 24 + .../Runtime/NetworkStartPosition.cs.meta | 11 + Assets/Packages/Mirror/Runtime/NetworkTime.cs | 177 +++ .../Mirror/Runtime/NetworkTime.cs.meta | 11 + .../Packages/Mirror/Runtime/NetworkWriter.cs | 564 +++++++ .../Mirror/Runtime/NetworkWriter.cs.meta | 11 + .../Mirror/Runtime/NetworkWriterPool.cs | 28 + .../Mirror/Runtime/NetworkWriterPool.cs.meta | 11 + Assets/Packages/Mirror/Runtime/StringHash.cs | 18 + .../Mirror/Runtime/StringHash.cs.meta | 11 + .../Packages/Mirror/Runtime/SyncDictionary.cs | 308 ++++ .../Mirror/Runtime/SyncDictionary.cs.meta | 11 + Assets/Packages/Mirror/Runtime/SyncList.cs | 353 ++++ .../Packages/Mirror/Runtime/SyncList.cs.meta | 11 + Assets/Packages/Mirror/Runtime/SyncObject.cs | 26 + .../Mirror/Runtime/SyncObject.cs.meta | 11 + Assets/Packages/Mirror/Runtime/SyncSet.cs | 330 ++++ .../Packages/Mirror/Runtime/SyncSet.cs.meta | 11 + Assets/Packages/Mirror/Runtime/Transport.meta | 8 + .../Runtime/Transport/LLAPITransport.cs | 323 ++++ .../Runtime/Transport/LLAPITransport.cs.meta | 11 + .../Runtime/Transport/MultiplexTransport.cs | 191 +++ .../Transport/MultiplexTransport.cs.meta | 11 + .../Mirror/Runtime/Transport/Telepathy.meta | 8 + .../Runtime/Transport/Telepathy/Client.cs | 193 +++ .../Transport/Telepathy/Client.cs.meta | 11 + .../Runtime/Transport/Telepathy/Common.cs | 289 ++++ .../Transport/Telepathy/Common.cs.meta | 11 + .../Runtime/Transport/Telepathy/EventType.cs | 9 + .../Transport/Telepathy/EventType.cs.meta | 11 + .../Runtime/Transport/Telepathy/LICENSE | 21 + .../Runtime/Transport/Telepathy/LICENSE.meta | 7 + .../Runtime/Transport/Telepathy/Logger.cs | 15 + .../Transport/Telepathy/Logger.cs.meta | 11 + .../Runtime/Transport/Telepathy/Message.cs | 18 + .../Transport/Telepathy/Message.cs.meta | 11 + .../Telepathy/NetworkStreamExtensions.cs | 55 + .../Telepathy/NetworkStreamExtensions.cs.meta | 11 + .../Runtime/Transport/Telepathy/SafeQueue.cs | 75 + .../Transport/Telepathy/SafeQueue.cs.meta | 11 + .../Runtime/Transport/Telepathy/Server.cs | 286 ++++ .../Transport/Telepathy/Server.cs.meta | 11 + .../Runtime/Transport/Telepathy/Telepathy.dll | Bin 0 -> 4096 bytes .../Transport/Telepathy/Telepathy.dll.meta | 30 + .../Runtime/Transport/Telepathy/Utils.cs | 44 + .../Runtime/Transport/Telepathy/Utils.cs.meta | 11 + .../Runtime/Transport/TelepathyTransport.cs | 180 +++ .../Transport/TelepathyTransport.cs.meta | 11 + .../Mirror/Runtime/Transport/Transport.cs | 190 +++ .../Runtime/Transport/Transport.cs.meta | 11 + .../Mirror/Runtime/Transport/Websocket.meta | 8 + .../Runtime/Transport/Websocket/Client.cs | 191 +++ .../Transport/Websocket/Client.cs.meta | 11 + .../Runtime/Transport/Websocket/ClientJs.cs | 118 ++ .../Transport/Websocket/ClientJs.cs.meta | 11 + .../Transport/Websocket/Ninja.WebSockets.meta | 8 + .../Websocket/Ninja.WebSockets/BufferPool.cs | 250 +++ .../Ninja.WebSockets/BufferPool.cs.meta | 11 + .../Ninja.WebSockets/Exceptions.meta | 8 + .../Exceptions/EntityTooLargeException.cs | 26 + .../EntityTooLargeException.cs.meta | 11 + .../InvalidHttpResponseCodeException.cs | 35 + .../InvalidHttpResponseCodeException.cs.meta | 11 + .../Ninja.WebSockets/Exceptions/README.txt | 1 + .../Exceptions/README.txt.meta | 7 + .../SecWebSocketKeyMissingException.cs | 25 + .../SecWebSocketKeyMissingException.cs.meta | 11 + .../ServerListenerSocketException.cs | 24 + .../ServerListenerSocketException.cs.meta | 11 + .../WebSocketBufferOverflowException.cs | 22 + .../WebSocketBufferOverflowException.cs.meta | 11 + .../WebSocketHandshakeFailedException.cs | 23 + .../WebSocketHandshakeFailedException.cs.meta | 11 + .../WebSocketVersionNotSupportedException.cs | 23 + ...SocketVersionNotSupportedException.cs.meta | 11 + .../Websocket/Ninja.WebSockets/HttpHelper.cs | 202 +++ .../Ninja.WebSockets/HttpHelper.cs.meta | 11 + .../Websocket/Ninja.WebSockets/IBufferPool.cs | 17 + .../Ninja.WebSockets/IBufferPool.cs.meta | 11 + .../Ninja.WebSockets/IPingPongManager.cs | 24 + .../Ninja.WebSockets/IPingPongManager.cs.meta | 11 + .../IWebSocketClientFactory.cs | 45 + .../IWebSocketClientFactory.cs.meta | 11 + .../IWebSocketServerFactory.cs | 40 + .../IWebSocketServerFactory.cs.meta | 11 + .../Websocket/Ninja.WebSockets/Internal.meta | 8 + .../Internal/BinaryReaderWriter.cs | 149 ++ .../Internal/BinaryReaderWriter.cs.meta | 11 + .../Ninja.WebSockets/Internal/Events.cs | 393 +++++ .../Ninja.WebSockets/Internal/Events.cs.meta | 11 + .../Internal/WebSocketFrame.cs | 31 + .../Internal/WebSocketFrame.cs.meta | 11 + .../Internal/WebSocketFrameCommon.cs | 62 + .../Internal/WebSocketFrameCommon.cs.meta | 11 + .../Internal/WebSocketFrameReader.cs | 168 ++ .../Internal/WebSocketFrameReader.cs.meta | 11 + .../Internal/WebSocketFrameWriter.cs | 100 ++ .../Internal/WebSocketFrameWriter.cs.meta | 11 + .../Internal/WebSocketImplementation.cs | 628 ++++++++ .../Internal/WebSocketImplementation.cs.meta | 11 + .../Internal/WebSocketOpCode.cs | 12 + .../Internal/WebSocketOpCode.cs.meta | 11 + .../Websocket/Ninja.WebSockets/LICENCE.md | 21 + .../Ninja.WebSockets/LICENCE.md.meta | 7 + .../Ninja.WebSockets/PingPongManager.cs | 139 ++ .../Ninja.WebSockets/PingPongManager.cs.meta | 11 + .../Ninja.WebSockets/PongEventArgs.cs | 26 + .../Ninja.WebSockets/PongEventArgs.cs.meta | 11 + .../Ninja.WebSockets/Properties.meta | 8 + .../Properties/PublishProfiles.meta | 8 + .../PublishProfiles/FolderProfile.pubxml | 13 + .../PublishProfiles/FolderProfile.pubxml.meta | 7 + .../WebSocketClientFactory.cs | 288 ++++ .../WebSocketClientFactory.cs.meta | 11 + .../WebSocketClientOptions.cs | 66 + .../WebSocketClientOptions.cs.meta | 11 + .../Ninja.WebSockets/WebSocketHttpContext.cs | 49 + .../WebSocketHttpContext.cs.meta | 11 + .../WebSocketServerFactory.cs | 169 ++ .../WebSocketServerFactory.cs.meta | 11 + .../WebSocketServerOptions.cs | 45 + .../WebSocketServerOptions.cs.meta | 11 + .../Runtime/Transport/Websocket/Plugins.meta | 8 + .../Websocket/Plugins/WebSocket.jslib | 108 ++ .../Websocket/Plugins/WebSocket.jslib.meta | 34 + .../Runtime/Transport/Websocket/Server.cs | 344 ++++ .../Transport/Websocket/Server.cs.meta | 11 + .../Transport/Websocket/WebsocketTransport.cs | 132 ++ .../Websocket/WebsocketTransport.cs.meta | 11 + Assets/Packages/Mirror/Runtime/UNetwork.cs | 107 ++ .../Packages/Mirror/Runtime/UNetwork.cs.meta | 11 + Assets/Telepathy.dll | Bin 14336 -> 0 bytes 280 files changed, 24425 insertions(+), 3 deletions(-) create mode 100644 Assets/Packages.meta create mode 100644 Assets/Packages/Mirror.meta create mode 100644 Assets/Packages/Mirror/Components.meta create mode 100644 Assets/Packages/Mirror/Components/Mirror.Components.asmdef create mode 100644 Assets/Packages/Mirror/Components/Mirror.Components.asmdef.meta create mode 100644 Assets/Packages/Mirror/Components/NetworkAnimator.cs create mode 100644 Assets/Packages/Mirror/Components/NetworkAnimator.cs.meta create mode 100644 Assets/Packages/Mirror/Components/NetworkLobbyManager.cs create mode 100644 Assets/Packages/Mirror/Components/NetworkLobbyManager.cs.meta create mode 100644 Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs create mode 100644 Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs.meta create mode 100644 Assets/Packages/Mirror/Components/NetworkProximityChecker.cs create mode 100644 Assets/Packages/Mirror/Components/NetworkProximityChecker.cs.meta create mode 100644 Assets/Packages/Mirror/Components/NetworkTransform.cs create mode 100644 Assets/Packages/Mirror/Components/NetworkTransform.cs.meta create mode 100644 Assets/Packages/Mirror/Components/NetworkTransformBase.cs create mode 100644 Assets/Packages/Mirror/Components/NetworkTransformBase.cs.meta create mode 100644 Assets/Packages/Mirror/Components/NetworkTransformChild.cs create mode 100644 Assets/Packages/Mirror/Components/NetworkTransformChild.cs.meta create mode 100644 Assets/Packages/Mirror/Editor.meta create mode 100644 Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef create mode 100644 Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef.meta create mode 100644 Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs create mode 100644 Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs create mode 100644 Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs create mode 100644 Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs create mode 100644 Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs create mode 100644 Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs create mode 100644 Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/PreprocessorDefine.cs create mode 100644 Assets/Packages/Mirror/Editor/PreprocessorDefine.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/SceneDrawer.cs create mode 100644 Assets/Packages/Mirror/Editor/SceneDrawer.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Extensions.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Extensions.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Helpers.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Helpers.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Program.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Program.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Readers.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Readers.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Weaver.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Weaver.cs.meta create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Writers.cs create mode 100644 Assets/Packages/Mirror/Editor/Weaver/Writers.cs.meta create mode 100644 Assets/Packages/Mirror/License.txt create mode 100644 Assets/Packages/Mirror/License.txt.meta create mode 100644 Assets/Packages/Mirror/Plugins.meta create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil.meta create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll rename Assets/{Telepathy.dll.meta => Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta} (90%) create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll.meta create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll create mode 100644 Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll.meta create mode 100644 Assets/Packages/Mirror/Readme.txt create mode 100644 Assets/Packages/Mirror/Readme.txt.meta create mode 100644 Assets/Packages/Mirror/Runtime.meta create mode 100644 Assets/Packages/Mirror/Runtime/AssemblyInfo.cs create mode 100644 Assets/Packages/Mirror/Runtime/AssemblyInfo.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/ClientScene.cs create mode 100644 Assets/Packages/Mirror/Runtime/ClientScene.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/CustomAttributes.cs create mode 100644 Assets/Packages/Mirror/Runtime/CustomAttributes.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs create mode 100644 Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs create mode 100644 Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/FloatBytePacker.cs create mode 100644 Assets/Packages/Mirror/Runtime/FloatBytePacker.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/LocalClient.cs create mode 100644 Assets/Packages/Mirror/Runtime/LocalClient.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/LocalConnections.cs create mode 100644 Assets/Packages/Mirror/Runtime/LocalConnections.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/LogFilter.cs create mode 100644 Assets/Packages/Mirror/Runtime/LogFilter.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/MessagePacker.cs create mode 100644 Assets/Packages/Mirror/Runtime/MessagePacker.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Messages.cs create mode 100644 Assets/Packages/Mirror/Runtime/Messages.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Mirror.asmdef create mode 100644 Assets/Packages/Mirror/Runtime/Mirror.asmdef.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkClient.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkClient.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkConnection.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkConnection.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkIdentity.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkIdentity.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkManager.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkManager.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkMessage.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkMessage.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkReader.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkReader.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkServer.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkServer.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkTime.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkTime.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkWriter.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkWriter.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs create mode 100644 Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/StringHash.cs create mode 100644 Assets/Packages/Mirror/Runtime/StringHash.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/SyncDictionary.cs create mode 100644 Assets/Packages/Mirror/Runtime/SyncDictionary.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/SyncList.cs create mode 100644 Assets/Packages/Mirror/Runtime/SyncList.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/SyncObject.cs create mode 100644 Assets/Packages/Mirror/Runtime/SyncObject.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/SyncSet.cs create mode 100644 Assets/Packages/Mirror/Runtime/SyncSet.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Telepathy.dll create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Telepathy.dll.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Utils.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Telepathy/Utils.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Transport.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Transport.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs create mode 100644 Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs.meta create mode 100644 Assets/Packages/Mirror/Runtime/UNetwork.cs create mode 100644 Assets/Packages/Mirror/Runtime/UNetwork.cs.meta delete mode 100644 Assets/Telepathy.dll diff --git a/Assets/Code/NetMessage.cs b/Assets/Code/NetMessage.cs index 287ccce..435aad9 100644 --- a/Assets/Code/NetMessage.cs +++ b/Assets/Code/NetMessage.cs @@ -1,4 +1,4 @@ -using Telepathy; +using Mirror; namespace NetMessage diff --git a/Assets/Packages.meta b/Assets/Packages.meta new file mode 100644 index 0000000..b4e5532 --- /dev/null +++ b/Assets/Packages.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d2723bee01450514382d1e32aa01f968 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror.meta b/Assets/Packages/Mirror.meta new file mode 100644 index 0000000..a7a3dd0 --- /dev/null +++ b/Assets/Packages/Mirror.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5cf8eb36be0834b3da408c694a41cb88 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components.meta b/Assets/Packages/Mirror/Components.meta new file mode 100644 index 0000000..c2771d9 --- /dev/null +++ b/Assets/Packages/Mirror/Components.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9bee879fbc8ef4b1a9a9f7088bfbf726 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/Mirror.Components.asmdef b/Assets/Packages/Mirror/Components/Mirror.Components.asmdef new file mode 100644 index 0000000..a61c7db --- /dev/null +++ b/Assets/Packages/Mirror/Components/Mirror.Components.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Mirror.Components", + "references": [ + "Mirror" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Components/Mirror.Components.asmdef.meta b/Assets/Packages/Mirror/Components/Mirror.Components.asmdef.meta new file mode 100644 index 0000000..263b6f0 --- /dev/null +++ b/Assets/Packages/Mirror/Components/Mirror.Components.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 72872094b21c16e48b631b2224833d49 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/NetworkAnimator.cs b/Assets/Packages/Mirror/Components/NetworkAnimator.cs new file mode 100644 index 0000000..e09c30c --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkAnimator.cs @@ -0,0 +1,427 @@ +using UnityEngine; +using UnityEngine.Serialization; + +namespace Mirror +{ + /// + /// A component to synchronize Mecanim animation states for networked objects. + /// + /// + /// The animation of game objects can be networked by this component. There are two models of authority for networked movement: + /// If the object has authority on the client, then it should animated locally on the owning client. The animation state information will be sent from the owning client to the server, then broadcast to all of the other clients. This is common for player objects. + /// 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 the animation parameters that are checked in the inspector view. It does not automatically sychronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients. + /// + [DisallowMultipleComponent] + [AddComponentMenu("Network/NetworkAnimator")] + [RequireComponent(typeof(NetworkIdentity))] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkAnimator.html")] + public class NetworkAnimator : NetworkBehaviour + { + /// + /// The animator component to synchronize. + /// + [FormerlySerializedAs("m_Animator")] + public Animator animator; + + // Note: not an object[] array because otherwise initialization is real annoying + int[] lastIntParameters; + float[] lastFloatParameters; + bool[] lastBoolParameters; + AnimatorControllerParameter[] parameters; + + int[] animationHash; // multiple layers + int[] transitionHash; + float sendTimer; + + bool sendMessagesAllowed + { + get + { + if (isServer) + { + if (!localPlayerAuthority) + return true; + + // This is a special case where we have localPlayerAuthority set + // on a NetworkIdentity but we have not assigned the client who has + // authority over it, no animator data will be sent over the network by the server. + // + // So we check here for a clientAuthorityOwner and if it is null we will + // let the server send animation data until we receive an owner. + if (netIdentity != null && netIdentity.clientAuthorityOwner == null) + return true; + } + + return hasAuthority; + } + } + + void Awake() + { + // 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 + parameters = animator.parameters; + lastIntParameters = new int[parameters.Length]; + lastFloatParameters = new float[parameters.Length]; + lastBoolParameters = new bool[parameters.Length]; + + animationHash = new int[animator.layerCount]; + transitionHash = new int[animator.layerCount]; + } + + void FixedUpdate() + { + if (!sendMessagesAllowed) + return; + + CheckSendRate(); + + for(int i = 0; i < animator.layerCount; i++) + { + int stateHash; + float normalizedTime; + if (!CheckAnimStateChanged(out stateHash, out normalizedTime, i)) + { + continue; + } + + NetworkWriter writer = new NetworkWriter(); + WriteParameters(writer); + + SendAnimationMessage(stateHash, normalizedTime, i, writer.ToArray()); + } + } + + bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layerId) + { + stateHash = 0; + normalizedTime = 0; + + if (animator.IsInTransition(layerId)) + { + AnimatorTransitionInfo tt = animator.GetAnimatorTransitionInfo(layerId); + if (tt.fullPathHash != transitionHash[layerId]) + { + // first time in this transition + transitionHash[layerId] = tt.fullPathHash; + animationHash[layerId] = 0; + return true; + } + return false; + } + + AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(layerId); + if (st.fullPathHash != animationHash[layerId]) + { + // first time in this animation state + if (animationHash[layerId] != 0) + { + // came from another animation directly - from Play() + stateHash = st.fullPathHash; + normalizedTime = st.normalizedTime; + } + transitionHash[layerId] = 0; + animationHash[layerId] = st.fullPathHash; + return true; + } + return false; + } + + void CheckSendRate() + { + if (sendMessagesAllowed && syncInterval != 0 && sendTimer < Time.time) + { + sendTimer = Time.time + syncInterval; + + NetworkWriter writer = new NetworkWriter(); + if (WriteParameters(writer)) + { + SendAnimationParametersMessage(writer.ToArray()); + } + } + } + + void SendAnimationMessage(int stateHash, float normalizedTime, int layerId, byte[] parameters) + { + if (isServer) + { + RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, parameters); + } + else if (ClientScene.readyConnection != null) + { + CmdOnAnimationServerMessage(stateHash, normalizedTime, layerId, parameters); + } + } + + void SendAnimationParametersMessage(byte[] parameters) + { + if (isServer) + { + RpcOnAnimationParametersClientMessage(parameters); + } + else if (ClientScene.readyConnection != null) + { + CmdOnAnimationParametersServerMessage(parameters); + } + } + + void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, NetworkReader reader) + { + if (hasAuthority) + return; + + // usually transitions will be triggered by parameters, if not, play anims directly. + // NOTE: this plays "animations", not transitions, so any transitions will be skipped. + // NOTE: there is no API to play a transition(?) + if (stateHash != 0) + { + animator.Play(stateHash, layerId, normalizedTime); + } + + ReadParameters(reader); + } + + void HandleAnimParamsMsg(NetworkReader reader) + { + if (hasAuthority) + return; + + ReadParameters(reader); + } + + void HandleAnimTriggerMsg(int hash) + { + animator.SetTrigger(hash); + } + + ulong NextDirtyBits() + { + ulong dirtyBits = 0; + for (int i = 0; i < parameters.Length; i++) + { + AnimatorControllerParameter par = parameters[i]; + bool changed = false; + if (par.type == AnimatorControllerParameterType.Int) + { + int newIntValue = animator.GetInteger(par.nameHash); + changed = newIntValue != lastIntParameters[i]; + if (changed) + { + lastIntParameters[i] = newIntValue; + } + } + else if (par.type == AnimatorControllerParameterType.Float) + { + float newFloatValue = animator.GetFloat(par.nameHash); + changed = Mathf.Abs(newFloatValue - lastFloatParameters[i]) > 0.001f; + if (changed) + { + lastFloatParameters[i] = newFloatValue; + } + } + else if (par.type == AnimatorControllerParameterType.Bool) + { + bool newBoolValue = animator.GetBool(par.nameHash); + changed = newBoolValue != lastBoolParameters[i]; + if (changed) + { + lastBoolParameters[i] = newBoolValue; + } + } + if (changed) + { + dirtyBits |= 1ul << i; + } + } + return dirtyBits; + } + + bool WriteParameters(NetworkWriter writer) + { + ulong dirtyBits = NextDirtyBits(); + writer.WritePackedUInt64(dirtyBits); + for (int i = 0; i < parameters.Length; i++) + { + if ((dirtyBits & (1ul << i)) == 0) + continue; + + AnimatorControllerParameter par = parameters[i]; + if (par.type == AnimatorControllerParameterType.Int) + { + int newIntValue = animator.GetInteger(par.nameHash); + writer.WritePackedInt32(newIntValue); + } + else if (par.type == AnimatorControllerParameterType.Float) + { + float newFloatValue = animator.GetFloat(par.nameHash); + writer.WriteSingle(newFloatValue); + } + else if (par.type == AnimatorControllerParameterType.Bool) + { + bool newBoolValue = animator.GetBool(par.nameHash); + writer.WriteBoolean(newBoolValue); + } + } + return dirtyBits != 0; + } + + void ReadParameters(NetworkReader reader) + { + ulong dirtyBits = reader.ReadPackedUInt64(); + for (int i = 0; i < parameters.Length; i++) + { + if ((dirtyBits & (1ul << i)) == 0) + continue; + + AnimatorControllerParameter par = parameters[i]; + if (par.type == AnimatorControllerParameterType.Int) + { + int newIntValue = reader.ReadPackedInt32(); + animator.SetInteger(par.nameHash, newIntValue); + } + else if (par.type == AnimatorControllerParameterType.Float) + { + float newFloatValue = reader.ReadSingle(); + animator.SetFloat(par.nameHash, newFloatValue); + } + else if (par.type == AnimatorControllerParameterType.Bool) + { + bool newBoolValue = reader.ReadBoolean(); + animator.SetBool(par.nameHash, newBoolValue); + } + } + } + + /// + /// Custom Serialization + /// + /// + /// + /// + public override bool OnSerialize(NetworkWriter writer, bool forceAll) + { + if (forceAll) + { + for(int i = 0; i < animator.layerCount; i++) + { + if (animator.IsInTransition(i)) + { + AnimatorStateInfo st = animator.GetNextAnimatorStateInfo(i); + writer.WriteInt32(st.fullPathHash); + writer.WriteSingle(st.normalizedTime); + } + else + { + AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i); + writer.WriteInt32(st.fullPathHash); + writer.WriteSingle(st.normalizedTime); + } + } + WriteParameters(writer); + return true; + } + return false; + } + + /// + /// Custom Deserialization + /// + /// + /// + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + if (initialState) + { + for(int i = 0; i < animator.layerCount; i++) + { + int stateHash = reader.ReadInt32(); + float normalizedTime = reader.ReadSingle(); + animator.Play(stateHash, i, normalizedTime); + } + + ReadParameters(reader); + } + } + + /// + /// Causes an animation trigger to be invoked for a networked object. + /// If local authority is set, and this is called from the client, then the trigger will be invoked on the server and all clients. If not, then this is called on the server, and the trigger will be called on all clients. + /// + /// Name of trigger. + public void SetTrigger(string triggerName) + { + SetTrigger(Animator.StringToHash(triggerName)); + } + + /// + /// Causes an animation trigger to be invoked for a networked object. + /// + /// Hash id of trigger (from the Animator). + public void SetTrigger(int hash) + { + if (hasAuthority && localPlayerAuthority) + { + if (ClientScene.readyConnection != null) + { + CmdOnAnimationTriggerServerMessage(hash); + } + return; + } + + if (isServer && !localPlayerAuthority) + { + RpcOnAnimationTriggerClientMessage(hash); + } + } + + #region server message handlers + [Command] + void CmdOnAnimationServerMessage(int stateHash, float normalizedTime, int layerId, byte[] parameters) + { + if (LogFilter.Debug) Debug.Log("OnAnimationMessage for netId=" + netId); + + // handle and broadcast + HandleAnimMsg(stateHash, normalizedTime, layerId, new NetworkReader(parameters)); + RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, parameters); + } + + [Command] + void CmdOnAnimationParametersServerMessage(byte[] parameters) + { + // handle and broadcast + HandleAnimParamsMsg(new NetworkReader(parameters)); + RpcOnAnimationParametersClientMessage(parameters); + } + + [Command] + void CmdOnAnimationTriggerServerMessage(int hash) + { + // handle and broadcast + HandleAnimTriggerMsg(hash); + RpcOnAnimationTriggerClientMessage(hash); + } + #endregion + + #region client message handlers + [ClientRpc] + void RpcOnAnimationClientMessage(int stateHash, float normalizedTime, int layerId, byte[] parameters) + { + HandleAnimMsg(stateHash, normalizedTime, layerId, new NetworkReader(parameters)); + } + + [ClientRpc] + void RpcOnAnimationParametersClientMessage(byte[] parameters) + { + HandleAnimParamsMsg(new NetworkReader(parameters)); + } + + // server sends this to one client + [ClientRpc] + void RpcOnAnimationTriggerClientMessage(int hash) + { + HandleAnimTriggerMsg(hash); + } + #endregion + } +} diff --git a/Assets/Packages/Mirror/Components/NetworkAnimator.cs.meta b/Assets/Packages/Mirror/Components/NetworkAnimator.cs.meta new file mode 100644 index 0000000..5fb8576 --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkAnimator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f6f3bf89aa97405989c802ba270f815 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/NetworkLobbyManager.cs b/Assets/Packages/Mirror/Components/NetworkLobbyManager.cs new file mode 100644 index 0000000..69039ac --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkLobbyManager.cs @@ -0,0 +1,659 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.Serialization; + +namespace Mirror +{ + /// + /// This is a specialized NetworkManager that includes a networked lobby. + /// + /// + /// The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects. + /// NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager. + /// The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called. + /// + [AddComponentMenu("Network/NetworkLobbyManager")] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkLobbyManager.html")] + public class NetworkLobbyManager : NetworkManager + { + public struct PendingPlayer + { + public NetworkConnection conn; + public GameObject lobbyPlayer; + } + + [Header("Lobby Settings")] + + [FormerlySerializedAs("m_ShowLobbyGUI")] + [SerializeField] + internal bool showLobbyGUI = true; + + [FormerlySerializedAs("m_MinPlayers")] + [SerializeField] + int minPlayers = 1; + + [FormerlySerializedAs("m_LobbyPlayerPrefab")] + [SerializeField] + NetworkLobbyPlayer lobbyPlayerPrefab; + + /// + /// The scene to use for the lobby. This is similar to the offlineScene of the NetworkManager. + /// + [Scene] + public string LobbyScene; + + /// + /// The scene to use for the playing the game from the lobby. This is similar to the onlineScene of the NetworkManager. + /// + [Scene] + public string GameplayScene; + + /// + /// List of players that are in the Lobby + /// + [FormerlySerializedAs("m_PendingPlayers")] + public List pendingPlayers = new List(); + + /// + /// These slots track players that enter the lobby. + /// The slotId on players is global to the game - across all players. + /// + public List lobbySlots = new List(); + + /// + /// True when all players have submitted a Ready message + /// + public bool allPlayersReady; + + public override void OnValidate() + { + // always >= 0 + maxConnections = Mathf.Max(maxConnections, 0); + + // always <= maxConnections + minPlayers = Mathf.Min(minPlayers, maxConnections); + + // always >= 0 + minPlayers = Mathf.Max(minPlayers, 0); + + if (lobbyPlayerPrefab != null) + { + NetworkIdentity identity = lobbyPlayerPrefab.GetComponent(); + if (identity == null) + { + lobbyPlayerPrefab = null; + Debug.LogError("LobbyPlayer prefab must have a NetworkIdentity component."); + } + } + + base.OnValidate(); + } + + internal void ReadyStatusChanged() + { + int CurrentPlayers = 0; + int ReadyPlayers = 0; + + foreach (NetworkLobbyPlayer item in lobbySlots) + { + if (item != null) + { + CurrentPlayers++; + if (item.readyToBegin) + ReadyPlayers++; + } + } + + if (CurrentPlayers == ReadyPlayers) + CheckReadyToBegin(); + else + allPlayersReady = false; + } + + /// + /// + /// + /// Connection of the client + public override void OnServerReady(NetworkConnection conn) + { + if (LogFilter.Debug) Debug.Log("NetworkLobbyManager OnServerReady"); + base.OnServerReady(conn); + + if (conn != null && conn.playerController != null) + { + GameObject lobbyPlayer = conn.playerController.gameObject; + + // if null or not a lobby player, dont replace it + if (lobbyPlayer != null && lobbyPlayer.GetComponent() != null) + SceneLoadedForPlayer(conn, lobbyPlayer); + } + } + + void SceneLoadedForPlayer(NetworkConnection conn, GameObject lobbyPlayer) + { + if (LogFilter.Debug) Debug.LogFormat("NetworkLobby SceneLoadedForPlayer scene: {0} {1}", SceneManager.GetActiveScene().name, conn); + + if (SceneManager.GetActiveScene().name == LobbyScene) + { + // cant be ready in lobby, add to ready list + PendingPlayer pending; + pending.conn = conn; + pending.lobbyPlayer = lobbyPlayer; + pendingPlayers.Add(pending); + return; + } + + GameObject gamePlayer = OnLobbyServerCreateGamePlayer(conn); + if (gamePlayer == null) + { + // get start position from base class + Transform startPos = GetStartPosition(); + gamePlayer = startPos != null + ? Instantiate(playerPrefab, startPos.position, startPos.rotation) + : Instantiate(playerPrefab, Vector3.zero, Quaternion.identity); + gamePlayer.name = playerPrefab.name; + } + + if (!OnLobbyServerSceneLoadedForPlayer(lobbyPlayer, gamePlayer)) + return; + + // replace lobby player with game player + NetworkServer.ReplacePlayerForConnection(conn, gamePlayer); + } + + /// + /// CheckReadyToBegin checks all of the players in the lobby to see if their readyToBegin flag is set. + /// If all of the players are ready, then the server switches from the LobbyScene to the PlayScene - essentially starting the game. This is called automatically in response to NetworkLobbyPlayer.SendReadyToBeginMessage(). + /// + public void CheckReadyToBegin() + { + if (SceneManager.GetActiveScene().name != LobbyScene) return; + + if (minPlayers > 0 && NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.playerController.gameObject.GetComponent().readyToBegin) < minPlayers) + { + allPlayersReady = false; + return; + } + + pendingPlayers.Clear(); + allPlayersReady = true; + OnLobbyServerPlayersReady(); + } + + void CallOnClientEnterLobby() + { + OnLobbyClientEnter(); + foreach (NetworkLobbyPlayer player in lobbySlots) + if (player != null) + { + player.OnClientEnterLobby(); + } + } + + void CallOnClientExitLobby() + { + OnLobbyClientExit(); + foreach (NetworkLobbyPlayer player in lobbySlots) + if (player != null) + { + player.OnClientExitLobby(); + } + } + + #region server handlers + + /// + /// + /// + /// Connection of the client + public override void OnServerConnect(NetworkConnection conn) + { + if (numPlayers >= maxConnections) + { + conn.Disconnect(); + return; + } + + // cannot join game in progress + if (SceneManager.GetActiveScene().name != LobbyScene) + { + conn.Disconnect(); + return; + } + + base.OnServerConnect(conn); + OnLobbyServerConnect(conn); + } + + /// + /// + /// + /// Connection of the client + public override void OnServerDisconnect(NetworkConnection conn) + { + if (conn.playerController != null) + { + NetworkLobbyPlayer player = conn.playerController.GetComponent(); + + if (player != null) + lobbySlots.Remove(player); + } + + allPlayersReady = false; + + foreach (NetworkLobbyPlayer player in lobbySlots) + { + if (player != null) + player.GetComponent().readyToBegin = false; + } + + if (SceneManager.GetActiveScene().name == LobbyScene) + RecalculateLobbyPlayerIndices(); + + base.OnServerDisconnect(conn); + OnLobbyServerDisconnect(conn); + } + + /// + /// + /// + /// Connection of the client + /// + public override void OnServerAddPlayer(NetworkConnection conn, AddPlayerMessage extraMessage) + { + if (SceneManager.GetActiveScene().name != LobbyScene) return; + + if (lobbySlots.Count == maxConnections) return; + + allPlayersReady = false; + + if (LogFilter.Debug) Debug.LogFormat("NetworkLobbyManager.OnServerAddPlayer playerPrefab:{0}", lobbyPlayerPrefab.name); + + GameObject newLobbyGameObject = OnLobbyServerCreateLobbyPlayer(conn); + if (newLobbyGameObject == null) + newLobbyGameObject = (GameObject)Instantiate(lobbyPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity); + + NetworkLobbyPlayer newLobbyPlayer = newLobbyGameObject.GetComponent(); + + lobbySlots.Add(newLobbyPlayer); + + RecalculateLobbyPlayerIndices(); + + NetworkServer.AddPlayerForConnection(conn, newLobbyGameObject); + } + + void RecalculateLobbyPlayerIndices() + { + if (lobbySlots.Count > 0) + { + for (int i = 0; i < lobbySlots.Count; i++) + { + lobbySlots[i].index = i; + } + } + } + + /// + /// + /// + /// + public override void ServerChangeScene(string sceneName) + { + if (sceneName == LobbyScene) + { + foreach (NetworkLobbyPlayer lobbyPlayer in lobbySlots) + { + if (lobbyPlayer == null) continue; + + // find the game-player object for this connection, and destroy it + NetworkIdentity identity = lobbyPlayer.GetComponent(); + + NetworkIdentity playerController = identity.connectionToClient.playerController; + NetworkServer.Destroy(playerController.gameObject); + + if (NetworkServer.active) + { + // re-add the lobby object + lobbyPlayer.GetComponent().readyToBegin = false; + NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, lobbyPlayer.gameObject); + } + } + } + else + { + if (dontDestroyOnLoad) + { + foreach (NetworkLobbyPlayer lobbyPlayer in lobbySlots) + { + if (lobbyPlayer != null) + { + lobbyPlayer.transform.SetParent(null); + DontDestroyOnLoad(lobbyPlayer); + } + } + } + } + + base.ServerChangeScene(sceneName); + } + + /// + /// + /// + /// + public override void OnServerSceneChanged(string sceneName) + { + if (sceneName != LobbyScene) + { + // call SceneLoadedForPlayer on any players that become ready while we were loading the scene. + foreach (PendingPlayer pending in pendingPlayers) + SceneLoadedForPlayer(pending.conn, pending.lobbyPlayer); + + pendingPlayers.Clear(); + } + + OnLobbyServerSceneChanged(sceneName); + } + + /// + /// + /// + public override void OnStartServer() + { + if (string.IsNullOrEmpty(LobbyScene)) + { + Debug.LogError("NetworkLobbyManager LobbyScene is empty. Set the LobbyScene in the inspector for the NetworkLobbyMangaer"); + return; + } + + if (string.IsNullOrEmpty(GameplayScene)) + { + Debug.LogError("NetworkLobbyManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkLobbyMangaer"); + return; + } + + OnLobbyStartServer(); + } + + /// + /// + /// + public override void OnStartHost() + { + OnLobbyStartHost(); + } + + /// + /// + /// + public override void OnStopServer() + { + lobbySlots.Clear(); + base.OnStopServer(); + } + + /// + /// + /// + public override void OnStopHost() + { + OnLobbyStopHost(); + } + + #endregion + + #region client handlers + + /// + /// + /// + public override void OnStartClient() + { + if (lobbyPlayerPrefab == null || lobbyPlayerPrefab.gameObject == null) + Debug.LogError("NetworkLobbyManager no LobbyPlayer prefab is registered. Please add a LobbyPlayer prefab."); + else + ClientScene.RegisterPrefab(lobbyPlayerPrefab.gameObject); + + if (playerPrefab == null) + Debug.LogError("NetworkLobbyManager no GamePlayer prefab is registered. Please add a GamePlayer prefab."); + else + ClientScene.RegisterPrefab(playerPrefab); + + OnLobbyStartClient(); + } + + /// + /// + /// + /// Connection of the client + public override void OnClientConnect(NetworkConnection conn) + { + OnLobbyClientConnect(conn); + CallOnClientEnterLobby(); + base.OnClientConnect(conn); + } + + /// + /// + /// + /// Connection of the client + public override void OnClientDisconnect(NetworkConnection conn) + { + OnLobbyClientDisconnect(conn); + base.OnClientDisconnect(conn); + } + + /// + /// + /// + public override void OnStopClient() + { + OnLobbyStopClient(); + CallOnClientExitLobby(); + + if (!string.IsNullOrEmpty(offlineScene)) + { + // Move the LobbyManager from the virtual DontDestroyOnLoad scene to the Game scene. + // This let's it be destroyed when client changes to the Offline scene. + SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); + } + } + + /// + /// + /// + /// + public override void OnClientChangeScene(string newSceneName) + { + if (LogFilter.Debug) Debug.LogFormat("OnClientChangeScene from {0} to {1}", SceneManager.GetActiveScene().name, newSceneName); + + if (SceneManager.GetActiveScene().name == LobbyScene && newSceneName == GameplayScene && dontDestroyOnLoad && NetworkClient.isConnected) + { + if (NetworkClient.connection != null && NetworkClient.connection.playerController != null) + { + GameObject lobbyPlayer = NetworkClient.connection.playerController.gameObject; + if (lobbyPlayer != null) + { + lobbyPlayer.transform.SetParent(null); + DontDestroyOnLoad(lobbyPlayer); + } + else + Debug.LogWarningFormat("OnClientChangeScene: lobbyPlayer is null"); + } + } + else + if (LogFilter.Debug) Debug.LogFormat("OnClientChangeScene {0} {1}", dontDestroyOnLoad, NetworkClient.isConnected); + } + + /// + /// + /// + /// Connection of the client + public override void OnClientSceneChanged(NetworkConnection conn) + { + if (SceneManager.GetActiveScene().name == LobbyScene) + { + if (NetworkClient.isConnected) + CallOnClientEnterLobby(); + } + else + CallOnClientExitLobby(); + + base.OnClientSceneChanged(conn); + OnLobbyClientSceneChanged(conn); + } + + #endregion + + #region lobby server virtuals + + /// + /// This is called on the host when a host is started. + /// + public virtual void OnLobbyStartHost() { } + + /// + /// This is called on the host when the host is stopped. + /// + public virtual void OnLobbyStopHost() { } + + /// + /// This is called on the server when the server is started - including when a host is started. + /// + public virtual void OnLobbyStartServer() { } + + /// + /// This is called on the server when a new client connects to the server. + /// + /// The new connection. + public virtual void OnLobbyServerConnect(NetworkConnection conn) { } + + /// + /// This is called on the server when a client disconnects. + /// + /// The connection that disconnected. + public virtual void OnLobbyServerDisconnect(NetworkConnection conn) { } + + /// + /// This is called on the server when a networked scene finishes loading. + /// + /// Name of the new scene. + public virtual void OnLobbyServerSceneChanged(string sceneName) { } + + /// + /// This allows customization of the creation of the lobby-player object on the server. + /// By default the lobbyPlayerPrefab is used to create the lobby-player, but this function allows that behaviour to be customized. + /// + /// The connection the player object is for. + /// The new lobby-player object. + public virtual GameObject OnLobbyServerCreateLobbyPlayer(NetworkConnection conn) + { + return null; + } + + /// + /// This allows customization of the creation of the GamePlayer object on the server. + /// By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the lobby-player on the connection. + /// + /// The connection the player object is for. + /// A new GamePlayer object. + public virtual GameObject OnLobbyServerCreateGamePlayer(NetworkConnection conn) + { + return null; + } + + // for users to apply settings from their lobby player object to their in-game player object + /// + /// This is called on the server when it is told that a client has finished switching from the lobby scene to a game player scene. + /// When switching from the lobby, the lobby-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the lobby-player to the game-player object. + /// + /// The lobby player object. + /// The game player object. + /// False to not allow this player to replace the lobby player. + public virtual bool OnLobbyServerSceneLoadedForPlayer(GameObject lobbyPlayer, GameObject gamePlayer) + { + return true; + } + + /// + /// This is called on the server when all the players in the lobby 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 lobby are ready, such as adding a countdown or a confirmation for a group leader. + /// + public virtual void OnLobbyServerPlayersReady() + { + // all players are readyToBegin, start the game + ServerChangeScene(GameplayScene); + } + + #endregion + + #region lobby client virtuals + + /// + /// This is a hook to allow custom behaviour when the game client enters the lobby. + /// + public virtual void OnLobbyClientEnter() { } + + /// + /// This is a hook to allow custom behaviour when the game client exits the lobby. + /// + public virtual void OnLobbyClientExit() { } + + /// + /// This is called on the client when it connects to server. + /// + /// The connection that connected. + public virtual void OnLobbyClientConnect(NetworkConnection conn) { } + + /// + /// This is called on the client when disconnected from a server. + /// + /// The connection that disconnected. + public virtual void OnLobbyClientDisconnect(NetworkConnection conn) { } + + /// + /// This is called on the client when a client is started. + /// + /// The connection for the lobby. + public virtual void OnLobbyStartClient() { } + + /// + /// This is called on the client when the client stops. + /// + public virtual void OnLobbyStopClient() { } + + /// + /// This is called on the client when the client is finished loading a new networked scene. + /// + /// The connection that finished loading a new networked scene. + public virtual void OnLobbyClientSceneChanged(NetworkConnection conn) { } + + /// + /// Called on the client when adding a player to the lobby fails. + /// This could be because the lobby is full, or the connection is not allowed to have more players. + /// + public virtual void OnLobbyClientAddPlayerFailed() { } + + #endregion + + #region optional UI + + /// + /// virtual so inheriting classes can roll their own + /// + public virtual void OnGUI() + { + if (!showLobbyGUI) + return; + + if (SceneManager.GetActiveScene().name != LobbyScene) + return; + + GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS"); + } + + #endregion + } +} diff --git a/Assets/Packages/Mirror/Components/NetworkLobbyManager.cs.meta b/Assets/Packages/Mirror/Components/NetworkLobbyManager.cs.meta new file mode 100644 index 0000000..35b6436 --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkLobbyManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 615e6c6589cf9e54cad646b5a11e0529 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs b/Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs new file mode 100644 index 0000000..45231c1 --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs @@ -0,0 +1,155 @@ +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Mirror +{ + /// + /// This component works in conjunction with the NetworkLobbyManager to make up the multiplayer lobby system. + /// The LobbyPrefab object of the NetworkLobbyManager must have this component on it. This component holds basic lobby player data required for the lobby to function. Game specific data for lobby players can be put in other components on the LobbyPrefab or in scripts derived from NetworkLobbyPlayer. + /// + [DisallowMultipleComponent] + [AddComponentMenu("Network/NetworkLobbyPlayer")] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkLobbyPlayer.html")] + public class NetworkLobbyPlayer : NetworkBehaviour + { + /// + /// This flag controls whether the default UI is shown for the lobby player. + /// As this UI is rendered using the old GUI system, it is only recommended for testing purposes. + /// + public bool showLobbyGUI = true; + + /// + /// This is a flag that control whether this player is ready for the game to begin. + /// When all players are ready to begin, the game will start. This should not be set directly, the SendReadyToBeginMessage function should be called on the client to set it on the server. + /// + [SyncVar(hook = nameof(ReadyStateChanged))] + public bool readyToBegin; + + /// + /// Current index of the player, e.g. Player1, Player2, etc. + /// + [SyncVar] + public int index; + + #region Unity Callbacks + + /// + /// Do not use Start - Override OnStartrHost / OnStartClient instead! + /// + public void Start() + { + if (NetworkManager.singleton as NetworkLobbyManager) + OnClientEnterLobby(); + else + Debug.LogError("LobbyPlayer could not find a NetworkLobbyManager. The LobbyPlayer requires a NetworkLobbyManager object to function. Make sure that there is one in the scene."); + } + + #endregion + + #region Commands + + [Command] + public void CmdChangeReadyState(bool readyState) + { + readyToBegin = readyState; + NetworkLobbyManager lobby = NetworkManager.singleton as NetworkLobbyManager; + if (lobby != null) + { + lobby.ReadyStatusChanged(); + } + } + + #endregion + + #region SyncVar Hooks + + void ReadyStateChanged(bool newReadyState) + { + OnClientReady(readyToBegin); + } + + #endregion + + #region Lobby Client Virtuals + + /// + /// This is a hook that is invoked on all player objects when entering the lobby. + /// Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called. + /// + public virtual void OnClientEnterLobby() { } + + /// + /// This is a hook that is invoked on all player objects when exiting the lobby. + /// + public virtual void OnClientExitLobby() { } + + /// + /// This is a hook that is invoked on clients when a LobbyPlayer switches between ready or not ready. + /// This function is called when the a client player calls SendReadyToBeginMessage() or SendNotReadyToBeginMessage(). + /// + /// Whether the player is ready or not. + public virtual void OnClientReady(bool readyState) { } + + #endregion + + #region Optional UI + + /// + /// Render a UI for the lobby. Override to provide your on UI + /// + public virtual void OnGUI() + { + if (!showLobbyGUI) + return; + + NetworkLobbyManager lobby = NetworkManager.singleton as NetworkLobbyManager; + if (lobby) + { + if (!lobby.showLobbyGUI) + return; + + if (SceneManager.GetActiveScene().name != lobby.LobbyScene) + return; + + GUILayout.BeginArea(new Rect(20f + (index * 100), 200f, 90f, 130f)); + + GUILayout.Label($"Player [{index + 1}]"); + + if (readyToBegin) + GUILayout.Label("Ready"); + else + GUILayout.Label("Not Ready"); + + if (((isServer && index > 0) || isServerOnly) && GUILayout.Button("REMOVE")) + { + // This button only shows on the Host for all players other than the Host + // Host and Players can't remove themselves (stop the client instead) + // Host can kick a Player this way. + GetComponent().connectionToClient.Disconnect(); + } + + GUILayout.EndArea(); + + if (NetworkClient.active && isLocalPlayer) + { + GUILayout.BeginArea(new Rect(20f, 300f, 120f, 20f)); + + if (readyToBegin) + { + if (GUILayout.Button("Cancel")) + CmdChangeReadyState(false); + } + else + { + if (GUILayout.Button("Ready")) + CmdChangeReadyState(true); + } + + GUILayout.EndArea(); + } + } + } + + #endregion + } +} diff --git a/Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs.meta b/Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs.meta new file mode 100644 index 0000000..3062e0e --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkLobbyPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 79874ac94d5b1314788ecf0e86bd23fd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/NetworkProximityChecker.cs b/Assets/Packages/Mirror/Components/NetworkProximityChecker.cs new file mode 100644 index 0000000..97c4bbf --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkProximityChecker.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Mirror +{ + /// + /// Component that controls visibility of networked objects for players. + /// Any object with this component on it will not be visible to players more than a (configurable) distance away. + /// + [AddComponentMenu("Network/NetworkProximityChecker")] + [RequireComponent(typeof(NetworkIdentity))] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkProximityChecker.html")] + public class NetworkProximityChecker : NetworkBehaviour + { + /// + /// Enumeration of methods to use to check proximity. + /// + public enum CheckMethod + { + Physics3D, + Physics2D + } + + /// + /// The maximim range that objects will be visible at. + /// + [Tooltip("The maximum range that objects will be visible at.")] + public int visRange = 10; + + /// + /// How often (in seconds) that this object should update the list of observers that can see it. + /// + [Tooltip("How often (in seconds) that this object should update the list of observers that can see it.")] + public float visUpdateInterval = 1; + + /// + /// Which method to use for checking proximity of players. + /// Physics3D uses 3D physics to determine proximity. + /// Physics2D uses 2D physics to determine proximity. + /// + [Tooltip("Which method to use for checking proximity of players.\n\nPhysics3D uses 3D physics to determine proximity.\nPhysics2D uses 2D physics to determine proximity.")] + public CheckMethod checkMethod = CheckMethod.Physics3D; + + /// + /// Flag to force this object to be hidden for players. + /// If this object is a player object, it will not be hidden for that player. + /// + [Tooltip("Enable to force this object to be hidden from players.")] + public bool forceHidden; + + // Layers are used anyway, might as well expose them to the user. + /// + /// Select only the Player's layer to avoid unnecessary SphereCasts against the Terrain, etc. + /// ~0 means 'Everything'. + /// + [Tooltip("Select only the Player's layer to avoid unnecessary SphereCasts against the Terrain, etc.")] + public LayerMask castLayers = ~0; + + float lastUpdateTime; + + // OverlapSphereNonAlloc array to avoid allocations. + // -> static so we don't create one per component + // -> this is worth it because proximity checking happens for just about + // every entity on the server! + // -> should be big enough to work in just about all cases + static Collider[] hitsBuffer3D = new Collider[10000]; + static Collider2D[] hitsBuffer2D = new Collider2D[10000]; + + void Update() + { + if (!NetworkServer.active) + return; + + if (Time.time - lastUpdateTime > visUpdateInterval) + { + netIdentity.RebuildObservers(false); + lastUpdateTime = Time.time; + } + } + + /// + /// Called when a new player enters + /// + /// + /// + public override bool OnCheckObserver(NetworkConnection newObserver) + { + if (forceHidden) + return false; + + return Vector3.Distance(newObserver.playerController.transform.position, transform.position) < visRange; + } + + /// + /// Called when a new player enters, and when scene changes occur + /// + /// List of players to be updated. Modify this set with all the players that can see this object + /// True if this is the first time the method is called for this object + /// True if this component calculated the list of observers + public override bool OnRebuildObservers(HashSet observers, bool initial) + { + // if force hidden then return without adding any observers. + if (forceHidden) + // always return true when overwriting OnRebuildObservers so that + // Mirror knows not to use the built in rebuild method. + return true; + + // find players within range + switch (checkMethod) + { + case CheckMethod.Physics3D: + { + // cast without allocating GC for maximum performance + int hitCount = Physics.OverlapSphereNonAlloc(transform.position, visRange, hitsBuffer3D, castLayers); + if (hitCount == hitsBuffer3D.Length) Debug.LogWarning("NetworkProximityChecker's OverlapSphere test for " + name + " has filled the whole buffer(" + hitsBuffer3D.Length + "). Some results might have been omitted. Consider increasing buffer size."); + + for (int i = 0; i < hitCount; i++) + { + Collider hit = hitsBuffer3D[i]; + // collider might be on pelvis, often the NetworkIdentity is in a parent + // (looks in the object itself and then parents) + NetworkIdentity identity = hit.GetComponentInParent(); + // (if an object has a connectionToClient, it is a player) + if (identity != null && identity.connectionToClient != null) + { + observers.Add(identity.connectionToClient); + } + } + break; + } + + case CheckMethod.Physics2D: + { + // cast without allocating GC for maximum performance + int hitCount = Physics2D.OverlapCircleNonAlloc(transform.position, visRange, hitsBuffer2D, castLayers); + if (hitCount == hitsBuffer2D.Length) Debug.LogWarning("NetworkProximityChecker's OverlapCircle test for " + name + " has filled the whole buffer(" + hitsBuffer2D.Length + "). Some results might have been omitted. Consider increasing buffer size."); + + for (int i = 0; i < hitCount; i++) + { + Collider2D hit = hitsBuffer2D[i]; + // collider might be on pelvis, often the NetworkIdentity is in a parent + // (looks in the object itself and then parents) + NetworkIdentity identity = hit.GetComponentInParent(); + // (if an object has a connectionToClient, it is a player) + if (identity != null && identity.connectionToClient != null) + { + observers.Add(identity.connectionToClient); + } + } + break; + } + } + + // always return true when overwriting OnRebuildObservers so that + // Mirror knows not to use the built in rebuild method. + return true; + } + + /// + /// Called when hiding and showing objects on the host + /// + /// + public override void OnSetLocalVisibility(bool visible) + { + foreach (Renderer rend in GetComponentsInChildren()) + { + rend.enabled = visible; + } + } + } +} diff --git a/Assets/Packages/Mirror/Components/NetworkProximityChecker.cs.meta b/Assets/Packages/Mirror/Components/NetworkProximityChecker.cs.meta new file mode 100644 index 0000000..79e50e8 --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkProximityChecker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1731d8de2d0c84333b08ebe1e79f4118 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/NetworkTransform.cs b/Assets/Packages/Mirror/Components/NetworkTransform.cs new file mode 100644 index 0000000..d55e26d --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkTransform.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace Mirror +{ + [DisallowMultipleComponent] + [AddComponentMenu("Network/NetworkTransform")] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkTransform.html")] + public class NetworkTransform : NetworkTransformBase + { + protected override Transform targetComponent => transform; + } +} diff --git a/Assets/Packages/Mirror/Components/NetworkTransform.cs.meta b/Assets/Packages/Mirror/Components/NetworkTransform.cs.meta new file mode 100644 index 0000000..d1af9ec --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkTransform.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f74aedd71d9a4f55b3ce499326d45fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/NetworkTransformBase.cs b/Assets/Packages/Mirror/Components/NetworkTransformBase.cs new file mode 100644 index 0000000..4ed5ad9 --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkTransformBase.cs @@ -0,0 +1,447 @@ +// vis2k: +// base class for NetworkTransform and NetworkTransformChild. +// New method is simple and stupid. No more 1500 lines of code. +// +// Server sends current data. +// Client saves it and interpolates last and latest data points. +// Update handles transform movement / rotation +// FixedUpdate handles rigidbody movement / rotation +// +// Notes: +// * Built-in Teleport detection in case of lags / teleport / obstacles +// * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp +// * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code. +// * Initial delay might happen if server sends packet immediately after moving +// just 1cm, hence we move 1cm and then wait 100ms for next packet +// * Only way for smooth movement is to use a fixed movement speed during +// interpolation. interpolation over time is never that good. +// +using UnityEngine; + +namespace Mirror +{ + public abstract class NetworkTransformBase : NetworkBehaviour + { + // rotation compression. not public so that other scripts can't modify + // it at runtime. alternatively we could send 1 extra byte for the mode + // each time so clients know how to decompress, but the whole point was + // to save bandwidth in the first place. + // -> can still be modified in the Inspector while the game is running, + // but would cause errors immediately and be pretty obvious. + [Tooltip("Compresses 16 Byte Quaternion into None=12, Much=3, Lots=2 Byte")] + [SerializeField] Compression compressRotation = Compression.Much; + public enum Compression { None, Much, Lots, NoRotation }; // easily understandable and funny + + // server + Vector3 lastPosition; + Quaternion lastRotation; + private Vector3 lastScale; + + // client + public class DataPoint + { + public float timeStamp; + // use local position/rotation for VR support + public Vector3 localPosition; + public Quaternion localRotation; + public Vector3 localScale; + public float movementSpeed; + } + // interpolation start and goal + DataPoint start; + DataPoint goal; + + // local authority send time + float lastClientSendTime; + + // target transform to sync. can be on a child. + protected abstract Transform targetComponent { get; } + + // serialization is needed by OnSerialize and by manual sending from authority + static void SerializeIntoWriter(NetworkWriter writer, Vector3 position, Quaternion rotation, Compression compressRotation, Vector3 scale) + { + // serialize position + writer.WriteVector3(position); + + // serialize rotation + // writing quaternion = 16 byte + // writing euler angles = 12 byte + // -> quaternion->euler->quaternion always works. + // -> gimbal lock only occurs when adding. + Vector3 euler = rotation.eulerAngles; + if (compressRotation == Compression.None) + { + // write 3 floats = 12 byte + writer.WriteSingle(euler.x); + writer.WriteSingle(euler.y); + writer.WriteSingle(euler.z); + } + else if (compressRotation == Compression.Much) + { + // write 3 byte. scaling [0,360] to [0,255] + writer.WriteByte(FloatBytePacker.ScaleFloatToByte(euler.x, 0, 360, byte.MinValue, byte.MaxValue)); + writer.WriteByte(FloatBytePacker.ScaleFloatToByte(euler.y, 0, 360, byte.MinValue, byte.MaxValue)); + writer.WriteByte(FloatBytePacker.ScaleFloatToByte(euler.z, 0, 360, byte.MinValue, byte.MaxValue)); + } + else if (compressRotation == Compression.Lots) + { + // write 2 byte, 5 bits for each float + writer.WriteUInt16(FloatBytePacker.PackThreeFloatsIntoUShort(euler.x, euler.y, euler.z, 0, 360)); + } + + // serialize scale + writer.WriteVector3(scale); + } + + public override bool OnSerialize(NetworkWriter writer, bool initialState) + { + // use local position/rotation/scale for VR support + SerializeIntoWriter(writer, targetComponent.transform.localPosition, targetComponent.transform.localRotation, compressRotation, targetComponent.transform.localScale); + return true; + } + + // try to estimate movement speed for a data point based on how far it + // moved since the previous one + // => if this is the first time ever then we use our best guess: + // -> delta based on transform.localPosition + // -> elapsed based on send interval hoping that it roughly matches + static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval) + { + Vector3 delta = to.localPosition - (from != null ? from.localPosition : transform.localPosition); + float elapsed = from != null ? to.timeStamp - from.timeStamp : sendInterval; + return elapsed > 0 ? delta.magnitude / elapsed : 0; // avoid NaN + } + + // serialization is needed by OnSerialize and by manual sending from authority + void DeserializeFromReader(NetworkReader reader) + { + // put it into a data point immediately + DataPoint temp = new DataPoint + { + // deserialize position + localPosition = reader.ReadVector3() + }; + + // deserialize rotation + if (compressRotation == Compression.None) + { + // read 3 floats = 16 byte + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + temp.localRotation = Quaternion.Euler(x, y, z); + } + else if (compressRotation == Compression.Much) + { + // read 3 byte. scaling [0,255] to [0,360] + float x = FloatBytePacker.ScaleByteToFloat(reader.ReadByte(), byte.MinValue, byte.MaxValue, 0, 360); + float y = FloatBytePacker.ScaleByteToFloat(reader.ReadByte(), byte.MinValue, byte.MaxValue, 0, 360); + float z = FloatBytePacker.ScaleByteToFloat(reader.ReadByte(), byte.MinValue, byte.MaxValue, 0, 360); + temp.localRotation = Quaternion.Euler(x, y, z); + } + else if (compressRotation == Compression.Lots) + { + // read 2 byte, 5 bits per float + Vector3 xyz = FloatBytePacker.UnpackUShortIntoThreeFloats(reader.ReadUInt16(), 0, 360); + temp.localRotation = Quaternion.Euler(xyz.x, xyz.y, xyz.z); + } + + temp.localScale = reader.ReadVector3(); + + temp.timeStamp = Time.time; + + // movement speed: based on how far it moved since last time + // has to be calculated before 'start' is overwritten + temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetComponent.transform, syncInterval); + + // reassign start wisely + // -> first ever data point? then make something up for previous one + // so that we can start interpolation without waiting for next. + if (start == null) + { + start = new DataPoint + { + timeStamp = Time.time - syncInterval, + // local position/rotation for VR support + localPosition = targetComponent.transform.localPosition, + localRotation = targetComponent.transform.localRotation, + localScale = targetComponent.transform.localScale, + movementSpeed = temp.movementSpeed + }; + } + // -> second or nth data point? then update previous, but: + // we start at where ever we are right now, so that it's + // perfectly smooth and we don't jump anywhere + // + // example if we are at 'x': + // + // A--x->B + // + // and then receive a new point C: + // + // A--x--B + // | + // | + // C + // + // then we don't want to just jump to B and start interpolation: + // + // x + // | + // | + // C + // + // we stay at 'x' and interpolate from there to C: + // + // x..B + // \ . + // \. + // C + // + else + { + float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition); + float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition); + + start = goal; + + // teleport / lag / obstacle detection: only continue at current + // position if we aren't too far away + // + // // local position/rotation for VR support + if (Vector3.Distance(targetComponent.transform.localPosition, start.localPosition) < oldDistance + newDistance) + { + start.localPosition = targetComponent.transform.localPosition; + start.localRotation = targetComponent.transform.localRotation; + start.localScale = targetComponent.transform.localScale; + } + } + + // set new destination in any case. new data is best data. + goal = temp; + } + + public override void OnDeserialize(NetworkReader reader, bool initialState) + { + // deserialize + DeserializeFromReader(reader); + } + + // local authority client sends sync message to server for broadcasting + [Command] + void CmdClientToServerSync(byte[] payload) + { + // deserialize payload + NetworkReader reader = new NetworkReader(payload); + DeserializeFromReader(reader); + + // server-only mode does no interpolation to save computations, + // but let's set the position directly + if (isServer && !isClient) + ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale); + + // set dirty so that OnSerialize broadcasts it + SetDirtyBit(1UL); + } + + // where are we in the timeline between start and goal? [0,1] + static float CurrentInterpolationFactor(DataPoint start, DataPoint goal) + { + if (start != null) + { + float difference = goal.timeStamp - start.timeStamp; + + // the moment we get 'goal', 'start' is supposed to + // start, so elapsed time is based on: + float elapsed = Time.time - goal.timeStamp; + return difference > 0 ? elapsed / difference : 0; // avoid NaN + } + return 0; + } + + static Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition) + { + if (start != null) + { + // Option 1: simply interpolate based on time. but stutter + // will happen, it's not that smooth. especially noticeable if + // the camera automatically follows the player + // float t = CurrentInterpolationFactor(); + // return Vector3.Lerp(start.position, goal.position, t); + + // Option 2: always += speed + // -> speed is 0 if we just started after idle, so always use max + // for best results + float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed); + return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime); + } + return currentPosition; + } + + static Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation) + { + if (start != null) + { + float t = CurrentInterpolationFactor(start, goal); + return Quaternion.Slerp(start.localRotation, goal.localRotation, t); + } + return defaultRotation; + } + + static Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale) + { + if (start != null) + { + float t = CurrentInterpolationFactor(start, goal); + return Vector3.Lerp(start.localScale, goal.localScale, t); + } + return currentScale; + } + + // teleport / lag / stuck detection + // -> checking distance is not enough since there could be just a tiny + // fence between us and the goal + // -> checking time always works, this way we just teleport if we still + // didn't reach the goal after too much time has elapsed + bool NeedsTeleport() + { + // calculate time between the two data points + float startTime = start != null ? start.timeStamp : Time.time - syncInterval; + float goalTime = goal != null ? goal.timeStamp : Time.time; + float difference = goalTime - startTime; + float timeSinceGoalReceived = Time.time - goalTime; + return timeSinceGoalReceived > difference * 5; + } + + // moved since last time we checked it? + bool HasEitherMovedRotatedScaled() + { + // moved or rotated or scaled? + // local position/rotation/scale for VR support + bool moved = lastPosition != targetComponent.transform.localPosition; + bool rotated = lastRotation != targetComponent.transform.localRotation; + bool scaled = lastScale != targetComponent.transform.localScale; + + // save last for next frame to compare + // (only if change was detected. otherwise slow moving objects might + // never sync because of C#'s float comparison tolerance. see also: + // https://github.com/vis2k/Mirror/pull/428) + bool change = moved || rotated || scaled; + if (change) + { + // local position/rotation for VR support + lastPosition = targetComponent.transform.localPosition; + lastRotation = targetComponent.transform.localRotation; + lastScale = targetComponent.transform.localScale; + } + return change; + } + + // set position carefully depending on the target component + void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale) + { + // local position/rotation for VR support + targetComponent.transform.localPosition = position; + if (Compression.NoRotation != compressRotation) + { + targetComponent.transform.localRotation = rotation; + } + targetComponent.transform.localScale = scale; + } + + void Update() + { + // if server then always sync to others. + if (isServer) + { + // just use OnSerialize via SetDirtyBit only sync when position + // changed. set dirty bits 0 or 1 + SetDirtyBit(HasEitherMovedRotatedScaled() ? 1UL : 0UL); + } + + // no 'else if' since host mode would be both + if (isClient) + { + // send to server if we have local authority (and aren't the server) + // -> only if connectionToServer has been initialized yet too + if (!isServer && hasAuthority) + { + // check only each 'syncInterval' + if (Time.time - lastClientSendTime >= syncInterval) + { + if (HasEitherMovedRotatedScaled()) + { + // serialize + // local position/rotation for VR support + NetworkWriter writer = new NetworkWriter(); + SerializeIntoWriter(writer, targetComponent.transform.localPosition, targetComponent.transform.localRotation, compressRotation, targetComponent.transform.localScale); + + // send to server + CmdClientToServerSync(writer.ToArray()); + } + lastClientSendTime = Time.time; + } + } + + // apply interpolation on client for all players + // unless this client has authority over the object. could be + // himself or another object that he was assigned authority over + if (!hasAuthority) + { + // received one yet? (initialized?) + if (goal != null) + { + // teleport or interpolate + if (NeedsTeleport()) + { + // local position/rotation for VR support + ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale); + } + else + { + // local position/rotation for VR support + ApplyPositionRotationScale(InterpolatePosition(start, goal, targetComponent.transform.localPosition), + InterpolateRotation(start, goal, targetComponent.transform.localRotation), + InterpolateScale(start, goal, targetComponent.transform.localScale)); + } + } + } + } + } + + static void DrawDataPointGizmo(DataPoint data, Color color) + { + // use a little offset because transform.localPosition might be in + // the ground in many cases + Vector3 offset = Vector3.up * 0.01f; + + // draw position + Gizmos.color = color; + Gizmos.DrawSphere(data.localPosition + offset, 0.5f); + + // draw forward and up + Gizmos.color = Color.blue; // like unity move tool + Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward); + + Gizmos.color = Color.green; // like unity move tool + Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up); + } + + static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color) + { + Gizmos.color = color; + Gizmos.DrawLine(data1.localPosition, data2.localPosition); + } + + // draw the data points for easier debugging + void OnDrawGizmos() + { + // draw start and goal points + if (start != null) DrawDataPointGizmo(start, Color.gray); + if (goal != null) DrawDataPointGizmo(goal, Color.white); + + // draw line between them + if (start != null && goal != null) DrawLineBetweenDataPoints(start, goal, Color.cyan); + } + } +} diff --git a/Assets/Packages/Mirror/Components/NetworkTransformBase.cs.meta b/Assets/Packages/Mirror/Components/NetworkTransformBase.cs.meta new file mode 100644 index 0000000..2c3c3e1 --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkTransformBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e77294d8ccbc4e7cb8ca2bd0d3e99ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Components/NetworkTransformChild.cs b/Assets/Packages/Mirror/Components/NetworkTransformChild.cs new file mode 100644 index 0000000..03a364f --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkTransformChild.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +namespace Mirror +{ + /// + /// 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. + /// + [AddComponentMenu("Network/NetworkTransformChild")] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkTransformChild.html")] + public class NetworkTransformChild : NetworkTransformBase + { + public Transform target; + protected override Transform targetComponent => target; + } +} diff --git a/Assets/Packages/Mirror/Components/NetworkTransformChild.cs.meta b/Assets/Packages/Mirror/Components/NetworkTransformChild.cs.meta new file mode 100644 index 0000000..9c068f2 --- /dev/null +++ b/Assets/Packages/Mirror/Components/NetworkTransformChild.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 734b48bea0b204338958ee3d885e11f0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor.meta b/Assets/Packages/Mirror/Editor.meta new file mode 100644 index 0000000..f679511 --- /dev/null +++ b/Assets/Packages/Mirror/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2539267b6934a4026a505690a1e1eda2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef b/Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef new file mode 100644 index 0000000..d18558b --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Mirror.Editor", + "references": [ + "Mirror" + ], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef.meta b/Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef.meta new file mode 100644 index 0000000..e2e6f2a --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Mirror.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1c7c33eb5480dd24c9e29a8250c1a775 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs b/Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs new file mode 100644 index 0000000..20db50b --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs @@ -0,0 +1,5 @@ +// This file was removed in Mirror 3.4.9 +// The purpose of this file is to get the old file overwritten +// when users update from the asset store to prevent a flood of errors +// from having the old file still in the project as a straggler. +// This file will be dropped from the Asset Store package in May 2019 diff --git a/Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs.meta b/Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs.meta new file mode 100644 index 0000000..1b537bc --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkAnimatorEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9589e903d4e98490fb1157762a307fd7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs b/Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs new file mode 100644 index 0000000..44d55f5 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + [CustomEditor(typeof(NetworkBehaviour), true)] + [CanEditMultipleObjects] + public class NetworkBehaviourInspector : Editor + { + bool initialized; + protected List syncVarNames = new List(); + bool syncsAnything; + bool[] showSyncLists; + + readonly GUIContent syncVarIndicatorContent = new GUIContent("SyncVar", "This variable has been marked with the [SyncVar] attribute."); + + internal virtual bool HideScriptField => false; + + // does this type sync anything? otherwise we don't need to show syncInterval + bool SyncsAnything(Type scriptClass) + { + // has OnSerialize that is not in NetworkBehaviour? + // then it either has a syncvar or custom OnSerialize. either way + // this means we have something to sync. + MethodInfo method = scriptClass.GetMethod("OnSerialize"); + if (method != null && method.DeclaringType != typeof(NetworkBehaviour)) + { + return true; + } + + // SyncObjects are serialized in NetworkBehaviour.OnSerialize, which + // is always there even if we don't use SyncObjects. so we need to + // search for SyncObjects manually. + // (look for 'Mirror.Sync'. not '.SyncObject' because we'd have to + // check base type for that again) + foreach (FieldInfo field in scriptClass.GetFields()) + { + if (field.FieldType.BaseType != null && + field.FieldType.BaseType.FullName != null && + field.FieldType.BaseType.FullName.Contains("Mirror.Sync")) + { + return true; + } + } + + return false; + } + + void Init(MonoScript script) + { + initialized = true; + Type scriptClass = script.GetClass(); + + // find public SyncVars to show (user doesn't want protected ones to be shown in inspector) + foreach (FieldInfo field in scriptClass.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + Attribute[] fieldMarkers = (Attribute[])field.GetCustomAttributes(typeof(SyncVarAttribute), true); + if (fieldMarkers.Length > 0) + { + syncVarNames.Add(field.Name); + } + } + + int numSyncLists = scriptClass.GetFields().Count( + field => field.FieldType.BaseType != null && + field.FieldType.BaseType.Name.Contains("SyncList")); + if (numSyncLists > 0) + { + showSyncLists = new bool[numSyncLists]; + } + + syncsAnything = SyncsAnything(scriptClass); + } + + public override void OnInspectorGUI() + { + if (!initialized) + { + serializedObject.Update(); + SerializedProperty scriptProperty = serializedObject.FindProperty("m_Script"); + if (scriptProperty == null) + return; + + MonoScript targetScript = scriptProperty.objectReferenceValue as MonoScript; + Init(targetScript); + } + + EditorGUI.BeginChangeCheck(); + serializedObject.Update(); + + // Loop through properties and create one field (including children) for each top level property. + SerializedProperty property = serializedObject.GetIterator(); + bool expanded = true; + while (property.NextVisible(expanded)) + { + bool isSyncVar = syncVarNames.Contains(property.name); + if (property.propertyType == SerializedPropertyType.ObjectReference) + { + if (property.name == "m_Script") + { + if (HideScriptField) + { + continue; + } + + EditorGUI.BeginDisabledGroup(true); + } + + EditorGUILayout.PropertyField(property, true); + + if (isSyncVar) + { + GUILayout.Label(syncVarIndicatorContent, EditorStyles.miniLabel, GUILayout.Width(EditorStyles.miniLabel.CalcSize(syncVarIndicatorContent).x)); + } + + if (property.name == "m_Script") + { + EditorGUI.EndDisabledGroup(); + } + } + else + { + EditorGUILayout.BeginHorizontal(); + + EditorGUILayout.PropertyField(property, true); + + if (isSyncVar) + { + GUILayout.Label(syncVarIndicatorContent, EditorStyles.miniLabel, GUILayout.Width(EditorStyles.miniLabel.CalcSize(syncVarIndicatorContent).x)); + } + + EditorGUILayout.EndHorizontal(); + } + expanded = false; + } + serializedObject.ApplyModifiedProperties(); + EditorGUI.EndChangeCheck(); + + // find SyncLists.. they are not properties. + int syncListIndex = 0; + foreach (FieldInfo field in serializedObject.targetObject.GetType().GetFields()) + { + if (field.FieldType.BaseType != null && field.FieldType.BaseType.Name.Contains("SyncList")) + { + showSyncLists[syncListIndex] = EditorGUILayout.Foldout(showSyncLists[syncListIndex], "SyncList " + field.Name + " [" + field.FieldType.Name + "]"); + if (showSyncLists[syncListIndex]) + { + EditorGUI.indentLevel += 1; + if (field.GetValue(serializedObject.targetObject) is IEnumerable synclist) + { + int index = 0; + IEnumerator enu = synclist.GetEnumerator(); + while (enu.MoveNext()) + { + if (enu.Current != null) + { + EditorGUILayout.LabelField("Item:" + index, enu.Current.ToString()); + } + index += 1; + } + } + EditorGUI.indentLevel -= 1; + } + syncListIndex += 1; + } + } + + // does it sync anything? then show extra properties + // (no need to show it if the class only has Cmds/Rpcs and no sync) + if (syncsAnything) + { + NetworkBehaviour networkBehaviour = target as NetworkBehaviour; + if (networkBehaviour != null) + { + // syncMode + serializedObject.FindProperty("syncMode").enumValueIndex = (int)(SyncMode) + EditorGUILayout.EnumPopup("Network Sync Mode", networkBehaviour.syncMode); + + // syncInterval + // [0,2] should be enough. anything >2s is too laggy anyway. + serializedObject.FindProperty("syncInterval").floatValue = EditorGUILayout.Slider( + new GUIContent("Network Sync Interval", + "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.)"), + networkBehaviour.syncInterval, 0, 2); + + // apply + serializedObject.ApplyModifiedProperties(); + } + } + } + } +} //namespace diff --git a/Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs.meta b/Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs.meta new file mode 100644 index 0000000..78d9fa8 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkBehaviourInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f02853db46b6346e4866594a96c3b0e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs b/Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs new file mode 100644 index 0000000..3a79340 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + [CustomEditor(typeof(NetworkIdentity), true)] + [CanEditMultipleObjects] + public class NetworkIdentityEditor : Editor + { + SerializedProperty serverOnlyProperty; + SerializedProperty localPlayerAuthorityProperty; + + readonly GUIContent serverOnlyLabel = new GUIContent("Server Only", "True if the object should only exist on the server."); + readonly GUIContent localPlayerAuthorityLabel = new GUIContent("Local Player Authority", "True if this object will be controlled by a player on a client."); + readonly GUIContent spawnLabel = new GUIContent("Spawn Object", "This causes an unspawned server object to be spawned on clients"); + + NetworkIdentity networkIdentity; + bool initialized; + bool showObservers; + + void Init() + { + if (initialized) + { + return; + } + initialized = true; + networkIdentity = target as NetworkIdentity; + + serverOnlyProperty = serializedObject.FindProperty("serverOnly"); + localPlayerAuthorityProperty = serializedObject.FindProperty("localPlayerAuthority"); + } + + public override void OnInspectorGUI() + { + if (serverOnlyProperty == null) + { + initialized = false; + } + + Init(); + + serializedObject.Update(); + + if (serverOnlyProperty.boolValue) + { + EditorGUILayout.PropertyField(serverOnlyProperty, serverOnlyLabel); + EditorGUILayout.LabelField("Local Player Authority cannot be set for server-only objects"); + } + else if (localPlayerAuthorityProperty.boolValue) + { + EditorGUILayout.LabelField("Server Only cannot be set for Local Player Authority objects"); + EditorGUILayout.PropertyField(localPlayerAuthorityProperty, localPlayerAuthorityLabel); + } + else + { + EditorGUILayout.PropertyField(serverOnlyProperty, serverOnlyLabel); + EditorGUILayout.PropertyField(localPlayerAuthorityProperty, localPlayerAuthorityLabel); + } + + serializedObject.ApplyModifiedProperties(); + + if (!Application.isPlaying) + { + return; + } + + // Runtime actions below here + + EditorGUILayout.Separator(); + + if (networkIdentity.observers != null && networkIdentity.observers.Count > 0) + { + showObservers = EditorGUILayout.Foldout(showObservers, "Observers"); + if (showObservers) + { + EditorGUI.indentLevel += 1; + foreach (KeyValuePair kvp in networkIdentity.observers) + { + if (kvp.Value.playerController != null) + EditorGUILayout.ObjectField("Connection " + kvp.Value.connectionId, kvp.Value.playerController.gameObject, typeof(GameObject), false); + else + EditorGUILayout.TextField("Connection " + kvp.Value.connectionId); + } + EditorGUI.indentLevel -= 1; + } + } + + if (PrefabUtility.IsPartOfPrefabAsset(networkIdentity.gameObject)) + return; + + if (networkIdentity.gameObject.activeSelf && networkIdentity.netId == 0 && NetworkServer.active) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(spawnLabel); + if (GUILayout.Toggle(false, "Spawn", EditorStyles.miniButtonLeft)) + { + NetworkServer.Spawn(networkIdentity.gameObject); + EditorUtility.SetDirty(target); // preview window STILL doens't update immediately.. + } + EditorGUILayout.EndHorizontal(); + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs.meta b/Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs.meta new file mode 100644 index 0000000..cb4a5cf --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkIdentityEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b6e3680cc14b4769bff378e5dbc3544 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs b/Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs new file mode 100644 index 0000000..9699f6d --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs @@ -0,0 +1,280 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityObject = UnityEngine.Object; + +namespace Mirror +{ + [CustomPreview(typeof(GameObject))] + class NetworkInformationPreview : ObjectPreview + { + class NetworkIdentityInfo + { + public GUIContent name; + public GUIContent value; + } + + class NetworkBehaviourInfo + { + // This is here just so we can check if it's enabled/disabled + public NetworkBehaviour behaviour; + public GUIContent name; + } + + class Styles + { + public GUIStyle labelStyle = new GUIStyle(EditorStyles.label); + public GUIStyle componentName = new GUIStyle(EditorStyles.boldLabel); + public GUIStyle disabledName = new GUIStyle(EditorStyles.miniLabel); + + public Styles() + { + Color fontColor = new Color(0.7f, 0.7f, 0.7f); + labelStyle.padding.right += 20; + labelStyle.normal.textColor = fontColor; + labelStyle.active.textColor = fontColor; + labelStyle.focused.textColor = fontColor; + labelStyle.hover.textColor = fontColor; + labelStyle.onNormal.textColor = fontColor; + labelStyle.onActive.textColor = fontColor; + labelStyle.onFocused.textColor = fontColor; + labelStyle.onHover.textColor = fontColor; + + componentName.normal.textColor = fontColor; + componentName.active.textColor = fontColor; + componentName.focused.textColor = fontColor; + componentName.hover.textColor = fontColor; + componentName.onNormal.textColor = fontColor; + componentName.onActive.textColor = fontColor; + componentName.onFocused.textColor = fontColor; + componentName.onHover.textColor = fontColor; + + disabledName.normal.textColor = fontColor; + disabledName.active.textColor = fontColor; + disabledName.focused.textColor = fontColor; + disabledName.hover.textColor = fontColor; + disabledName.onNormal.textColor = fontColor; + disabledName.onActive.textColor = fontColor; + disabledName.onFocused.textColor = fontColor; + disabledName.onHover.textColor = fontColor; + } + } + + List info; + List behavioursInfo; + NetworkIdentity identity; + GUIContent title; + Styles styles = new Styles(); + + public override void Initialize(UnityObject[] targets) + { + base.Initialize(targets); + GetNetworkInformation(target as GameObject); + } + + public override GUIContent GetPreviewTitle() + { + if (title == null) + { + title = new GUIContent("Network Information"); + } + return title; + } + + public override bool HasPreviewGUI() + { + return info != null && info.Count > 0; + } + + public override void OnPreviewGUI(Rect r, GUIStyle background) + { + if (Event.current.type != EventType.Repaint) + return; + + if (info == null || info.Count == 0) + return; + + if (styles == null) + styles = new Styles(); + + // Get required label size for the names of the information values we're going to show + // There are two columns, one with label for the name of the info and the next for the value + Vector2 maxNameLabelSize = new Vector2(140, 16); + Vector2 maxValueLabelSize = GetMaxNameLabelSize(); + + //Apply padding + RectOffset previewPadding = new RectOffset(-5, -5, -5, -5); + Rect paddedr = previewPadding.Add(r); + + //Centering + float initialX = paddedr.x + 10; + float initialY = paddedr.y + 10; + + Rect labelRect = new Rect(initialX, initialY, maxNameLabelSize.x, maxNameLabelSize.y); + Rect idLabelRect = new Rect(maxNameLabelSize.x, initialY, maxValueLabelSize.x, maxValueLabelSize.y); + + foreach (NetworkIdentityInfo info in info) + { + GUI.Label(labelRect, info.name, styles.labelStyle); + GUI.Label(idLabelRect, info.value, styles.componentName); + labelRect.y += labelRect.height; + labelRect.x = initialX; + idLabelRect.y += idLabelRect.height; + } + + // Show behaviours list in a different way than the name/value pairs above + float lastY = labelRect.y; + if (behavioursInfo != null && behavioursInfo.Count > 0) + { + Vector2 maxBehaviourLabelSize = GetMaxBehaviourLabelSize(); + Rect behaviourRect = new Rect(initialX, labelRect.y + 10, maxBehaviourLabelSize.x, maxBehaviourLabelSize.y); + + GUI.Label(behaviourRect, new GUIContent("Network Behaviours"), styles.labelStyle); + behaviourRect.x += 20; // indent names + behaviourRect.y += behaviourRect.height; + + foreach (NetworkBehaviourInfo info in behavioursInfo) + { + if (info.behaviour == null) + { + // could be the case in the editor after existing play mode. + continue; + } + + GUI.Label(behaviourRect, info.name, info.behaviour.enabled ? styles.componentName : styles.disabledName); + behaviourRect.y += behaviourRect.height; + lastY = behaviourRect.y; + } + + if (identity.observers != null && identity.observers.Count > 0) + { + Rect observerRect = new Rect(initialX, lastY + 10, 200, 20); + + GUI.Label(observerRect, new GUIContent("Network observers"), styles.labelStyle); + observerRect.x += 20; // indent names + observerRect.y += observerRect.height; + + foreach (KeyValuePair kvp in identity.observers) + { + GUI.Label(observerRect, kvp.Value.address + ":" + kvp.Value.connectionId, styles.componentName); + observerRect.y += observerRect.height; + lastY = observerRect.y; + } + } + + if (identity.clientAuthorityOwner != null) + { + Rect ownerRect = new Rect(initialX, lastY + 10, 400, 20); + GUI.Label(ownerRect, new GUIContent("Client Authority: " + identity.clientAuthorityOwner), styles.labelStyle); + } + } + } + + // Get the maximum size used by the value of information items + Vector2 GetMaxNameLabelSize() + { + Vector2 maxLabelSize = Vector2.zero; + foreach (NetworkIdentityInfo info in info) + { + Vector2 labelSize = styles.labelStyle.CalcSize(info.value); + if (maxLabelSize.x < labelSize.x) + { + maxLabelSize.x = labelSize.x; + } + if (maxLabelSize.y < labelSize.y) + { + maxLabelSize.y = labelSize.y; + } + } + return maxLabelSize; + } + + Vector2 GetMaxBehaviourLabelSize() + { + Vector2 maxLabelSize = Vector2.zero; + foreach (NetworkBehaviourInfo behaviour in behavioursInfo) + { + Vector2 labelSize = styles.labelStyle.CalcSize(behaviour.name); + if (maxLabelSize.x < labelSize.x) + { + maxLabelSize.x = labelSize.x; + } + if (maxLabelSize.y < labelSize.y) + { + maxLabelSize.y = labelSize.y; + } + } + return maxLabelSize; + } + + void GetNetworkInformation(GameObject gameObject) + { + identity = gameObject.GetComponent(); + if (identity != null) + { + info = new List + { + GetAssetId(), + GetString("Scene ID", identity.sceneId.ToString("X")) + }; + + if (!Application.isPlaying) + { + return; + } + + info.Add(GetString("Network ID", identity.netId.ToString())); + + info.Add(GetBoolean("Is Client", identity.isClient)); + info.Add(GetBoolean("Is Server", identity.isServer)); + info.Add(GetBoolean("Has Authority", identity.hasAuthority)); + info.Add(GetBoolean("Is Local Player", identity.isLocalPlayer)); + + NetworkBehaviour[] behaviours = gameObject.GetComponents(); + if (behaviours.Length > 0) + { + behavioursInfo = new List(); + foreach (NetworkBehaviour behaviour in behaviours) + { + NetworkBehaviourInfo info = new NetworkBehaviourInfo + { + name = new GUIContent(behaviour.GetType().FullName), + behaviour = behaviour + }; + behavioursInfo.Add(info); + } + } + } + } + + NetworkIdentityInfo GetAssetId() + { + string assetId = identity.assetId.ToString(); + if (string.IsNullOrEmpty(assetId)) + { + assetId = ""; + } + return GetString("Asset ID", assetId); + } + + static NetworkIdentityInfo GetString(string name, string value) + { + NetworkIdentityInfo info = new NetworkIdentityInfo + { + name = new GUIContent(name), + value = new GUIContent(value) + }; + return info; + } + + static NetworkIdentityInfo GetBoolean(string name, bool value) + { + NetworkIdentityInfo info = new NetworkIdentityInfo + { + name = new GUIContent(name), + value = new GUIContent((value ? "Yes" : "No")) + }; + return info; + } + } +} diff --git a/Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs.meta b/Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs.meta new file mode 100644 index 0000000..9bf2de4 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkInformationPreview.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 51a99294efe134232932c34606737356 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs b/Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs new file mode 100644 index 0000000..0552cfa --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs @@ -0,0 +1,112 @@ +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; + +namespace Mirror +{ + [CustomEditor(typeof(NetworkManager), true)] + [CanEditMultipleObjects] + public class NetworkManagerEditor : Editor + { + SerializedProperty spawnListProperty; + + ReorderableList spawnList; + + protected NetworkManager networkManager; + + protected void Init() + { + if (spawnList == null) + { + + networkManager = target as NetworkManager; + + spawnListProperty = serializedObject.FindProperty("spawnPrefabs"); + + spawnList = new ReorderableList(serializedObject, spawnListProperty) + { + drawHeaderCallback = DrawHeader, + drawElementCallback = DrawChild, + onReorderCallback = Changed, + onRemoveCallback = RemoveButton, + onChangedCallback = Changed, + onAddCallback = AddButton, + elementHeight = 16 // this uses a 16x16 icon. other sizes make it stretch. + }; + } + } + + public override void OnInspectorGUI() + { + Init(); + DrawDefaultInspector(); + EditorGUI.BeginChangeCheck(); + spawnList.DoLayoutList(); + if (EditorGUI.EndChangeCheck()) + { + serializedObject.ApplyModifiedProperties(); + } + } + + static void DrawHeader(Rect headerRect) + { + GUI.Label(headerRect, "Registered Spawnable Prefabs:"); + } + + internal void DrawChild(Rect r, int index, bool isActive, bool isFocused) + { + SerializedProperty prefab = spawnListProperty.GetArrayElementAtIndex(index); + GameObject go = (GameObject)prefab.objectReferenceValue; + + GUIContent label; + if (go == null) + { + label = new GUIContent("Empty", "Drag a prefab with a NetworkIdentity here"); + } + else + { + NetworkIdentity identity = go.GetComponent(); + label = new GUIContent(go.name, identity != null ? "AssetId: [" + identity.assetId + "]" : "No Network Identity"); + } + + GameObject newGameObject = (GameObject)EditorGUI.ObjectField(r, label, go, typeof(GameObject), false); + + if (newGameObject != go) + { + if (newGameObject != null && !newGameObject.GetComponent()) + { + Debug.LogError("Prefab " + newGameObject + " cannot be added as spawnable as it doesn't have a NetworkIdentity."); + return; + } + prefab.objectReferenceValue = newGameObject; + } + } + + internal void Changed(ReorderableList list) + { + EditorUtility.SetDirty(target); + } + + internal void AddButton(ReorderableList list) + { + spawnListProperty.arraySize += 1; + list.index = spawnListProperty.arraySize - 1; + + SerializedProperty obj = spawnListProperty.GetArrayElementAtIndex(spawnListProperty.arraySize - 1); + obj.objectReferenceValue = null; + + spawnList.index = spawnList.count - 1; + + Changed(list); + } + + internal void RemoveButton(ReorderableList list) + { + spawnListProperty.DeleteArrayElementAtIndex(spawnList.index); + if (list.index >= spawnListProperty.arraySize) + { + list.index = spawnListProperty.arraySize - 1; + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs.meta b/Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs.meta new file mode 100644 index 0000000..7fe8dbc --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkManagerEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 519712eb07f7a44039df57664811c2c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs b/Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs new file mode 100644 index 0000000..866a50b --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEngine; + +namespace Mirror +{ + public class NetworkScenePostProcess : MonoBehaviour + { + [PostProcessScene] + public static void OnPostProcessScene() + { + // find all NetworkIdentities in all scenes + // => can't limit it to GetActiveScene() because that wouldn't work + // for additive scene loads (the additively loaded scene is never + // the active scene) + // => ignore DontDestroyOnLoad scene! this avoids weird situations + // like in NetworkZones when we destroy the local player and + // load another scene afterwards, yet the local player is still + // in the FindObjectsOfType result with scene=DontDestroyOnLoad + // for some reason + // => OfTypeAll so disabled objects are included too + // => Unity 2019 returns prefabs here too, so filter them out. + IEnumerable identities = Resources.FindObjectsOfTypeAll() + .Where(identity => identity.gameObject.hideFlags != HideFlags.NotEditable && + identity.gameObject.hideFlags != HideFlags.HideAndDontSave && + identity.gameObject.scene.name != "DontDestroyOnLoad" && + !PrefabUtility.IsPartOfPrefabAsset(identity.gameObject)); + + foreach (NetworkIdentity identity in identities) + { + // 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, + // and we don't want to set main scene's objects inactive again + if (!identity.isClient && !identity.isServer) + { + // valid scene object? + // otherwise it might be an unopened scene that still has null + // sceneIds. builds are interrupted if they contain 0 sceneIds, + // but it's still possible that we call LoadScene in Editor + // for a previously unopened scene. + // (and only do SetActive if this was actually a scene object) + if (identity.sceneId != 0) + { + // set scene hash + identity.SetSceneIdSceneHashPartInternal(); + + // disable it + // 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) as GameObject; + #else + GameObject prefabGO = PrefabUtility.GetPrefabParent(identity.gameObject) as 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) + { + if (prefabRootGO.GetComponentsInChildren().Length > 1) + { + Debug.LogWarningFormat("Prefab '{0}' has several NetworkIdentity components attached to itself or its children, this is not supported.", prefabRootGO.name); + } + } + } + } + // throwing an exception would only show it for one object + // because this function would return afterwards. + else Debug.LogError("Scene " + identity.gameObject.scene.path + " needs to be opened and resaved, because the scene object " + identity.name + " has no valid sceneId yet."); + } + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs.meta b/Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs.meta new file mode 100644 index 0000000..b567cc9 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/NetworkScenePostProcess.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3ec1c414d821444a9e77f18a2c130ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/PreprocessorDefine.cs b/Assets/Packages/Mirror/Editor/PreprocessorDefine.cs new file mode 100644 index 0000000..dd55178 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/PreprocessorDefine.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using UnityEditor; + +namespace Mirror +{ + static class PreprocessorDefine + { + /// + /// Add define symbols as soon as Unity gets done compiling. + /// + [InitializeOnLoadMethod] + static void AddDefineSymbols() + { + HashSet defines = new HashSet(PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';')) + { + "MIRROR", + "MIRROR_1726_OR_NEWER", + "MIRROR_3_0_OR_NEWER", + "MIRROR_3_12_OR_NEWER" + }; + PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, string.Join(";", defines)); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/PreprocessorDefine.cs.meta b/Assets/Packages/Mirror/Editor/PreprocessorDefine.cs.meta new file mode 100644 index 0000000..30806d0 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/PreprocessorDefine.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f1d66fe74ec6f42dd974cba37d25d453 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/SceneDrawer.cs b/Assets/Packages/Mirror/Editor/SceneDrawer.cs new file mode 100644 index 0000000..b6c04f4 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/SceneDrawer.cs @@ -0,0 +1,56 @@ +using UnityEditor; +using UnityEngine; + +namespace Mirror +{ + + [CustomPropertyDrawer(typeof(SceneAttribute))] + public class SceneDrawer : PropertyDrawer + { + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + + if (property.propertyType == SerializedPropertyType.String) + { + SceneAsset sceneObject = GetSceneObject(property.stringValue); + SceneAsset scene = (SceneAsset)EditorGUI.ObjectField(position, label, sceneObject, typeof(SceneAsset), true); + if (scene == null) + { + property.stringValue = ""; + } + else if (scene.name != property.stringValue) + { + SceneAsset sceneObj = GetSceneObject(scene.name); + if (sceneObj == null) + { + Debug.LogWarning("The scene " + scene.name + " cannot be used. To use this scene add it to the build settings for the project"); + } + else + { + property.stringValue = scene.name; + } + } + } + else + EditorGUI.LabelField(position, label.text, "Use [Scene] with strings."); + } + protected SceneAsset GetSceneObject(string sceneObjectName) + { + if (string.IsNullOrEmpty(sceneObjectName)) + { + return null; + } + + foreach (EditorBuildSettingsScene editorScene in EditorBuildSettings.scenes) + { + if (editorScene.path.IndexOf(sceneObjectName) != -1) + { + return AssetDatabase.LoadAssetAtPath(editorScene.path, typeof(SceneAsset)) as SceneAsset; + } + } + Debug.LogWarning("Scene [" + sceneObjectName + "] cannot be used. Add this scene to the 'Scenes in the Build' in build settings."); + return null; + } + } +} diff --git a/Assets/Packages/Mirror/Editor/SceneDrawer.cs.meta b/Assets/Packages/Mirror/Editor/SceneDrawer.cs.meta new file mode 100644 index 0000000..6a996dc --- /dev/null +++ b/Assets/Packages/Mirror/Editor/SceneDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b24704a46211b4ea294aba8f58715cea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver.meta b/Assets/Packages/Mirror/Editor/Weaver.meta new file mode 100644 index 0000000..121fbf4 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d9f8e6274119b4ce29e498cfb8aca8a4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs b/Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs new file mode 100644 index 0000000..501d378 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEditor.Compilation; +using UnityEngine; +using UnityAssembly = UnityEditor.Compilation.Assembly; + +namespace Mirror.Weaver +{ + public static class CompilationFinishedHook + { + const string MirrorRuntimeAssemblyName = "Mirror"; + const string MirrorWeaverAssemblyName = "Mirror.Weaver"; + + public static Action OnWeaverMessage; // delegate for subscription to Weaver debug messages + public static Action OnWeaverWarning; // delegate for subscription to Weaver warning messages + public static Action OnWeaverError; // delete for subscription to Weaver error messages + + public static bool WeaverEnabled { get; set; } // controls whether we weave any assemblies when CompilationPipeline delegates are invoked + public static bool UnityLogEnabled = true; // controls weather Weaver errors are reported direct to the Unity console (tests enable this) + public static bool WeaveFailed { get; private set; } // holds the result status of our latest Weave operation + + // debug message handler that also calls OnMessageMethod delegate + static void HandleMessage(string msg) + { + if (UnityLogEnabled) Debug.Log(msg); + if (OnWeaverMessage != null) OnWeaverMessage.Invoke(msg); + } + + // warning message handler that also calls OnWarningMethod delegate + static void HandleWarning(string msg) + { + if (UnityLogEnabled) Debug.LogWarning(msg); + if (OnWeaverWarning != null) OnWeaverWarning.Invoke(msg); + } + + // error message handler that also calls OnErrorMethod delegate + static void HandleError(string msg) + { + if (UnityLogEnabled) Debug.LogError(msg); + if (OnWeaverError != null) OnWeaverError.Invoke(msg); + } + + [InitializeOnLoadMethod] + static void OnInitializeOnLoad() + { + CompilationPipeline.assemblyCompilationFinished += OnCompilationFinished; + } + + static string FindMirrorRuntime() + { + foreach (UnityAssembly assembly in CompilationPipeline.GetAssemblies()) + { + if (assembly.name == MirrorRuntimeAssemblyName) + { + return assembly.outputPath; + } + } + return ""; + } + + static bool CompilerMessagesContainError(CompilerMessage[] messages) + { + return messages.Any(msg => msg.type == CompilerMessageType.Error); + } + + static void OnCompilationFinished(string assemblyPath, CompilerMessage[] messages) + { + // Do nothing if there were compile errors on the target + if (CompilerMessagesContainError(messages)) + { + Debug.Log("Weaver: stop because compile errors on target"); + return; + } + + // Should not run on the editor only assemblies + if (assemblyPath.Contains("-Editor") || assemblyPath.Contains(".Editor")) + { + return; + } + + // don't weave mirror files + string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); + if (assemblyName == MirrorRuntimeAssemblyName || assemblyName == MirrorWeaverAssemblyName) + { + return; + } + + // find Mirror.dll + string mirrorRuntimeDll = FindMirrorRuntime(); + if (string.IsNullOrEmpty(mirrorRuntimeDll)) + { + Debug.LogError("Failed to find Mirror runtime assembly"); + return; + } + if (!File.Exists(mirrorRuntimeDll)) + { + // this is normal, it happens with any assembly that is built before mirror + // such as unity packages or your own assemblies + // those don't need to be weaved + // if any assembly depends on mirror, then it will be built after + return; + } + + // find UnityEngine.CoreModule.dll + string unityEngineCoreModuleDLL = UnityEditorInternal.InternalEditorUtility.GetEngineCoreModuleAssemblyPath(); + if (string.IsNullOrEmpty(unityEngineCoreModuleDLL)) + { + Debug.LogError("Failed to find UnityEngine assembly"); + return; + } + + // build directory list for later asm/symbol resolving using CompilationPipeline refs + HashSet dependencyPaths = new HashSet(); + dependencyPaths.Add(Path.GetDirectoryName(assemblyPath)); + foreach (UnityAssembly unityAsm in CompilationPipeline.GetAssemblies()) + { + if (unityAsm.outputPath != assemblyPath) continue; + + foreach (string unityAsmRef in unityAsm.compiledAssemblyReferences) + { + dependencyPaths.Add(Path.GetDirectoryName(unityAsmRef)); + } + } + + // passing null in the outputDirectory param will do an in-place update of the assembly + if (Program.Process(unityEngineCoreModuleDLL, mirrorRuntimeDll, null, new[] { assemblyPath }, dependencyPaths.ToArray(), HandleWarning, HandleError)) + { + WeaveFailed = false; + //Debug.Log("Weaving succeeded for: " + assemblyPath); + } + else + { + WeaveFailed = true; + if (UnityLogEnabled) Debug.LogError("Weaving failed for: " + assemblyPath); + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs.meta new file mode 100644 index 0000000..ed537ab --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/CompilationFinishedHook.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de2aeb2e8068f421a9a1febe408f7051 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Extensions.cs b/Assets/Packages/Mirror/Editor/Weaver/Extensions.cs new file mode 100644 index 0000000..f2fad51 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Extensions.cs @@ -0,0 +1,151 @@ +using System; +using Mono.CecilX; + +namespace Mirror.Weaver +{ + public static class Extensions + { + public static bool IsDerivedFrom(this TypeDefinition td, TypeReference baseClass) + { + if (!td.IsClass) + return false; + + // are ANY parent classes of baseClass? + TypeReference parent = td.BaseType; + while (parent != null) + { + string parentName = parent.FullName; + + // strip generic parameters + int index = parentName.IndexOf('<'); + if (index != -1) + { + parentName = parentName.Substring(0, index); + } + + if (parentName == baseClass.FullName) + { + return true; + } + try + { + parent = parent.Resolve().BaseType; + } + catch (AssemblyResolutionException) + { + // this can happen for plugins. + //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString()); + break; + } + } + return false; + } + + public static TypeReference GetEnumUnderlyingType(this TypeDefinition td) + { + foreach (FieldDefinition field in td.Fields) + { + if (!field.IsStatic) + return field.FieldType; + } + throw new ArgumentException($"Invalid enum {td.FullName}"); + } + + public static bool ImplementsInterface(this TypeDefinition td, TypeReference baseInterface) + { + TypeDefinition typedef = td; + while (typedef != null) + { + foreach (InterfaceImplementation iface in typedef.Interfaces) + { + if (iface.InterfaceType.FullName == baseInterface.FullName) + return true; + } + + try + { + TypeReference parent = typedef.BaseType; + typedef = parent?.Resolve(); + } + catch (AssemblyResolutionException) + { + // this can happen for pluins. + //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString()); + break; + } + } + + return false; + } + + public static bool IsArrayType(this TypeReference tr) + { + if ((tr.IsArray && ((ArrayType)tr).ElementType.IsArray) || // jagged array + (tr.IsArray && ((ArrayType)tr).Rank > 1)) // multidimensional array + return false; + return true; + } + + public static bool CanBeResolved(this TypeReference parent) + { + while (parent != null) + { + if (parent.Scope.Name == "Windows") + { + return false; + } + + if (parent.Scope.Name == "mscorlib") + { + TypeDefinition resolved = parent.Resolve(); + return resolved != null; + } + + try + { + parent = parent.Resolve().BaseType; + } + catch + { + return false; + } + } + return true; + } + + + // Given a method of a generic class such as ArraySegment.get_Count, + // and a generic instance such as ArraySegment + // Creates a reference to the specialized method ArraySegment.get_Count; + // Note that calling ArraySegment.get_Count directly gives an invalid IL error + public static MethodReference MakeHostInstanceGeneric(this MethodReference self, GenericInstanceType instanceType) + { + + MethodReference reference = new MethodReference(self.Name, self.ReturnType, instanceType) + { + CallingConvention = self.CallingConvention, + HasThis = self.HasThis, + ExplicitThis = self.ExplicitThis + }; + + foreach (ParameterDefinition parameter in self.Parameters) + reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType)); + + foreach (GenericParameter generic_parameter in self.GenericParameters) + reference.GenericParameters.Add(new GenericParameter(generic_parameter.Name, reference)); + + return Weaver.CurrentAssembly.MainModule.ImportReference(reference); + } + + public static CustomAttribute GetCustomAttribute(this MethodDefinition method, string attributeName) + { + foreach (CustomAttribute ca in method.CustomAttributes) + { + if (ca.AttributeType.FullName == attributeName) + return ca; + } + return null; + } + + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Extensions.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Extensions.cs.meta new file mode 100644 index 0000000..78660f9 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 562a5cf0254cc45738e9aa549a7100b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Helpers.cs b/Assets/Packages/Mirror/Editor/Weaver/Helpers.cs new file mode 100644 index 0000000..a776954 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Helpers.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Mono.CecilX; +using Mono.CecilX.Cil; +using Mono.CecilX.Mdb; +using Mono.CecilX.Pdb; + +namespace Mirror.Weaver +{ + class Helpers + { + // This code is taken from SerializationWeaver + + class AddSearchDirectoryHelper + { + delegate void AddSearchDirectoryDelegate(string directory); + readonly AddSearchDirectoryDelegate _addSearchDirectory; + + public AddSearchDirectoryHelper(IAssemblyResolver assemblyResolver) + { + // reflection is used because IAssemblyResolver doesn't implement AddSearchDirectory but both DefaultAssemblyResolver and NuGetAssemblyResolver do + MethodInfo addSearchDirectory = assemblyResolver.GetType().GetMethod("AddSearchDirectory", BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(string) }, null); + if (addSearchDirectory == null) + throw new Exception("Assembly resolver doesn't implement AddSearchDirectory method."); + _addSearchDirectory = (AddSearchDirectoryDelegate)Delegate.CreateDelegate(typeof(AddSearchDirectoryDelegate), assemblyResolver, addSearchDirectory); + } + + public void AddSearchDirectory(string directory) + { + _addSearchDirectory(directory); + } + } + + public static string UnityEngineDLLDirectoryName() + { + string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase); + return directoryName?.Replace(@"file:\", ""); + } + + public static ISymbolReaderProvider GetSymbolReaderProvider(string inputFile) + { + string nakedFileName = inputFile.Substring(0, inputFile.Length - 4); + if (File.Exists(nakedFileName + ".pdb")) + { + Console.WriteLine("Symbols will be read from " + nakedFileName + ".pdb"); + return new PdbReaderProvider(); + } + if (File.Exists(nakedFileName + ".dll.mdb")) + { + Console.WriteLine("Symbols will be read from " + nakedFileName + ".dll.mdb"); + return new MdbReaderProvider(); + } + Console.WriteLine("No symbols for " + inputFile); + return null; + } + + public static string DestinationFileFor(string outputDir, string assemblyPath) + { + string fileName = Path.GetFileName(assemblyPath); + Debug.Assert(fileName != null, "fileName != null"); + + return Path.Combine(outputDir, fileName); + } + + public static string PrettyPrintType(TypeReference type) + { + // generic instances, such as List + if (type.IsGenericInstance) + { + GenericInstanceType giType = (GenericInstanceType)type; + return giType.Name.Substring(0, giType.Name.Length - 2) + "<" + string.Join(", ", giType.GenericArguments.Select(PrettyPrintType).ToArray()) + ">"; + } + + // generic types, such as List + if (type.HasGenericParameters) + { + return type.Name.Substring(0, type.Name.Length - 2) + "<" + string.Join(", ", type.GenericParameters.Select(x => x.Name).ToArray()) + ">"; + } + + // non-generic type such as Int + return type.Name; + } + + public static ReaderParameters ReaderParameters(string assemblyPath, IEnumerable extraPaths, IAssemblyResolver assemblyResolver, string unityEngineDLLPath, string mirrorNetDLLPath) + { + ReaderParameters parameters = new ReaderParameters {ReadWrite = true}; + if (assemblyResolver == null) + assemblyResolver = new DefaultAssemblyResolver(); + AddSearchDirectoryHelper helper = new AddSearchDirectoryHelper(assemblyResolver); + helper.AddSearchDirectory(Path.GetDirectoryName(assemblyPath)); + helper.AddSearchDirectory(UnityEngineDLLDirectoryName()); + helper.AddSearchDirectory(Path.GetDirectoryName(unityEngineDLLPath)); + helper.AddSearchDirectory(Path.GetDirectoryName(mirrorNetDLLPath)); + if (extraPaths != null) + { + foreach (string path in extraPaths) + helper.AddSearchDirectory(path); + } + parameters.AssemblyResolver = assemblyResolver; + parameters.SymbolReaderProvider = GetSymbolReaderProvider(assemblyPath); + return parameters; + } + + public static WriterParameters GetWriterParameters(ReaderParameters readParams) + { + WriterParameters writeParams = new WriterParameters(); + if (readParams.SymbolReaderProvider is PdbReaderProvider) + { + //Log("Will export symbols of pdb format"); + writeParams.SymbolWriterProvider = new PdbWriterProvider(); + } + else if (readParams.SymbolReaderProvider is MdbReaderProvider) + { + //Log("Will export symbols of mdb format"); + writeParams.SymbolWriterProvider = new MdbWriterProvider(); + } + return writeParams; + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Helpers.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Helpers.cs.meta new file mode 100644 index 0000000..231f539 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Helpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c4ed76daf48547c5abb7c58f8d20886 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef b/Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef new file mode 100644 index 0000000..5122428 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Mirror.Weaver", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef.meta b/Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef.meta new file mode 100644 index 0000000..b65a0cd --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Mirror.Weaver.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1d0b9d21c3ff546a4aa32399dfd33474 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors.meta new file mode 100644 index 0000000..eb719b4 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e538d627280d2471b8c72fdea822ca49 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs new file mode 100644 index 0000000..2a1688f --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs @@ -0,0 +1,153 @@ +// all the [Command] code from NetworkBehaviourProcessor in one place +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class CommandProcessor + { + const string CmdPrefix = "InvokeCmd"; + + /* + // generates code like: + public void CallCmdThrust(float thrusting, int spin) + { + if (isServer) + { + // we are ON the server, invoke directly + CmdThrust(thrusting, spin); + return; + } + + NetworkWriter networkWriter = new NetworkWriter(); + networkWriter.Write(thrusting); + networkWriter.WritePackedUInt32((uint)spin); + base.SendCommandInternal(cmdName, networkWriter, cmdName); + } + */ + public static MethodDefinition ProcessCommandCall(TypeDefinition td, MethodDefinition md, CustomAttribute ca) + { + MethodDefinition cmd = new MethodDefinition("Call" + md.Name, + MethodAttributes.Public | MethodAttributes.HideBySig, + Weaver.voidType); + + // add parameters + foreach (ParameterDefinition pd in md.Parameters) + { + cmd.Parameters.Add(new ParameterDefinition(pd.Name, ParameterAttributes.None, pd.ParameterType)); + } + + ILProcessor cmdWorker = cmd.Body.GetILProcessor(); + + NetworkBehaviourProcessor.WriteSetupLocals(cmdWorker); + + if (Weaver.GenerateLogErrors) + { + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldstr, "Call Command function " + md.Name)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Call, Weaver.logErrorReference)); + } + + // local client check + Instruction localClientLabel = cmdWorker.Create(OpCodes.Nop); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldarg_0)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Call, Weaver.getBehaviourIsServer)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Brfalse, localClientLabel)); + + // call the cmd function directly. + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldarg_0)); + for (int i = 0; i < md.Parameters.Count; i++) + { + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldarg, i + 1)); + } + cmdWorker.Append(cmdWorker.Create(OpCodes.Call, md)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ret)); + cmdWorker.Append(localClientLabel); + + // NetworkWriter writer = new NetworkWriter(); + NetworkBehaviourProcessor.WriteCreateWriter(cmdWorker); + + // write all the arguments that the user passed to the Cmd call + if (!NetworkBehaviourProcessor.WriteArguments(cmdWorker, md, false)) + return null; + + string cmdName = md.Name; + int index = cmdName.IndexOf(CmdPrefix); + if (index > -1) + { + cmdName = cmdName.Substring(CmdPrefix.Length); + } + + // invoke internal send and return + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldarg_0)); // load 'base.' to call the SendCommand function with + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldtoken, td)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Call, Weaver.getTypeFromHandleReference)); // invokerClass + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldstr, cmdName)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldloc_0)); // writer + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldc_I4, NetworkBehaviourProcessor.GetChannelId(ca))); + cmdWorker.Append(cmdWorker.Create(OpCodes.Call, Weaver.sendCommandInternal)); + + NetworkBehaviourProcessor.WriteRecycleWriter(cmdWorker); + + cmdWorker.Append(cmdWorker.Create(OpCodes.Ret)); + + return cmd; + } + + /* + // generates code like: + protected static void InvokeCmdCmdThrust(NetworkBehaviour obj, NetworkReader reader) + { + if (!NetworkServer.active) + { + return; + } + ((ShipControl)obj).CmdThrust(reader.ReadSingle(), (int)reader.ReadPackedUInt32()); + } + */ + public static MethodDefinition ProcessCommandInvoke(TypeDefinition td, MethodDefinition md) + { + MethodDefinition cmd = new MethodDefinition(CmdPrefix + md.Name, + MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, + Weaver.voidType); + + ILProcessor cmdWorker = cmd.Body.GetILProcessor(); + Instruction label = cmdWorker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteServerActiveCheck(cmdWorker, md.Name, label, "Command"); + + // setup for reader + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldarg_0)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Castclass, td)); + + if (!NetworkBehaviourProcessor.ProcessNetworkReaderParameters(md, cmdWorker, false)) + return null; + + // invoke actual command function + cmdWorker.Append(cmdWorker.Create(OpCodes.Callvirt, md)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ret)); + + NetworkBehaviourProcessor.AddInvokeParameters(cmd.Parameters); + + return cmd; + } + + public static bool ProcessMethodsValidateCommand(MethodDefinition md, CustomAttribute ca) + { + if (!md.Name.StartsWith("Cmd")) + { + Weaver.Error($"{md} must start with Cmd. Consider renaming it to Cmd{md.Name}"); + return false; + } + + if (md.IsStatic) + { + Weaver.Error($"{md} cannot be static"); + return false; + } + + // validate + return NetworkBehaviourProcessor.ProcessMethodsValidateFunction(md) && + NetworkBehaviourProcessor.ProcessMethodsValidateParameters(md, ca); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta new file mode 100644 index 0000000..20c3e15 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/CommandProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 73f6c9cdbb9e54f65b3a0a35cc8e55c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs new file mode 100644 index 0000000..131a6a5 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs @@ -0,0 +1,135 @@ +// this class generates OnSerialize/OnDeserialize when inheriting from MessageBase +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + static class MessageClassProcessor + { + public static void Process(TypeDefinition td) + { + Weaver.DLog(td, "MessageClassProcessor Start"); + + GenerateSerialization(td); + if (Weaver.WeavingFailed) + { + return; + } + + GenerateDeSerialization(td); + Weaver.DLog(td, "MessageClassProcessor Done"); + } + + static void GenerateSerialization(TypeDefinition td) + { + Weaver.DLog(td, " GenerateSerialization"); + foreach (MethodDefinition m in td.Methods) + { + if (m.Name == "Serialize") + return; + } + + if (td.Fields.Count == 0) + { + return; + } + + // check for self-referencing types + foreach (FieldDefinition field in td.Fields) + { + if (field.FieldType.FullName == td.FullName) + { + Weaver.Error($"{td} has field ${field} that references itself"); + return; + } + } + + MethodDefinition serializeFunc = new MethodDefinition("Serialize", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + Weaver.voidType); + + serializeFunc.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkWriterType))); + ILProcessor serWorker = serializeFunc.Body.GetILProcessor(); + + foreach (FieldDefinition field in td.Fields) + { + if (field.IsStatic || field.IsPrivate || field.IsSpecialName) + continue; + + if (field.FieldType.Resolve().HasGenericParameters && !field.FieldType.FullName.StartsWith("System.ArraySegment`1", System.StringComparison.Ordinal)) + { + Weaver.Error($"{field} cannot have generic type {field.FieldType}. Consider creating a class that derives the generic type"); + return; + } + + if (field.FieldType.Resolve().IsInterface) + { + Weaver.Error($"{field} has unsupported type. Use a concrete class instead of interface {field.FieldType}"); + return; + } + + MethodReference writeFunc = Writers.GetWriteFunc(field.FieldType); + if (writeFunc != null) + { + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldfld, field)); + serWorker.Append(serWorker.Create(OpCodes.Call, writeFunc)); + } + else + { + Weaver.Error($"{field} has unsupported type"); + return; + } + } + serWorker.Append(serWorker.Create(OpCodes.Ret)); + + td.Methods.Add(serializeFunc); + } + + static void GenerateDeSerialization(TypeDefinition td) + { + Weaver.DLog(td, " GenerateDeserialization"); + foreach (MethodDefinition m in td.Methods) + { + if (m.Name == "Deserialize") + return; + } + + if (td.Fields.Count == 0) + { + return; + } + + MethodDefinition serializeFunc = new MethodDefinition("Deserialize", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + Weaver.voidType); + + serializeFunc.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkReaderType))); + ILProcessor serWorker = serializeFunc.Body.GetILProcessor(); + + foreach (FieldDefinition field in td.Fields) + { + if (field.IsStatic || field.IsPrivate || field.IsSpecialName) + continue; + + MethodReference readerFunc = Readers.GetReadFunc(field.FieldType); + if (readerFunc != null) + { + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Call, readerFunc)); + serWorker.Append(serWorker.Create(OpCodes.Stfld, field)); + } + else + { + Weaver.Error($"{field} has unsupported type"); + return; + } + } + serWorker.Append(serWorker.Create(OpCodes.Ret)); + + td.Methods.Add(serializeFunc); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs.meta new file mode 100644 index 0000000..875cf9a --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/MessageClassProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3544c9f00f6e5443ea3c30873c5a06ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs new file mode 100644 index 0000000..c5f880e --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs @@ -0,0 +1,77 @@ +// this class only shows warnings in case we use SyncVars etc. for MonoBehaviour. +using Mono.CecilX; + +namespace Mirror.Weaver +{ + static class MonoBehaviourProcessor + { + public static void Process(TypeDefinition td) + { + ProcessSyncVars(td); + ProcessMethods(td); + } + + static void ProcessSyncVars(TypeDefinition td) + { + // find syncvars + foreach (FieldDefinition fd in td.Fields) + { + foreach (CustomAttribute ca in fd.CustomAttributes) + { + if (ca.AttributeType.FullName == Weaver.SyncVarType.FullName) + { + Weaver.Error($"[SyncVar] {fd} must be inside a NetworkBehaviour. {td} is not a NetworkBehaviour"); + } + } + + if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType)) + { + Weaver.Error($"{fd} is a SyncObject and must be inside a NetworkBehaviour. {td} is not a NetworkBehaviour"); + } + } + } + + static void ProcessMethods(TypeDefinition td) + { + // find command and RPC functions + foreach (MethodDefinition md in td.Methods) + { + foreach (CustomAttribute ca in md.CustomAttributes) + { + if (ca.AttributeType.FullName == Weaver.CommandType.FullName) + { + Weaver.Error($"[Command] {md} must be declared inside a NetworkBehaviour"); + } + + if (ca.AttributeType.FullName == Weaver.ClientRpcType.FullName) + { + Weaver.Error($"[ClienRpc] {md} must be declared inside a NetworkBehaviour"); + } + + if (ca.AttributeType.FullName == Weaver.TargetRpcType.FullName) + { + Weaver.Error($"[TargetRpc] {md} must be declared inside a NetworkBehaviour"); + } + + string attributeName = ca.Constructor.DeclaringType.ToString(); + + switch (attributeName) + { + case "Mirror.ServerAttribute": + Weaver.Error($"[Server] {md} must be declared inside a NetworkBehaviour"); + break; + case "Mirror.ServerCallbackAttribute": + Weaver.Error($"[ServerCallback] {md} must be declared inside a NetworkBehaviour"); + break; + case "Mirror.ClientAttribute": + Weaver.Error($"[Client] {md} must be declared inside a NetworkBehaviour"); + break; + case "Mirror.ClientCallbackAttribute": + Weaver.Error($"[ClientCallback] {md} must be declared inside a NetworkBehaviour"); + break; + } + } + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta new file mode 100644 index 0000000..ef3f5f4 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/MonoBehaviourProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35c16722912b64af894e4f6668f2e54c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs new file mode 100644 index 0000000..0b12e03 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -0,0 +1,858 @@ +// this class processes SyncVars, Cmds, Rpcs, etc. of NetworkBehaviours +using System; +using System.Linq; +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + class NetworkBehaviourProcessor + { + readonly List syncVars = new List(); + readonly List syncObjects = new List(); + readonly Dictionary syncVarNetIds = new Dictionary(); // + readonly List commands = new List(); + readonly List clientRpcs = new List(); + readonly List targetRpcs = new List(); + readonly List eventRpcs = new List(); + readonly List commandInvocationFuncs = new List(); + readonly List clientRpcInvocationFuncs = new List(); + readonly List targetRpcInvocationFuncs = new List(); + readonly List eventRpcInvocationFuncs = new List(); + + readonly List commandCallFuncs = new List(); + readonly List clientRpcCallFuncs = new List(); + readonly List targetRpcCallFuncs = new List(); + + readonly TypeDefinition netBehaviourSubclass; + + public NetworkBehaviourProcessor(TypeDefinition td) + { + Weaver.DLog(td, "NetworkBehaviourProcessor"); + netBehaviourSubclass = td; + } + + public void Process() + { + if (netBehaviourSubclass.HasGenericParameters) + { + Weaver.Error($"{netBehaviourSubclass} cannot have generic parameters"); + return; + } + Weaver.DLog(netBehaviourSubclass, "Process Start"); + MarkAsProcessed(netBehaviourSubclass); + SyncVarProcessor.ProcessSyncVars(netBehaviourSubclass, syncVars, syncObjects, syncVarNetIds); + + ProcessMethods(); + + SyncEventProcessor.ProcessEvents(netBehaviourSubclass, eventRpcs, eventRpcInvocationFuncs); + if (Weaver.WeavingFailed) + { + return; + } + GenerateConstants(); + + GenerateSerialization(); + if (Weaver.WeavingFailed) + { + return; + } + + GenerateDeSerialization(); + Weaver.DLog(netBehaviourSubclass, "Process Done"); + } + + /* + generates code like: + if (!NetworkClient.active) + Debug.LogError((object) "Command function CmdRespawn called on server."); + + which is used in InvokeCmd, InvokeRpc, etc. + */ + public static void WriteClientActiveCheck(ILProcessor worker, string mdName, Instruction label, string errString) + { + // client active check + worker.Append(worker.Create(OpCodes.Call, Weaver.NetworkClientGetActive)); + worker.Append(worker.Create(OpCodes.Brtrue, label)); + + worker.Append(worker.Create(OpCodes.Ldstr, errString + " " + mdName + " called on server.")); + worker.Append(worker.Create(OpCodes.Call, Weaver.logErrorReference)); + worker.Append(worker.Create(OpCodes.Ret)); + worker.Append(label); + } + /* + generates code like: + if (!NetworkServer.active) + Debug.LogError((object) "Command CmdMsgWhisper called on client."); + */ + public static void WriteServerActiveCheck(ILProcessor worker, string mdName, Instruction label, string errString) + { + // server active check + worker.Append(worker.Create(OpCodes.Call, Weaver.NetworkServerGetActive)); + worker.Append(worker.Create(OpCodes.Brtrue, label)); + + worker.Append(worker.Create(OpCodes.Ldstr, errString + " " + mdName + " called on client.")); + worker.Append(worker.Create(OpCodes.Call, Weaver.logErrorReference)); + worker.Append(worker.Create(OpCodes.Ret)); + worker.Append(label); + } + + public static void WriteSetupLocals(ILProcessor worker) + { + worker.Body.InitLocals = true; + worker.Body.Variables.Add(new VariableDefinition(Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkWriterType))); + } + + public static void WriteCreateWriter(ILProcessor worker) + { + // create writer + worker.Append(worker.Create(OpCodes.Call, Weaver.GetPooledWriterReference)); + worker.Append(worker.Create(OpCodes.Stloc_0)); + } + + public static void WriteRecycleWriter(ILProcessor worker) + { + // NetworkWriterPool.Recycle(writer); + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Call, Weaver.RecycleWriterReference)); + } + + public static bool WriteArguments(ILProcessor worker, MethodDefinition md, bool skipFirst) + { + // write each argument + short argNum = 1; + foreach (ParameterDefinition pd in md.Parameters) + { + if (argNum == 1 && skipFirst) + { + argNum += 1; + continue; + } + + MethodReference writeFunc = Writers.GetWriteFunc(pd.ParameterType); + if (writeFunc == null) + { + Weaver.Error($"{md} has invalid parameter {pd}" ); + return false; + } + // use built-in writer func on writer object + worker.Append(worker.Create(OpCodes.Ldloc_0)); // writer object + worker.Append(worker.Create(OpCodes.Ldarg, argNum)); // argument + worker.Append(worker.Create(OpCodes.Call, writeFunc)); // call writer func on writer object + argNum += 1; + } + return true; + } + + #region mark / check type as processed + public const string ProcessedFunctionName = "MirrorProcessed"; + + // by adding an empty MirrorProcessed() function + public static bool WasProcessed(TypeDefinition td) + { + return td.Methods.Any(method => method.Name == ProcessedFunctionName); + } + + public static void MarkAsProcessed(TypeDefinition td) + { + if (!WasProcessed(td)) + { + MethodDefinition versionMethod = new MethodDefinition(ProcessedFunctionName, MethodAttributes.Private, Weaver.voidType); + ILProcessor worker = versionMethod.Body.GetILProcessor(); + worker.Append(worker.Create(OpCodes.Ret)); + td.Methods.Add(versionMethod); + } + } + #endregion + + void GenerateConstants() + { + if (commands.Count == 0 && clientRpcs.Count == 0 && targetRpcs.Count == 0 && eventRpcs.Count == 0 && syncObjects.Count == 0) + return; + + Weaver.DLog(netBehaviourSubclass, " GenerateConstants "); + + // find static constructor + MethodDefinition cctor = null; + bool cctorFound = false; + foreach (MethodDefinition md in netBehaviourSubclass.Methods) + { + if (md.Name == ".cctor") + { + cctor = md; + cctorFound = true; + } + } + if (cctor != null) + { + // remove the return opcode from end of function. will add our own later. + if (cctor.Body.Instructions.Count != 0) + { + Instruction ret = cctor.Body.Instructions[cctor.Body.Instructions.Count - 1]; + if (ret.OpCode == OpCodes.Ret) + { + cctor.Body.Instructions.RemoveAt(cctor.Body.Instructions.Count - 1); + } + else + { + Weaver.Error($"{netBehaviourSubclass} has invalid class constructor"); + return; + } + } + } + else + { + // make one! + cctor = new MethodDefinition(".cctor", MethodAttributes.Private | + MethodAttributes.HideBySig | + MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName | + MethodAttributes.Static, + Weaver.voidType); + } + + // find instance constructor + MethodDefinition ctor = null; + + foreach (MethodDefinition md in netBehaviourSubclass.Methods) + { + if (md.Name == ".ctor") + { + ctor = md; + + Instruction ret = ctor.Body.Instructions[ctor.Body.Instructions.Count - 1]; + if (ret.OpCode == OpCodes.Ret) + { + ctor.Body.Instructions.RemoveAt(ctor.Body.Instructions.Count - 1); + } + else + { + Weaver.Error($"{netBehaviourSubclass} has invalid constructor"); + return; + } + + break; + } + } + + if (ctor == null) + { + Weaver.Error($"{netBehaviourSubclass} has invalid constructor"); + return; + } + + ILProcessor ctorWorker = ctor.Body.GetILProcessor(); + ILProcessor cctorWorker = cctor.Body.GetILProcessor(); + + for (int i = 0; i < commands.Count; ++i) + { + GenerateRegisterCommandDelegate(cctorWorker, Weaver.registerCommandDelegateReference, commandInvocationFuncs[i], commands[i].Name); + } + + for (int i = 0; i < clientRpcs.Count; ++i) + { + GenerateRegisterCommandDelegate(cctorWorker, Weaver.registerRpcDelegateReference, clientRpcInvocationFuncs[i], clientRpcs[i].Name); + } + + for (int i = 0; i < targetRpcs.Count; ++i) + { + GenerateRegisterCommandDelegate(cctorWorker, Weaver.registerRpcDelegateReference, targetRpcInvocationFuncs[i], targetRpcs[i].Name); + } + + for (int i = 0; i < eventRpcs.Count; ++i) + { + GenerateRegisterCommandDelegate(cctorWorker, Weaver.registerEventDelegateReference, eventRpcInvocationFuncs[i], eventRpcs[i].Name); + } + + foreach (FieldDefinition fd in syncObjects) + { + SyncObjectInitializer.GenerateSyncObjectInitializer(ctorWorker, fd); + } + + cctorWorker.Append(cctorWorker.Create(OpCodes.Ret)); + if (!cctorFound) + { + netBehaviourSubclass.Methods.Add(cctor); + } + + // finish ctor + ctorWorker.Append(ctorWorker.Create(OpCodes.Ret)); + + // in case class had no cctor, it might have BeforeFieldInit, so injected cctor would be called too late + netBehaviourSubclass.Attributes &= ~TypeAttributes.BeforeFieldInit; + } + + /* + // This generates code like: + NetworkBehaviour.RegisterCommandDelegate(base.GetType(), "CmdThrust", new NetworkBehaviour.CmdDelegate(ShipControl.InvokeCmdCmdThrust)); + */ + void GenerateRegisterCommandDelegate(ILProcessor awakeWorker, MethodReference registerMethod, MethodDefinition func, string cmdName) + { + awakeWorker.Append(awakeWorker.Create(OpCodes.Ldtoken, netBehaviourSubclass)); + awakeWorker.Append(awakeWorker.Create(OpCodes.Call, Weaver.getTypeFromHandleReference)); + awakeWorker.Append(awakeWorker.Create(OpCodes.Ldstr, cmdName)); + awakeWorker.Append(awakeWorker.Create(OpCodes.Ldnull)); + awakeWorker.Append(awakeWorker.Create(OpCodes.Ldftn, func)); + + awakeWorker.Append(awakeWorker.Create(OpCodes.Newobj, Weaver.CmdDelegateConstructor)); + awakeWorker.Append(awakeWorker.Create(OpCodes.Call, registerMethod)); + } + + void GenerateSerialization() + { + Weaver.DLog(netBehaviourSubclass, " GenerateSerialization"); + + foreach (MethodDefinition m in netBehaviourSubclass.Methods) + { + if (m.Name == "OnSerialize") + return; + } + + if (syncVars.Count == 0) + { + // no synvars, no need for custom OnSerialize + return; + } + + MethodDefinition serialize = new MethodDefinition("OnSerialize", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + Weaver.boolType); + + serialize.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkWriterType))); + serialize.Parameters.Add(new ParameterDefinition("forceAll", ParameterAttributes.None, Weaver.boolType)); + ILProcessor serWorker = serialize.Body.GetILProcessor(); + + serialize.Body.InitLocals = true; + + // loc_0, this local variable is to determine if any variable was dirty + VariableDefinition dirtyLocal = new VariableDefinition(Weaver.boolType); + serialize.Body.Variables.Add(dirtyLocal); + + MethodReference baseSerialize = Resolvers.ResolveMethodInParents(netBehaviourSubclass.BaseType, Weaver.CurrentAssembly, "OnSerialize"); + if (baseSerialize != null) + { + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); // base + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); // writer + serWorker.Append(serWorker.Create(OpCodes.Ldarg_2)); // forceAll + serWorker.Append(serWorker.Create(OpCodes.Call, baseSerialize)); + serWorker.Append(serWorker.Create(OpCodes.Stloc_0)); // set dirtyLocal to result of base.OnSerialize() + } + + // Generates: if (forceAll); + Instruction initialStateLabel = serWorker.Create(OpCodes.Nop); + serWorker.Append(serWorker.Create(OpCodes.Ldarg_2)); // forceAll + serWorker.Append(serWorker.Create(OpCodes.Brfalse, initialStateLabel)); + + foreach (FieldDefinition syncVar in syncVars) + { + // Generates a writer call for each sync variable + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); // writer + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); // this + serWorker.Append(serWorker.Create(OpCodes.Ldfld, syncVar)); + MethodReference writeFunc = Writers.GetWriteFunc(syncVar.FieldType); + if (writeFunc != null) + { + serWorker.Append(serWorker.Create(OpCodes.Call, writeFunc)); + } + else + { + Weaver.Error($"{syncVar} has unsupported type. Use a supported Mirror type instead"); + return; + } + } + + // always return true if forceAll + + // Generates: return true + serWorker.Append(serWorker.Create(OpCodes.Ldc_I4_1)); + serWorker.Append(serWorker.Create(OpCodes.Ret)); + + // Generates: end if (forceAll); + serWorker.Append(initialStateLabel); + + // write dirty bits before the data fields + // Generates: writer.WritePackedUInt64 (base.get_syncVarDirtyBits ()); + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); // writer + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); // base + serWorker.Append(serWorker.Create(OpCodes.Call, Weaver.NetworkBehaviourDirtyBitsReference)); + serWorker.Append(serWorker.Create(OpCodes.Call, Writers.GetWriteFunc(Weaver.uint64Type))); + + // generate a writer call for any dirty variable in this class + + // start at number of syncvars in parent + int dirtyBit = Weaver.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); + foreach (FieldDefinition syncVar in syncVars) + { + Instruction varLabel = serWorker.Create(OpCodes.Nop); + + // Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL) + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); // base + serWorker.Append(serWorker.Create(OpCodes.Call, Weaver.NetworkBehaviourDirtyBitsReference)); + serWorker.Append(serWorker.Create(OpCodes.Ldc_I8, 1L << dirtyBit)); // 8 bytes = long + serWorker.Append(serWorker.Create(OpCodes.And)); + serWorker.Append(serWorker.Create(OpCodes.Brfalse, varLabel)); + + // Generates a call to the writer for that field + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); // writer + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); // base + serWorker.Append(serWorker.Create(OpCodes.Ldfld, syncVar)); + + MethodReference writeFunc = Writers.GetWriteFunc(syncVar.FieldType); + if (writeFunc != null) + { + serWorker.Append(serWorker.Create(OpCodes.Call, writeFunc)); + } + else + { + Weaver.Error($"{syncVar} has unsupported type. Use a supported Mirror type instead"); + return; + } + + // something was dirty + serWorker.Append(serWorker.Create(OpCodes.Ldc_I4_1)); + serWorker.Append(serWorker.Create(OpCodes.Stloc_0)); // set dirtyLocal to true + + serWorker.Append(varLabel); + dirtyBit += 1; + } + + if (Weaver.GenerateLogErrors) + { + serWorker.Append(serWorker.Create(OpCodes.Ldstr, "Injected Serialize " + netBehaviourSubclass.Name)); + serWorker.Append(serWorker.Create(OpCodes.Call, Weaver.logErrorReference)); + } + + // generate: return dirtyLocal + serWorker.Append(serWorker.Create(OpCodes.Ldloc_0)); + serWorker.Append(serWorker.Create(OpCodes.Ret)); + netBehaviourSubclass.Methods.Add(serialize); + } + + public static int GetChannelId(CustomAttribute ca) + { + foreach (CustomAttributeNamedArgument customField in ca.Fields) + { + if (customField.Name == "channel") + { + return (int)customField.Argument.Value; + } + } + + return 0; + } + + void DeserializeField(FieldDefinition syncVar, ILProcessor serWorker, MethodDefinition deserialize) + { + // check for Hook function + if (!SyncVarProcessor.CheckForHookFunction(netBehaviourSubclass, syncVar, out MethodDefinition foundMethod)) + { + return; + } + + if (syncVar.FieldType.FullName == Weaver.gameObjectType.FullName || + syncVar.FieldType.FullName == Weaver.NetworkIdentityType.FullName) + { + // GameObject/NetworkIdentity SyncVar: + // OnSerialize sends writer.Write(go); + // OnDeserialize reads to __netId manually so we can use + // lookups in the getter (so it still works if objects + // move in and out of range repeatedly) + FieldDefinition netIdField = syncVarNetIds[syncVar]; + + VariableDefinition tmpValue = new VariableDefinition(Weaver.uint32Type); + deserialize.Body.Variables.Add(tmpValue); + + // read id and store in a local variable + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Call, Readers.GetReadFunc(Weaver.uint32Type))); + serWorker.Append(serWorker.Create(OpCodes.Stloc, tmpValue)); + + if (foundMethod != null) + { + // call Hook(this.GetSyncVarGameObject/NetworkIdentity(reader.ReadPackedUInt32())) + // because we send/receive the netID, not the GameObject/NetworkIdentity + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); // this. + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldloc, tmpValue)); + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldflda, syncVar)); + if (syncVar.FieldType.FullName == Weaver.gameObjectType.FullName) + serWorker.Append(serWorker.Create(OpCodes.Callvirt, Weaver.getSyncVarGameObjectReference)); + else if (syncVar.FieldType.FullName == Weaver.NetworkIdentityType.FullName) + serWorker.Append(serWorker.Create(OpCodes.Callvirt, Weaver.getSyncVarNetworkIdentityReference)); + serWorker.Append(serWorker.Create(OpCodes.Call, foundMethod)); + } + // set the netid field + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldloc, tmpValue)); + serWorker.Append(serWorker.Create(OpCodes.Stfld, netIdField)); + } + else + { + MethodReference readFunc = Readers.GetReadFunc(syncVar.FieldType); + if (readFunc == null) + { + Weaver.Error($"{syncVar} has unsupported type. Use a supported Mirror type instead"); + return; + } + VariableDefinition tmpValue = new VariableDefinition(syncVar.FieldType); + deserialize.Body.Variables.Add(tmpValue); + + // read value and put it in a local variable + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Call, readFunc)); + serWorker.Append(serWorker.Create(OpCodes.Stloc, tmpValue)); + + if (foundMethod != null) + { + // call hook + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldloc, tmpValue)); + serWorker.Append(serWorker.Create(OpCodes.Call, foundMethod)); + } + // set the property + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldloc, tmpValue)); + serWorker.Append(serWorker.Create(OpCodes.Stfld, syncVar)); + } + + } + + void GenerateDeSerialization() + { + Weaver.DLog(netBehaviourSubclass, " GenerateDeSerialization"); + + foreach (MethodDefinition m in netBehaviourSubclass.Methods) + { + if (m.Name == "OnDeserialize") + return; + } + + if (syncVars.Count == 0) + { + // no synvars, no need for custom OnDeserialize + return; + } + + MethodDefinition serialize = new MethodDefinition("OnDeserialize", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + Weaver.voidType); + + serialize.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkReaderType))); + serialize.Parameters.Add(new ParameterDefinition("initialState", ParameterAttributes.None, Weaver.boolType)); + ILProcessor serWorker = serialize.Body.GetILProcessor(); + // setup local for dirty bits + serialize.Body.InitLocals = true; + VariableDefinition dirtyBitsLocal = new VariableDefinition(Weaver.int64Type); + serialize.Body.Variables.Add(dirtyBitsLocal); + + MethodReference baseDeserialize = Resolvers.ResolveMethodInParents(netBehaviourSubclass.BaseType, Weaver.CurrentAssembly, "OnDeserialize"); + if (baseDeserialize != null) + { + serWorker.Append(serWorker.Create(OpCodes.Ldarg_0)); // base + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); // reader + serWorker.Append(serWorker.Create(OpCodes.Ldarg_2)); // initialState + serWorker.Append(serWorker.Create(OpCodes.Call, baseDeserialize)); + } + + // Generates: if (initialState); + Instruction initialStateLabel = serWorker.Create(OpCodes.Nop); + + serWorker.Append(serWorker.Create(OpCodes.Ldarg_2)); + serWorker.Append(serWorker.Create(OpCodes.Brfalse, initialStateLabel)); + + foreach (FieldDefinition syncVar in syncVars) + { + DeserializeField(syncVar, serWorker, serialize); + } + + serWorker.Append(serWorker.Create(OpCodes.Ret)); + + // Generates: end if (initialState); + serWorker.Append(initialStateLabel); + + + // get dirty bits + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Call, Readers.GetReadFunc(Weaver.uint64Type))); + serWorker.Append(serWorker.Create(OpCodes.Stloc_0)); + + // conditionally read each syncvar + int dirtyBit = Weaver.GetSyncVarStart(netBehaviourSubclass.BaseType.FullName); // start at number of syncvars in parent + foreach (FieldDefinition syncVar in syncVars) + { + Instruction varLabel = serWorker.Create(OpCodes.Nop); + + // check if dirty bit is set + serWorker.Append(serWorker.Create(OpCodes.Ldloc_0)); + serWorker.Append(serWorker.Create(OpCodes.Ldc_I8, 1L << dirtyBit)); + serWorker.Append(serWorker.Create(OpCodes.And)); + serWorker.Append(serWorker.Create(OpCodes.Brfalse, varLabel)); + + DeserializeField(syncVar, serWorker, serialize); + + serWorker.Append(varLabel); + dirtyBit += 1; + } + + if (Weaver.GenerateLogErrors) + { + serWorker.Append(serWorker.Create(OpCodes.Ldstr, "Injected Deserialize " + netBehaviourSubclass.Name)); + serWorker.Append(serWorker.Create(OpCodes.Call, Weaver.logErrorReference)); + } + + serWorker.Append(serWorker.Create(OpCodes.Ret)); + netBehaviourSubclass.Methods.Add(serialize); + } + + public static bool ProcessNetworkReaderParameters(MethodDefinition md, ILProcessor worker, bool skipFirst) + { + int count = 0; + + // read cmd args from NetworkReader + foreach (ParameterDefinition arg in md.Parameters) + { + if (count++ == 0 && skipFirst) + { + continue; + } + MethodReference readFunc = Readers.GetReadFunc(arg.ParameterType); //? + + if (readFunc != null) + { + worker.Append(worker.Create(OpCodes.Ldarg_1)); + worker.Append(worker.Create(OpCodes.Call, readFunc)); + + // conversion.. is this needed? + if (arg.ParameterType.FullName == Weaver.singleType.FullName) + { + worker.Append(worker.Create(OpCodes.Conv_R4)); + } + else if (arg.ParameterType.FullName == Weaver.doubleType.FullName) + { + worker.Append(worker.Create(OpCodes.Conv_R8)); + } + } + else + { + Weaver.Error($"{md} has invalid parameter {arg}. Unsupported type {arg.ParameterType}, use a supported Mirror type instead"); + return false; + } + } + return true; + } + + public static void AddInvokeParameters(ICollection collection) + { + collection.Add(new ParameterDefinition("obj", ParameterAttributes.None, Weaver.NetworkBehaviourType2)); + collection.Add(new ParameterDefinition("reader", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkReaderType))); + } + + public static bool ProcessMethodsValidateFunction(MethodReference md) + { + if (md.ReturnType.FullName == Weaver.IEnumeratorType.FullName) + { + Weaver.Error($"{md} cannot be a coroutine"); + return false; + } + if (md.ReturnType.FullName != Weaver.voidType.FullName) + { + Weaver.Error($"{md} cannot return a value. Make it void instead"); + return false; + } + if (md.HasGenericParameters) + { + Weaver.Error($"{md} cannot have generic parameters"); + return false; + } + return true; + } + + public static bool ProcessMethodsValidateParameters(MethodReference md, CustomAttribute ca) + { + for (int i = 0; i < md.Parameters.Count; ++i) + { + ParameterDefinition p = md.Parameters[i]; + if (p.IsOut) + { + Weaver.Error($"{md} cannot have out parameters"); + return false; + } + if (p.IsOptional) + { + Weaver.Error($"{md} cannot have optional parameters"); + return false; + } + if (p.ParameterType.Resolve().IsAbstract) + { + Weaver.Error($"{md} has invalid parameter {p}. Use concrete type instead of abstract type {p.ParameterType}"); + return false; + } + if (p.ParameterType.IsByReference) + { + Weaver.Error($"{md} has invalid parameter {p}. Use supported type instead of reference type {p.ParameterType}"); + return false; + } + // TargetRPC is an exception to this rule and can have a NetworkConnection as first parameter + if (p.ParameterType.FullName == Weaver.NetworkConnectionType.FullName && + !(ca.AttributeType.FullName == Weaver.TargetRpcType.FullName && i == 0)) + { + Weaver.Error($"{md} has invalid parameer {p}. Cannot pass NeworkConnections"); + return false; + } + if (p.ParameterType.Resolve().IsDerivedFrom(Weaver.ComponentType)) + { + if (p.ParameterType.FullName != Weaver.NetworkIdentityType.FullName) + { + Weaver.Error($"{md} has invalid parameter {p}. Cannot pass components in remote method calls"); + return false; + } + } + } + return true; + } + + void ProcessMethods() + { + HashSet names = new HashSet(); + + // find command and RPC functions + foreach (MethodDefinition md in netBehaviourSubclass.Methods) + { + foreach (CustomAttribute ca in md.CustomAttributes) + { + if (ca.AttributeType.FullName == Weaver.CommandType.FullName) + { + ProcessCommand(names, md, ca); + break; + } + + if (ca.AttributeType.FullName == Weaver.TargetRpcType.FullName) + { + ProcessTargetRpc(names, md, ca); + break; + } + + if (ca.AttributeType.FullName == Weaver.ClientRpcType.FullName) + { + ProcessClientRpc(names, md, ca); + break; + } + } + } + + // cmds + foreach (MethodDefinition md in commandInvocationFuncs) + { + netBehaviourSubclass.Methods.Add(md); + } + foreach (MethodDefinition md in commandCallFuncs) + { + netBehaviourSubclass.Methods.Add(md); + } + + // rpcs + foreach (MethodDefinition md in clientRpcInvocationFuncs) + { + netBehaviourSubclass.Methods.Add(md); + } + foreach (MethodDefinition md in targetRpcInvocationFuncs) + { + netBehaviourSubclass.Methods.Add(md); + } + foreach (MethodDefinition md in clientRpcCallFuncs) + { + netBehaviourSubclass.Methods.Add(md); + } + foreach (MethodDefinition md in targetRpcCallFuncs) + { + netBehaviourSubclass.Methods.Add(md); + } + } + + void ProcessClientRpc(HashSet names, MethodDefinition md, CustomAttribute ca) + { + if (!RpcProcessor.ProcessMethodsValidateRpc(md, ca)) + { + return; + } + + if (names.Contains(md.Name)) + { + Weaver.Error("Duplicate ClientRpc name [" + netBehaviourSubclass.FullName + ":" + md.Name + "]"); + return; + } + names.Add(md.Name); + clientRpcs.Add(md); + + MethodDefinition rpcFunc = RpcProcessor.ProcessRpcInvoke(netBehaviourSubclass, md); + if (rpcFunc != null) + { + clientRpcInvocationFuncs.Add(rpcFunc); + } + + MethodDefinition rpcCallFunc = RpcProcessor.ProcessRpcCall(netBehaviourSubclass, md, ca); + if (rpcCallFunc != null) + { + clientRpcCallFuncs.Add(rpcCallFunc); + Weaver.WeaveLists.replaceMethods[md.FullName] = rpcCallFunc; + } + } + + void ProcessTargetRpc(HashSet names, MethodDefinition md, CustomAttribute ca) + { + if (!TargetRpcProcessor.ProcessMethodsValidateTargetRpc(md, ca)) + return; + + if (names.Contains(md.Name)) + { + Weaver.Error("Duplicate Target Rpc name [" + netBehaviourSubclass.FullName + ":" + md.Name + "]"); + return; + } + names.Add(md.Name); + targetRpcs.Add(md); + + MethodDefinition rpcFunc = TargetRpcProcessor.ProcessTargetRpcInvoke(netBehaviourSubclass, md); + if (rpcFunc != null) + { + targetRpcInvocationFuncs.Add(rpcFunc); + } + + MethodDefinition rpcCallFunc = TargetRpcProcessor.ProcessTargetRpcCall(netBehaviourSubclass, md, ca); + if (rpcCallFunc != null) + { + targetRpcCallFuncs.Add(rpcCallFunc); + Weaver.WeaveLists.replaceMethods[md.FullName] = rpcCallFunc; + } + } + + void ProcessCommand(HashSet names, MethodDefinition md, CustomAttribute ca) + { + if (!CommandProcessor.ProcessMethodsValidateCommand(md, ca)) + return; + + if (names.Contains(md.Name)) + { + Weaver.Error("Duplicate Command name [" + netBehaviourSubclass.FullName + ":" + md.Name + "]"); + return; + } + + names.Add(md.Name); + commands.Add(md); + + MethodDefinition cmdFunc = CommandProcessor.ProcessCommandInvoke(netBehaviourSubclass, md); + if (cmdFunc != null) + { + commandInvocationFuncs.Add(cmdFunc); + } + + MethodDefinition cmdCallFunc = CommandProcessor.ProcessCommandCall(netBehaviourSubclass, md, ca); + if (cmdCallFunc != null) + { + commandCallFuncs.Add(cmdCallFunc); + Weaver.WeaveLists.replaceMethods[md.FullName] = cmdCallFunc; + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta new file mode 100644 index 0000000..67c27dc --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8118d606be3214e5d99943ec39530dd8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs new file mode 100644 index 0000000..b45e724 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs @@ -0,0 +1,356 @@ +using System; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class PropertySiteProcessor + { + public static void ProcessSitesModule(ModuleDefinition moduleDef) + { + DateTime startTime = DateTime.Now; + + //Search through the types + foreach (TypeDefinition td in moduleDef.Types) + { + if (td.IsClass) + { + ProcessSiteClass(td); + } + } + if (Weaver.WeaveLists.generateContainerClass != null) + { + moduleDef.Types.Add(Weaver.WeaveLists.generateContainerClass); + Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.WeaveLists.generateContainerClass); + + foreach (MethodDefinition f in Weaver.WeaveLists.generatedReadFunctions) + { + Weaver.CurrentAssembly.MainModule.ImportReference(f); + } + + foreach (MethodDefinition f in Weaver.WeaveLists.generatedWriteFunctions) + { + Weaver.CurrentAssembly.MainModule.ImportReference(f); + } + } + Console.WriteLine(" ProcessSitesModule " + moduleDef.Name + " elapsed time:" + (DateTime.Now - startTime)); + } + + static void ProcessSiteClass(TypeDefinition td) + { + //Console.WriteLine(" ProcessSiteClass " + td); + foreach (MethodDefinition md in td.Methods) + { + ProcessSiteMethod(td, md); + } + + foreach (TypeDefinition nested in td.NestedTypes) + { + ProcessSiteClass(nested); + } + } + + static void ProcessSiteMethod(TypeDefinition td, MethodDefinition md) + { + // process all references to replaced members with properties + //Weaver.DLog(td, " ProcessSiteMethod " + md); + + if (md.Name == ".cctor" || + md.Name == NetworkBehaviourProcessor.ProcessedFunctionName || + md.Name.StartsWith("CallCmd") || + md.Name.StartsWith("InvokeCmd") || + md.Name.StartsWith("InvokeRpc") || + md.Name.StartsWith("InvokeSyn")) + return; + + if (md.Body != null && md.Body.Instructions != null) + { + // TODO move this to NetworkBehaviourProcessor + foreach (CustomAttribute attr in md.CustomAttributes) + { + switch (attr.Constructor.DeclaringType.ToString()) + { + case "Mirror.ServerAttribute": + InjectServerGuard(td, md, true); + break; + case "Mirror.ServerCallbackAttribute": + InjectServerGuard(td, md, false); + break; + case "Mirror.ClientAttribute": + InjectClientGuard(td, md, true); + break; + case "Mirror.ClientCallbackAttribute": + InjectClientGuard(td, md, false); + break; + } + } + + for (int iCount= 0; iCount < md.Body.Instructions.Count;) + { + Instruction instr = md.Body.Instructions[iCount]; + iCount += ProcessInstruction(md, instr, iCount); + } + } + } + + static void InjectServerGuard(TypeDefinition td, MethodDefinition md, bool logWarning) + { + if (!Weaver.IsNetworkBehaviour(td)) + { + Weaver.Error($"[Server] {md} must be declared in a NetworkBehaviour"); + return; + } + ILProcessor worker = md.Body.GetILProcessor(); + Instruction top = md.Body.Instructions[0]; + + worker.InsertBefore(top, worker.Create(OpCodes.Call, Weaver.NetworkServerGetActive)); + worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top)); + if (logWarning) + { + worker.InsertBefore(top, worker.Create(OpCodes.Ldstr, "[Server] function '" + md.FullName + "' called on client")); + worker.InsertBefore(top, worker.Create(OpCodes.Call, Weaver.logWarningReference)); + } + InjectGuardParameters(md, worker, top); + InjectGuardReturnValue(md, worker, top); + worker.InsertBefore(top, worker.Create(OpCodes.Ret)); + } + + static void InjectClientGuard(TypeDefinition td, MethodDefinition md, bool logWarning) + { + if (!Weaver.IsNetworkBehaviour(td)) + { + Weaver.Error($"[Client] {md} must be declared in a NetworkBehaviour"); + return; + } + ILProcessor worker = md.Body.GetILProcessor(); + Instruction top = md.Body.Instructions[0]; + + worker.InsertBefore(top, worker.Create(OpCodes.Call, Weaver.NetworkClientGetActive)); + worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top)); + if (logWarning) + { + worker.InsertBefore(top, worker.Create(OpCodes.Ldstr, "[Client] function '" + md.FullName + "' called on server")); + worker.InsertBefore(top, worker.Create(OpCodes.Call, Weaver.logWarningReference)); + } + + InjectGuardParameters(md, worker, top); + InjectGuardReturnValue(md, worker, top); + worker.InsertBefore(top, worker.Create(OpCodes.Ret)); + } + + // replaces syncvar write access with the NetworkXYZ.get property calls + static void ProcessInstructionSetterField(MethodDefinition md, Instruction i, FieldDefinition opField) + { + // dont replace property call sites in constructors + if (md.Name == ".ctor") + return; + + // does it set a field that we replaced? + if (Weaver.WeaveLists.replacementSetterProperties.TryGetValue(opField, out MethodDefinition replacement)) + { + //replace with property + //DLog(td, " replacing " + md.Name + ":" + i); + i.OpCode = OpCodes.Call; + i.Operand = replacement; + //DLog(td, " replaced " + md.Name + ":" + i); + } + } + + // replaces syncvar read access with the NetworkXYZ.get property calls + static void ProcessInstructionGetterField(MethodDefinition md, Instruction i, FieldDefinition opField) + { + // dont replace property call sites in constructors + if (md.Name == ".ctor") + return; + + // does it set a field that we replaced? + if (Weaver.WeaveLists.replacementGetterProperties.TryGetValue(opField, out MethodDefinition replacement)) + { + //replace with property + //DLog(td, " replacing " + md.Name + ":" + i); + i.OpCode = OpCodes.Call; + i.Operand = replacement; + //DLog(td, " replaced " + md.Name + ":" + i); + } + } + + static int ProcessInstruction(MethodDefinition md, Instruction instr, int iCount) + { + if (instr.OpCode == OpCodes.Call || instr.OpCode == OpCodes.Callvirt) + { + if (instr.Operand is MethodReference opMethod) + { + ProcessInstructionMethod(md, instr, opMethod, iCount); + } + } + + if (instr.OpCode == OpCodes.Stfld) + { + // this instruction sets the value of a field. cache the field reference. + if (instr.Operand is FieldDefinition opField) + { + ProcessInstructionSetterField(md, instr, opField); + } + } + + if (instr.OpCode == OpCodes.Ldfld) + { + // this instruction gets the value of a field. cache the field reference. + if (instr.Operand is FieldDefinition opField) + { + ProcessInstructionGetterField(md, instr, opField); + } + } + + if (instr.OpCode == OpCodes.Ldflda) + { + // loading a field by reference, watch out for initobj instruction + // see https://github.com/vis2k/Mirror/issues/696 + + if (instr.Operand is FieldDefinition opField) + { + return ProcessInstructionLoadAddress(md, instr, opField, iCount); + } + } + + return 1; + } + + static int ProcessInstructionLoadAddress(MethodDefinition md, Instruction instr, FieldDefinition opField, int iCount) + { + // dont replace property call sites in constructors + if (md.Name == ".ctor") + return 1; + + // does it set a field that we replaced? + if (Weaver.WeaveLists.replacementSetterProperties.TryGetValue(opField, out MethodDefinition replacement)) + { + // we have a replacement for this property + // is the next instruction a initobj? + Instruction nextInstr = md.Body.Instructions[iCount + 1]; + + if (nextInstr.OpCode == OpCodes.Initobj) + { + // we need to replace this code with: + // var tmp = new MyStruct(); + // this.set_Networkxxxx(tmp); + ILProcessor worker = md.Body.GetILProcessor(); + VariableDefinition tmpVariable = new VariableDefinition(opField.FieldType); + md.Body.Variables.Add(tmpVariable); + + worker.InsertBefore(instr, worker.Create(OpCodes.Ldloca, tmpVariable)); + worker.InsertBefore(instr, worker.Create(OpCodes.Initobj, opField.FieldType)); + worker.InsertBefore(instr, worker.Create(OpCodes.Ldloc, tmpVariable)); + worker.InsertBefore(instr, worker.Create(OpCodes.Call, replacement)); + + worker.Remove(instr); + worker.Remove(nextInstr); + return 4; + + } + + } + + return 1; + } + + static void ProcessInstructionMethod(MethodDefinition md, Instruction instr, MethodReference opMethodRef, int iCount) + { + //DLog(td, "ProcessInstructionMethod " + opMethod.Name); + if (opMethodRef.Name == "Invoke") + { + // Events use an "Invoke" method to call the delegate. + // this code replaces the "Invoke" instruction with the generated "Call***" instruction which send the event to the server. + // but the "Invoke" instruction is called on the event field - where the "call" instruction is not. + // so the earlier instruction that loads the event field is replaced with a Noop. + + // go backwards until find a ldfld instruction that matches ANY event + bool found = false; + while (iCount > 0 && !found) + { + iCount -= 1; + Instruction inst = md.Body.Instructions[iCount]; + if (inst.OpCode == OpCodes.Ldfld) + { + FieldReference opField = inst.Operand as FieldReference; + + // find replaceEvent with matching name + // NOTE: original weaver compared .Name, not just the MethodDefinition, + // that's why we use dict. + if (Weaver.WeaveLists.replaceEvents.TryGetValue(opField.Name, out MethodDefinition replacement)) + { + instr.Operand = replacement; + inst.OpCode = OpCodes.Nop; + found = true; + } + } + } + } + else + { + // should it be replaced? + // NOTE: original weaver compared .FullName, not just the MethodDefinition, + // that's why we use dict. + if (Weaver.WeaveLists.replaceMethods.TryGetValue(opMethodRef.FullName, out MethodDefinition replacement)) + { + //DLog(td, " replacing " + md.Name + ":" + i); + instr.Operand = replacement; + //DLog(td, " replaced " + md.Name + ":" + i); + } + } + } + + + // this is required to early-out from a function with "ref" or "out" parameters + static void InjectGuardParameters(MethodDefinition md, ILProcessor worker, Instruction top) + { + int offset = md.Resolve().IsStatic ? 0 : 1; + for (int index = 0; index < md.Parameters.Count; index++) + { + ParameterDefinition param = md.Parameters[index]; + if (param.IsOut) + { + TypeReference elementType = param.ParameterType.GetElementType(); + if (elementType.IsPrimitive) + { + worker.InsertBefore(top, worker.Create(OpCodes.Ldarg, index + offset)); + worker.InsertBefore(top, worker.Create(OpCodes.Ldc_I4_0)); + worker.InsertBefore(top, worker.Create(OpCodes.Stind_I4)); + } + else + { + md.Body.Variables.Add(new VariableDefinition(elementType)); + md.Body.InitLocals = true; + + worker.InsertBefore(top, worker.Create(OpCodes.Ldarg, index + offset)); + worker.InsertBefore(top, worker.Create(OpCodes.Ldloca_S, (byte)(md.Body.Variables.Count - 1))); + worker.InsertBefore(top, worker.Create(OpCodes.Initobj, elementType)); + worker.InsertBefore(top, worker.Create(OpCodes.Ldloc, md.Body.Variables.Count - 1)); + worker.InsertBefore(top, worker.Create(OpCodes.Stobj, elementType)); + } + } + } + } + + // this is required to early-out from a function with a return value. + static void InjectGuardReturnValue(MethodDefinition md, ILProcessor worker, Instruction top) + { + if (md.ReturnType.FullName != Weaver.voidType.FullName) + { + if (md.ReturnType.IsPrimitive) + { + worker.InsertBefore(top, worker.Create(OpCodes.Ldc_I4_0)); + } + else + { + md.Body.Variables.Add(new VariableDefinition(md.ReturnType)); + md.Body.InitLocals = true; + + worker.InsertBefore(top, worker.Create(OpCodes.Ldloca_S, (byte)(md.Body.Variables.Count - 1))); + worker.InsertBefore(top, worker.Create(OpCodes.Initobj, md.ReturnType)); + worker.InsertBefore(top, worker.Create(OpCodes.Ldloc, md.Body.Variables.Count - 1)); + } + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs.meta new file mode 100644 index 0000000..e8c2500 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/PropertySiteProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d48f1ab125e9940a995603796bccc59e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs new file mode 100644 index 0000000..d87bf22 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs @@ -0,0 +1,98 @@ +using System; +using Mono.CecilX; +using UnityEditor.Compilation; +using System.Linq; +using System.Collections.Generic; +using System.IO; + +namespace Mirror.Weaver +{ + public static class ReaderWriterProcessor + { + // find all readers and writers and register them + public static void ProcessReadersAndWriters(AssemblyDefinition CurrentAssembly) + { + Readers.Init(); + Writers.Init(); + + foreach (Assembly unityAsm in CompilationPipeline.GetAssemblies()) + { + if (unityAsm.name != CurrentAssembly.Name.Name) + { + try + { + using (DefaultAssemblyResolver asmResolver = new DefaultAssemblyResolver()) + using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(unityAsm.outputPath, new ReaderParameters { ReadWrite = false, ReadSymbols = false, AssemblyResolver = asmResolver })) + { + ProcessAssemblyClasses(CurrentAssembly, assembly); + } + } + catch(FileNotFoundException) + { + // During first import, this gets called before some assemblies + // are built, just skip them + } + } + } + + ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly); + } + + static void ProcessAssemblyClasses(AssemblyDefinition CurrentAssembly, AssemblyDefinition assembly) + { + foreach (TypeDefinition klass in assembly.MainModule.Types) + { + // extension methods only live in static classes + // static classes are represented as sealed and abstract + if (klass.IsAbstract && klass.IsSealed) + { + LoadWriters(CurrentAssembly, klass); + LoadReaders(CurrentAssembly, klass); + } + } + } + + static void LoadWriters(AssemblyDefinition currentAssembly, TypeDefinition klass) + { + // register all the writers in this class. Skip the ones with wrong signature + foreach (MethodDefinition method in klass.Methods) + { + if (method.Parameters.Count != 2) + continue; + + if (method.Parameters[0].ParameterType.FullName != "Mirror.NetworkWriter") + continue; + + if (method.ReturnType.FullName != "System.Void") + continue; + + if (method.GetCustomAttribute("System.Runtime.CompilerServices.ExtensionAttribute") == null) + continue; + + TypeReference dataType = method.Parameters[1].ParameterType; + Writers.Register(dataType, currentAssembly.MainModule.ImportReference(method)); + } + } + + static void LoadReaders(AssemblyDefinition currentAssembly, TypeDefinition klass) + { + // register all the reader in this class. Skip the ones with wrong signature + foreach (MethodDefinition method in klass.Methods) + { + if (method.Parameters.Count != 1) + continue; + + if (method.Parameters[0].ParameterType.FullName != "Mirror.NetworkReader") + continue; + + if (method.ReturnType.FullName == "System.Void") + continue; + + if (method.GetCustomAttribute("System.Runtime.CompilerServices.ExtensionAttribute") == null) + continue; + + Readers.Register(method.ReturnType, currentAssembly.MainModule.ImportReference(method)); + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta new file mode 100644 index 0000000..c14d6fa --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f3263602f0a374ecd8d08588b1fc2f76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs new file mode 100644 index 0000000..947742e --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs @@ -0,0 +1,110 @@ +// all the [Rpc] code from NetworkBehaviourProcessor in one place +using Mono.CecilX; +using Mono.CecilX.Cil; +namespace Mirror.Weaver +{ + public static class RpcProcessor + { + public const string RpcPrefix = "InvokeRpc"; + + public static MethodDefinition ProcessRpcInvoke(TypeDefinition td, MethodDefinition md) + { + MethodDefinition rpc = new MethodDefinition( + RpcPrefix + md.Name, + MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig, + Weaver.voidType); + + ILProcessor rpcWorker = rpc.Body.GetILProcessor(); + Instruction label = rpcWorker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteClientActiveCheck(rpcWorker, md.Name, label, "RPC"); + + // setup for reader + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldarg_0)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Castclass, td)); + + if (!NetworkBehaviourProcessor.ProcessNetworkReaderParameters(md, rpcWorker, false)) + return null; + + // invoke actual command function + rpcWorker.Append(rpcWorker.Create(OpCodes.Callvirt, md)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Ret)); + + NetworkBehaviourProcessor.AddInvokeParameters(rpc.Parameters); + + return rpc; + } + + /* generates code like: + public void CallRpcTest (int param) + { + NetworkWriter writer = new NetworkWriter (); + writer.WritePackedUInt32((uint)param); + base.SendRPCInternal(typeof(class),"RpcTest", writer, 0); + } + */ + public static MethodDefinition ProcessRpcCall(TypeDefinition td, MethodDefinition md, CustomAttribute ca) + { + MethodDefinition rpc = new MethodDefinition("Call" + md.Name, MethodAttributes.Public | + MethodAttributes.HideBySig, + Weaver.voidType); + + // add paramters + foreach (ParameterDefinition pd in md.Parameters) + { + rpc.Parameters.Add(new ParameterDefinition(pd.Name, ParameterAttributes.None, pd.ParameterType)); + } + + ILProcessor rpcWorker = rpc.Body.GetILProcessor(); + + NetworkBehaviourProcessor.WriteSetupLocals(rpcWorker); + + NetworkBehaviourProcessor.WriteCreateWriter(rpcWorker); + + // write all the arguments that the user passed to the Rpc call + if (!NetworkBehaviourProcessor.WriteArguments(rpcWorker, md, false)) + return null; + + string rpcName = md.Name; + int index = rpcName.IndexOf(RpcPrefix); + if (index > -1) + { + rpcName = rpcName.Substring(RpcPrefix.Length); + } + + // invoke SendInternal and return + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldarg_0)); // this + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldtoken, td)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Call, Weaver.getTypeFromHandleReference)); // invokerClass + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldstr, rpcName)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldloc_0)); // writer + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldc_I4, NetworkBehaviourProcessor.GetChannelId(ca))); + rpcWorker.Append(rpcWorker.Create(OpCodes.Callvirt, Weaver.sendRpcInternal)); + + NetworkBehaviourProcessor.WriteRecycleWriter(rpcWorker); + + rpcWorker.Append(rpcWorker.Create(OpCodes.Ret)); + + return rpc; + } + + public static bool ProcessMethodsValidateRpc(MethodDefinition md, CustomAttribute ca) + { + if (!md.Name.StartsWith("Rpc")) + { + Weaver.Error($"{md} must start with Rpc. Consider renaming it to Rpc{md.Name}"); + return false; + } + + if (md.IsStatic) + { + Weaver.Error($"{md} must not be static"); + return false; + } + + // validate + return NetworkBehaviourProcessor.ProcessMethodsValidateFunction(md) && + NetworkBehaviourProcessor.ProcessMethodsValidateParameters(md, ca); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta new file mode 100644 index 0000000..22375ba --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/RpcProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3cb7051ff41947e59bba58bdd2b73fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs new file mode 100644 index 0000000..a095967 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs @@ -0,0 +1,19 @@ +// this class generates OnSerialize/OnDeserialize for SyncLists +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + static class SyncDictionaryProcessor + { + /// + /// Generates serialization methods for synclists + /// + /// The synclist class + public static void Process(TypeDefinition td) + { + SyncObjectProcessor.GenerateSerialization(td, 0, "SerializeKey", "DeserializeKey"); + SyncObjectProcessor.GenerateSerialization(td, 1, "SerializeItem", "DeserializeItem"); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta new file mode 100644 index 0000000..0a7c2aa --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncDictionaryProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29e4a45f69822462ab0b15adda962a29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs new file mode 100644 index 0000000..3a4e36f --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs @@ -0,0 +1,152 @@ +// all the SyncEvent code from NetworkBehaviourProcessor in one place +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class SyncEventProcessor + { + public static MethodDefinition ProcessEventInvoke(TypeDefinition td, EventDefinition ed) + { + // find the field that matches the event + FieldDefinition eventField = null; + foreach (FieldDefinition fd in td.Fields) + { + if (fd.FullName == ed.FullName) + { + eventField = fd; + break; + } + } + if (eventField == null) + { + Weaver.Error($"{td} not found. Did you declare the event?"); + return null; + } + + MethodDefinition cmd = new MethodDefinition("InvokeSyncEvent" + ed.Name, MethodAttributes.Family | + MethodAttributes.Static | + MethodAttributes.HideBySig, + Weaver.voidType); + + ILProcessor cmdWorker = cmd.Body.GetILProcessor(); + Instruction label1 = cmdWorker.Create(OpCodes.Nop); + Instruction label2 = cmdWorker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteClientActiveCheck(cmdWorker, ed.Name, label1, "Event"); + + // null event check + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldarg_0)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Castclass, td)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldfld, eventField)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Brtrue, label2)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ret)); + cmdWorker.Append(label2); + + // setup reader + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldarg_0)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Castclass, td)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ldfld, eventField)); + + // read the event arguments + MethodReference invoke = Resolvers.ResolveMethod(eventField.FieldType, Weaver.CurrentAssembly, "Invoke"); + if (!NetworkBehaviourProcessor.ProcessNetworkReaderParameters(invoke.Resolve(), cmdWorker, false)) + return null; + + // invoke actual event delegate function + cmdWorker.Append(cmdWorker.Create(OpCodes.Callvirt, invoke)); + cmdWorker.Append(cmdWorker.Create(OpCodes.Ret)); + + NetworkBehaviourProcessor.AddInvokeParameters(cmd.Parameters); + + return cmd; + } + + public static MethodDefinition ProcessEventCall(TypeDefinition td, EventDefinition ed, CustomAttribute ca) + { + MethodReference invoke = Resolvers.ResolveMethod(ed.EventType, Weaver.CurrentAssembly, "Invoke"); + MethodDefinition evt = new MethodDefinition("Call" + ed.Name, MethodAttributes.Public | + MethodAttributes.HideBySig, + Weaver.voidType); + // add paramters + foreach (ParameterDefinition pd in invoke.Parameters) + { + evt.Parameters.Add(new ParameterDefinition(pd.Name, ParameterAttributes.None, pd.ParameterType)); + } + + ILProcessor evtWorker = evt.Body.GetILProcessor(); + Instruction label = evtWorker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteSetupLocals(evtWorker); + + NetworkBehaviourProcessor.WriteServerActiveCheck(evtWorker, ed.Name, label, "Event"); + + NetworkBehaviourProcessor.WriteCreateWriter(evtWorker); + + // write all the arguments that the user passed to the syncevent + if (!NetworkBehaviourProcessor.WriteArguments(evtWorker, invoke.Resolve(), false)) + return null; + + // invoke interal send and return + evtWorker.Append(evtWorker.Create(OpCodes.Ldarg_0)); // this + evtWorker.Append(evtWorker.Create(OpCodes.Ldtoken, td)); + evtWorker.Append(evtWorker.Create(OpCodes.Call, Weaver.getTypeFromHandleReference)); // invokerClass + evtWorker.Append(evtWorker.Create(OpCodes.Ldstr, ed.Name)); + evtWorker.Append(evtWorker.Create(OpCodes.Ldloc_0)); // writer + evtWorker.Append(evtWorker.Create(OpCodes.Ldc_I4, NetworkBehaviourProcessor.GetChannelId(ca))); + evtWorker.Append(evtWorker.Create(OpCodes.Call, Weaver.sendEventInternal)); + + NetworkBehaviourProcessor.WriteRecycleWriter(evtWorker); + + evtWorker.Append(evtWorker.Create(OpCodes.Ret)); + + return evt; + } + + public static void ProcessEvents(TypeDefinition td, List events, List eventInvocationFuncs) + { + // find events + foreach (EventDefinition ed in td.Events) + { + foreach (CustomAttribute ca in ed.CustomAttributes) + { + if (ca.AttributeType.FullName == Weaver.SyncEventType.FullName) + { + if (!ed.Name.StartsWith("Event")) + { + Weaver.Error($"{ed} must start with Event. Consider renaming it to Event{ed.Name}"); + return; + } + + if (ed.EventType.Resolve().HasGenericParameters) + { + Weaver.Error($"{ed} must not have generic parameters. Consider creating a new class that inherits from {ed.EventType} instead"); + return; + } + + events.Add(ed); + MethodDefinition eventFunc = ProcessEventInvoke(td, ed); + if (eventFunc == null) + { + return; + } + + td.Methods.Add(eventFunc); + eventInvocationFuncs.Add(eventFunc); + + Weaver.DLog(td, "ProcessEvent " + ed); + + MethodDefinition eventCallFunc = ProcessEventCall(td, ed, ca); + td.Methods.Add(eventCallFunc); + + Weaver.WeaveLists.replaceEvents[ed.Name] = eventCallFunc; // original weaver compares .Name, not EventDefinition. + + Weaver.DLog(td, " Event: " + ed.Name); + break; + } + } + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs.meta new file mode 100644 index 0000000..81b9576 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncEventProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5d8b25543a624384944b599e5a832a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs new file mode 100644 index 0000000..20db50b --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs @@ -0,0 +1,5 @@ +// This file was removed in Mirror 3.4.9 +// The purpose of this file is to get the old file overwritten +// when users update from the asset store to prevent a flood of errors +// from having the old file still in the project as a straggler. +// This file will be dropped from the Asset Store package in May 2019 diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs.meta new file mode 100644 index 0000000..d3f5278 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListInitializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97068e5d8cc14490b85933feb119d827 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs new file mode 100644 index 0000000..0da1746 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs @@ -0,0 +1,18 @@ +// this class generates OnSerialize/OnDeserialize for SyncLists +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + static class SyncListProcessor + { + /// + /// Generates serialization methods for synclists + /// + /// The synclist class + public static void Process(TypeDefinition td) + { + SyncObjectProcessor.GenerateSerialization(td, 0, "SerializeItem", "DeserializeItem"); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs.meta new file mode 100644 index 0000000..b73b047 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f3445268e45d437fac325837aff3246 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs new file mode 100644 index 0000000..20db50b --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs @@ -0,0 +1,5 @@ +// This file was removed in Mirror 3.4.9 +// The purpose of this file is to get the old file overwritten +// when users update from the asset store to prevent a flood of errors +// from having the old file still in the project as a straggler. +// This file will be dropped from the Asset Store package in May 2019 diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs.meta new file mode 100644 index 0000000..8f234cd --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncListStructProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28fb192f6a9bc1247b90aa4710f6d34f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs new file mode 100644 index 0000000..14e27f6 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs @@ -0,0 +1,89 @@ +// SyncObject code +using System; +using System.Linq; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class SyncObjectInitializer + { + public static void GenerateSyncObjectInitializer(ILProcessor methodWorker, FieldDefinition fd) + { + // call syncobject constructor + GenerateSyncObjectInstanceInitializer(methodWorker, fd); + + // register syncobject in network behaviour + GenerateSyncObjectRegistration(methodWorker, fd); + } + + // generates 'syncListInt = new SyncListInt()' if user didn't do that yet + static void GenerateSyncObjectInstanceInitializer(ILProcessor ctorWorker, FieldDefinition fd) + { + // check the ctor's instructions for an Stfld op-code for this specific sync list field. + foreach (Instruction ins in ctorWorker.Body.Instructions) + { + if (ins.OpCode.Code == Code.Stfld) + { + FieldDefinition field = (FieldDefinition)ins.Operand; + if (field.DeclaringType == fd.DeclaringType && field.Name == fd.Name) + { + // Already initialized by the user in the field definition, e.g: + // public SyncListInt Foo = new SyncListInt(); + return; + } + } + } + + // Not initialized by the user in the field definition, e.g: + // public SyncListInt Foo; + MethodReference objectConstructor; + try + { + objectConstructor = Weaver.CurrentAssembly.MainModule.ImportReference(fd.FieldType.Resolve().Methods.First(x => x.Name == ".ctor" && !x.HasParameters)); + } + catch (Exception) + { + Weaver.Error($"{fd} does not have a default constructor"); + return; + } + + ctorWorker.Append(ctorWorker.Create(OpCodes.Ldarg_0)); + ctorWorker.Append(ctorWorker.Create(OpCodes.Newobj, objectConstructor)); + ctorWorker.Append(ctorWorker.Create(OpCodes.Stfld, fd)); + } + + public static bool ImplementsSyncObject(TypeReference typeRef) + { + try + { + // value types cant inherit from SyncObject + if (typeRef.IsValueType) + { + return false; + } + + return typeRef.Resolve().ImplementsInterface(Weaver.SyncObjectType); + } + catch + { + // sometimes this will fail if we reference a weird library that can't be resolved, so we just swallow that exception and return false + } + + return false; + } + + /* + // generates code like: + this.InitSyncObject(m_sizes); + */ + static void GenerateSyncObjectRegistration(ILProcessor methodWorker, FieldDefinition fd) + { + methodWorker.Append(methodWorker.Create(OpCodes.Ldarg_0)); + methodWorker.Append(methodWorker.Create(OpCodes.Ldarg_0)); + methodWorker.Append(methodWorker.Create(OpCodes.Ldfld, fd)); + + methodWorker.Append(methodWorker.Create(OpCodes.Call, Weaver.InitSyncObjectReference)); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta new file mode 100644 index 0000000..22f976e --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectInitializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d02219b00b3674e59a2151f41e791688 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs new file mode 100644 index 0000000..9eb7494 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs @@ -0,0 +1,123 @@ +using System; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class SyncObjectProcessor + { + /// + /// Generates the serialization and deserialization methods for a specified generic argument + /// + /// The type of the class that needs serialization methods + /// Which generic argument to serialize, 0 is the first one + /// The name of the serialize method + /// The name of the deserialize method + public static void GenerateSerialization(TypeDefinition td, int genericArgument, string serializeMethod, string deserializeMethod) + { + // find item type + GenericInstanceType gt = (GenericInstanceType)td.BaseType; + if (gt.GenericArguments.Count <= genericArgument) + { + Weaver.Error($"{td} should have {genericArgument} generic arguments"); + return; + } + TypeReference itemType = Weaver.CurrentAssembly.MainModule.ImportReference(gt.GenericArguments[genericArgument]); + + Weaver.DLog(td, "SyncObjectProcessor Start item:" + itemType.FullName); + + MethodReference writeItemFunc = GenerateSerialization(serializeMethod, td, itemType); + if (Weaver.WeavingFailed) + { + return; + } + + MethodReference readItemFunc = GenerateDeserialization(deserializeMethod, td, itemType); + + if (readItemFunc == null || writeItemFunc == null) + return; + + Weaver.DLog(td, "SyncObjectProcessor Done"); + } + + // serialization of individual element + static MethodReference GenerateSerialization(string methodName, TypeDefinition td, TypeReference itemType) + { + Weaver.DLog(td, " GenerateSerialization"); + foreach (MethodDefinition m in td.Methods) + { + if (m.Name == methodName) + return m; + } + + MethodDefinition serializeFunc = new MethodDefinition(methodName, MethodAttributes.Public | + MethodAttributes.Virtual | + MethodAttributes.Public | + MethodAttributes.HideBySig, + Weaver.voidType); + + serializeFunc.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkWriterType))); + serializeFunc.Parameters.Add(new ParameterDefinition("item", ParameterAttributes.None, itemType)); + ILProcessor serWorker = serializeFunc.Body.GetILProcessor(); + + if (itemType.IsGenericInstance) + { + Weaver.Error($"{td} cannot have generic elements {itemType}"); + return null; + } + + MethodReference writeFunc = Writers.GetWriteFunc(itemType); + if (writeFunc != null) + { + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Ldarg_2)); + serWorker.Append(serWorker.Create(OpCodes.Call, writeFunc)); + } + else + { + Weaver.Error($"{td} cannot have item of type {itemType}. Use a type supported by mirror instead"); + return null; + } + serWorker.Append(serWorker.Create(OpCodes.Ret)); + + td.Methods.Add(serializeFunc); + return serializeFunc; + } + + static MethodReference GenerateDeserialization(string methodName, TypeDefinition td, TypeReference itemType) + { + Weaver.DLog(td, " GenerateDeserialization"); + foreach (MethodDefinition m in td.Methods) + { + if (m.Name == methodName) + return m; + } + + MethodDefinition deserializeFunction = new MethodDefinition(methodName, MethodAttributes.Public | + MethodAttributes.Virtual | + MethodAttributes.Public | + MethodAttributes.HideBySig, + itemType); + + deserializeFunction.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkReaderType))); + + ILProcessor serWorker = deserializeFunction.Body.GetILProcessor(); + + MethodReference readerFunc = Readers.GetReadFunc(itemType); + if (readerFunc != null) + { + serWorker.Append(serWorker.Create(OpCodes.Ldarg_1)); + serWorker.Append(serWorker.Create(OpCodes.Call, readerFunc)); + serWorker.Append(serWorker.Create(OpCodes.Ret)); + } + else + { + Weaver.Error($"{td} cannot have item of type {itemType}. Use a type supported by mirror instead"); + return null; + } + + td.Methods.Add(deserializeFunction); + return deserializeFunction; + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta new file mode 100644 index 0000000..0efe434 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncObjectProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78f71efc83cde4917b7d21efa90bcc9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs new file mode 100644 index 0000000..86be11c --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs @@ -0,0 +1,335 @@ +// all the [SyncVar] code from NetworkBehaviourProcessor in one place +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class SyncVarProcessor + { + const int SyncVarLimit = 64; // ulong = 64 bytes + + // returns false for error, not for no-hook-exists + public static bool CheckForHookFunction(TypeDefinition td, FieldDefinition syncVar, out MethodDefinition foundMethod) + { + foundMethod = null; + foreach (CustomAttribute ca in syncVar.CustomAttributes) + { + if (ca.AttributeType.FullName == Weaver.SyncVarType.FullName) + { + foreach (CustomAttributeNamedArgument customField in ca.Fields) + { + if (customField.Name == "hook") + { + string hookFunctionName = customField.Argument.Value as string; + + foreach (MethodDefinition m in td.Methods) + { + if (m.Name == hookFunctionName) + { + if (m.Parameters.Count == 1) + { + if (m.Parameters[0].ParameterType != syncVar.FieldType) + { + Weaver.Error($"{m} should have signature:\npublic void {hookFunctionName}({syncVar.FieldType} value) {{ }}"); + return false; + } + foundMethod = m; + return true; + } + Weaver.Error($"{m} should have signature:\npublic void {hookFunctionName}({syncVar.FieldType} value) {{ }}"); + return false; + } + } + Weaver.Error($"No hook implementation found for {syncVar}. Add this method to your class:\npublic void {hookFunctionName}({syncVar.FieldType} value) {{ }}" ); + return false; + } + } + } + } + return true; + } + + public static MethodDefinition ProcessSyncVarGet(FieldDefinition fd, string originalName, FieldDefinition netFieldId) + { + //Create the get method + MethodDefinition get = new MethodDefinition( + "get_Network" + originalName, MethodAttributes.Public | + MethodAttributes.SpecialName | + MethodAttributes.HideBySig, + fd.FieldType); + + ILProcessor getWorker = get.Body.GetILProcessor(); + + // [SyncVar] GameObject? + if (fd.FieldType.FullName == Weaver.gameObjectType.FullName) + { + // return this.GetSyncVarGameObject(ref field, uint netId); + getWorker.Append(getWorker.Create(OpCodes.Ldarg_0)); // this. + getWorker.Append(getWorker.Create(OpCodes.Ldarg_0)); + getWorker.Append(getWorker.Create(OpCodes.Ldfld, netFieldId)); + getWorker.Append(getWorker.Create(OpCodes.Ldarg_0)); + getWorker.Append(getWorker.Create(OpCodes.Ldflda, fd)); + getWorker.Append(getWorker.Create(OpCodes.Call, Weaver.getSyncVarGameObjectReference)); + getWorker.Append(getWorker.Create(OpCodes.Ret)); + } + // [SyncVar] NetworkIdentity? + else if (fd.FieldType.FullName == Weaver.NetworkIdentityType.FullName) + { + // return this.GetSyncVarNetworkIdentity(ref field, uint netId); + getWorker.Append(getWorker.Create(OpCodes.Ldarg_0)); // this. + getWorker.Append(getWorker.Create(OpCodes.Ldarg_0)); + getWorker.Append(getWorker.Create(OpCodes.Ldfld, netFieldId)); + getWorker.Append(getWorker.Create(OpCodes.Ldarg_0)); + getWorker.Append(getWorker.Create(OpCodes.Ldflda, fd)); + getWorker.Append(getWorker.Create(OpCodes.Call, Weaver.getSyncVarNetworkIdentityReference)); + getWorker.Append(getWorker.Create(OpCodes.Ret)); + } + // [SyncVar] int, string, etc. + else + { + getWorker.Append(getWorker.Create(OpCodes.Ldarg_0)); + getWorker.Append(getWorker.Create(OpCodes.Ldfld, fd)); + getWorker.Append(getWorker.Create(OpCodes.Ret)); + } + + get.Body.Variables.Add(new VariableDefinition(fd.FieldType)); + get.Body.InitLocals = true; + get.SemanticsAttributes = MethodSemanticsAttributes.Getter; + + return get; + } + + public static MethodDefinition ProcessSyncVarSet(TypeDefinition td, FieldDefinition fd, string originalName, long dirtyBit, FieldDefinition netFieldId) + { + //Create the set method + MethodDefinition set = new MethodDefinition("set_Network" + originalName, MethodAttributes.Public | + MethodAttributes.SpecialName | + MethodAttributes.HideBySig, + Weaver.voidType); + + ILProcessor setWorker = set.Body.GetILProcessor(); + + CheckForHookFunction(td, fd, out MethodDefinition hookFunctionMethod); + + if (hookFunctionMethod != null) + { + //if (NetworkServer.localClientActive && !getSyncVarHookGuard(dirtyBit)) + Instruction label = setWorker.Create(OpCodes.Nop); + setWorker.Append(setWorker.Create(OpCodes.Call, Weaver.NetworkServerGetLocalClientActive)); + setWorker.Append(setWorker.Create(OpCodes.Brfalse, label)); + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + setWorker.Append(setWorker.Create(OpCodes.Ldc_I8, dirtyBit)); + setWorker.Append(setWorker.Create(OpCodes.Call, Weaver.getSyncVarHookGuard)); + setWorker.Append(setWorker.Create(OpCodes.Brtrue, label)); + + // setSyncVarHookGuard(dirtyBit, true); + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + setWorker.Append(setWorker.Create(OpCodes.Ldc_I8, dirtyBit)); + setWorker.Append(setWorker.Create(OpCodes.Ldc_I4_1)); + setWorker.Append(setWorker.Create(OpCodes.Call, Weaver.setSyncVarHookGuard)); + + // call hook + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + setWorker.Append(setWorker.Create(OpCodes.Ldarg_1)); + setWorker.Append(setWorker.Create(OpCodes.Call, hookFunctionMethod)); + + // setSyncVarHookGuard(dirtyBit, false); + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + setWorker.Append(setWorker.Create(OpCodes.Ldc_I8, dirtyBit)); + setWorker.Append(setWorker.Create(OpCodes.Ldc_I4_0)); + setWorker.Append(setWorker.Create(OpCodes.Call, Weaver.setSyncVarHookGuard)); + + setWorker.Append(label); + } + + // this + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + + // new value to set + setWorker.Append(setWorker.Create(OpCodes.Ldarg_1)); + + // reference to field to set + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + setWorker.Append(setWorker.Create(OpCodes.Ldflda, fd)); + + // dirty bit + setWorker.Append(setWorker.Create(OpCodes.Ldc_I8, dirtyBit)); // 8 byte integer aka long + + + if (fd.FieldType.FullName == Weaver.gameObjectType.FullName) + { + // reference to netId Field to set + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + setWorker.Append(setWorker.Create(OpCodes.Ldflda, netFieldId)); + + setWorker.Append(setWorker.Create(OpCodes.Call, Weaver.setSyncVarGameObjectReference)); + } + else if (fd.FieldType.FullName == Weaver.NetworkIdentityType.FullName) + { + // reference to netId Field to set + setWorker.Append(setWorker.Create(OpCodes.Ldarg_0)); + setWorker.Append(setWorker.Create(OpCodes.Ldflda, netFieldId)); + + setWorker.Append(setWorker.Create(OpCodes.Call, Weaver.setSyncVarNetworkIdentityReference)); + } + else + { + // make generic version of SetSyncVar with field type + GenericInstanceMethod gm = new GenericInstanceMethod(Weaver.setSyncVarReference); + gm.GenericArguments.Add(fd.FieldType); + + // invoke SetSyncVar + setWorker.Append(setWorker.Create(OpCodes.Call, gm)); + } + + setWorker.Append(setWorker.Create(OpCodes.Ret)); + + set.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.In, fd.FieldType)); + set.SemanticsAttributes = MethodSemanticsAttributes.Setter; + + return set; + } + + public static void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary syncVarNetIds, long dirtyBit) + { + string originalName = fd.Name; + Weaver.DLog(td, "Sync Var " + fd.Name + " " + fd.FieldType + " " + Weaver.gameObjectType); + + // GameObject/NetworkIdentity SyncVars have a new field for netId + FieldDefinition netIdField = null; + if (fd.FieldType.FullName == Weaver.gameObjectType.FullName || + fd.FieldType.FullName == Weaver.NetworkIdentityType.FullName) + { + netIdField = new FieldDefinition("___" + fd.Name + "NetId", + FieldAttributes.Private, + Weaver.uint32Type); + + syncVarNetIds[fd] = netIdField; + } + + MethodDefinition get = ProcessSyncVarGet(fd, originalName, netIdField); + MethodDefinition set = ProcessSyncVarSet(td, fd, originalName, dirtyBit, netIdField); + + //NOTE: is property even needed? Could just use a setter function? + //create the property + PropertyDefinition propertyDefinition = new PropertyDefinition("Network" + originalName, PropertyAttributes.None, fd.FieldType) + { + GetMethod = get, SetMethod = set + }; + + //add the methods and property to the type. + td.Methods.Add(get); + td.Methods.Add(set); + td.Properties.Add(propertyDefinition); + Weaver.WeaveLists.replacementSetterProperties[fd] = set; + + // replace getter field if GameObject/NetworkIdentity so it uses + // netId instead + // -> only for GameObjects, otherwise an int syncvar's getter would + // end up in recursion. + if (fd.FieldType.FullName == Weaver.gameObjectType.FullName || + fd.FieldType.FullName == Weaver.NetworkIdentityType.FullName) + { + Weaver.WeaveLists.replacementGetterProperties[fd] = get; + } + } + + public static void ProcessSyncVars(TypeDefinition td, List syncVars, List syncObjects, Dictionary syncVarNetIds) + { + int numSyncVars = 0; + + // 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 + int dirtyBitCounter = Weaver.GetSyncVarStart(td.BaseType.FullName); + + syncVarNetIds.Clear(); + + // find syncvars + foreach (FieldDefinition fd in td.Fields) + { + foreach (CustomAttribute ca in fd.CustomAttributes) + { + if (ca.AttributeType.FullName == Weaver.SyncVarType.FullName) + { + TypeDefinition resolvedField = fd.FieldType.Resolve(); + + if (resolvedField.IsDerivedFrom(Weaver.NetworkBehaviourType)) + { + Weaver.Error($"{fd} has invalid type. SyncVars cannot be NetworkBehaviours"); + return; + } + + if (resolvedField.IsDerivedFrom(Weaver.ScriptableObjectType)) + { + Weaver.Error($"{fd} has invalid type. SyncVars cannot be scriptable objects"); + return; + } + + if ((fd.Attributes & FieldAttributes.Static) != 0) + { + Weaver.Error($"{fd} cannot be static"); + return; + } + + if (resolvedField.HasGenericParameters) + { + Weaver.Error($"{fd} has invalid type. SyncVars cannot have generic parameters"); + return; + } + + if (resolvedField.IsInterface) + { + Weaver.Error($"{fd} has invalid type. Use a concrete type instead of interface {fd.FieldType}"); + return; + } + + if (fd.FieldType.IsArray) + { + Weaver.Error($"{fd} has invalid type. Use SyncLists instead of arrays"); + return; + } + + if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType)) + { + Log.Warning($"{fd} has [SyncVar] attribute. SyncLists should not be marked with SyncVar"); + break; + } + + syncVars.Add(fd); + + ProcessSyncVar(td, fd, syncVarNetIds, 1L << dirtyBitCounter); + dirtyBitCounter += 1; + numSyncVars += 1; + + if (dirtyBitCounter == SyncVarLimit) + { + Weaver.Error($"{td} has too many SyncVars. Consider refactoring your class into multiple components"); + return; + } + break; + } + } + + if (fd.FieldType.Resolve().ImplementsInterface(Weaver.SyncObjectType)) + { + if (fd.IsStatic) + { + Weaver.Error($"{fd} cannot be static"); + return; + } + + syncObjects.Add(fd); + } + } + + // add all the new SyncVar __netId fields + foreach (FieldDefinition fd in syncVarNetIds.Values) + { + td.Fields.Add(fd); + } + + Weaver.SetNumSyncVars(td.FullName, numSyncVars); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs.meta new file mode 100644 index 0000000..982f768 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/SyncVarProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f52c39bddd95d42b88f9cd554dfd9198 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs b/Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs new file mode 100644 index 0000000..e20b2c9 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs @@ -0,0 +1,151 @@ +// all the [TargetRpc] code from NetworkBehaviourProcessor in one place +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + public static class TargetRpcProcessor + { + const string TargetRpcPrefix = "InvokeTargetRpc"; + + // 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.FullName == Weaver.NetworkConnectionType.FullName; + } + + public static MethodDefinition ProcessTargetRpcInvoke(TypeDefinition td, MethodDefinition md) + { + MethodDefinition rpc = new MethodDefinition(RpcProcessor.RpcPrefix + md.Name, MethodAttributes.Family | + MethodAttributes.Static | + MethodAttributes.HideBySig, + Weaver.voidType); + + ILProcessor rpcWorker = rpc.Body.GetILProcessor(); + Instruction label = rpcWorker.Create(OpCodes.Nop); + + NetworkBehaviourProcessor.WriteClientActiveCheck(rpcWorker, md.Name, label, "TargetRPC"); + + // setup for reader + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldarg_0)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Castclass, td)); + + // NetworkConnection parameter is optional + bool hasNetworkConnection = HasNetworkConnectionParameter(md); + if (hasNetworkConnection) + { + //ClientScene.readyconnection + rpcWorker.Append(rpcWorker.Create(OpCodes.Call, Weaver.ReadyConnectionReference)); + } + + // process reader parameters and skip first one if first one is NetworkConnection + if (!NetworkBehaviourProcessor.ProcessNetworkReaderParameters(md, rpcWorker, hasNetworkConnection)) + return null; + + // invoke actual command function + rpcWorker.Append(rpcWorker.Create(OpCodes.Callvirt, md)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Ret)); + + NetworkBehaviourProcessor.AddInvokeParameters(rpc.Parameters); + + return rpc; + } + + /* generates code like: + public void CallTargetTest (NetworkConnection conn, int param) + { + NetworkWriter writer = new NetworkWriter (); + writer.WritePackedUInt32 ((uint)param); + base.SendTargetRPCInternal (conn, typeof(class), "TargetTest", val); + } + + or if optional: + public void CallTargetTest (int param) + { + NetworkWriter writer = new NetworkWriter (); + writer.WritePackedUInt32 ((uint)param); + base.SendTargetRPCInternal (null, typeof(class), "TargetTest", val); + } + */ + public static MethodDefinition ProcessTargetRpcCall(TypeDefinition td, MethodDefinition md, CustomAttribute ca) + { + MethodDefinition rpc = new MethodDefinition("Call" + md.Name, MethodAttributes.Public | + MethodAttributes.HideBySig, + Weaver.voidType); + + // add parameters + foreach (ParameterDefinition pd in md.Parameters) + { + rpc.Parameters.Add(new ParameterDefinition(pd.Name, ParameterAttributes.None, pd.ParameterType)); + } + + ILProcessor rpcWorker = rpc.Body.GetILProcessor(); + + NetworkBehaviourProcessor.WriteSetupLocals(rpcWorker); + + NetworkBehaviourProcessor.WriteCreateWriter(rpcWorker); + + // NetworkConnection parameter is optional + bool hasNetworkConnection = HasNetworkConnectionParameter(md); + + // write all the arguments that the user passed to the TargetRpc call + // (skip first one if first one is NetworkConnection) + if (!NetworkBehaviourProcessor.WriteArguments(rpcWorker, md, hasNetworkConnection)) + return null; + + string rpcName = md.Name; + int index = rpcName.IndexOf(TargetRpcPrefix); + if (index > -1) + { + rpcName = rpcName.Substring(TargetRpcPrefix.Length); + } + + // invoke SendInternal and return + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldarg_0)); // this + if (HasNetworkConnectionParameter(md)) + { + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldarg_1)); // connection + } + else + { + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldnull)); // null + } + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldtoken, td)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Call, Weaver.getTypeFromHandleReference)); // invokerClass + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldstr, rpcName)); + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldloc_0)); // writer + rpcWorker.Append(rpcWorker.Create(OpCodes.Ldc_I4, NetworkBehaviourProcessor.GetChannelId(ca))); + rpcWorker.Append(rpcWorker.Create(OpCodes.Callvirt, Weaver.sendTargetRpcInternal)); + + NetworkBehaviourProcessor.WriteRecycleWriter(rpcWorker); + + rpcWorker.Append(rpcWorker.Create(OpCodes.Ret)); + + return rpc; + } + + public static bool ProcessMethodsValidateTargetRpc(MethodDefinition md, CustomAttribute ca) + { + if (!md.Name.StartsWith("Target")) + { + Weaver.Error($"{md} must start with Target. Consider renaming it to Target{md.Name}"); + return false; + } + + if (md.IsStatic) + { + Weaver.Error($"{md} must not be static"); + return false; + } + + if (!NetworkBehaviourProcessor.ProcessMethodsValidateFunction(md)) + { + return false; + } + + // validate + return NetworkBehaviourProcessor.ProcessMethodsValidateParameters(md, ca); + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta new file mode 100644 index 0000000..0ff7cc5 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Processors/TargetRpcProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb3ce6c6f3f2942ae88178b86f5a8282 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Program.cs b/Assets/Packages/Mirror/Editor/Weaver/Program.cs new file mode 100644 index 0000000..62163d2 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Program.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Mirror.Weaver +{ + public static class Log + { + public static Action WarningMethod; + public static Action ErrorMethod; + + public static void Warning(string msg) + { + WarningMethod("Mirror.Weaver warning: " + msg); + } + + public static void Error(string msg) + { + ErrorMethod("Mirror.Weaver error: " + msg); + } + } + + public static class Program + { + public static bool Process(string unityEngine, string netDLL, string outputDirectory, string[] assemblies, string[] extraAssemblyPaths, Action printWarning, Action printError) + { + CheckDLLPath(unityEngine); + CheckDLLPath(netDLL); + CheckOutputDirectory(outputDirectory); + CheckAssemblies(assemblies); + Log.WarningMethod = printWarning; + Log.ErrorMethod = printError; + return Weaver.WeaveAssemblies(assemblies, extraAssemblyPaths, outputDirectory, unityEngine, netDLL); + } + + static void CheckDLLPath(string path) + { + if (!File.Exists(path)) + throw new Exception("dll could not be located at " + path + "!"); + } + + static void CheckAssemblies(IEnumerable assemblyPaths) + { + foreach (string assemblyPath in assemblyPaths) + CheckAssemblyPath(assemblyPath); + } + + static void CheckAssemblyPath(string assemblyPath) + { + if (!File.Exists(assemblyPath)) + throw new Exception("Assembly " + assemblyPath + " does not exist!"); + } + + static void CheckOutputDirectory(string outputDir) + { + if (outputDir != null && !Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Program.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Program.cs.meta new file mode 100644 index 0000000..3f62978 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Program.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2a21c60c40a4c4d679c2b71a7c40882e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Readers.cs b/Assets/Packages/Mirror/Editor/Weaver/Readers.cs new file mode 100644 index 0000000..a50798d --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Readers.cs @@ -0,0 +1,371 @@ +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; +using Mono.CecilX.Rocks; + + +namespace Mirror.Weaver +{ + public static class Readers + { + const int MaxRecursionCount = 128; + static Dictionary readFuncs; + + public static void Init() + { + readFuncs = new Dictionary(); + } + + internal static void Register(TypeReference dataType, MethodReference methodReference) + { + readFuncs[dataType.FullName] = methodReference; + } + + public static MethodReference GetReadFunc(TypeReference variable, int recursionCount = 0) + { + if (readFuncs.TryGetValue(variable.FullName, out MethodReference foundFunc)) + { + return foundFunc; + } + + TypeDefinition td = variable.Resolve(); + if (td == null) + { + Weaver.Error($"{variable} is not a supported type"); + return null; + } + + if (variable.IsByReference) + { + // error?? + Weaver.Error($"{variable} is not a supported reference type"); + return null; + } + + MethodDefinition newReaderFunc; + + if (variable.IsArray) + { + newReaderFunc = GenerateArrayReadFunc(variable, recursionCount); + } + else if (td.IsEnum) + { + return GetReadFunc(td.GetEnumUnderlyingType(), recursionCount); + } + else if (variable.FullName.StartsWith("System.ArraySegment`1", System.StringComparison.Ordinal)) + { + newReaderFunc = GenerateArraySegmentReadFunc(variable, recursionCount); + } + else + { + newReaderFunc = GenerateStructReadFunction(variable, recursionCount); + } + + if (newReaderFunc == null) + { + Weaver.Error($"{variable} is not a supported type"); + return null; + } + RegisterReadFunc(variable.FullName, newReaderFunc); + return newReaderFunc; + } + + static void RegisterReadFunc(string name, MethodDefinition newReaderFunc) + { + readFuncs[name] = newReaderFunc; + Weaver.WeaveLists.generatedReadFunctions.Add(newReaderFunc); + + Weaver.ConfirmGeneratedCodeClass(); + Weaver.WeaveLists.generateContainerClass.Methods.Add(newReaderFunc); + } + + static MethodDefinition GenerateArrayReadFunc(TypeReference variable, int recursionCount) + { + if (!variable.IsArrayType()) + { + Weaver.Error($"{variable} is an unsupported type. Jagged and multidimensional arrays are not supported"); + return null; + } + + TypeReference elementType = variable.GetElementType(); + MethodReference elementReadFunc = GetReadFunc(elementType, recursionCount + 1); + if (elementReadFunc == null) + { + return null; + } + + string functionName = "_ReadArray" + variable.GetElementType().Name + "_"; + if (variable.DeclaringType != null) + { + functionName += variable.DeclaringType.Name; + } + else + { + functionName += "None"; + } + + // create new reader for this type + MethodDefinition readerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + variable); + + readerFunc.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkReaderType))); + + readerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + readerFunc.Body.Variables.Add(new VariableDefinition(variable)); + readerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + readerFunc.Body.InitLocals = true; + + ILProcessor worker = readerFunc.Body.GetILProcessor(); + + // int length = reader.ReadPackedInt32(); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Call, GetReadFunc(Weaver.int32Type))); + worker.Append(worker.Create(OpCodes.Stloc_0)); + + // if (length < 0) { + // return null + // } + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Ldc_I4_0)); + Instruction labelEmptyArray = worker.Create(OpCodes.Nop); + worker.Append(worker.Create(OpCodes.Bge, labelEmptyArray)); + // return null + worker.Append(worker.Create(OpCodes.Ldnull)); + worker.Append(worker.Create(OpCodes.Ret)); + worker.Append(labelEmptyArray); + + + // T value = new T[length]; + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Newarr, variable.GetElementType())); + worker.Append(worker.Create(OpCodes.Stloc_1)); + + + // for (int i=0; i< length ; i++) { + worker.Append(worker.Create(OpCodes.Ldc_I4_0)); + worker.Append(worker.Create(OpCodes.Stloc_2)); + Instruction labelHead = worker.Create(OpCodes.Nop); + worker.Append(worker.Create(OpCodes.Br, labelHead)); + + // loop body + Instruction labelBody = worker.Create(OpCodes.Nop); + worker.Append(labelBody); + // value[i] = reader.ReadT(); + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldloc_2)); + worker.Append(worker.Create(OpCodes.Ldelema, variable.GetElementType())); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Call, elementReadFunc)); + worker.Append(worker.Create(OpCodes.Stobj, variable.GetElementType())); + + + worker.Append(worker.Create(OpCodes.Ldloc_2)); + worker.Append(worker.Create(OpCodes.Ldc_I4_1)); + worker.Append(worker.Create(OpCodes.Add)); + worker.Append(worker.Create(OpCodes.Stloc_2)); + + // loop while check + worker.Append(labelHead); + worker.Append(worker.Create(OpCodes.Ldloc_2)); + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Blt, labelBody)); + + // return value; + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ret)); + return readerFunc; + } + + static MethodDefinition GenerateArraySegmentReadFunc(TypeReference variable, int recursionCount) + { + GenericInstanceType genericInstance = (GenericInstanceType)variable; + TypeReference elementType = genericInstance.GenericArguments[0]; + + MethodReference elementReadFunc = GetReadFunc(elementType, recursionCount + 1); + if (elementReadFunc == null) + { + return null; + } + + string functionName = "_ReadArraySegment_" + variable.GetElementType().Name + "_"; + if (variable.DeclaringType != null) + { + functionName += variable.DeclaringType.Name; + } + else + { + functionName += "None"; + } + + // create new reader for this type + MethodDefinition readerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + variable); + + readerFunc.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkReaderType))); + + // int lengh + readerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + // T[] array + readerFunc.Body.Variables.Add(new VariableDefinition(elementType.MakeArrayType())); + // int i; + readerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + readerFunc.Body.InitLocals = true; + + ILProcessor worker = readerFunc.Body.GetILProcessor(); + + // int length = reader.ReadPackedInt32(); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Call, GetReadFunc(Weaver.int32Type))); + worker.Append(worker.Create(OpCodes.Stloc_0)); + + // T[] array = new int[length] + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Newarr, elementType)); + worker.Append(worker.Create(OpCodes.Stloc_1)); + + + // loop through array and deserialize each element + // generates code like this + // for (int i=0; i< length ; i++) + // { + // value[i] = reader.ReadXXX(); + // } + worker.Append(worker.Create(OpCodes.Ldc_I4_0)); + worker.Append(worker.Create(OpCodes.Stloc_2)); + Instruction labelHead = worker.Create(OpCodes.Nop); + worker.Append(worker.Create(OpCodes.Br, labelHead)); + + // loop body + Instruction labelBody = worker.Create(OpCodes.Nop); + worker.Append(labelBody); + { + // value[i] = reader.ReadT(); + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldloc_2)); + worker.Append(worker.Create(OpCodes.Ldelema, elementType)); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Call, elementReadFunc)); + worker.Append(worker.Create(OpCodes.Stobj, elementType)); + } + + worker.Append(worker.Create(OpCodes.Ldloc_2)); + worker.Append(worker.Create(OpCodes.Ldc_I4_1)); + worker.Append(worker.Create(OpCodes.Add)); + worker.Append(worker.Create(OpCodes.Stloc_2)); + + // loop while check + worker.Append(labelHead); + worker.Append(worker.Create(OpCodes.Ldloc_2)); + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Blt, labelBody)); + + // return new ArraySegment(array); + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Newobj, Weaver.ArraySegmentConstructorReference.MakeHostInstanceGeneric(genericInstance))); + worker.Append(worker.Create(OpCodes.Ret)); + return readerFunc; + } + + static MethodDefinition GenerateStructReadFunction(TypeReference variable, int recursionCount) + { + if (recursionCount > MaxRecursionCount) + { + Weaver.Error($"{variable} can't be deserialized because it references itself"); + return null; + } + + if (!Weaver.IsValidTypeToGenerate(variable.Resolve())) + { + return null; + } + + string functionName = "_Read" + variable.Name + "_"; + if (variable.DeclaringType != null) + { + functionName += variable.DeclaringType.Name; + } + else + { + functionName += "None"; + } + + // create new reader for this type + MethodDefinition readerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + variable); + + // create local for return value + readerFunc.Body.Variables.Add(new VariableDefinition(variable)); + readerFunc.Body.InitLocals = true; + + readerFunc.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkReaderType))); + + ILProcessor worker = readerFunc.Body.GetILProcessor(); + + if (variable.IsValueType) + { + // structs are created with Initobj + worker.Append(worker.Create(OpCodes.Ldloca, 0)); + worker.Append(worker.Create(OpCodes.Initobj, variable)); + } + else + { + // classes are created with their constructor + + MethodDefinition ctor = Resolvers.ResolveDefaultPublicCtor(variable); + if (ctor == null) + { + Weaver.Error($"{variable} can't be deserialized bcause i has no default constructor"); + return null; + } + + worker.Append(worker.Create(OpCodes.Newobj, ctor)); + worker.Append(worker.Create(OpCodes.Stloc_0)); + } + + uint fields = 0; + foreach (FieldDefinition field in variable.Resolve().Fields) + { + if (field.IsStatic || field.IsPrivate) + continue; + + // mismatched ldloca/ldloc for struct/class combinations is invalid IL, which causes crash at runtime + OpCode opcode = variable.IsValueType ? OpCodes.Ldloca : OpCodes.Ldloc; + worker.Append(worker.Create(opcode, 0)); + + MethodReference readFunc = GetReadFunc(field.FieldType, recursionCount + 1); + if (readFunc != null) + { + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Call, readFunc)); + } + else + { + Weaver.Error($"{field} has an unsupported type"); + return null; + } + + worker.Append(worker.Create(OpCodes.Stfld, field)); + fields++; + } + if (fields == 0) + { + Log.Warning($"{variable} has no public or non-static fields to deserialize"); + } + + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Ret)); + return readerFunc; + } + + } + +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Readers.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Readers.cs.meta new file mode 100644 index 0000000..838ff59 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Readers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be40277098a024539bf63d0205cae824 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs b/Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs new file mode 100644 index 0000000..434aed4 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs @@ -0,0 +1,141 @@ +// all the resolve functions for the weaver +// NOTE: these functions should be made extensions, but right now they still +// make heavy use of Weaver.fail and we'd have to check each one's return +// value for null otherwise. +// (original FieldType.Resolve returns null if not found too, so +// exceptions would be a bit inconsistent here) +using Mono.CecilX; + +namespace Mirror.Weaver +{ + public static class Resolvers + { + public static MethodReference ResolveMethod(TypeReference tr, AssemblyDefinition scriptDef, string name) + { + //Console.WriteLine("ResolveMethod " + t.ToString () + " " + name); + if (tr == null) + { + Weaver.Error("Type missing for " + name); + return null; + } + foreach (MethodDefinition methodRef in tr.Resolve().Methods) + { + if (methodRef.Name == name) + { + return scriptDef.MainModule.ImportReference(methodRef); + } + } + Weaver.Error($"{tr}.{name}() not found"); + return null; + } + + // TODO reuse ResolveMethod in here after Weaver.fail was removed + public static MethodReference ResolveMethodInParents(TypeReference tr, AssemblyDefinition scriptDef, string name) + { + if (tr == null) + { + Weaver.Error("Type missing for " + name); + return null; + } + foreach (MethodDefinition methodRef in tr.Resolve().Methods) + { + if (methodRef.Name == name) + { + return scriptDef.MainModule.ImportReference(methodRef); + } + } + // Could not find the method in this class, try the parent + return ResolveMethodInParents(tr.Resolve().BaseType, scriptDef, name); + } + + // System.Byte[] arguments need a version with a string + public static MethodReference ResolveMethodWithArg(TypeReference tr, AssemblyDefinition scriptDef, string name, string argTypeFullName) + { + foreach (MethodDefinition methodRef in tr.Resolve().Methods) + { + if (methodRef.Name == name) + { + if (methodRef.Parameters.Count == 1) + { + if (methodRef.Parameters[0].ParameterType.FullName == argTypeFullName) + { + return scriptDef.MainModule.ImportReference(methodRef); + } + } + } + } + Weaver.Error($"{tr}.{name}({argTypeFullName}) not found"); + return null; + } + + // reuse ResolveMethodWithArg string version + public static MethodReference ResolveMethodWithArg(TypeReference tr, AssemblyDefinition scriptDef, string name, TypeReference argType) + { + return ResolveMethodWithArg(tr, scriptDef, name, argType.FullName); + } + + public static MethodDefinition ResolveDefaultPublicCtor(TypeReference variable) + { + foreach (MethodDefinition methodRef in variable.Resolve().Methods) + { + if (methodRef.Name == ".ctor" && + methodRef.Resolve().IsPublic && + methodRef.Parameters.Count == 0) + { + return methodRef; + } + } + return null; + } + + public static GenericInstanceMethod ResolveMethodGeneric(TypeReference t, AssemblyDefinition scriptDef, string name, TypeReference genericType) + { + foreach (MethodDefinition methodRef in t.Resolve().Methods) + { + if (methodRef.Name == name) + { + if (methodRef.Parameters.Count == 0) + { + if (methodRef.GenericParameters.Count == 1) + { + MethodReference tmp = scriptDef.MainModule.ImportReference(methodRef); + GenericInstanceMethod gm = new GenericInstanceMethod(tmp); + gm.GenericArguments.Add(genericType); + if (gm.GenericArguments[0].FullName == genericType.FullName) + { + return gm; + } + } + } + } + } + + Weaver.Error($"{t}.{name}<{genericType}>() not found"); + return null; + } + + public static FieldReference ResolveField(TypeReference tr, AssemblyDefinition scriptDef, string name) + { + foreach (FieldDefinition fd in tr.Resolve().Fields) + { + if (fd.Name == name) + { + return scriptDef.MainModule.ImportReference(fd); + } + } + return null; + } + + public static MethodReference ResolveProperty(TypeReference tr, AssemblyDefinition scriptDef, string name) + { + foreach (PropertyDefinition pd in tr.Resolve().Properties) + { + if (pd.Name == name) + { + return scriptDef.MainModule.ImportReference(pd.GetMethod); + } + } + return null; + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs.meta new file mode 100644 index 0000000..f4f6602 --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Resolvers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3039a59c76aec43c797ad66930430367 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Weaver.cs b/Assets/Packages/Mirror/Editor/Weaver/Weaver.cs new file mode 100644 index 0000000..cf905ad --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Weaver.cs @@ -0,0 +1,598 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + // This data is flushed each time - if we are run multiple times in the same process/domain + class WeaverLists + { + // setter functions that replace [SyncVar] member variable references. dict + public Dictionary replacementSetterProperties = new Dictionary(); + // getter functions that replace [SyncVar] member variable references. dict + public Dictionary replacementGetterProperties = new Dictionary(); + + // [Command]/[ClientRpc] functions that should be replaced. dict + public Dictionary replaceMethods = new Dictionary(); + + // [SyncEvent] invoke functions that should be replaced. dict + public Dictionary replaceEvents = new Dictionary(); + + public List generatedReadFunctions = new List(); + public List generatedWriteFunctions = new List(); + + public TypeDefinition generateContainerClass; + + // amount of SyncVars per class. dict + public Dictionary numSyncVars = new Dictionary(); + } + + class Weaver + { + public static WeaverLists WeaveLists { get; private set; } + public static AssemblyDefinition CurrentAssembly { get; private set; } + public static ModuleDefinition CorLibModule { get; private set; } + public static AssemblyDefinition UnityAssembly { get; private set; } + public static AssemblyDefinition NetAssembly { get; private set; } + public static bool WeavingFailed { get; private set; } + public static bool GenerateLogErrors { get; set; } + + // private properties + static readonly bool DebugLogEnabled = true; + + // Network types + public static TypeReference NetworkBehaviourType; + public static TypeReference NetworkBehaviourType2; + public static TypeReference MonoBehaviourType; + public static TypeReference ScriptableObjectType; + public static TypeReference NetworkConnectionType; + + public static TypeReference MessageBaseType; + public static TypeReference SyncListType; + public static TypeReference SyncSetType; + public static TypeReference SyncDictionaryType; + + public static MethodReference NetworkBehaviourDirtyBitsReference; + public static MethodReference GetPooledWriterReference; + public static MethodReference RecycleWriterReference; + public static TypeReference NetworkClientType; + public static TypeReference NetworkServerType; + + public static TypeReference NetworkReaderType; + + public static TypeReference NetworkWriterType; + + public static TypeReference NetworkIdentityType; + public static TypeReference IEnumeratorType; + + public static TypeReference ClientSceneType; + public static MethodReference ReadyConnectionReference; + + public static TypeReference ComponentType; + + public static TypeReference CmdDelegateReference; + public static MethodReference CmdDelegateConstructor; + + public static MethodReference NetworkServerGetActive; + public static MethodReference NetworkServerGetLocalClientActive; + public static MethodReference NetworkClientGetActive; + public static MethodReference getBehaviourIsServer; + + // custom attribute types + public static TypeReference SyncVarType; + public static TypeReference CommandType; + public static TypeReference ClientRpcType; + public static TypeReference TargetRpcType; + public static TypeReference SyncEventType; + public static TypeReference SyncObjectType; + public static MethodReference InitSyncObjectReference; + + // array segment + public static TypeReference ArraySegmentType; + public static MethodReference ArraySegmentConstructorReference; + public static MethodReference ArraySegmentArrayReference; + public static MethodReference ArraySegmentOffsetReference; + public static MethodReference ArraySegmentCountReference; + + // system types + public static TypeReference voidType; + public static TypeReference singleType; + public static TypeReference doubleType; + public static TypeReference boolType; + public static TypeReference int64Type; + public static TypeReference uint64Type; + public static TypeReference int32Type; + public static TypeReference uint32Type; + public static TypeReference objectType; + public static TypeReference typeType; + public static TypeReference gameObjectType; + public static TypeReference transformType; + + public static MethodReference setSyncVarReference; + public static MethodReference setSyncVarHookGuard; + public static MethodReference getSyncVarHookGuard; + public static MethodReference setSyncVarGameObjectReference; + public static MethodReference getSyncVarGameObjectReference; + public static MethodReference setSyncVarNetworkIdentityReference; + public static MethodReference getSyncVarNetworkIdentityReference; + public static MethodReference registerCommandDelegateReference; + public static MethodReference registerRpcDelegateReference; + public static MethodReference registerEventDelegateReference; + public static MethodReference getTypeReference; + public static MethodReference getTypeFromHandleReference; + public static MethodReference logErrorReference; + public static MethodReference logWarningReference; + public static MethodReference sendCommandInternal; + public static MethodReference sendRpcInternal; + public static MethodReference sendTargetRpcInternal; + public static MethodReference sendEventInternal; + + public static void DLog(TypeDefinition td, string fmt, params object[] args) + { + if (!DebugLogEnabled) + return; + + Console.WriteLine("[" + td.Name + "] " + string.Format(fmt, args)); + } + + // display weaver error + // and mark process as failed + public static void Error(string message) + { + Log.Error(message); + WeavingFailed = true; + } + + public static int GetSyncVarStart(string className) + { + return WeaveLists.numSyncVars.ContainsKey(className) + ? WeaveLists.numSyncVars[className] + : 0; + } + + public static void SetNumSyncVars(string className, int num) + { + WeaveLists.numSyncVars[className] = num; + } + + internal static void ConfirmGeneratedCodeClass() + { + if (WeaveLists.generateContainerClass == null) + { + WeaveLists.generateContainerClass = new TypeDefinition("Mirror", "GeneratedNetworkCode", + TypeAttributes.BeforeFieldInit | TypeAttributes.Class | TypeAttributes.AnsiClass | TypeAttributes.Public | TypeAttributes.AutoClass, + objectType); + + const MethodAttributes methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName; + MethodDefinition method = new MethodDefinition(".ctor", methodAttributes, voidType); + method.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); + method.Body.Instructions.Add(Instruction.Create(OpCodes.Call, Resolvers.ResolveMethod(objectType, CurrentAssembly, ".ctor"))); + method.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + + WeaveLists.generateContainerClass.Methods.Add(method); + } + } + + static bool ProcessNetworkBehaviourType(TypeDefinition td) + { + if (!NetworkBehaviourProcessor.WasProcessed(td)) + { + DLog(td, "Found NetworkBehaviour " + td.FullName); + + NetworkBehaviourProcessor proc = new NetworkBehaviourProcessor(td); + proc.Process(); + return true; + } + return false; + } + + static void SetupUnityTypes() + { + gameObjectType = UnityAssembly.MainModule.GetType("UnityEngine.GameObject"); + transformType = UnityAssembly.MainModule.GetType("UnityEngine.Transform"); + + NetworkClientType = NetAssembly.MainModule.GetType("Mirror.NetworkClient"); + NetworkServerType = NetAssembly.MainModule.GetType("Mirror.NetworkServer"); + + SyncVarType = NetAssembly.MainModule.GetType("Mirror.SyncVarAttribute"); + CommandType = NetAssembly.MainModule.GetType("Mirror.CommandAttribute"); + ClientRpcType = NetAssembly.MainModule.GetType("Mirror.ClientRpcAttribute"); + TargetRpcType = NetAssembly.MainModule.GetType("Mirror.TargetRpcAttribute"); + SyncEventType = NetAssembly.MainModule.GetType("Mirror.SyncEventAttribute"); + SyncObjectType = NetAssembly.MainModule.GetType("Mirror.SyncObject"); + } + + static void SetupCorLib() + { + AssemblyNameReference name = AssemblyNameReference.Parse("mscorlib"); + ReaderParameters parameters = new ReaderParameters + { + AssemblyResolver = CurrentAssembly.MainModule.AssemblyResolver + }; + CorLibModule = CurrentAssembly.MainModule.AssemblyResolver.Resolve(name, parameters).MainModule; + } + + static TypeReference ImportCorLibType(string fullName) + { + TypeDefinition type = CorLibModule.GetType(fullName) ?? CorLibModule.ExportedTypes.First(t => t.FullName == fullName).Resolve(); + if (type != null) + { + return CurrentAssembly.MainModule.ImportReference(type); + } + Error("Failed to import mscorlib type: " + fullName + " because Resolve failed. (Might happen when trying to Resolve in NetStandard dll, see also: https://github.com/vis2k/Mirror/issues/791)"); + return null; + } + + static void SetupTargetTypes() + { + // system types + SetupCorLib(); + voidType = ImportCorLibType("System.Void"); + singleType = ImportCorLibType("System.Single"); + doubleType = ImportCorLibType("System.Double"); + boolType = ImportCorLibType("System.Boolean"); + int64Type = ImportCorLibType("System.Int64"); + uint64Type = ImportCorLibType("System.UInt64"); + int32Type = ImportCorLibType("System.Int32"); + uint32Type = ImportCorLibType("System.UInt32"); + objectType = ImportCorLibType("System.Object"); + typeType = ImportCorLibType("System.Type"); + IEnumeratorType = ImportCorLibType("System.Collections.IEnumerator"); + + ArraySegmentType = ImportCorLibType("System.ArraySegment`1"); + ArraySegmentArrayReference = Resolvers.ResolveProperty(ArraySegmentType, CurrentAssembly, "Array"); + ArraySegmentCountReference = Resolvers.ResolveProperty(ArraySegmentType, CurrentAssembly, "Count"); + ArraySegmentOffsetReference = Resolvers.ResolveProperty(ArraySegmentType, CurrentAssembly, "Offset"); + ArraySegmentConstructorReference = Resolvers.ResolveMethod(ArraySegmentType, CurrentAssembly, ".ctor"); + + + NetworkReaderType = NetAssembly.MainModule.GetType("Mirror.NetworkReader"); + NetworkWriterType = NetAssembly.MainModule.GetType("Mirror.NetworkWriter"); + + NetworkServerGetActive = Resolvers.ResolveMethod(NetworkServerType, CurrentAssembly, "get_active"); + NetworkServerGetLocalClientActive = Resolvers.ResolveMethod(NetworkServerType, CurrentAssembly, "get_localClientActive"); + NetworkClientGetActive = Resolvers.ResolveMethod(NetworkClientType, CurrentAssembly, "get_active"); + + CmdDelegateReference = NetAssembly.MainModule.GetType("Mirror.NetworkBehaviour/CmdDelegate"); + CmdDelegateConstructor = Resolvers.ResolveMethod(CmdDelegateReference, CurrentAssembly, ".ctor"); + CurrentAssembly.MainModule.ImportReference(gameObjectType); + CurrentAssembly.MainModule.ImportReference(transformType); + + TypeReference networkIdentityTmp = NetAssembly.MainModule.GetType("Mirror.NetworkIdentity"); + NetworkIdentityType = CurrentAssembly.MainModule.ImportReference(networkIdentityTmp); + + NetworkBehaviourType = NetAssembly.MainModule.GetType("Mirror.NetworkBehaviour"); + NetworkBehaviourType2 = CurrentAssembly.MainModule.ImportReference(NetworkBehaviourType); + NetworkConnectionType = NetAssembly.MainModule.GetType("Mirror.NetworkConnection"); + + MonoBehaviourType = UnityAssembly.MainModule.GetType("UnityEngine.MonoBehaviour"); + ScriptableObjectType = UnityAssembly.MainModule.GetType("UnityEngine.ScriptableObject"); + + NetworkConnectionType = NetAssembly.MainModule.GetType("Mirror.NetworkConnection"); + NetworkConnectionType = CurrentAssembly.MainModule.ImportReference(NetworkConnectionType); + + MessageBaseType = NetAssembly.MainModule.GetType("Mirror.MessageBase"); + SyncListType = NetAssembly.MainModule.GetType("Mirror.SyncList`1"); + SyncSetType = NetAssembly.MainModule.GetType("Mirror.SyncSet`1"); + SyncDictionaryType = NetAssembly.MainModule.GetType("Mirror.SyncDictionary`2"); + + NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, CurrentAssembly, "syncVarDirtyBits"); + TypeDefinition NetworkWriterPoolType = NetAssembly.MainModule.GetType("Mirror.NetworkWriterPool"); + GetPooledWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, CurrentAssembly, "GetWriter"); + RecycleWriterReference = Resolvers.ResolveMethod(NetworkWriterPoolType, CurrentAssembly, "Recycle"); + + ComponentType = UnityAssembly.MainModule.GetType("UnityEngine.Component"); + ClientSceneType = NetAssembly.MainModule.GetType("Mirror.ClientScene"); + ReadyConnectionReference = Resolvers.ResolveMethod(ClientSceneType, CurrentAssembly, "get_readyConnection"); + + getBehaviourIsServer = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "get_isServer"); + setSyncVarReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "SetSyncVar"); + setSyncVarHookGuard = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "setSyncVarHookGuard"); + getSyncVarHookGuard = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "getSyncVarHookGuard"); + + setSyncVarGameObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "SetSyncVarGameObject"); + getSyncVarGameObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "GetSyncVarGameObject"); + setSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "SetSyncVarNetworkIdentity"); + getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "GetSyncVarNetworkIdentity"); + registerCommandDelegateReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "RegisterCommandDelegate"); + registerRpcDelegateReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "RegisterRpcDelegate"); + registerEventDelegateReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "RegisterEventDelegate"); + getTypeReference = Resolvers.ResolveMethod(objectType, CurrentAssembly, "GetType"); + getTypeFromHandleReference = Resolvers.ResolveMethod(typeType, CurrentAssembly, "GetTypeFromHandle"); + logErrorReference = Resolvers.ResolveMethod(UnityAssembly.MainModule.GetType("UnityEngine.Debug"), CurrentAssembly, "LogError"); + logWarningReference = Resolvers.ResolveMethod(UnityAssembly.MainModule.GetType("UnityEngine.Debug"), CurrentAssembly, "LogWarning"); + sendCommandInternal = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "SendCommandInternal"); + sendRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "SendRPCInternal"); + sendTargetRpcInternal = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "SendTargetRPCInternal"); + sendEventInternal = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "SendEventInternal"); + + SyncObjectType = CurrentAssembly.MainModule.ImportReference(SyncObjectType); + InitSyncObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, CurrentAssembly, "InitSyncObject"); + } + + public static bool IsNetworkBehaviour(TypeDefinition td) + { + return td.IsDerivedFrom(NetworkBehaviourType); + } + + public static bool IsValidTypeToGenerate(TypeDefinition variable) + { + // a valid type is a simple class or struct. so we generate only code for types we dont know, and if they are not inside + // this assembly it must mean that we are trying to serialize a variable outside our scope. and this will fail. + // no need to report an error here, the caller will report a better error + string assembly = CurrentAssembly.MainModule.Name; + return variable.Module.Name == assembly; + } + + static void CheckMonoBehaviour(TypeDefinition td) + { + if (td.IsDerivedFrom(MonoBehaviourType)) + { + MonoBehaviourProcessor.Process(td); + } + } + + static bool CheckNetworkBehaviour(TypeDefinition td) + { + if (!td.IsClass) + return false; + + if (!IsNetworkBehaviour(td)) + { + CheckMonoBehaviour(td); + return false; + } + + // process this and base classes from parent to child order + + List behaviourClasses = new List(); + + TypeDefinition parent = td; + while (parent != null) + { + if (parent.FullName == NetworkBehaviourType.FullName) + { + break; + } + try + { + behaviourClasses.Insert(0, parent); + parent = parent.BaseType.Resolve(); + } + catch (AssemblyResolutionException) + { + // this can happen for plugins. + //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString()); + break; + } + } + + bool didWork = false; + foreach (TypeDefinition behaviour in behaviourClasses) + { + didWork |= ProcessNetworkBehaviourType(behaviour); + } + return didWork; + } + + static bool CheckMessageBase(TypeDefinition td) + { + if (!td.IsClass) + return false; + + bool didWork = false; + + // are ANY parent classes MessageBase + TypeReference parent = td.BaseType; + while (parent != null) + { + if (parent.FullName == MessageBaseType.FullName) + { + MessageClassProcessor.Process(td); + didWork = true; + break; + } + try + { + parent = parent.Resolve().BaseType; + } + catch (AssemblyResolutionException) + { + // this can happen for plugins. + //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString()); + break; + } + } + + // check for embedded types + foreach (TypeDefinition embedded in td.NestedTypes) + { + didWork |= CheckMessageBase(embedded); + } + + return didWork; + } + + static bool CheckSyncList(TypeDefinition td) + { + if (!td.IsClass) + return false; + + bool didWork = false; + + // are ANY parent classes SyncListStruct + TypeReference parent = td.BaseType; + while (parent != null) + { + if (parent.FullName.StartsWith(SyncListType.FullName, StringComparison.Ordinal)) + { + SyncListProcessor.Process(td); + didWork = true; + break; + } + if (parent.FullName.StartsWith(SyncSetType.FullName, StringComparison.Ordinal)) + { + SyncListProcessor.Process(td); + didWork = true; + break; + } + if (parent.FullName.StartsWith(SyncDictionaryType.FullName, StringComparison.Ordinal)) + { + SyncDictionaryProcessor.Process(td); + didWork = true; + break; + } + try + { + parent = parent.Resolve().BaseType; + } + catch (AssemblyResolutionException) + { + // this can happen for pluins. + //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString()); + break; + } + } + + // check for embedded types + foreach (TypeDefinition embedded in td.NestedTypes) + { + didWork |= CheckSyncList(embedded); + } + + return didWork; + } + + static bool Weave(string assName, IEnumerable dependencies, string unityEngineDLLPath, string mirrorNetDLLPath, string outputDir) + { + using (DefaultAssemblyResolver asmResolver = new DefaultAssemblyResolver()) + using (CurrentAssembly = AssemblyDefinition.ReadAssembly(assName, new ReaderParameters { ReadWrite = true, ReadSymbols = true, AssemblyResolver = asmResolver })) + { + asmResolver.AddSearchDirectory(Path.GetDirectoryName(assName)); + asmResolver.AddSearchDirectory(Helpers.UnityEngineDLLDirectoryName()); + asmResolver.AddSearchDirectory(Path.GetDirectoryName(unityEngineDLLPath)); + asmResolver.AddSearchDirectory(Path.GetDirectoryName(mirrorNetDLLPath)); + if (dependencies != null) + { + foreach (string path in dependencies) + { + asmResolver.AddSearchDirectory(path); + } + } + + SetupTargetTypes(); + System.Diagnostics.Stopwatch rwstopwatch = System.Diagnostics.Stopwatch.StartNew(); + ReaderWriterProcessor.ProcessReadersAndWriters(CurrentAssembly); + rwstopwatch.Stop(); + Console.WriteLine("Find all reader and writers took " + rwstopwatch.ElapsedMilliseconds + " milliseconds"); + + ModuleDefinition moduleDefinition = CurrentAssembly.MainModule; + Console.WriteLine("Script Module: {0}", moduleDefinition.Name); + + // Process each NetworkBehaviour + bool didWork = false; + + // We need to do 2 passes, because SyncListStructs might be referenced from other modules, so we must make sure we generate them first. + for (int pass = 0; pass < 2; pass++) + { + System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew(); + foreach (TypeDefinition td in moduleDefinition.Types) + { + if (td.IsClass && td.BaseType.CanBeResolved()) + { + try + { + if (pass == 0) + { + didWork |= CheckSyncList(td); + } + else + { + didWork |= CheckNetworkBehaviour(td); + didWork |= CheckMessageBase(td); + } + } + catch (Exception ex) + { + Error(ex.ToString()); + throw ex; + } + } + + if (WeavingFailed) + { + return false; + } + } + watch.Stop(); + Console.WriteLine("Pass: " + pass + " took " + watch.ElapsedMilliseconds + " milliseconds"); + } + + if (didWork) + { + // this must be done for ALL code, not just NetworkBehaviours + try + { + PropertySiteProcessor.ProcessSitesModule(CurrentAssembly.MainModule); + } + catch (Exception e) + { + Log.Error("ProcessPropertySites exception: " + e); + return false; + } + + if (WeavingFailed) + { + //Log.Error("Failed phase II."); + return false; + } + + // write to outputDir if specified, otherwise perform in-place write + WriterParameters writeParams = new WriterParameters { WriteSymbols = true }; + if (outputDir != null) + { + CurrentAssembly.Write(Helpers.DestinationFileFor(outputDir, assName), writeParams); + } + else + { + CurrentAssembly.Write(writeParams); + } + } + } + + return true; + } + + public static bool WeaveAssemblies(IEnumerable assemblies, IEnumerable dependencies, string outputDir, string unityEngineDLLPath, string mirrorNetDLLPath) + { + WeavingFailed = false; + WeaveLists = new WeaverLists(); + + using (UnityAssembly = AssemblyDefinition.ReadAssembly(unityEngineDLLPath)) + using (NetAssembly = AssemblyDefinition.ReadAssembly(mirrorNetDLLPath)) + { + SetupUnityTypes(); + + try + { + foreach (string ass in assemblies) + { + if (!Weave(ass, dependencies, unityEngineDLLPath, mirrorNetDLLPath, outputDir)) + { + return false; + } + } + } + catch (Exception e) + { + Log.Error("Exception :" + e); + return false; + } + } + return true; + } + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Weaver.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Weaver.cs.meta new file mode 100644 index 0000000..0ea2dfe --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Weaver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de160f52931054064852f2afd7e7a86f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Editor/Weaver/Writers.cs b/Assets/Packages/Mirror/Editor/Weaver/Writers.cs new file mode 100644 index 0000000..040960d --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Writers.cs @@ -0,0 +1,350 @@ +using System.Collections.Generic; +using Mono.CecilX; +using Mono.CecilX.Cil; + +namespace Mirror.Weaver +{ + + public static class Writers + { + const int MaxRecursionCount = 128; + + static Dictionary writeFuncs; + + public static void Init() + { + writeFuncs = new Dictionary(); + } + + public static void Register(TypeReference dataType, MethodReference methodReference) + { + writeFuncs[dataType.FullName] = methodReference; + } + + public static MethodReference GetWriteFunc(TypeReference variable, int recursionCount = 0) + { + if (writeFuncs.TryGetValue(variable.FullName, out MethodReference foundFunc)) + { + return foundFunc; + } + + if (variable.IsByReference) + { + // error?? + Weaver.Error($"{variable} has unsupported type. Use one of Mirror supported types instead"); + return null; + } + + MethodDefinition newWriterFunc; + + if (variable.IsArray) + { + newWriterFunc = GenerateArrayWriteFunc(variable, recursionCount); + } + else if (variable.Resolve().IsEnum) + { + return GetWriteFunc(variable.Resolve().GetEnumUnderlyingType(), recursionCount); + } + else if (variable.FullName.StartsWith("System.ArraySegment`1", System.StringComparison.Ordinal)) + { + newWriterFunc = GenerateArraySegmentWriteFunc(variable, recursionCount); + } + else + { + newWriterFunc = GenerateStructWriterFunction(variable, recursionCount); + } + + if (newWriterFunc == null) + { + return null; + } + + RegisterWriteFunc(variable.FullName, newWriterFunc); + return newWriterFunc; + } + + static void RegisterWriteFunc(string name, MethodDefinition newWriterFunc) + { + writeFuncs[name] = newWriterFunc; + Weaver.WeaveLists.generatedWriteFunctions.Add(newWriterFunc); + + Weaver.ConfirmGeneratedCodeClass(); + Weaver.WeaveLists.generateContainerClass.Methods.Add(newWriterFunc); + } + + static MethodDefinition GenerateStructWriterFunction(TypeReference variable, int recursionCount) + { + if (recursionCount > MaxRecursionCount) + { + Weaver.Error($"{variable} can't be serialized because it references itself"); + return null; + } + + if (!Weaver.IsValidTypeToGenerate(variable.Resolve())) + { + return null; + } + + string functionName = "_Write" + variable.Name + "_"; + if (variable.DeclaringType != null) + { + functionName += variable.DeclaringType.Name; + } + else + { + functionName += "None"; + } + // create new writer for this type + MethodDefinition writerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + Weaver.voidType); + + writerFunc.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkWriterType))); + writerFunc.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(variable))); + + ILProcessor worker = writerFunc.Body.GetILProcessor(); + + uint fields = 0; + foreach (FieldDefinition field in variable.Resolve().Fields) + { + if (field.IsStatic || field.IsPrivate) + continue; + + if (field.FieldType.Resolve().HasGenericParameters) + { + Weaver.Error($"{field} has unsupported type. Create a derived class instead of using generics"); + return null; + } + + if (field.FieldType.Resolve().IsInterface) + { + Weaver.Error($"{field} has unsupported type. Use a concrete class instead of an interface"); + return null; + } + + MethodReference writeFunc = GetWriteFunc(field.FieldType, recursionCount + 1); + if (writeFunc != null) + { + fields++; + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Ldarg_1)); + worker.Append(worker.Create(OpCodes.Ldfld, field)); + worker.Append(worker.Create(OpCodes.Call, writeFunc)); + } + else + { + Weaver.Error($"{field} has unsupported type. Use a type supported by Mirror instead"); + return null; + } + } + if (fields == 0) + { + Log.Warning($" {variable} has no no public or non-static fields to serialize"); + } + worker.Append(worker.Create(OpCodes.Ret)); + return writerFunc; + } + + static MethodDefinition GenerateArrayWriteFunc(TypeReference variable, int recursionCount) + { + + if (!variable.IsArrayType()) + { + Weaver.Error($"{variable} is an unsupported type. Jagged and multidimensional arrays are not supported"); + return null; + } + + TypeReference elementType = variable.GetElementType(); + MethodReference elementWriteFunc = GetWriteFunc(elementType, recursionCount + 1); + if (elementWriteFunc == null) + { + return null; + } + + string functionName = "_WriteArray" + variable.GetElementType().Name + "_"; + if (variable.DeclaringType != null) + { + functionName += variable.DeclaringType.Name; + } + else + { + functionName += "None"; + } + + // create new writer for this type + MethodDefinition writerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + Weaver.voidType); + + writerFunc.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkWriterType))); + writerFunc.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(variable))); + + writerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + writerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + writerFunc.Body.InitLocals = true; + + ILProcessor worker = writerFunc.Body.GetILProcessor(); + + // if (value == null) + // { + // writer.WritePackedInt32(-1); + // return; + // } + Instruction labelNull = worker.Create(OpCodes.Nop); + worker.Append(worker.Create(OpCodes.Ldarg_1)); + worker.Append(worker.Create(OpCodes.Brtrue, labelNull)); + + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Ldc_I4_M1)); + worker.Append(worker.Create(OpCodes.Call, GetWriteFunc(Weaver.int32Type))); + worker.Append(worker.Create(OpCodes.Ret)); + + // int length = value.Length; + worker.Append(labelNull); + worker.Append(worker.Create(OpCodes.Ldarg_1)); + worker.Append(worker.Create(OpCodes.Ldlen)); + worker.Append(worker.Create(OpCodes.Stloc_0)); + + // writer.WritePackedInt32(length); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Call, GetWriteFunc(Weaver.int32Type))); + + // for (int i=0; i< value.length; i++) { + worker.Append(worker.Create(OpCodes.Ldc_I4_0)); + worker.Append(worker.Create(OpCodes.Stloc_1)); + Instruction labelHead = worker.Create(OpCodes.Nop); + worker.Append(worker.Create(OpCodes.Br, labelHead)); + + // loop body + Instruction labelBody = worker.Create(OpCodes.Nop); + worker.Append(labelBody); + // writer.Write(value[i]); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Ldarg_1)); + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldelema, variable.GetElementType())); + worker.Append(worker.Create(OpCodes.Ldobj, variable.GetElementType())); + worker.Append(worker.Create(OpCodes.Call, elementWriteFunc)); + + + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldc_I4_1)); + worker.Append(worker.Create(OpCodes.Add)); + worker.Append(worker.Create(OpCodes.Stloc_1)); + + + // end for loop + worker.Append(labelHead); + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldarg_1)); + worker.Append(worker.Create(OpCodes.Ldlen)); + worker.Append(worker.Create(OpCodes.Conv_I4)); + worker.Append(worker.Create(OpCodes.Blt, labelBody)); + + // return + worker.Append(worker.Create(OpCodes.Ret)); + return writerFunc; + } + + static MethodDefinition GenerateArraySegmentWriteFunc(TypeReference variable, int recursionCount) + { + GenericInstanceType genericInstance = (GenericInstanceType)variable; + TypeReference elementType = genericInstance.GenericArguments[0]; + MethodReference elementWriteFunc = GetWriteFunc(elementType, recursionCount + 1); + + if (elementWriteFunc == null) + { + return null; + } + + string functionName = "_WriteArraySegment_" + elementType.Name + "_"; + if (variable.DeclaringType != null) + { + functionName += variable.DeclaringType.Name; + } + else + { + functionName += "None"; + } + + // create new writer for this type + MethodDefinition writerFunc = new MethodDefinition(functionName, + MethodAttributes.Public | + MethodAttributes.Static | + MethodAttributes.HideBySig, + Weaver.voidType); + + writerFunc.Parameters.Add(new ParameterDefinition("writer", ParameterAttributes.None, Weaver.CurrentAssembly.MainModule.ImportReference(Weaver.NetworkWriterType))); + writerFunc.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, variable)); + + writerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + writerFunc.Body.Variables.Add(new VariableDefinition(Weaver.int32Type)); + writerFunc.Body.InitLocals = true; + + ILProcessor worker = writerFunc.Body.GetILProcessor(); + + MethodReference countref = Weaver.ArraySegmentCountReference.MakeHostInstanceGeneric(genericInstance); + + // int length = value.Count; + worker.Append(worker.Create(OpCodes.Ldarga_S, (byte)1)); + worker.Append(worker.Create(OpCodes.Call, countref)); + worker.Append(worker.Create(OpCodes.Stloc_0)); + + + // writer.WritePackedInt32(length); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Call, GetWriteFunc(Weaver.int32Type))); + + // Loop through the ArraySegment and call the writer for each element. + // generates this: + // for (int i=0; i< length; i++) + // { + // writer.Write(value.Array[i + value.Offset]); + // } + worker.Append(worker.Create(OpCodes.Ldc_I4_0)); + worker.Append(worker.Create(OpCodes.Stloc_1)); + Instruction labelHead = worker.Create(OpCodes.Nop); + worker.Append(worker.Create(OpCodes.Br, labelHead)); + + // loop body + Instruction labelBody = worker.Create(OpCodes.Nop); + worker.Append(labelBody); + { + // writer.Write(value.Array[i + value.Offset]); + worker.Append(worker.Create(OpCodes.Ldarg_0)); + worker.Append(worker.Create(OpCodes.Ldarga_S, (byte)1)); + worker.Append(worker.Create(OpCodes.Call, Weaver.ArraySegmentArrayReference.MakeHostInstanceGeneric(genericInstance))); + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldarga_S, (byte)1)); + worker.Append(worker.Create(OpCodes.Call, Weaver.ArraySegmentOffsetReference.MakeHostInstanceGeneric(genericInstance))); + worker.Append(worker.Create(OpCodes.Add)); + worker.Append(worker.Create(OpCodes.Ldelema, elementType)); + worker.Append(worker.Create(OpCodes.Ldobj, elementType)); + worker.Append(worker.Create(OpCodes.Call, elementWriteFunc)); + } + + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldc_I4_1)); + worker.Append(worker.Create(OpCodes.Add)); + worker.Append(worker.Create(OpCodes.Stloc_1)); + + + // end for loop + worker.Append(labelHead); + worker.Append(worker.Create(OpCodes.Ldloc_1)); + worker.Append(worker.Create(OpCodes.Ldloc_0)); + worker.Append(worker.Create(OpCodes.Blt, labelBody)); + + // return + worker.Append(worker.Create(OpCodes.Ret)); + return writerFunc; + } + + } +} diff --git a/Assets/Packages/Mirror/Editor/Weaver/Writers.cs.meta b/Assets/Packages/Mirror/Editor/Weaver/Writers.cs.meta new file mode 100644 index 0000000..3769f7f --- /dev/null +++ b/Assets/Packages/Mirror/Editor/Weaver/Writers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a90060ad76ea044aba613080dd922709 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/License.txt b/Assets/Packages/Mirror/License.txt new file mode 100644 index 0000000..2925e86 --- /dev/null +++ b/Assets/Packages/Mirror/License.txt @@ -0,0 +1,3 @@ +The Mirror DLLs in the Plugins folder are MIT licensed: + +https://github.com/vis2k/Mirror \ No newline at end of file diff --git a/Assets/Packages/Mirror/License.txt.meta b/Assets/Packages/Mirror/License.txt.meta new file mode 100644 index 0000000..6129dfe --- /dev/null +++ b/Assets/Packages/Mirror/License.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dbf30d11d3879431f87403d009e47bf7 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Plugins.meta b/Assets/Packages/Mirror/Plugins.meta new file mode 100644 index 0000000..9504239 --- /dev/null +++ b/Assets/Packages/Mirror/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 05eb4061e2eb94061b9a08c918fff99b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Plugins/Mono.Cecil.meta b/Assets/Packages/Mirror/Plugins/Mono.Cecil.meta new file mode 100644 index 0000000..a104e2e --- /dev/null +++ b/Assets/Packages/Mirror/Plugins/Mono.Cecil.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ce126b4e1a7d13b4c865cd92929f13c3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll b/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll new file mode 100644 index 0000000000000000000000000000000000000000..f6815d3b41f5d04b8389ae99b07df13246c3b6da GIT binary patch literal 43520 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 am-K3pEI#Dm6C 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 literal 0 HcmV?d00001 diff --git a/Assets/Telepathy.dll.meta b/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta similarity index 90% rename from Assets/Telepathy.dll.meta rename to Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta index d0a95b4..f75f642 100644 --- a/Assets/Telepathy.dll.meta +++ b/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Mdb.dll.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a8ec6c9ba48cb7348879483314d7ce07 +guid: a078fc7c0dc14d047a28dea9c93fd259 PluginImporter: externalObjects: {} serializedVersion: 2 @@ -9,7 +9,6 @@ PluginImporter: isPreloaded: 0 isOverridable: 0 isExplicitlyReferenced: 0 - validateReferences: 1 platformData: - first: Any: diff --git a/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll b/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll new file mode 100644 index 0000000000000000000000000000000000000000..3b58436e37e6705aa683b65555cbb77a256c82db GIT binary patch literal 87552 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 Sp+?bvIT-!FzyIIW!2buACErW{ literal 0 HcmV?d00001 diff --git a/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta b/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta new file mode 100644 index 0000000..773758f --- /dev/null +++ b/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Pdb.dll.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 534d998d93b238041bddcd864f7f1088 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll b/Assets/Packages/Mirror/Plugins/Mono.Cecil/Mono.CecilX.Rocks.dll new file mode 100644 index 0000000000000000000000000000000000000000..7d820f0cc4eb2ead833303c0e6e2dea9f7b0eef1 GIT binary patch literal 27648 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

HFvly*KXxyP)w+jBy^lSZV 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 z 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 m>yU%d1DU0u6LOc8SEtI>6JOR-v Player > Other Settings) + +Documentation: + https://vis2k.github.io/Mirror/ + +Support: + Discord: https://discordapp.com/invite/N9QVxbM + Bug Reports: https://github.com/vis2k/Mirror/issues diff --git a/Assets/Packages/Mirror/Readme.txt.meta b/Assets/Packages/Mirror/Readme.txt.meta new file mode 100644 index 0000000..d52ccce --- /dev/null +++ b/Assets/Packages/Mirror/Readme.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f6d84e019c68446f28415a923b460a03 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime.meta b/Assets/Packages/Mirror/Runtime.meta new file mode 100644 index 0000000..85ee3eb --- /dev/null +++ b/Assets/Packages/Mirror/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9f4328ccc5f724e45afe2215d275b5d5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/AssemblyInfo.cs b/Assets/Packages/Mirror/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..f0c4858 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Mirror.Tests")] \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/AssemblyInfo.cs.meta b/Assets/Packages/Mirror/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..cf3201c --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e28d5f410e25b42e6a76a2ffc10e4675 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/ClientScene.cs b/Assets/Packages/Mirror/Runtime/ClientScene.cs new file mode 100644 index 0000000..fa5d099 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/ClientScene.cs @@ -0,0 +1,760 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using UnityEngine; +using Guid = System.Guid; +using Object = UnityEngine.Object; + +namespace Mirror +{ + ///

+ /// A client manager which contains static client information and functions. + /// This manager contains references to tracked static local objects such as spawner registrations. It also has the default message handlers used by clients when they registered none themselves. The manager handles adding/removing player objects to the game after a client connection has been set as ready. + /// The ClientScene is a singleton, and it has static convenience methods such as ClientScene.Ready(). + /// The ClientScene is used by the NetworkManager, but it can be used by itself. + /// As the ClientScene manages player objects on the client, it is where clients request to add players. The NetworkManager does this via the ClientScene automatically when auto-add-players is set, but it can be done through code using the function ClientScene.AddPlayer(). This sends an AddPlayer message to the server and will cause a player object to be created for this client. + /// Like NetworkServer, the ClientScene understands the concept of the local client. The function ClientScene.ConnectLocalServer() is used to become a host by starting a local client (when a server is already running). + /// + public static class ClientScene + { + static bool isSpawnFinished; + + /// + /// NetworkIdentity of the localPlayer + /// + public static NetworkIdentity localPlayer { get; private set; } + + /// + /// Returns true when a client's connection has been set to ready. + /// A client that is ready recieves state updates from the server, while a client that is not ready does not. This useful when the state of the game is not normal, such as a scene change or end-of-game. + /// This is read-only. To change the ready state of a client, use ClientScene.Ready(). The server is able to set the ready state of clients using NetworkServer.SetClientReady(), NetworkServer.SetClientNotReady() and NetworkServer.SetAllClientsNotReady(). + /// This is done when changing scenes so that clients don't receive state update messages during scene loading. + /// + public static bool ready { get; internal set; } + + /// + /// The NetworkConnection object that is currently "ready". This is the connection to the server where objects are spawned from. + /// This connection can be used to send messages to the server. There can only be one ready connection at a time. There can be multiple NetworkClient instances in existence, each with their own NetworkConnections, but there is only one ClientScene instance and corresponding ready connection. + /// + public static NetworkConnection readyConnection { get; private set; } + + /// + /// This is a dictionary of the prefabs that are registered on the client with ClientScene.RegisterPrefab(). + /// The key to the dictionary is the prefab asset Id. + /// + public static Dictionary prefabs = new Dictionary(); + + /// + /// This is dictionary of the disabled NetworkIdentity objects in the scene that could be spawned by messages from the server. + /// The key to the dictionary is the NetworkIdentity sceneId. + /// + public static Dictionary spawnableObjects; + + // spawn handlers + static readonly Dictionary spawnHandlers = new Dictionary(); + static readonly Dictionary unspawnHandlers = new Dictionary(); + + // this is never called, and if we do call it in NetworkClient.Shutdown + // then the client's player object won't be removed after disconnecting! + internal static void Shutdown() + { + ClearSpawners(); + spawnableObjects = null; + readyConnection = null; + ready = false; + isSpawnFinished = false; + DestroyAllClientObjects(); + } + + // this is called from message handler for Owner message + internal static void InternalAddPlayer(NetworkIdentity identity) + { + if (LogFilter.Debug) Debug.LogWarning("ClientScene.InternalAddPlayer"); + + // NOTE: It can be "normal" when changing scenes for the player to be destroyed and recreated. + // But, the player structures are not cleaned up, we'll just replace the old player + localPlayer = identity; + if (readyConnection != null) + { + readyConnection.playerController = identity; + } + else + { + Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer"); + } + } + + /// + /// This adds a player GameObject for this client. + /// This causes an AddPlayer message to be sent to the server, and NetworkManager.OnServerAddPlayer is called. + /// + /// True if player was added. + public static bool AddPlayer() => AddPlayer(null); + + /// + /// This adds a player GameObject for this client. This causes an AddPlayer message to be sent to the server, and NetworkManager.OnServerAddPlayer is called. If an extra message was passed to AddPlayer, then OnServerAddPlayer will be called with a NetworkReader that contains the contents of the message. + /// + /// The connection to become ready for this client. + /// True if player was added. + public static bool AddPlayer(NetworkConnection readyConn) => AddPlayer(readyConn, null); + + /// + /// This adds a player GameObject for this client. This causes an AddPlayer message to be sent to the server, and NetworkManager.OnServerAddPlayer is called. If an extra message was passed to AddPlayer, then OnServerAddPlayer will be called with a NetworkReader that contains the contents of the message. + /// extraMessage can contain character selection, etc. + /// + /// The connection to become ready for this client. + /// An extra message object that can be passed to the server for this player. + /// True if player was added. + public static bool AddPlayer(NetworkConnection readyConn, byte[] extraData) + { + // ensure valid ready connection + if (readyConn != null) + { + ready = true; + readyConnection = readyConn; + } + + if (!ready) + { + Debug.LogError("Must call AddPlayer() with a connection the first time to become ready."); + return false; + } + + if (readyConnection.playerController != null) + { + Debug.LogError("ClientScene.AddPlayer: a PlayerController was already added. Did you call AddPlayer twice?"); + return false; + } + + if (LogFilter.Debug) Debug.Log("ClientScene.AddPlayer() called with connection [" + readyConnection + "]"); + + AddPlayerMessage message = new AddPlayerMessage() + { + value = extraData + }; + readyConnection.Send(message); + return true; + } + + /// + /// Removes the player from the game. + /// + /// True if succcessful + public static bool RemovePlayer() + { + if (LogFilter.Debug) Debug.Log("ClientScene.RemovePlayer() called with connection [" + readyConnection + "]"); + + if (readyConnection.playerController != null) + { + readyConnection.Send(new RemovePlayerMessage()); + + Object.Destroy(readyConnection.playerController.gameObject); + + readyConnection.playerController = null; + localPlayer = null; + + return true; + } + return false; + } + + /// + /// Signal that the client connection is ready to enter the game. + /// This could be for example when a client enters an ongoing game and has finished loading the current scene. The server should respond to the SYSTEM_READY event with an appropriate handler which instantiates the players object for example. + /// + /// The client connection which is ready. + /// True if succcessful + public static bool Ready(NetworkConnection conn) + { + if (ready) + { + Debug.LogError("A connection has already been set as ready. There can only be one."); + return false; + } + + if (LogFilter.Debug) Debug.Log("ClientScene.Ready() called with connection [" + conn + "]"); + + if (conn != null) + { + conn.Send(new ReadyMessage()); + ready = true; + readyConnection = conn; + readyConnection.isReady = true; + return true; + } + Debug.LogError("Ready() called with invalid connection object: conn=null"); + return false; + } + + internal static void HandleClientDisconnect(NetworkConnection conn) + { + if (readyConnection == conn && ready) + { + ready = false; + readyConnection = null; + } + } + + 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() + { + // add all unspawned NetworkIdentities to spawnable objects + spawnableObjects = Resources.FindObjectsOfTypeAll() + .Where(ConsiderForSpawning) + .ToDictionary(identity => identity.sceneId, identity => identity); + } + + static NetworkIdentity SpawnSceneObject(ulong sceneId) + { + if (spawnableObjects.TryGetValue(sceneId, out NetworkIdentity identity)) + { + spawnableObjects.Remove(sceneId); + return identity; + } + Debug.LogWarning("Could not find scene object with sceneid:" + sceneId.ToString("X")); + return null; + } + + // spawn handlers and prefabs + static bool GetPrefab(Guid assetId, out GameObject prefab) + { + prefab = null; + return assetId != Guid.Empty && + prefabs.TryGetValue(assetId, out prefab) && prefab != null; + } + + /// + /// Registers a prefab with the spawning system. + /// When a NetworkIdentity object is spawned on a server with NetworkServer.SpawnObject(), and the prefab that the object was created from was registered with RegisterPrefab(), the client will use that prefab to instantiate a corresponding client object with the same netId. + /// The NetworkManager has a list of spawnable prefabs, it uses this function to register those prefabs with the ClientScene. + /// The set of current spawnable object is available in the ClientScene static member variable ClientScene.prefabs, which is a dictionary of NetworkAssetIds and prefab references. + /// + /// A Prefab that will be spawned. + /// An assetId to be assigned to this prefab. This allows a dynamically created game object to be registered for an already known asset Id. + public static void RegisterPrefab(GameObject prefab, Guid newAssetId) + { + NetworkIdentity identity = prefab.GetComponent(); + if (identity) + { + identity.assetId = newAssetId; + + if (LogFilter.Debug) Debug.Log("Registering prefab '" + prefab.name + "' as asset:" + identity.assetId); + prefabs[identity.assetId] = prefab; + } + else + { + Debug.LogError("Could not register '" + prefab.name + "' since it contains no NetworkIdentity component"); + } + } + + /// + /// Registers a prefab with the spawning system. + /// When a NetworkIdentity object is spawned on a server with NetworkServer.SpawnObject(), and the prefab that the object was created from was registered with RegisterPrefab(), the client will use that prefab to instantiate a corresponding client object with the same netId. + /// The NetworkManager has a list of spawnable prefabs, it uses this function to register those prefabs with the ClientScene. + /// The set of current spawnable object is available in the ClientScene static member variable ClientScene.prefabs, which is a dictionary of NetworkAssetIds and prefab references. + /// + /// A Prefab that will be spawned. + public static void RegisterPrefab(GameObject prefab) + { + NetworkIdentity identity = prefab.GetComponent(); + if (identity) + { + if (LogFilter.Debug) Debug.Log("Registering prefab '" + prefab.name + "' as asset:" + identity.assetId); + prefabs[identity.assetId] = prefab; + + NetworkIdentity[] identities = prefab.GetComponentsInChildren(); + if (identities.Length > 1) + { + Debug.LogWarning("The prefab '" + prefab.name + + "' has multiple NetworkIdentity components. There can only be one NetworkIdentity on a prefab, and it must be on the root object."); + } + } + else + { + Debug.LogError("Could not register '" + prefab.name + "' since it contains no NetworkIdentity component"); + } + } + + /// + /// Registers a prefab with the spawning system. + /// When a NetworkIdentity object is spawned on a server with NetworkServer.SpawnObject(), and the prefab that the object was created from was registered with RegisterPrefab(), the client will use that prefab to instantiate a corresponding client object with the same netId. + /// The NetworkManager has a list of spawnable prefabs, it uses this function to register those prefabs with the ClientScene. + /// The set of current spawnable object is available in the ClientScene static member variable ClientScene.prefabs, which is a dictionary of NetworkAssetIds and prefab references. + /// + /// A Prefab that will be spawned. + /// A method to use as a custom spawnhandler on clients. + /// A method to use as a custom un-spawnhandler on clients. + public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError("Could not register '" + prefab.name + "' since it contains no NetworkIdentity component"); + return; + } + + if (spawnHandler == null || unspawnHandler == null) + { + Debug.LogError("RegisterPrefab custom spawn function null for " + identity.assetId); + return; + } + + if (identity.assetId == Guid.Empty) + { + Debug.LogError("RegisterPrefab game object " + prefab.name + " has no prefab. Use RegisterSpawnHandler() instead?"); + return; + } + + if (LogFilter.Debug) Debug.Log("Registering custom prefab '" + prefab.name + "' as asset:" + identity.assetId + " " + spawnHandler.GetMethodName() + "/" + unspawnHandler.GetMethodName()); + + spawnHandlers[identity.assetId] = spawnHandler; + unspawnHandlers[identity.assetId] = unspawnHandler; + } + + /// + /// Removes a registered spawn prefab that was setup with ClientScene.RegisterPrefab. + /// + /// The prefab to be removed from registration. + public static void UnregisterPrefab(GameObject prefab) + { + NetworkIdentity identity = prefab.GetComponent(); + if (identity == null) + { + Debug.LogError("Could not unregister '" + prefab.name + "' since it contains no NetworkIdentity component"); + return; + } + spawnHandlers.Remove(identity.assetId); + unspawnHandlers.Remove(identity.assetId); + } + + /// + /// This is an advanced spawning function that registers a custom assetId with the UNET spawning system. + /// This can be used to register custom spawning methods for an assetId - instead of the usual method of registering spawning methods for a 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. + /// + /// Custom assetId string. + /// A method to use as a custom spawnhandler on clients. + /// A method to use as a custom un-spawnhandler on clients. + public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) + { + if (spawnHandler == null || unspawnHandler == null) + { + Debug.LogError("RegisterSpawnHandler custom spawn function null for " + assetId); + return; + } + + if (LogFilter.Debug) Debug.Log("RegisterSpawnHandler asset '" + assetId + "' " + spawnHandler.GetMethodName() + "/" + unspawnHandler.GetMethodName()); + + spawnHandlers[assetId] = spawnHandler; + unspawnHandlers[assetId] = unspawnHandler; + } + + /// + /// Removes a registered spawn handler function that was registered with ClientScene.RegisterHandler(). + /// + /// The assetId for the handler to be removed for. + public static void UnregisterSpawnHandler(Guid assetId) + { + spawnHandlers.Remove(assetId); + unspawnHandlers.Remove(assetId); + } + + /// + /// This clears the registered spawn prefabs and spawn handler functions for this client. + /// + public static void ClearSpawners() + { + prefabs.Clear(); + spawnHandlers.Clear(); + unspawnHandlers.Clear(); + } + + static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj) + { + if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null) + { + handler(obj); + return true; + } + return false; + } + + /// + /// Destroys all networked objects on the client. + /// This can be used to clean up when a network connection is closed. + /// + public static void DestroyAllClientObjects() + { + foreach (NetworkIdentity identity in NetworkIdentity.spawned.Values) + { + if (identity != null && identity.gameObject != null) + { + if (!InvokeUnSpawnHandler(identity.assetId, identity.gameObject)) + { + if (identity.sceneId == 0) + { + Object.Destroy(identity.gameObject); + } + else + { + identity.MarkForReset(); + identity.gameObject.SetActive(false); + } + } + } + } + NetworkIdentity.spawned.Clear(); + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkIdentity.spawned[netId] instead.")] + public static GameObject FindLocalObject(uint netId) + { + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity)) + { + return identity.gameObject; + } + return null; + } + + static void ApplySpawnPayload(NetworkIdentity identity, Vector3 position, Quaternion rotation, Vector3 scale, ArraySegment payload, uint netId) + { + if (!identity.gameObject.activeSelf) + { + identity.gameObject.SetActive(true); + } + + // apply local values for VR support + identity.transform.localPosition = position; + identity.transform.localRotation = rotation; + identity.transform.localScale = scale; + + // deserialize components if any payload + // (Count is 0 if there were no components) + if (payload.Count > 0) + { + NetworkReader payloadReader = new NetworkReader(payload); + identity.OnUpdateVars(payloadReader, true); + } + + identity.netId = netId; + NetworkIdentity.spawned[netId] = identity; + + // objects spawned as part of initial state are started on a second pass + if (isSpawnFinished) + { + identity.OnStartClient(); + CheckForOwner(identity); + } + } + + internal static void OnSpawnPrefab(NetworkConnection _, SpawnPrefabMessage msg) + { + if (msg.assetId == Guid.Empty) + { + Debug.LogError("OnObjSpawn netId: " + msg.netId + " has invalid asset Id"); + return; + } + if (LogFilter.Debug) Debug.Log("Client spawn handler instantiating [netId:" + msg.netId + " asset ID:" + msg.assetId + " pos:" + msg.position + "]"); + + // owner? + if (msg.owner) + { + OnSpawnMessageForOwner(msg.netId); + } + + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity localObject) && localObject != null) + { + // this object already exists (was in the scene), just apply the update to existing object + localObject.Reset(); + ApplySpawnPayload(localObject, msg.position, msg.rotation, msg.scale, msg.payload, msg.netId); + return; + } + + if (GetPrefab(msg.assetId, out GameObject prefab)) + { + GameObject obj = Object.Instantiate(prefab, msg.position, msg.rotation); + if (LogFilter.Debug) + { + Debug.Log("Client spawn handler instantiating [netId:" + msg.netId + " asset ID:" + msg.assetId + " pos:" + msg.position + " rotation: " + msg.rotation + "]"); + } + + localObject = obj.GetComponent(); + if (localObject == null) + { + Debug.LogError("Client object spawned for " + msg.assetId + " does not have a NetworkIdentity"); + return; + } + localObject.Reset(); + localObject.pendingOwner = msg.owner; + ApplySpawnPayload(localObject, msg.position, msg.rotation, msg.scale, msg.payload, msg.netId); + } + // lookup registered factory for type: + else if (spawnHandlers.TryGetValue(msg.assetId, out SpawnDelegate handler)) + { + GameObject obj = handler(msg.position, msg.assetId); + if (obj == null) + { + Debug.LogWarning("Client spawn handler for " + msg.assetId + " returned null"); + return; + } + localObject = obj.GetComponent(); + if (localObject == null) + { + Debug.LogError("Client object spawned for " + msg.assetId + " does not have a network identity"); + return; + } + localObject.Reset(); + localObject.pendingOwner = msg.owner; + localObject.assetId = msg.assetId; + ApplySpawnPayload(localObject, msg.position, msg.rotation, msg.scale, msg.payload, msg.netId); + } + else + { + Debug.LogError("Failed to spawn server object, did you forget to add it to the NetworkManager? assetId=" + msg.assetId + " netId=" + msg.netId); + } + } + + internal static void OnSpawnSceneObject(NetworkConnection _, SpawnSceneObjectMessage msg) + { + if (LogFilter.Debug) Debug.Log("Client spawn scene handler instantiating [netId:" + msg.netId + " sceneId:" + msg.sceneId + " pos:" + msg.position); + + // owner? + if (msg.owner) + { + OnSpawnMessageForOwner(msg.netId); + } + + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity localObject) && localObject != null) + { + // this object already exists (was in the scene) + localObject.Reset(); + ApplySpawnPayload(localObject, msg.position, msg.rotation, msg.scale, msg.payload, msg.netId); + return; + } + + NetworkIdentity spawnedId = SpawnSceneObject(msg.sceneId); + if (spawnedId == null) + { + Debug.LogError("Spawn scene object not found for " + msg.sceneId.ToString("X") + " SpawnableObjects.Count=" + spawnableObjects.Count); + + // dump the whole spawnable objects dict for easier debugging + if (LogFilter.Debug) + { + foreach (KeyValuePair kvp in spawnableObjects) + Debug.Log("Spawnable: SceneId=" + kvp.Key + " name=" + kvp.Value.name); + } + + return; + } + + if (LogFilter.Debug) Debug.Log("Client spawn for [netId:" + msg.netId + "] [sceneId:" + msg.sceneId + "] obj:" + spawnedId.gameObject.name); + spawnedId.Reset(); + spawnedId.pendingOwner = msg.owner; + ApplySpawnPayload(spawnedId, msg.position, msg.rotation, msg.scale, msg.payload, msg.netId); + } + + internal static void OnObjectSpawnStarted(NetworkConnection _, ObjectSpawnStartedMessage msg) + { + if (LogFilter.Debug) Debug.Log("SpawnStarted"); + + PrepareToSpawnSceneObjects(); + isSpawnFinished = false; + } + + internal static void OnObjectSpawnFinished(NetworkConnection _, ObjectSpawnFinishedMessage msg) + { + if (LogFilter.Debug) Debug.Log("SpawnFinished"); + + // 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 NetworkIdentity.spawned.Values.OrderBy(uv => uv.netId)) + { + if (!identity.isClient) + { + identity.OnStartClient(); + CheckForOwner(identity); + } + } + isSpawnFinished = true; + } + + internal static void OnObjectHide(NetworkConnection _, ObjectHideMessage msg) + { + DestroyObject(msg.netId); + } + + internal static void OnObjectDestroy(NetworkConnection _, ObjectDestroyMessage msg) + { + DestroyObject(msg.netId); + } + + static void DestroyObject(uint netId) + { + if (LogFilter.Debug) Debug.Log("ClientScene.OnObjDestroy netId:" + netId); + + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null) + { + localObject.OnNetworkDestroy(); + + if (!InvokeUnSpawnHandler(localObject.assetId, localObject.gameObject)) + { + // default handling + if (localObject.sceneId == 0) + { + Object.Destroy(localObject.gameObject); + } + else + { + // scene object.. disable it in scene instead of destroying + localObject.gameObject.SetActive(false); + spawnableObjects[localObject.sceneId] = localObject; + } + } + NetworkIdentity.spawned.Remove(netId); + localObject.MarkForReset(); + } + else + { + if (LogFilter.Debug) Debug.LogWarning("Did not find target for destroy message for " + netId); + } + } + + internal static void OnLocalClientObjectDestroy(NetworkConnection _, ObjectDestroyMessage msg) + { + if (LogFilter.Debug) Debug.Log("ClientScene.OnLocalObjectObjDestroy netId:" + msg.netId); + + NetworkIdentity.spawned.Remove(msg.netId); + } + + internal static void OnLocalClientObjectHide(NetworkConnection _, ObjectHideMessage msg) + { + if (LogFilter.Debug) Debug.Log("ClientScene::OnLocalObjectObjHide netId:" + msg.netId); + + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity localObject) && localObject != null) + { + localObject.OnSetLocalVisibility(false); + } + } + + internal static void OnLocalClientSpawnPrefab(NetworkConnection _, SpawnPrefabMessage msg) + { + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity localObject) && localObject != null) + { + localObject.OnSetLocalVisibility(true); + } + } + + internal static void OnLocalClientSpawnSceneObject(NetworkConnection _, SpawnSceneObjectMessage msg) + { + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity localObject) && localObject != null) + { + localObject.OnSetLocalVisibility(true); + } + } + + internal static void OnUpdateVarsMessage(NetworkConnection _, UpdateVarsMessage msg) + { + if (LogFilter.Debug) Debug.Log("ClientScene.OnUpdateVarsMessage " + msg.netId); + + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity localObject) && localObject != null) + { + localObject.OnUpdateVars(new NetworkReader(msg.payload), false); + } + else + { + Debug.LogWarning("Did not find target for sync message for " + msg.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."); + } + } + + internal static void OnRPCMessage(NetworkConnection _, RpcMessage msg) + { + if (LogFilter.Debug) Debug.Log("ClientScene.OnRPCMessage hash:" + msg.functionHash + " netId:" + msg.netId); + + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + identity.HandleRPC(msg.componentIndex, msg.functionHash, new NetworkReader(msg.payload)); + } + } + + internal static void OnSyncEventMessage(NetworkConnection _, SyncEventMessage msg) + { + if (LogFilter.Debug) Debug.Log("ClientScene.OnSyncEventMessage " + msg.netId); + + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + identity.HandleSyncEvent(msg.componentIndex, msg.functionHash, new NetworkReader(msg.payload)); + } + else + { + Debug.LogWarning("Did not find target for SyncEvent message for " + msg.netId); + } + } + + internal static void OnClientAuthority(NetworkConnection _, ClientAuthorityMessage msg) + { + if (LogFilter.Debug) Debug.Log("ClientScene.OnClientAuthority for netId: " + msg.netId); + + if (NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + identity.HandleClientAuthority(msg.authority); + } + } + + // called for the one object in the spawn message which is the owner! + internal static void OnSpawnMessageForOwner(uint netId) + { + if (LogFilter.Debug) Debug.Log("ClientScene.OnOwnerMessage - connectionId=" + readyConnection.connectionId + " netId: " + netId); + + // is there already an owner that is a different object?? + if (readyConnection.playerController != null) + { + readyConnection.playerController.SetNotLocalPlayer(); + } + + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null) + { + // this object already exists + localObject.connectionToServer = readyConnection; + localObject.SetLocalPlayer(); + InternalAddPlayer(localObject); + } + } + + static void CheckForOwner(NetworkIdentity identity) + { + if (identity.pendingOwner) + { + // found owner, turn into a local player + + // Set isLocalPlayer to true on this NetworkIdentity and trigger OnStartLocalPlayer in all scripts on the same GO + identity.connectionToServer = readyConnection; + identity.SetLocalPlayer(); + + if (LogFilter.Debug) Debug.Log("ClientScene.OnOwnerMessage - player=" + identity.name); + if (readyConnection.connectionId < 0) + { + Debug.LogError("Owner message received on a local client."); + return; + } + InternalAddPlayer(identity); + + identity.pendingOwner = false; + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/ClientScene.cs.meta b/Assets/Packages/Mirror/Runtime/ClientScene.cs.meta new file mode 100644 index 0000000..c4f3a09 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/ClientScene.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96fc7967f813e4960b9119d7c2118494 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/CustomAttributes.cs b/Assets/Packages/Mirror/Runtime/CustomAttributes.cs new file mode 100644 index 0000000..40bdd7d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/CustomAttributes.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel; +using UnityEngine; + +namespace Mirror +{ + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkBehaviour.syncInterval field instead. Can be modified in the Inspector too.")] + [AttributeUsage(AttributeTargets.Class)] + public class NetworkSettingsAttribute : Attribute + { + public float sendInterval = 0.1f; + } + + [AttributeUsage(AttributeTargets.Field)] + public class SyncVarAttribute : Attribute + { + public string hook; + } + + [AttributeUsage(AttributeTargets.Method)] + public class CommandAttribute : Attribute + { + public int channel = Channels.DefaultReliable; // this is zero + } + + [AttributeUsage(AttributeTargets.Method)] + public class ClientRpcAttribute : Attribute + { + public int channel = Channels.DefaultReliable; // this is zero + } + + [AttributeUsage(AttributeTargets.Method)] + public class TargetRpcAttribute : Attribute + { + public int channel = Channels.DefaultReliable; // this is zero + } + + [AttributeUsage(AttributeTargets.Event)] + public class SyncEventAttribute : Attribute + { + public int channel = Channels.DefaultReliable; // this is zero + } + + [AttributeUsage(AttributeTargets.Method)] + public class ServerAttribute : Attribute {} + + [AttributeUsage(AttributeTargets.Method)] + public class ServerCallbackAttribute : Attribute {} + + [AttributeUsage(AttributeTargets.Method)] + public class ClientAttribute : Attribute {} + + [AttributeUsage(AttributeTargets.Method)] + public class ClientCallbackAttribute : Attribute {} + + // For Scene property Drawer + public class SceneAttribute : PropertyAttribute {} +} diff --git a/Assets/Packages/Mirror/Runtime/CustomAttributes.cs.meta b/Assets/Packages/Mirror/Runtime/CustomAttributes.cs.meta new file mode 100644 index 0000000..22a1db2 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/CustomAttributes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c04c722ee2ffd49c8a56ab33667b10b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs b/Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs new file mode 100644 index 0000000..fe57e17 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs @@ -0,0 +1,16 @@ +using System; + +namespace Mirror +{ + internal static class DotNetCompatibility + { + internal static string GetMethodName(this Delegate func) + { +#if NETFX_CORE + return func.GetMethodInfo().Name; +#else + return func.Method.Name; +#endif + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs.meta b/Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs.meta new file mode 100644 index 0000000..8742197 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/DotNetCompatibility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b307f850ccbbe450295acf24d70e5c28 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs b/Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs new file mode 100644 index 0000000..f6ec793 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs @@ -0,0 +1,38 @@ +namespace Mirror +{ + // implementation of N-day EMA + // it calculates an exponential moving average roughy 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 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; + } + } + + public double Value { get; private set; } + + public double Var { get; private set; } + } +} diff --git a/Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs.meta b/Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs.meta new file mode 100644 index 0000000..5ce3055 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/ExponentialMovingAverage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05e858cbaa54b4ce4a48c8c7f50c1914 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/FloatBytePacker.cs b/Assets/Packages/Mirror/Runtime/FloatBytePacker.cs new file mode 100644 index 0000000..2293d21 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/FloatBytePacker.cs @@ -0,0 +1,60 @@ +using UnityEngine; + +namespace Mirror +{ + public static class FloatBytePacker + { + // ScaleFloatToByte( -1f, -1f, 1f, byte.MinValue, byte.MaxValue) => 0 + // ScaleFloatToByte( 0f, -1f, 1f, byte.MinValue, byte.MaxValue) => 127 + // ScaleFloatToByte(0.5f, -1f, 1f, byte.MinValue, byte.MaxValue) => 191 + // ScaleFloatToByte( 1f, -1f, 1f, byte.MinValue, byte.MaxValue) => 255 + public static byte ScaleFloatToByte(float value, float minValue, float maxValue, byte minTarget, byte maxTarget) + { + // note: C# byte - byte => int, hence so many casts + int targetRange = maxTarget - minTarget; // max byte - min byte only fits into something bigger + float valueRange = maxValue - minValue; + float valueRelative = value - minValue; + return (byte)(minTarget + (byte)(valueRelative/valueRange * targetRange)); + } + + // ScaleByteToFloat( 0, byte.MinValue, byte.MaxValue, -1, 1) => -1 + // ScaleByteToFloat(127, byte.MinValue, byte.MaxValue, -1, 1) => -0.003921569 + // ScaleByteToFloat(191, byte.MinValue, byte.MaxValue, -1, 1) => 0.4980392 + // ScaleByteToFloat(255, byte.MinValue, byte.MaxValue, -1, 1) => 1 + public static float ScaleByteToFloat(byte value, byte minValue, byte maxValue, float minTarget, float maxTarget) + { + // note: C# byte - byte => int, hence so many casts + float targetRange = maxTarget - minTarget; + byte valueRange = (byte)(maxValue - minValue); + byte valueRelative = (byte)(value - minValue); + return minTarget + (valueRelative / (float)valueRange * targetRange); + } + + // eulerAngles have 3 floats, putting them into 2 bytes of [x,y],[z,0] + // would be a waste. instead we compress into 5 bits each => 15 bits. + // so a ushort. + public static ushort PackThreeFloatsIntoUShort(float u, float v, float w, float minValue, float maxValue) + { + // 5 bits max value = 1+2+4+8+16 = 31 = 0x1F + byte lower = ScaleFloatToByte(u, minValue, maxValue, 0x00, 0x1F); + byte middle = ScaleFloatToByte(v, minValue, maxValue, 0x00, 0x1F); + byte upper = ScaleFloatToByte(w, minValue, maxValue, 0x00, 0x1F); + ushort combined = (ushort)(upper << 10 | middle << 5 | lower); + return combined; + } + + // see PackThreeFloatsIntoUShort for explanation + public static Vector3 UnpackUShortIntoThreeFloats(ushort combined, float minTarget, float maxTarget) + { + byte lower = (byte)(combined & 0x1F); + byte middle = (byte)((combined >> 5) & 0x1F); + byte upper = (byte)(combined >> 10); // nothing on the left, no & needed + + // note: we have to use 4 bits per float, so between 0x00 and 0x0F + float u = ScaleByteToFloat(lower, 0x00, 0x1F, minTarget, maxTarget); + float v = ScaleByteToFloat(middle, 0x00, 0x1F, minTarget, maxTarget); + float w = ScaleByteToFloat(upper, 0x00, 0x1F, minTarget, maxTarget); + return new Vector3(u, v, w); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/FloatBytePacker.cs.meta b/Assets/Packages/Mirror/Runtime/FloatBytePacker.cs.meta new file mode 100644 index 0000000..92145fe --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/FloatBytePacker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: afd3cca6a786d4208b1d0f7f2b168901 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/LocalClient.cs b/Assets/Packages/Mirror/Runtime/LocalClient.cs new file mode 100644 index 0000000..20db50b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/LocalClient.cs @@ -0,0 +1,5 @@ +// This file was removed in Mirror 3.4.9 +// The purpose of this file is to get the old file overwritten +// when users update from the asset store to prevent a flood of errors +// from having the old file still in the project as a straggler. +// This file will be dropped from the Asset Store package in May 2019 diff --git a/Assets/Packages/Mirror/Runtime/LocalClient.cs.meta b/Assets/Packages/Mirror/Runtime/LocalClient.cs.meta new file mode 100644 index 0000000..6c073e6 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/LocalClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c4d04450e91c438385de7300abef1b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/LocalConnections.cs b/Assets/Packages/Mirror/Runtime/LocalConnections.cs new file mode 100644 index 0000000..33962d4 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/LocalConnections.cs @@ -0,0 +1,47 @@ +using System; +using UnityEngine; + +namespace Mirror +{ + // a server's connection TO a LocalClient. + // sending messages on this connection causes the client's handler function to be invoked directly + class ULocalConnectionToClient : NetworkConnection + { + public ULocalConnectionToClient() : base ("localClient") + { + // local player always has connectionId == 0 + connectionId = 0; + } + + internal override bool SendBytes(byte[] bytes, int channelId = Channels.DefaultReliable) + { + NetworkClient.localClientPacketQueue.Enqueue(bytes); + return true; + } + } + + // a localClient's connection TO a server. + // send messages on this connection causes the server's handler function to be invoked directly. + internal class ULocalConnectionToServer : NetworkConnection + { + public ULocalConnectionToServer() : base("localServer") + { + // local player always has connectionId == 0 + connectionId = 0; + } + + internal override bool SendBytes(byte[] bytes, int channelId = Channels.DefaultReliable) + { + if (bytes.Length == 0) + { + Debug.LogError("LocalConnection.SendBytes cannot send zero bytes"); + return false; + } + + // handle the server's message directly + // TODO any way to do this without NetworkServer.localConnection? + NetworkServer.localConnection.TransportReceive(new ArraySegment(bytes)); + return true; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/LocalConnections.cs.meta b/Assets/Packages/Mirror/Runtime/LocalConnections.cs.meta new file mode 100644 index 0000000..2a332c4 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/LocalConnections.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a88758df7db2043d6a9d926e0b6d4191 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/LogFilter.cs b/Assets/Packages/Mirror/Runtime/LogFilter.cs new file mode 100644 index 0000000..3b225f7 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/LogFilter.cs @@ -0,0 +1,7 @@ +namespace Mirror +{ + public static class LogFilter + { + public static bool Debug = false; + } +} diff --git a/Assets/Packages/Mirror/Runtime/LogFilter.cs.meta b/Assets/Packages/Mirror/Runtime/LogFilter.cs.meta new file mode 100644 index 0000000..41cab50 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/LogFilter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6928b080072948f7b2909b4025fcc79 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/MessagePacker.cs b/Assets/Packages/Mirror/Runtime/MessagePacker.cs new file mode 100644 index 0000000..e134465 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/MessagePacker.cs @@ -0,0 +1,136 @@ +using System; +using System.ComponentModel; +using UnityEngine; + +namespace Mirror +{ + // message packing all in one place, instead of constructing headers in all + // kinds of different places + // + // MsgType (1-n bytes) + // Content (ContentSize bytes) + // + // -> we use varint for headers because most messages will result in 1 byte + // type/size headers then instead of always + // using 2 bytes for shorts. + // -> this reduces bandwidth by 10% if average message size is 20 bytes + // (probably even shorter) + public static class MessagePacker + { + public static int GetId() where T : IMessageBase + { + // paul: 16 bits is enough to avoid collisions + // - keeps the message size small because it gets varinted + // - in case of collisions, Mirror will display an error + return typeof(T).FullName.GetStableHashCode() & 0xFFFF; + } + + // pack message before sending + // -> pass writer instead of byte[] so we can reuse it + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use Pack instead")] + public static byte[] PackMessage(int msgType, MessageBase msg) + { + NetworkWriter writer = NetworkWriterPool.GetWriter(); + try + { + // write message type + writer.WriteInt16((short)msgType); + + // serialize message into writer + msg.Serialize(writer); + + // return byte[] + return writer.ToArray(); + } + finally + { + NetworkWriterPool.Recycle(writer); + } + } + + // pack message before sending + public static byte[] Pack(T message) where T : IMessageBase + { + NetworkWriter writer = NetworkWriterPool.GetWriter(); + try + { + // write message type + int msgType = GetId(); + writer.WriteUInt16((ushort)msgType); + + // serialize message into writer + message.Serialize(writer); + + // return byte[] + return writer.ToArray(); + } + finally + { + NetworkWriterPool.Recycle(writer); + } + } + + // unpack a message we received + public static T Unpack(byte[] data) where T : IMessageBase, new() + { + NetworkReader reader = new NetworkReader(data); + + int msgType = GetId(); + + int id = reader.ReadUInt16(); + if (id != msgType) + throw new FormatException("Invalid message, could not unpack " + typeof(T).FullName); + + T message = new T(); + message.Deserialize(reader); + return 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! + public static bool UnpackMessage(NetworkReader messageReader, out int msgType) + { + // read message type (varint) + try + { + msgType = messageReader.ReadUInt16(); + return true; + } + catch (System.IO.EndOfStreamException) + { + msgType = 0; + return false; + } + } + + internal static NetworkMessageDelegate MessageHandler(Action handler) where T : IMessageBase, new() => networkMessage => + { + // 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; + try + { + message = networkMessage.ReadMessage(); + } + catch (Exception exception) + { + Debug.LogError("Closed connection: " + networkMessage.conn.connectionId + ". This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: " + exception); + networkMessage.conn.Disconnect(); + return; + } + handler(networkMessage.conn, message); + }; + } +} diff --git a/Assets/Packages/Mirror/Runtime/MessagePacker.cs.meta b/Assets/Packages/Mirror/Runtime/MessagePacker.cs.meta new file mode 100644 index 0000000..7ca61aa --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/MessagePacker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2db134099f0df4d96a84ae7a0cd9b4bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Messages.cs b/Assets/Packages/Mirror/Runtime/Messages.cs new file mode 100644 index 0000000..99c31ad --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Messages.cs @@ -0,0 +1,492 @@ +using System; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Mirror +{ + public interface IMessageBase + { + void Deserialize(NetworkReader reader); + + void Serialize(NetworkWriter writer); + } + + public abstract class MessageBase : IMessageBase + { + // De-serialize the contents of the reader into this message + public virtual void Deserialize(NetworkReader reader) {} + + // Serialize the contents of this message into the writer + public virtual void Serialize(NetworkWriter writer) {} + } + + #region General Typed Messages + public class StringMessage : MessageBase + { + public string value; + + public StringMessage() {} + + public StringMessage(string v) + { + value = v; + } + + public override void Deserialize(NetworkReader reader) + { + value = reader.ReadString(); + } + + public override void Serialize(NetworkWriter writer) + { + writer.WriteString(value); + } + } + + public class ByteMessage : MessageBase + { + public byte value; + + public ByteMessage() {} + + public ByteMessage(byte v) + { + value = v; + } + + public override void Deserialize(NetworkReader reader) + { + value = reader.ReadByte(); + } + + public override void Serialize(NetworkWriter writer) + { + writer.WriteByte(value); + } + } + + public class BytesMessage : MessageBase + { + public byte[] value; + + public BytesMessage() {} + + public BytesMessage(byte[] v) + { + value = v; + } + + public override void Deserialize(NetworkReader reader) + { + value = reader.ReadBytesAndSize(); + } + + public override void Serialize(NetworkWriter writer) + { + writer.WriteBytesAndSize(value); + } + } + + public class IntegerMessage : MessageBase + { + public int value; + + public IntegerMessage() {} + + public IntegerMessage(int v) + { + value = v; + } + + public override void Deserialize(NetworkReader reader) + { + value = reader.ReadPackedInt32(); + } + + public override void Serialize(NetworkWriter writer) + { + writer.WritePackedInt32(value); + } + } + + public class DoubleMessage : MessageBase + { + public double value; + + public DoubleMessage() {} + + public DoubleMessage(double v) + { + value = v; + } + + public override void Deserialize(NetworkReader reader) + { + value = reader.ReadDouble(); + } + + public override void Serialize(NetworkWriter writer) + { + writer.WriteDouble(value); + } + } + + public class EmptyMessage : MessageBase + { + public override void Deserialize(NetworkReader reader) {} + + public override void Serialize(NetworkWriter writer) {} + } + #endregion + + #region Public System Messages + public class ErrorMessage : ByteMessage {} + + public struct ReadyMessage : IMessageBase + { + public void Deserialize(NetworkReader reader) { } + + public void Serialize(NetworkWriter writer) { } + } + + public struct NotReadyMessage : IMessageBase + { + public void Deserialize(NetworkReader reader) { } + + public void Serialize(NetworkWriter writer) { } + } + + public struct AddPlayerMessage : IMessageBase + { + public byte[] value; + + public void Deserialize(NetworkReader reader) + { + value = reader.ReadBytesAndSize(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WriteBytesAndSize(value); + } + } + + public struct RemovePlayerMessage : IMessageBase + { + public void Deserialize(NetworkReader reader) { } + + public void Serialize(NetworkWriter writer) { } + } + + public struct DisconnectMessage : IMessageBase + { + public void Deserialize(NetworkReader reader) { } + + public void Serialize(NetworkWriter writer) { } + } + + public struct ConnectMessage : IMessageBase + { + public void Deserialize(NetworkReader reader) { } + + public void Serialize(NetworkWriter writer) { } + } + + public struct SceneMessage : IMessageBase + { + public string sceneName; + public LoadSceneMode sceneMode; // Single = 0, Additive = 1 + public LocalPhysicsMode physicsMode; // None = 0, Physics3D = 1, Physics2D = 2 + + public void Deserialize(NetworkReader reader) + { + sceneName = reader.ReadString(); + sceneMode = (LoadSceneMode)reader.ReadByte(); + physicsMode = (LocalPhysicsMode)reader.ReadByte(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WriteString(sceneName); + writer.WriteByte((byte)sceneMode); + writer.WriteByte((byte)physicsMode); + } + } + #endregion + + #region System Messages requried for code gen path + struct CommandMessage : IMessageBase + { + public uint netId; + public int componentIndex; + public int functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + componentIndex = (int)reader.ReadPackedUInt32(); + functionHash = reader.ReadInt32(); // hash is always 4 full bytes, WritePackedInt would send 1 extra byte here + payload = reader.ReadBytesAndSizeSegment(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + writer.WritePackedUInt32((uint)componentIndex); + writer.WriteInt32(functionHash); + writer.WriteBytesAndSizeSegment(payload); + } + } + + struct RpcMessage : IMessageBase + { + public uint netId; + public int componentIndex; + public int functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + componentIndex = (int)reader.ReadPackedUInt32(); + functionHash = reader.ReadInt32(); // hash is always 4 full bytes, WritePackedInt would send 1 extra byte here + payload = reader.ReadBytesAndSizeSegment(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + writer.WritePackedUInt32((uint)componentIndex); + writer.WriteInt32(functionHash); + writer.WriteBytesAndSizeSegment(payload); + } + } + + struct SyncEventMessage : IMessageBase + { + public uint netId; + public int componentIndex; + public int functionHash; + // the parameters for the Cmd function + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + componentIndex = (int)reader.ReadPackedUInt32(); + functionHash = reader.ReadInt32(); // hash is always 4 full bytes, WritePackedInt would send 1 extra byte here + payload = reader.ReadBytesAndSizeSegment(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + writer.WritePackedUInt32((uint)componentIndex); + writer.WriteInt32(functionHash); + writer.WriteBytesAndSizeSegment(payload); + } + } + #endregion + + #region Internal System Messages + struct SpawnPrefabMessage : IMessageBase + { + public uint netId; + public bool owner; + public Guid assetId; + public Vector3 position; + public Quaternion rotation; + public Vector3 scale; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + owner = reader.ReadBoolean(); + assetId = reader.ReadGuid(); + position = reader.ReadVector3(); + rotation = reader.ReadQuaternion(); + scale = reader.ReadVector3(); + payload = reader.ReadBytesAndSizeSegment(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + writer.WriteBoolean(owner); + writer.WriteGuid(assetId); + writer.WriteVector3(position); + writer.WriteQuaternion(rotation); + writer.WriteVector3(scale); + writer.WriteBytesAndSizeSegment(payload); + } + } + + struct SpawnSceneObjectMessage : IMessageBase + { + public uint netId; + public bool owner; + public ulong sceneId; + public Vector3 position; + public Quaternion rotation; + public Vector3 scale; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + owner = reader.ReadBoolean(); + sceneId = reader.ReadUInt64(); + position = reader.ReadVector3(); + rotation = reader.ReadQuaternion(); + scale = reader.ReadVector3(); + payload = reader.ReadBytesAndSizeSegment(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + writer.WriteBoolean(owner); + writer.WriteUInt64(sceneId); + writer.WriteVector3(position); + writer.WriteQuaternion(rotation); + writer.WriteVector3(scale); + writer.WriteBytesAndSizeSegment(payload); + } + } + + struct ObjectSpawnStartedMessage : IMessageBase + { + public void Deserialize(NetworkReader reader) { } + + public void Serialize(NetworkWriter writer) { } + } + + struct ObjectSpawnFinishedMessage : IMessageBase + { + public void Deserialize(NetworkReader reader) { } + + public void Serialize(NetworkWriter writer) { } + } + + struct ObjectDestroyMessage : IMessageBase + { + public uint netId; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + } + } + + struct ObjectHideMessage : IMessageBase + { + public uint netId; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + } + } + + struct ClientAuthorityMessage : IMessageBase + { + public uint netId; + public bool authority; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + authority = reader.ReadBoolean(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + writer.WriteBoolean(authority); + } + } + + struct UpdateVarsMessage : IMessageBase + { + public uint netId; + // the serialized component data + // -> ArraySegment to avoid unnecessary allocations + public ArraySegment payload; + + public void Deserialize(NetworkReader reader) + { + netId = reader.ReadPackedUInt32(); + payload = reader.ReadBytesAndSizeSegment(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WritePackedUInt32(netId); + writer.WriteBytesAndSizeSegment(payload); + } + } + + // A client sends this message to the server + // to calculate RTT and synchronize time + struct NetworkPingMessage : IMessageBase + { + public double clientTime; + + public NetworkPingMessage(double value) + { + clientTime = value; + } + + public void Deserialize(NetworkReader reader) + { + clientTime = reader.ReadDouble(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WriteDouble(clientTime); + } + } + + // The server responds with this message + // The client can use this to calculate RTT and sync time + struct NetworkPongMessage : IMessageBase + { + public double clientTime; + public double serverTime; + + public void Deserialize(NetworkReader reader) + { + clientTime = reader.ReadDouble(); + serverTime = reader.ReadDouble(); + } + + public void Serialize(NetworkWriter writer) + { + writer.WriteDouble(clientTime); + writer.WriteDouble(serverTime); + } + } + #endregion +} diff --git a/Assets/Packages/Mirror/Runtime/Messages.cs.meta b/Assets/Packages/Mirror/Runtime/Messages.cs.meta new file mode 100644 index 0000000..9afe21b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Messages.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 938f6f28a6c5b48a0bbd7782342d763b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Mirror.asmdef b/Assets/Packages/Mirror/Runtime/Mirror.asmdef new file mode 100644 index 0000000..4f3dbbd --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Mirror.asmdef @@ -0,0 +1,8 @@ +{ + "name": "Mirror", + "references": [], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Mirror.asmdef.meta b/Assets/Packages/Mirror/Runtime/Mirror.asmdef.meta new file mode 100644 index 0000000..202009b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Mirror.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 30817c1a0e6d646d99c048fc403f5979 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs b/Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs new file mode 100644 index 0000000..aee1e10 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs @@ -0,0 +1,775 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using UnityEngine; + +namespace Mirror +{ + /// + /// Sync to everyone, or only to owner. + /// + public enum SyncMode { Observers, Owner } + + /// + /// Base class which should be inherited by scripts which contain networking functionality. + /// + /// + /// This is a MonoBehaviour class so scripts which need to use the networking feature should inherit this class instead of MonoBehaviour. It allows you to invoke networked actions, receive various callbacks, and automatically synchronize state from server-to-client. + /// The NetworkBehaviour component requires a NetworkIdentity on the game object. There can be multiple NetworkBehaviours on a single game object. For an object with sub-components in a hierarchy, the NetworkIdentity must be on the root object, and NetworkBehaviour scripts must also be on the root object. + /// Some of the built-in components of the networking system are derived from NetworkBehaviour, including NetworkTransport, NetworkAnimator and NetworkProximityChecker. + /// + [RequireComponent(typeof(NetworkIdentity))] + [AddComponentMenu("")] + public class NetworkBehaviour : MonoBehaviour + { + float lastSyncTime; + + // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. + /// + /// sync mode for OnSerialize + /// + [HideInInspector] public SyncMode syncMode = SyncMode.Observers; + + // hidden because NetworkBehaviourInspector shows it only if has OnSerialize. + /// + /// sync interval for OnSerialize (in seconds) + /// + [HideInInspector] public float syncInterval = 0.1f; + + /// + /// This value is set on the NetworkIdentity and is accessible here for convenient access for scripts. + /// + public bool localPlayerAuthority => netIdentity.localPlayerAuthority; + + /// + /// Returns true if this object is active on an active server. + /// This is only true if the object has been spawned. This is different from NetworkServer.active, which is true if the server itself is active rather than this object being active. + /// + public bool isServer => netIdentity.isServer; + + /// + /// Returns true if running as a client and this object was spawned by a server. + /// + public bool isClient => netIdentity.isClient; + + /// + /// This returns true if this object is the one that represents the player on the local machine. + /// In multiplayer games, there are multiple instances of the Player object. The client needs to know which one is for "themselves" so that only that player processes input and potentially has a camera attached. The IsLocalPlayer function will return true only for the player instance that belongs to the player on the local machine, so it can be used to filter out input for non-local players. + /// + public bool isLocalPlayer => netIdentity.isLocalPlayer; + + /// + /// True if this object only exists on the server + /// + public bool isServerOnly => isServer && !isClient; + + /// + /// True if this object exists on a client that is not also acting as a server + /// + public bool isClientOnly => isClient && !isServer; + + /// + /// This returns true if this object is the authoritative version of the object in the distributed network application. + /// The localPlayerAuthority value on the NetworkIdentity determines how authority is determined. For most objects, authority is held by the server / host. For objects with localPlayerAuthority set, authority is held by the client of that player. + /// + public bool hasAuthority => netIdentity.hasAuthority; + + /// + /// The unique network Id of this object. + /// This is assigned at runtime by the network server and will be unique for all objects for that network session. + /// + public uint netId => netIdentity.netId; + + /// + /// The NetworkConnection associated with this NetworkIdentity. This is only valid for player objects on the server. + /// + public NetworkConnection connectionToServer => netIdentity.connectionToServer; + + /// + /// The NetworkConnection associated with this NetworkIdentity. This is only valid for player objects on the server. + /// + public NetworkConnection connectionToClient => netIdentity.connectionToClient; + + protected ulong syncVarDirtyBits { get; private set; } + private ulong syncVarHookGuard; + + protected bool getSyncVarHookGuard(ulong dirtyBit) + { + return (syncVarHookGuard & dirtyBit) != 0UL; + } + protected void setSyncVarHookGuard(ulong dirtyBit, bool value) + { + if (value) + syncVarHookGuard |= dirtyBit; + else + syncVarHookGuard &= ~dirtyBit; + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use syncObjects instead.")] + protected List m_SyncObjects => syncObjects; + + /// + /// objects that can synchronize themselves, such as synclists + /// + protected readonly List syncObjects = new List(); + + /// + /// NetworkIdentity component caching for easier access + /// + NetworkIdentity netIdentityCache; + + /// + /// Returns the NetworkIdentity of this object + /// + public NetworkIdentity netIdentity + { + get + { + if (netIdentityCache == null) + { + netIdentityCache = GetComponent(); + } + if (netIdentityCache == null) + { + Debug.LogError("There is no NetworkIdentity on " + name + ". Please add one."); + } + return netIdentityCache; + } + } + + /// + /// Returns the index of the component on this object + /// + public int ComponentIndex + { + get + { + // note: FindIndex causes allocations, we search manually instead + for (int i = 0; i < netIdentity.NetworkBehaviours.Length; i++) + { + NetworkBehaviour component = netIdentity.NetworkBehaviours[i]; + if (component == this) + return i; + } + + // this should never happen + Debug.LogError("Could not find component in GameObject. You should not add/remove components in networked objects dynamically", this); + + return -1; + } + } + + // this gets called in the constructor by the weaver + // for every SyncObject in the component (e.g. SyncLists). + // We collect all of them and we synchronize them with OnSerialize/OnDeserialize + protected void InitSyncObject(SyncObject syncObject) + { + syncObjects.Add(syncObject); + } + + #region Commands + + private static int GetMethodHash(Type invokeClass, string methodName) + { + // (invokeClass + ":" + cmdName).GetStableHashCode() would cause allocations. + // so hash1 + hash2 is better. + unchecked + { + int hash = invokeClass.FullName.GetStableHashCode(); + return hash * 503 + methodName.GetStableHashCode(); + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + protected void SendCommandInternal(Type invokeClass, string cmdName, NetworkWriter writer, int channelId) + { + // 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 " + cmdName + " called on server without an active client."); + return; + } + // local players can always send commands, regardless of authority, other objects must have authority. + if (!(isLocalPlayer || hasAuthority)) + { + Debug.LogWarning("Trying to send command for object without authority."); + return; + } + + if (ClientScene.readyConnection == null) + { + Debug.LogError("Send command attempted with no client running [client=" + connectionToServer + "]."); + return; + } + + // construct the message + CommandMessage message = new CommandMessage + { + netId = netId, + componentIndex = ComponentIndex, + functionHash = GetMethodHash(invokeClass, cmdName), // type+func so Inventory.RpcUse != Equipment.RpcUse + payload = writer.ToArraySegment() // segment to avoid reader allocations + }; + + ClientScene.readyConnection.Send(message, channelId); + } + + /// + /// Manually invoke a Command. + /// + /// Hash of the Command name. + /// Parameters to pass to the command. + /// Returns true if successful. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool InvokeCommand(int cmdHash, NetworkReader reader) + { + return InvokeHandlerDelegate(cmdHash, MirrorInvokeType.Command, reader); + } + #endregion + + #region Client RPCs + [EditorBrowsable(EditorBrowsableState.Never)] + protected void SendRPCInternal(Type invokeClass, string rpcName, NetworkWriter writer, int channelId) + { + // this was in Weaver before + if (!NetworkServer.active) + { + Debug.LogError("RPC Function " + rpcName + " called on Client."); + return; + } + // This cannot use NetworkServer.active, as that is not specific to this object. + if (!isServer) + { + Debug.LogWarning("ClientRpc " + rpcName + " called on un-spawned object: " + name); + return; + } + + // construct the message + RpcMessage message = new RpcMessage + { + netId = netId, + componentIndex = ComponentIndex, + functionHash = GetMethodHash(invokeClass, rpcName), // type+func so Inventory.RpcUse != Equipment.RpcUse + payload = writer.ToArraySegment() // segment to avoid reader allocations + }; + + NetworkServer.SendToReady(netIdentity, message, channelId); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + protected void SendTargetRPCInternal(NetworkConnection conn, Type invokeClass, string rpcName, NetworkWriter writer, int channelId) + { + // this was in Weaver before + if (!NetworkServer.active) + { + Debug.LogError("TargetRPC Function " + rpcName + " called on client."); + return; + } + // connection parameter is optional. assign if null. + if (conn == null) + { + conn = connectionToClient; + } + // this was in Weaver before + if (conn is ULocalConnectionToServer) + { + Debug.LogError("TargetRPC Function " + rpcName + " called on connection to server"); + return; + } + // This cannot use NetworkServer.active, as that is not specific to this object. + if (!isServer) + { + Debug.LogWarning("TargetRpc " + rpcName + " called on un-spawned object: " + name); + return; + } + + // construct the message + RpcMessage message = new RpcMessage + { + netId = netId, + componentIndex = ComponentIndex, + functionHash = GetMethodHash(invokeClass, rpcName), // type+func so Inventory.RpcUse != Equipment.RpcUse + payload = writer.ToArraySegment() // segment to avoid reader allocations + }; + + conn.Send(message, channelId); + } + + /// + /// Manually invoke an RPC function. + /// + /// Hash of the RPC name. + /// Parameters to pass to the RPC function. + /// Returns true if successful. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool InvokeRPC(int rpcHash, NetworkReader reader) + { + return InvokeHandlerDelegate(rpcHash, MirrorInvokeType.ClientRpc, reader); + } + #endregion + + #region Sync Events + [EditorBrowsable(EditorBrowsableState.Never)] + protected void SendEventInternal(Type invokeClass, string eventName, NetworkWriter writer, int channelId) + { + if (!NetworkServer.active) + { + Debug.LogWarning("SendEvent no server?"); + return; + } + + // construct the message + SyncEventMessage message = new SyncEventMessage + { + netId = netId, + componentIndex = ComponentIndex, + functionHash = GetMethodHash(invokeClass, eventName), // type+func so Inventory.RpcUse != Equipment.RpcUse + payload = writer.ToArraySegment() // segment to avoid reader allocations + }; + + NetworkServer.SendToReady(netIdentity, message, channelId); + } + + /// + /// Manually invoke a SyncEvent. + /// + /// Hash of the SyncEvent name. + /// Parameters to pass to the SyncEvent. + /// Returns true if successful. + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool InvokeSyncEvent(int eventHash, NetworkReader reader) + { + return InvokeHandlerDelegate(eventHash, MirrorInvokeType.SyncEvent, reader); + } + #endregion + + #region Code Gen Path Helpers + /// + /// Delegate for Command functions. + /// + /// + /// + public delegate void CmdDelegate(NetworkBehaviour obj, NetworkReader reader); + + protected class Invoker + { + public MirrorInvokeType invokeType; + public Type invokeClass; + public CmdDelegate invokeFunction; + } + + static readonly Dictionary cmdHandlerDelegates = new Dictionary(); + + // helper function register a Command/Rpc/SyncEvent delegate + [EditorBrowsable(EditorBrowsableState.Never)] + protected static void RegisterDelegate(Type invokeClass, string cmdName, MirrorInvokeType invokerType, CmdDelegate func) + { + int cmdHash = GetMethodHash(invokeClass, cmdName); // type+func so Inventory.RpcUse != Equipment.RpcUse + + if (cmdHandlerDelegates.ContainsKey(cmdHash)) + { + // something already registered this hash + Invoker oldInvoker = cmdHandlerDelegates[cmdHash]; + if (oldInvoker.invokeClass == invokeClass && oldInvoker.invokeType == invokerType && oldInvoker.invokeFunction == func) + { + // it's all right, it was the same function + return; + } + + Debug.LogError($"Function {oldInvoker.invokeClass}.{oldInvoker.invokeFunction.GetMethodName()} and {invokeClass}.{oldInvoker.invokeFunction.GetMethodName()} have the same hash. Please rename one of them"); + } + Invoker invoker = new Invoker + { + invokeType = invokerType, + invokeClass = invokeClass, + invokeFunction = func + }; + cmdHandlerDelegates[cmdHash] = invoker; + if (LogFilter.Debug) Debug.Log("RegisterDelegate hash:" + cmdHash + " invokerType: " + invokerType + " method:" + func.GetMethodName()); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + protected static void RegisterCommandDelegate(Type invokeClass, string cmdName, CmdDelegate func) + { + RegisterDelegate(invokeClass, cmdName, MirrorInvokeType.Command, func); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + protected static void RegisterRpcDelegate(Type invokeClass, string rpcName, CmdDelegate func) + { + RegisterDelegate(invokeClass, rpcName, MirrorInvokeType.ClientRpc, func); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + protected static void RegisterEventDelegate(Type invokeClass, string eventName, CmdDelegate func) + { + RegisterDelegate(invokeClass, eventName, MirrorInvokeType.SyncEvent, func); + } + + static bool GetInvokerForHash(int cmdHash, MirrorInvokeType invokeType, out Invoker invoker) + { + if (cmdHandlerDelegates.TryGetValue(cmdHash, out invoker) && + invoker != null && + invoker.invokeType == invokeType) + { + return true; + } + + // debug message if not found, or null, or mismatched type + // (no need to throw an error, an attacker might just be trying to + // call an cmd with an rpc's hash) + if (LogFilter.Debug) Debug.Log("GetInvokerForHash hash:" + cmdHash + " not found"); + return false; + } + + // InvokeCmd/Rpc/SyncEventDelegate can all use the same function here + internal bool InvokeHandlerDelegate(int cmdHash, MirrorInvokeType invokeType, NetworkReader reader) + { + if (GetInvokerForHash(cmdHash, invokeType, out Invoker invoker) && + invoker.invokeClass.IsInstanceOfType(this)) + { + invoker.invokeFunction(this, reader); + return true; + } + return false; + } + #endregion + + #region Helpers + // helper function for [SyncVar] GameObjects. + [EditorBrowsable(EditorBrowsableState.Never)] + protected void SetSyncVarGameObject(GameObject newGameObject, ref GameObject gameObjectField, ulong dirtyBit, ref uint netIdField) + { + if (getSyncVarHookGuard(dirtyBit)) + return; + + uint newNetId = 0; + if (newGameObject != null) + { + NetworkIdentity identity = newGameObject.GetComponent(); + if (identity != null) + { + newNetId = identity.netId; + if (newNetId == 0) + { + Debug.LogWarning("SetSyncVarGameObject GameObject " + newGameObject + " has a zero netId. Maybe it is not spawned yet?"); + } + } + } + + // netId changed? + if (newNetId != netIdField) + { + if (LogFilter.Debug) Debug.Log("SetSyncVar GameObject " + GetType().Name + " bit [" + dirtyBit + "] netfieldId:" + netIdField + "->" + newNetId); + SetDirtyBit(dirtyBit); + gameObjectField = newGameObject; // assign new one on the server, and in case we ever need it on client too + netIdField = newNetId; + } + } + + // helper function for [SyncVar] GameObjects. + // -> ref GameObject as second argument makes OnDeserialize processing easier + [EditorBrowsable(EditorBrowsableState.Never)] + protected GameObject GetSyncVarGameObject(uint netId, ref GameObject gameObjectField) + { + // server always uses the field + if (isServer) + { + return gameObjectField; + } + + // client always looks up based on netId because objects might get in and out of range + // over and over again, which shouldn't null them forever + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity) && identity != null) + return identity.gameObject; + return null; + } + + // helper function for [SyncVar] NetworkIdentities. + [EditorBrowsable(EditorBrowsableState.Never)] + protected void SetSyncVarNetworkIdentity(NetworkIdentity newIdentity, ref NetworkIdentity identityField, ulong dirtyBit, ref uint netIdField) + { + if (getSyncVarHookGuard(dirtyBit)) + return; + + uint newNetId = 0; + if (newIdentity != null) + { + newNetId = newIdentity.netId; + if (newNetId == 0) + { + Debug.LogWarning("SetSyncVarNetworkIdentity NetworkIdentity " + newIdentity + " has a zero netId. Maybe it is not spawned yet?"); + } + } + + // netId changed? + if (newNetId != netIdField) + { + if (LogFilter.Debug) Debug.Log("SetSyncVarNetworkIdentity NetworkIdentity " + GetType().Name + " bit [" + dirtyBit + "] netIdField:" + netIdField + "->" + newNetId); + SetDirtyBit(dirtyBit); + netIdField = newNetId; + identityField = newIdentity; // assign new one on the server, and in case we ever need it on client too + } + } + + // helper function for [SyncVar] NetworkIdentities. + // -> ref GameObject as second argument makes OnDeserialize processing easier + [EditorBrowsable(EditorBrowsableState.Never)] + protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdentity identityField) + { + // server always uses the field + if (isServer) + { + return identityField; + } + + // client always looks up based on netId because objects might get in and out of range + // over and over again, which shouldn't null them forever + NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity); + return identity; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + protected void SetSyncVar(T value, ref T fieldValue, ulong dirtyBit) + { + // newly initialized or changed value? + if (!EqualityComparer.Default.Equals(value, fieldValue)) + { + if (LogFilter.Debug) Debug.Log("SetSyncVar " + GetType().Name + " bit [" + dirtyBit + "] " + fieldValue + "->" + value); + SetDirtyBit(dirtyBit); + fieldValue = value; + } + } + #endregion + + /// + /// Used to set the behaviour as dirty, so that a network update will be sent for the object. + /// these are masks, not bit numbers, ie. 0x004 not 2 + /// + /// Bit mask to set. + public void SetDirtyBit(ulong dirtyBit) + { + syncVarDirtyBits |= dirtyBit; + } + + /// + /// This clears all the dirty bits that were set on this script by SetDirtyBits(); + /// This is automatically invoked when an update is sent for this object, but can be called manually as well. + /// + public void ClearAllDirtyBits() + { + lastSyncTime = Time.time; + syncVarDirtyBits = 0L; + + // flush all unsynchronized changes in syncobjects + // note: don't use List.ForEach here, this is a hot path + // List.ForEach: 432b/frame + // for: 231b/frame + for (int i = 0; i < syncObjects.Count; ++i) + { + syncObjects[i].Flush(); + } + } + + bool AnySyncObjectDirty() + { + // note: don't use Linq here. 1200 networked objects: + // Linq: 187KB GC/frame;, 2.66ms time + // for: 8KB GC/frame; 1.28ms time + for (int i = 0; i < syncObjects.Count; ++i) + { + if (syncObjects[i].IsDirty) + { + return true; + } + } + return false; + } + + internal bool IsDirty() + { + if (Time.time - lastSyncTime >= syncInterval) + { + return syncVarDirtyBits != 0L || AnySyncObjectDirty(); + } + return false; + } + + /// + /// Virtual function to override to send custom serialization data. The corresponding function to send serialization data is OnDeserialize(). + /// + /// + /// The initialState flag is useful to differentiate between the first time an object is serialized and when incremental updates can be sent. The first time an object is sent to a client, it must include a full state snapshot, but subsequent updates can save on bandwidth by including only incremental changes. Note that SyncVar hook functions are not called when initialState is true, only for incremental updates. + /// If a class has SyncVars, then an implementation of this function and OnDeserialize() are added automatically to the class. So a class that has SyncVars cannot also have custom serialization functions. + /// The OnSerialize function should return true to indicate that an update should be sent. If it returns true, then the dirty bits for that script are set to zero, if it returns false then the dirty bits are not changed. This allows multiple changes to a script to be accumulated over time and sent when the system is ready, instead of every frame. + /// + /// Writer to use to write to the stream. + /// If this is being called to send initial state. + /// True if data was written. + public virtual bool OnSerialize(NetworkWriter writer, bool initialState) + { + if (initialState) + { + return SerializeObjectsAll(writer); + } + return SerializeObjectsDelta(writer); + } + + /// + /// Virtual function to override to receive custom serialization data. The corresponding function to send serialization data is OnSerialize(). + /// + /// Reader to read from the stream. + /// True if being sent initial state. + public virtual void OnDeserialize(NetworkReader reader, bool initialState) + { + if (initialState) + { + DeSerializeObjectsAll(reader); + } + else + { + DeSerializeObjectsDelta(reader); + } + } + + ulong DirtyObjectBits() + { + ulong dirtyObjects = 0; + for (int i = 0; i < syncObjects.Count; i++) + { + SyncObject syncObject = syncObjects[i]; + if (syncObject.IsDirty) + { + dirtyObjects |= 1UL << i; + } + } + return dirtyObjects; + } + + public bool 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) + { + bool dirty = false; + // write the mask + writer.WritePackedUInt64(DirtyObjectBits()); + // serializable objects, such as synclists + for (int i = 0; i < syncObjects.Count; i++) + { + SyncObject syncObject = syncObjects[i]; + if (syncObject.IsDirty) + { + syncObject.OnSerializeDelta(writer); + dirty = true; + } + } + return dirty; + } + + void DeSerializeObjectsAll(NetworkReader reader) + { + for (int i = 0; i < syncObjects.Count; i++) + { + SyncObject syncObject = syncObjects[i]; + syncObject.OnDeserializeAll(reader); + } + } + + void DeSerializeObjectsDelta(NetworkReader reader) + { + ulong dirty = reader.ReadPackedUInt64(); + for (int i = 0; i < syncObjects.Count; i++) + { + SyncObject syncObject = syncObjects[i]; + if ((dirty & (1UL << i)) != 0) + { + syncObject.OnDeserializeDelta(reader); + } + } + } + + /// + /// 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. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void OnNetworkDestroy() {} + + /// + /// 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 virtual void OnStartServer() {} + + /// + /// 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 virtual void OnStartClient() {} + + /// + /// 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 virtual void OnStartLocalPlayer() {} + + /// + /// This is invoked on behaviours that have authority, based on context and 'NetworkIdentity.localPlayerAuthority.' + /// This is called after OnStartServer and OnStartClient. + /// When is called on the server, this will be called on the client that owns the object. When an object is spawned with NetworkServer.SpawnWithClientAuthority, this will be called on the client that owns the object. + /// + public virtual void OnStartAuthority() {} + + /// + /// 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 virtual void OnStopAuthority() {} + + /// + /// Callback used by the visibility system to (re)construct the set of observers that can see this object. + /// Implementations of this callback should add network connections of players that can see this object to the observers set. + /// + /// The new set of observers for this object. + /// True if the set of observers is being built for the first time. + /// true when overwriting so that Mirror knows that we wanted to rebuild observers ourselves. otherwise it uses built in rebuild. + public virtual bool OnRebuildObservers(HashSet observers, bool initialize) + { + return 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 visibile 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. + /// + /// New visibility state. + public virtual void OnSetLocalVisibility(bool vis) {} + + /// + /// Callback used by the visibility system to determine if an observer (player) can see this object. + /// If this function returns true, the network connection will be added as an observer. + /// + /// Network connection of a player. + /// True if the player can see this object. + public virtual bool OnCheckObserver(NetworkConnection conn) + { + return true; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs.meta new file mode 100644 index 0000000..84e619d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 655ee8cba98594f70880da5cc4dc442d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkClient.cs b/Assets/Packages/Mirror/Runtime/NetworkClient.cs new file mode 100644 index 0000000..07473b3 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkClient.cs @@ -0,0 +1,464 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using UnityEngine; + +namespace Mirror +{ + public enum ConnectState + { + None, + Connecting, + Connected, + Disconnected + } + + // TODO make fully static after removing obsoleted singleton! + /// + /// This is a network client class used by the networking system. It contains a NetworkConnection that is used to connect to a network server. + /// The NetworkClient handle connection state, messages handlers, and connection configuration. There can be many NetworkClient instances in a process at a time, but only one that is connected to a game server (NetworkServer) that uses spawned objects. + /// NetworkClient has an internal update function where it handles events from the transport layer. This includes asynchronous connect events, disconnect events and incoming data from a server. + /// The NetworkManager has a NetworkClient instance that it uses for games that it starts, but the NetworkClient may be used by itself. + /// + public class NetworkClient + { + /// + /// Obsolete: Use directly. + /// Singleton isn't needed anymore, all functions are static now. For example: NetworkClient.Send(message) instead of NetworkClient.singleton.Send(message). + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkClient directly. Singleton isn't needed anymore, all functions are static now. For example: NetworkClient.Send(message) instead of NetworkClient.singleton.Send(message).")] + public static NetworkClient singleton = new NetworkClient(); + + /// + /// A list of all the active network clients in the current process. + /// This is NOT a list of all clients that are connected to the remote server, it is client instances on the local game. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkClient directly instead. There is always exactly one client.")] + public static List allClients => new List { singleton }; + + /// + /// The registered network message handlers. + /// + public static readonly Dictionary handlers = new Dictionary(); + + /// + /// The NetworkConnection object this client is using. + /// + public static NetworkConnection connection { get; internal set; } + + internal static ConnectState connectState = ConnectState.None; + + /// + /// The IP address of the server that this client is connected to. + /// This will be empty if the client has not connected yet. + /// + public static string serverIp => connection.address; + + /// + /// active is true while a client is connecting/connected + /// (= while the network is active) + /// + public static bool active => connectState == ConnectState.Connecting || connectState == ConnectState.Connected; + + /// + /// This gives the current connection status of the client. + /// + public static bool isConnected => connectState == ConnectState.Connected; + + /// + /// NetworkClient can connect to local server in host mode too + /// + public static bool isLocalClient => connection is ULocalConnectionToServer; + + // local client in host mode might call Cmds/Rpcs during Update, but we + // want to apply them in LateUpdate like all other Transport messages + // to avoid race conditions. keep packets in Queue until LateUpdate. + internal static Queue localClientPacketQueue = new Queue(); + + /// + /// Connect client to a NetworkServer instance. + /// + /// + public static void Connect(string address) + { + if (LogFilter.Debug) Debug.Log("Client Connect: " + address); + + RegisterSystemHandlers(false); + Transport.activeTransport.enabled = true; + InitializeTransportHandlers(); + + connectState = ConnectState.Connecting; + Transport.activeTransport.ClientConnect(address); + + // setup all the handlers + connection = new NetworkConnection(address, 0); + connection.SetHandlers(handlers); + } + + /// + /// connect host mode + /// + internal static void ConnectLocalServer() + { + if (LogFilter.Debug) Debug.Log("Client Connect Local Server"); + + RegisterSystemHandlers(true); + + connectState = ConnectState.Connected; + + // create local connection to server + connection = new ULocalConnectionToServer(); + connection.SetHandlers(handlers); + + // create server connection to local client + ULocalConnectionToClient connectionToClient = new ULocalConnectionToClient(); + NetworkServer.SetLocalConnection(connectionToClient); + + localClientPacketQueue.Enqueue(MessagePacker.Pack(new ConnectMessage())); + } + + /// + /// Called by the server to set the LocalClient's LocalPlayer object during NetworkServer.AddPlayer() + /// + /// + internal static void AddLocalPlayer(NetworkIdentity localPlayer) + { + if (LogFilter.Debug) Debug.Log("Local client AddLocalPlayer " + localPlayer.gameObject.name + " conn=" + connection.connectionId); + connection.isReady = true; + connection.playerController = localPlayer; + if (localPlayer != null) + { + localPlayer.isClient = true; + NetworkIdentity.spawned[localPlayer.netId] = localPlayer; + localPlayer.connectionToServer = connection; + } + // there is no SystemOwnerMessage for local client. add to ClientScene here instead + ClientScene.InternalAddPlayer(localPlayer); + } + + static void InitializeTransportHandlers() + { + Transport.activeTransport.OnClientConnected.AddListener(OnConnected); + Transport.activeTransport.OnClientDataReceived.AddListener(OnDataReceived); + Transport.activeTransport.OnClientDisconnected.AddListener(OnDisconnected); + Transport.activeTransport.OnClientError.AddListener(OnError); + } + + static void OnError(Exception exception) + { + Debug.LogException(exception); + } + + static void OnDisconnected() + { + connectState = ConnectState.Disconnected; + + ClientScene.HandleClientDisconnect(connection); + + connection?.InvokeHandler(new DisconnectMessage()); + } + + internal static void OnDataReceived(ArraySegment data) + { + if (connection != null) + { + connection.TransportReceive(data); + } + else Debug.LogError("Skipped Data message handling because connection is null."); + } + + static void OnConnected() + { + if (connection != null) + { + // reset network time stats + NetworkTime.Reset(); + + // 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(); + connection.InvokeHandler(new ConnectMessage()); + } + else Debug.LogError("Skipped Connect message handling because connection is null."); + } + + /// + /// Disconnect from server. + /// The disconnect message will be invoked. + /// + public static void Disconnect() + { + connectState = ConnectState.Disconnected; + ClientScene.HandleClientDisconnect(connection); + + // local or remote connection? + if (isLocalClient) + { + if (isConnected) + { + localClientPacketQueue.Enqueue(MessagePacker.Pack(new DisconnectMessage())); + } + NetworkServer.RemoveLocalConnection(); + } + else + { + if (connection != null) + { + connection.Disconnect(); + connection.Dispose(); + connection = null; + RemoveTransportHandlers(); + } + } + } + + static void RemoveTransportHandlers() + { + // so that we don't register them more than once + Transport.activeTransport.OnClientConnected.RemoveListener(OnConnected); + Transport.activeTransport.OnClientDataReceived.RemoveListener(OnDataReceived); + Transport.activeTransport.OnClientDisconnected.RemoveListener(OnDisconnected); + Transport.activeTransport.OnClientError.RemoveListener(OnError); + } + + /// + /// Obsolete: Use instead with no message id instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use SendMessage instead with no message id instead")] + public static bool Send(short msgType, MessageBase msg) + { + if (connection != null) + { + if (connectState != ConnectState.Connected) + { + Debug.LogError("NetworkClient Send when not connected to a server"); + return false; + } + return connection.Send(msgType, msg); + } + Debug.LogError("NetworkClient Send with no connection"); + return false; + } + + /// + /// This sends a network message with a message Id to the server. This message is sent on channel zero, which by default is the reliable channel. + /// The message must be an instance of a class derived from MessageBase. + /// The message id passed to Send() is used to identify the handler function to invoke on the server when the message is received. + /// + /// The message type to unregister. + /// + /// + /// True if message was sent. + public static bool Send(T message, int channelId = Channels.DefaultReliable) where T : IMessageBase + { + if (connection != null) + { + if (connectState != ConnectState.Connected) + { + Debug.LogError("NetworkClient Send when not connected to a server"); + return false; + } + return connection.Send(message, channelId); + } + Debug.LogError("NetworkClient Send with no connection"); + return false; + } + + internal static void Update() + { + // local or remote connection? + if (isLocalClient) + { + // process internal messages so they are applied at the correct time + while (localClientPacketQueue.Count > 0) + { + byte[] packet = localClientPacketQueue.Dequeue(); + OnDataReceived(new ArraySegment(packet)); + } + } + else + { + // only update things while connected + if (active && connectState == ConnectState.Connected) + { + NetworkTime.UpdateClient(); + } + } + } + + /* TODO use or remove + void GenerateConnectError(byte error) + { + Debug.LogError("Mirror Client Error Connect Error: " + error); + GenerateError(error); + } + + void GenerateDataError(byte error) + { + NetworkError dataError = (NetworkError)error; + Debug.LogError("Mirror Client Data Error: " + dataError); + GenerateError(error); + } + + void GenerateDisconnectError(byte error) + { + NetworkError disconnectError = (NetworkError)error; + Debug.LogError("Mirror Client Disconnect Error: " + disconnectError); + GenerateError(error); + } + + void GenerateError(byte error) + { + int msgId = MessageBase.GetId(); + if (handlers.TryGetValue(msgId, out NetworkMessageDelegate msgDelegate)) + { + ErrorMessage msg = new ErrorMessage + { + value = error + }; + + // write the message to a local buffer + NetworkWriter writer = new NetworkWriter(); + msg.Serialize(writer); + + NetworkMessage netMsg = new NetworkMessage + { + msgType = msgId, + reader = new NetworkReader(writer.ToArray()), + conn = connection + }; + msgDelegate(netMsg); + } + } + */ + + /// + /// Obsolete: Use instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkTime.rtt instead")] + public static float GetRTT() + { + return (float)NetworkTime.rtt; + } + + internal static void RegisterSystemHandlers(bool localClient) + { + // local client / regular 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 (localClient) + { + RegisterHandler(ClientScene.OnLocalClientObjectDestroy); + RegisterHandler(ClientScene.OnLocalClientObjectHide); + RegisterHandler((conn, msg) => { }); + RegisterHandler(ClientScene.OnLocalClientSpawnPrefab); + RegisterHandler(ClientScene.OnLocalClientSpawnSceneObject); + RegisterHandler((conn, msg) => { }); // host mode doesn't need spawning + RegisterHandler((conn, msg) => { }); // host mode doesn't need spawning + RegisterHandler((conn, msg) => { }); + } + else + { + RegisterHandler(ClientScene.OnObjectDestroy); + RegisterHandler(ClientScene.OnObjectHide); + RegisterHandler(NetworkTime.OnClientPong); + RegisterHandler(ClientScene.OnSpawnPrefab); + RegisterHandler(ClientScene.OnSpawnSceneObject); + RegisterHandler(ClientScene.OnObjectSpawnStarted); + RegisterHandler(ClientScene.OnObjectSpawnFinished); + RegisterHandler(ClientScene.OnUpdateVarsMessage); + } + RegisterHandler(ClientScene.OnClientAuthority); + RegisterHandler(ClientScene.OnRPCMessage); + RegisterHandler(ClientScene.OnSyncEventMessage); + } + + /// + /// Obsolete: Use instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use RegisterHandler instead")] + public static void RegisterHandler(int msgType, NetworkMessageDelegate handler) + { + if (handlers.ContainsKey(msgType)) + { + if (LogFilter.Debug) Debug.Log("NetworkClient.RegisterHandler replacing " + handler + " - " + msgType); + } + handlers[msgType] = handler; + } + + /// + /// Obsolete: Use instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use RegisterHandler instead")] + public static void RegisterHandler(MsgType msgType, NetworkMessageDelegate handler) + { + RegisterHandler((int)msgType, handler); + } + + /// + /// Register a handler for a particular message type. + /// There are several system message types which you can add handlers for. You can also add your own message types. + /// + /// The message type to unregister. + /// + public static void RegisterHandler(Action handler) where T : IMessageBase, new() + { + int msgType = MessagePacker.GetId(); + if (handlers.ContainsKey(msgType)) + { + if (LogFilter.Debug) Debug.Log("NetworkClient.RegisterHandler replacing " + handler + " - " + msgType); + } + handlers[msgType] = MessagePacker.MessageHandler(handler); + } + + /// + /// Obsolete: Use instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use UnregisterHandler instead")] + public static void UnregisterHandler(int msgType) + { + handlers.Remove(msgType); + } + + /// + /// Obsolete: Use instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use UnregisterHandler instead")] + public static void UnregisterHandler(MsgType msgType) + { + UnregisterHandler((int)msgType); + } + + /// + /// Unregisters a network message handler. + /// + /// The message type to unregister. + public static void UnregisterHandler() where T : IMessageBase + { + // use int to minimize collisions + int msgType = MessagePacker.GetId(); + handlers.Remove(msgType); + } + + /// + /// Shut down a client. + /// This should be done when a client is no longer going to be used. + /// + public static void Shutdown() + { + if (LogFilter.Debug) Debug.Log("Shutting down client."); + ClientScene.Shutdown(); + connectState = ConnectState.None; + } + + /// + /// Obsolete: Call instead. There is only one client. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Call NetworkClient.Shutdown() instead. There is only one client.")] + public static void ShutdownAll() + { + Shutdown(); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkClient.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkClient.cs.meta new file mode 100644 index 0000000..b43b514 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abe6be14204d94224a3e7cd99dd2ea73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkConnection.cs b/Assets/Packages/Mirror/Runtime/NetworkConnection.cs new file mode 100644 index 0000000..613147b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkConnection.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using UnityEngine; + +namespace Mirror +{ + /// + /// A High level network connection. This is used for connections from client-to-server and for connection from server-to-client. + /// + /// + /// A NetworkConnection corresponds to a specific connection for a host in the transport layer. It has a connectionId that is assigned by the transport layer and passed to the Initialize function. + /// A NetworkClient has one NetworkConnection. A NetworkServerSimple manages multiple NetworkConnections. The NetworkServer has multiple "remote" connections and a "local" connection for the local client. + /// The NetworkConnection class provides message sending and handling facilities. For sending data over a network, there are methods to send message objects, byte arrays, and NetworkWriter objects. To handle data arriving from the network, handler functions can be registered for message Ids, byte arrays can be processed by HandleBytes(), and NetworkReader object can be processed by HandleReader(). + /// NetworkConnection objects also act as observers for networked objects. When a connection is an observer of a networked object with a NetworkIdentity, then the object will be visible to corresponding client for the connection, and incremental state changes will be sent to the client. + /// NetworkConnection objects can "own" networked game objects. Owned objects will be destroyed on the server by default when the connection is destroyed. A connection owns the player objects created by its client, and other objects with client-authority assigned to the corresponding client. + /// There are many virtual functions on NetworkConnection that allow its behaviour to be customized. NetworkClient and NetworkServer can both be made to instantiate custom classes derived from NetworkConnection by setting their networkConnectionClass member variable. + /// + public class NetworkConnection : IDisposable + { + public readonly HashSet visList = new HashSet(); + + Dictionary messageHandlers; + + /// + /// Unique identifier for this connection that is assigned by the transport layer. + /// + /// + /// On a server, this Id is unique for every connection on the server. On a client this Id is local to the client, it is not the same as the Id on the server for this connection. + /// Transport layers connections begin at one. So on a client with a single connection to a server, the connectionId of that connection will be one. In NetworkServer, the connectionId of the local connection is zero. + /// Clients do not know their connectionId on the server, and do not know the connectionId of other clients on the server. + /// + public int connectionId = -1; + + /// + /// Flag that tells if the connection has been marked as "ready" by a client calling ClientScene.Ready(). + /// This property is read-only. It is set by the system on the client when ClientScene.Ready() is called, and set by the system on the server when a ready message is received from a client. + /// A client that is ready is sent spawned objects by the server and updates to the state of spawned objects. A client that is not ready is not sent spawned objects. + /// + public bool isReady; + + /// + /// The IP address / URL / FQDN associated with the connection. + /// + public string address; + + /// + /// The last time that a message was received on this connection. + /// This includes internal system messages (such as Commands and ClientRpc calls) and user messages. + /// + public float lastMessageTime; + + /// + /// The NetworkIdentity for this connection. + /// + public NetworkIdentity playerController { get; internal set; } + + /// + /// A list of the NetworkIdentity objects owned by this connection. This list is read-only. + /// This includes the player object for the connection - if it has localPlayerAutority set, and any objects spawned with local authority or set with AssignLocalAuthority. + /// This list can be used to validate messages from clients, to ensure that clients are only trying to control objects that they own. + /// + public readonly HashSet clientOwnedObjects = new HashSet(); + + /// + /// Setting this to true will log the contents of network message to the console. + /// + /// + /// Warning: this can be a lot of data and can be very slow. Both incoming and outgoing messages are logged. The format of the logs is: + /// ConnectionSend con:1 bytes:11 msgId:5 FB59D743FD120000000000 ConnectionRecv con:1 bytes:27 msgId:8 14F21000000000016800AC3FE090C240437846403CDDC0BD3B0000 + /// Note that these are application-level network messages, not protocol-level packets. There will typically be multiple network messages combined in a single protocol packet. + /// + public bool logNetworkMessages; + + // this is always true for regular connections, false for local + // connections because it's set in the constructor and never reset. + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("isConnected will be removed because it's pointless. A NetworkConnection is always connected.")] + public bool isConnected { get; protected set; } + + // this is always 0 for regular connections, -1 for local + // connections because it's set in the constructor and never reset. + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("hostId will be removed because it's not needed ever since we removed LLAPI as default. It's always 0 for regular connections and -1 for local connections. Use connection.GetType() == typeof(NetworkConnection) to check if it's a regular or local connection.")] + public int hostId = -1; + + /// + /// Creates a new NetworkConnection with the specified address + /// + /// + public NetworkConnection(string networkAddress) + { + address = networkAddress; + } + + /// + /// Creates a new NetworkConnection with the specified address and connectionId + /// + /// + /// + public NetworkConnection(string networkAddress, int networkConnectionId) + { + address = networkAddress; + connectionId = networkConnectionId; +#pragma warning disable 618 + isConnected = true; + hostId = 0; +#pragma warning restore 618 + } + + ~NetworkConnection() + { + Dispose(false); + } + + /// + /// Disposes of this connection, releasing channel buffers that it holds. + /// + public void Dispose() + { + Dispose(true); + // Take yourself off the Finalization queue + // to prevent finalization code for this object + // from executing a second time. + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + foreach (uint netId in clientOwnedObjects) + { + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity)) + { + identity.clientAuthorityOwner = null; + } + } + clientOwnedObjects.Clear(); + } + + /// + /// Disconnects this connection. + /// + public void Disconnect() + { + // don't clear address so we can still access it in NetworkManager.OnServerDisconnect + // => it's reset in Initialize anyway and there is no address empty check anywhere either + //address = ""; + + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + isReady = false; + ClientScene.HandleClientDisconnect(this); + + // server? then disconnect that client (not for host local player though) + if (Transport.activeTransport.ServerActive() && connectionId != 0) + { + Transport.activeTransport.ServerDisconnect(connectionId); + } + // not server and not host mode? then disconnect client + else + { + Transport.activeTransport.ClientDisconnect(); + } + + RemoveObservers(); + } + + internal void SetHandlers(Dictionary handlers) + { + messageHandlers = handlers; + } + + /// + /// Obsolete: Use NetworkClient/NetworkServer.RegisterHandler{T} instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkClient/NetworkServer.RegisterHandler instead")] + public void RegisterHandler(short msgType, NetworkMessageDelegate handler) + { + if (messageHandlers.ContainsKey(msgType)) + { + if (LogFilter.Debug) Debug.Log("NetworkConnection.RegisterHandler replacing " + msgType); + } + messageHandlers[msgType] = handler; + } + + /// + /// Obsolete: Use and instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkClient/NetworkServer.UnregisterHandler instead")] + public void UnregisterHandler(short msgType) + { + messageHandlers.Remove(msgType); + } + + /// + /// Obsolete: use instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("use Send instead")] + public virtual bool Send(int msgType, MessageBase msg, int channelId = Channels.DefaultReliable) + { + // pack message and send + byte[] message = MessagePacker.PackMessage(msgType, msg); + return SendBytes(message, channelId); + } + + /// + /// This sends a network message with a message ID on the connection. This message is sent on channel zero, which by default is the reliable channel. + /// + /// The message type to unregister. + /// The message to send. + /// The transport layer channel to send on. + /// + public virtual bool Send(T msg, int channelId = Channels.DefaultReliable) where T: IMessageBase + { + // pack message and send + byte[] message = MessagePacker.Pack(msg); + return SendBytes(message, channelId); + } + + // internal because no one except Mirror should send bytes directly to + // the client. they would be detected as a message. send messages instead. + internal virtual bool SendBytes(byte[] bytes, int channelId = Channels.DefaultReliable) + { + if (logNetworkMessages) Debug.Log("ConnectionSend con:" + connectionId + " bytes:" + BitConverter.ToString(bytes)); + + if (bytes.Length > Transport.activeTransport.GetMaxPacketSize(channelId)) + { + Debug.LogError("NetworkConnection.SendBytes cannot send packet larger than " + Transport.activeTransport.GetMaxPacketSize(channelId) + " bytes"); + return false; + } + + if (bytes.Length == 0) + { + // zero length packets getting into the packet queues are bad. + Debug.LogError("NetworkConnection.SendBytes cannot send zero bytes"); + return false; + } + + return TransportSend(channelId, bytes); + } + + public override string ToString() + { + return $"connectionId: {connectionId} isReady: {isReady}"; + } + + internal void AddToVisList(NetworkIdentity identity) + { + visList.Add(identity); + + // spawn identity for this conn + NetworkServer.ShowForConnection(identity, this); + } + + internal void RemoveFromVisList(NetworkIdentity identity, bool isDestroyed) + { + visList.Remove(identity); + + if (!isDestroyed) + { + // hide identity for this conn + NetworkServer.HideForConnection(identity, this); + } + } + + internal void RemoveObservers() + { + foreach (NetworkIdentity identity in visList) + { + identity.RemoveObserverInternal(this); + } + visList.Clear(); + } + + /// + /// Obsolete: Use instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use InvokeHandler instead")] + public bool InvokeHandlerNoData(int msgType) + { + return InvokeHandler(msgType, null); + } + + internal bool InvokeHandler(int msgType, NetworkReader reader) + { + if (messageHandlers.TryGetValue(msgType, out NetworkMessageDelegate msgDelegate)) + { + NetworkMessage message = new NetworkMessage + { + msgType = msgType, + reader = reader, + conn = this + }; + + msgDelegate(message); + return true; + } + Debug.LogError("Unknown message ID " + msgType + " connId:" + connectionId); + return false; + } + + /// + /// This function invokes the registered handler function for a message. + /// Network connections used by the NetworkClient and NetworkServer use this function for handling network messages. + /// + /// The message type to unregister. + /// The message object to process. + /// + public bool InvokeHandler(T msg) where T : IMessageBase + { + int msgType = MessagePacker.GetId(); + byte[] data = MessagePacker.Pack(msg); + return InvokeHandler(msgType, new NetworkReader(data)); + } + + // note: original HLAPI HandleBytes function handled >1 message in a while loop, but this wasn't necessary + // anymore because NetworkServer/NetworkClient Update both use while loops to handle >1 data events per + // frame already. + // -> in other words, we always receive 1 message per Receive call, never two. + // -> can be tested easily with a 1000ms send delay and then logging amount received in while loops here + // and in NetworkServer/Client Update. HandleBytes already takes exactly one. + /// + /// This virtual function allows custom network connection classes to process data from the network before it is passed to the application. + /// + /// The data recieved. + public virtual void TransportReceive(ArraySegment buffer) + { + // unpack message + NetworkReader reader = new NetworkReader(buffer); + if (MessagePacker.UnpackMessage(reader, out int msgType)) + { + // logging + if (logNetworkMessages) Debug.Log("ConnectionRecv con:" + connectionId + " msgType:" + msgType + " content:" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count)); + + // try to invoke the handler for that message + if (InvokeHandler(msgType, reader)) + { + lastMessageTime = Time.time; + } + } + else + { + Debug.LogError("Closed connection: " + connectionId + ". Invalid message header."); + Disconnect(); + } + } + + /// + /// This virtual function allows custom network connection classes to process data send by the application before it goes to the network transport layer. + /// + /// Channel to send data on. + /// Data to send. + /// + public virtual bool TransportSend(int channelId, byte[] bytes) + { + if (Transport.activeTransport.ClientConnected()) + { + return Transport.activeTransport.ClientSend(channelId, bytes); + } + else if (Transport.activeTransport.ServerActive()) + { + return Transport.activeTransport.ServerSend(connectionId, channelId, bytes); + } + return false; + } + + internal void AddOwnedObject(NetworkIdentity obj) + { + clientOwnedObjects.Add(obj.netId); + } + + internal void RemoveOwnedObject(NetworkIdentity obj) + { + clientOwnedObjects.Remove(obj.netId); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkConnection.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkConnection.cs.meta new file mode 100644 index 0000000..3688d9c --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkConnection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11ea41db366624109af1f0834bcdde2f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkIdentity.cs b/Assets/Packages/Mirror/Runtime/NetworkIdentity.cs new file mode 100644 index 0000000..362b14c --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkIdentity.cs @@ -0,0 +1,1253 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Security.Cryptography; +using UnityEngine; +using UnityEngine.Serialization; +#if UNITY_EDITOR +using UnityEditor; +#if UNITY_2018_3_OR_NEWER +using UnityEditor.Experimental.SceneManagement; +#endif +#endif + +namespace Mirror +{ + /// + /// The NetworkIdentity identifies objects across the network, between server and clients. Its primary data is a NetworkInstanceId which is allocated by the server and then set on clients. This is used in network communications to be able to lookup game objects on different machines. + /// + /// + /// The NetworkIdentity is used to synchronize information in the object with the network. Only the server should create instances of objects which have NetworkIdentity as otherwise they will not be properly connected to the system. + /// For complex objects with a hierarchy of subcomponents, the NetworkIdentity must be on the root of the hierarchy. It is not supported to have multiple NetworkIdentity components on subcomponents of a hierarchy. + /// NetworkBehaviour scripts require a NetworkIdentity on the game object to be able to function. + /// The NetworkIdentity manages the dirty state of the NetworkBehaviours of the object. When it discovers that NetworkBehaviours are dirty, it causes an update packet to be created and sent to clients. + /// The flow for serialization updates managed by the NetworkIdentity is: + /// * Each NetworkBehaviour has a dirty mask. This mask is available inside OnSerialize as syncVarDirtyBits + /// * Each SyncVar in a NetworkBehaviour script is assigned a bit in the dirty mask. + /// * Changing the value of SyncVars causes the bit for that SyncVar to be set in the dirty mask + /// * Alternatively, calling SetDirtyBit() writes directly to the dirty mask + /// * NetworkIdentity objects are checked on the server as part of it's update loop + /// * If any NetworkBehaviours on a NetworkIdentity are dirty, then an UpdateVars packet is created for that object + /// * The UpdateVars packet is populated by calling OnSerialize on each NetworkBehaviour on the object + /// * NetworkBehaviours that are NOT dirty write a zero to the packet for their dirty bits + /// * NetworkBehaviours that are dirty write their dirty mask, then the values for the SyncVars that have changed + /// * If OnSerialize returns true for a NetworkBehaviour, the dirty mask is reset for that NetworkBehaviour, so it will not send again until its value changes. + /// * The UpdateVars packet is sent to ready clients that are observing the object + /// On the client: + /// * an UpdateVars packet is received for an object + /// * The OnDeserialize function is called for each NetworkBehaviour script on the object + /// * Each NetworkBehaviour script on the object reads a dirty mask. + /// * If the dirty mask for a NetworkBehaviour is zero, the OnDeserialize functions returns without reading any more + /// * If the dirty mask is non-zero value, then the OnDeserialize function reads the values for the SyncVars that correspond to the dirty bits that are set + /// * If there are SyncVar hook functions, those are invoked with the value read from the stream. + /// + [ExecuteInEditMode] + [DisallowMultipleComponent] + [AddComponentMenu("Network/NetworkIdentity")] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkIdentity.html")] + public sealed class NetworkIdentity : MonoBehaviour + { + // configuration + bool m_IsServer; + NetworkBehaviour[] networkBehavioursCache; + + // member used to mark a identity for future reset + // check MarkForReset for more information. + bool m_Reset; + + /// + /// Returns true if running as a client and this object was spawned by a server. + /// + public bool isClient { get; internal set; } + + /// + /// Returns true if NetworkServer.active and server is not stopped. + /// + public bool isServer + { + get => m_IsServer && NetworkServer.active && netId != 0; + internal set => m_IsServer = value; + } + + /// + /// This returns true if this object is the one that represents the player on the local machine. + /// This is set when the server has spawned an object for this particular client. + /// + public bool isLocalPlayer { get; private set; } + + internal bool pendingOwner { get; set; } + + /// + /// This returns true if this object is the authoritative version of the object in the distributed network application. + /// This value is determined at runtime, as opposed to localPlayerAuthority which is set on the prefab. For most objects, authority is held by the server / host. For objects with localPlayerAuthority set, authority is held by the client of that player. + /// For objects that had their authority set by AssignClientAuthority on the server, this will be true on the client that owns the object. NOT on other clients. + /// + public bool hasAuthority { get; private set; } + + /// + /// The set of network connections (players) that can see this object. + /// null until OnStartServer was called. this is necessary for SendTo* to work properly in server-only mode. + /// + public Dictionary observers; + + /// + /// Unique identifier for this particular object instance, used for tracking objects between networked clients and the server. + /// This is a unique identifier for this particular GameObject instance. Use it to track GameObjects between networked clients and the server. + /// + public uint netId { get; internal set; } + + /// + /// A unique identifier for NetworkIdentity objects within a scene. + /// This is used for spawning scene objects on clients. + /// + public ulong sceneId => m_SceneId; + + /// + /// Flag to make this object only exist when the game is running as a server (or host). + /// + [FormerlySerializedAs("m_ServerOnly")] + public bool serverOnly; + + /// + /// localPlayerAuthority means that the client of the "owning" player has authority over their own player object. + /// Authority for this object will be on the player's client. So hasAuthority will be true on that client - and false on the server and on other clients. + /// + [FormerlySerializedAs("m_LocalPlayerAuthority")] + public bool localPlayerAuthority; + + /// + /// The client that has authority for this object. This will be null if no client has authority. + /// This is set for player objects with localPlayerAuthority, and for objects set with AssignClientAuthority, and spawned with SpawnWithClientAuthority. + /// + public NetworkConnection clientAuthorityOwner { get; internal set; } + + /// + /// The NetworkConnection associated with this NetworkIdentity. This is only valid for player objects on a local client. + /// + public NetworkConnection connectionToServer { get; internal set; } + + /// + /// The NetworkConnection associated with this NetworkIdentity. This is only valid for player objects on the server. + /// Use it to return details such as the connection's identity, IP address and ready status. + /// + public NetworkConnection connectionToClient { get; internal set; } + + /// + /// All spawned NetworkIdentities by netId. Available on server and client. + /// + public static readonly Dictionary spawned = new Dictionary(); + + public NetworkBehaviour[] NetworkBehaviours => networkBehavioursCache = networkBehavioursCache ?? GetComponents(); + + [SerializeField] string m_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 + // - 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 + /// + /// Unique identifier used to find the source assets when server spawns the on clients. + /// + public Guid assetId + { + get + { +#if UNITY_EDITOR + // This is important because sometimes OnValidate does not run (like when adding view to prefab with no child links) + if (string.IsNullOrEmpty(m_AssetId)) + SetupIDs(); +#endif + // convert string to Guid and use .Empty to avoid exception if + // we would use 'new Guid("")' + return string.IsNullOrEmpty(m_AssetId) ? Guid.Empty : new Guid(m_AssetId); + } + internal set + { + string newAssetIdString = value.ToString("N"); + if (string.IsNullOrEmpty(m_AssetId) || m_AssetId == newAssetIdString) + { + m_AssetId = newAssetIdString; + } + else Debug.LogWarning("SetDynamicAssetId object already has an assetId <" + m_AssetId + ">"); + } + } + + // persistent scene id + // (see AssignSceneID comments) + // suppress "Field 'NetworkIdentity.m_SceneId' is never assigned to, and will always have its default value 0" + // when building standalone + #pragma warning disable CS0649 + [SerializeField] ulong m_SceneId; + #pragma warning restore CS0649 + + // keep track of all sceneIds to detect scene duplicates + static readonly Dictionary sceneIds = new Dictionary(); + + // used when adding players + internal void SetClientOwner(NetworkConnection conn) + { + if (clientAuthorityOwner != null) + { + Debug.LogError("SetClientOwner m_ClientAuthorityOwner already set!"); + } + clientAuthorityOwner = conn; + clientAuthorityOwner.AddOwnedObject(this); + } + + internal void ForceAuthority(bool authority) + { + if (hasAuthority == authority) + { + return; + } + + hasAuthority = authority; + if (authority) + { + OnStartAuthority(); + } + else + { + OnStopAuthority(); + } + } + + static uint nextNetworkId = 1; + internal static uint GetNextNetworkId() => nextNetworkId++; + + /// + /// Resets nextNetworkId = 1 + /// + public static void ResetNextNetworkId() => nextNetworkId = 1; + + /// + /// The delegate type for the clientAuthorityCallback. + /// + /// The network connection that is gaining or losing authority. + /// The object whose client authority status is being changed. + /// The new state of client authority of the object for the connection. + public delegate void ClientAuthorityCallback(NetworkConnection conn, NetworkIdentity identity, bool authorityState); + + /// + /// A callback that can be populated to be notified when the client-authority state of objects changes. + /// Whenever an object is spawned using SpawnWithClientAuthority, or the client authority status of an object is changed with AssignClientAuthority or RemoveClientAuthority, then this callback will be invoked. + /// This callback is used by the NetworkMigrationManager to distribute client authority state to peers for host migration. If the NetworkMigrationManager is not being used, this callback does not need to be populated. + /// + public static ClientAuthorityCallback clientAuthorityCallback; + + // used when the player object for a connection changes + internal void SetNotLocalPlayer() + { + isLocalPlayer = false; + + if (NetworkServer.active && NetworkServer.localClientActive) + { + // dont change authority for objects on the host + return; + } + hasAuthority = false; + } + + // this is used when a connection is destroyed, since the "observers" property is read-only + internal void RemoveObserverInternal(NetworkConnection conn) + { + observers?.Remove(conn.connectionId); + } + + void Awake() + { + // detect runtime sceneId duplicates, e.g. if a user tries to + // Instantiate a sceneId object at runtime. if we don't detect it, + // then the client won't know which of the two objects to use for a + // SpawnSceneObject message, and it's likely going to be the wrong + // object. + // + // This might happen if for example we have a Dungeon GameObject + // which contains a Skeleton monster as child, and when a player + // runs into the Dungeon we create a Dungeon Instance of that + // Dungeon, which would duplicate a scene object. + // + // see also: https://github.com/vis2k/Mirror/issues/384 + if (Application.isPlaying && sceneId != 0) + { + if (sceneIds.TryGetValue(sceneId, out NetworkIdentity existing) && existing != this) + { + Debug.LogError(name + "'s sceneId: " + sceneId.ToString("X") + " is already taken by: " + existing.name + ". Don't call Instantiate for NetworkIdentities that were in the scene since the beginning (aka scene objects). Otherwise the client won't know which object to use for a SpawnSceneObject message."); + Destroy(gameObject); + } + else + { + sceneIds[sceneId] = this; + } + } + } + + void OnValidate() + { +#if UNITY_EDITOR + if (serverOnly && localPlayerAuthority) + { + Debug.LogWarning("Disabling Local Player Authority for " + gameObject + " because it is server-only."); + localPlayerAuthority = false; + } + + SetupIDs(); +#endif + } + +#if UNITY_EDITOR + void AssignAssetID(GameObject prefab) => AssignAssetID(AssetDatabase.GetAssetPath(prefab)); + void AssignAssetID(string path) => m_AssetId = AssetDatabase.AssetPathToGUID(path); + + bool ThisIsAPrefab() => PrefabUtility.IsPartOfPrefabAsset(gameObject); + + bool ThisIsASceneObjectWithPrefabParent(out GameObject prefab) + { + prefab = null; + + if (!PrefabUtility.IsPartOfPrefabInstance(gameObject)) + { + return false; + } + prefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); + + if (prefab == null) + { + Debug.LogError("Failed to find prefab parent for scene object [name:" + gameObject.name + "]"); + return false; + } + return true; + } + + static uint GetRandomUInt() + { + // 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); + } + } + + // persistent sceneId assignment + // (because scene objects have no persistent unique ID in Unity) + // + // original UNET used OnPostProcessScene to assign an index based on + // FindObjectOfType order. + // -> this didn't work because FindObjectOfType order isn't deterministic. + // -> one workaround is to sort them by sibling paths, but it can still + // get out of sync when we open scene2 in editor and we have + // DontDestroyOnLoad objects that messed with the sibling index. + // + // we absolutely need a persistent id. challenges: + // * it needs to be 0 for prefabs + // => we set it to 0 in SetupIDs() if prefab! + // * it needs to be only assigned in edit time, not at runtime because + // only the objects that were in the scene since beginning should have + // a scene id. + // => Application.isPlaying check solves that + // * it needs to detect duplicated sceneIds after duplicating scene + // objects + // => sceneIds dict takes care of that + // * duplicating the whole scene file shouldn't result in duplicate + // scene objects + // => buildIndex is shifted into sceneId for that. + // => if we have no scenes in build index then it doesn't matter + // because by definition a build can't switch to other scenes + // => if we do have scenes in build index then it will be != -1 + // note: the duplicated scene still needs to be opened once for it to + // be set properly + // * scene objects need the correct scene index byte even if the scene's + // build index was changed or a duplicated scene wasn't opened yet. + // => OnPostProcessScene is the only function that gets called for + // each scene before runtime, so this is where we set the scene + // byte. + // * disabled scenes in build settings should result in same scene index + // in editor and in build + // => .gameObject.scene.buildIndex filters out disabled scenes by + // default + // * generated sceneIds absolutely need to set scene dirty and force the + // user to resave. + // => Undo.RecordObject does that perfectly. + // * sceneIds should never be generated temporarily for unopened scenes + // when building, otherwise editor and build get out of sync + // => BuildPipeline.isBuildingPlayer check solves that + void AssignSceneID() + { + // we only ever assign sceneIds at edit time, never at runtime. + // by definition, only the original scene objects should get one. + // -> if we assign at runtime then server and client would generate + // different random numbers! + if (Application.isPlaying) + return; + + // no valid sceneId yet, or duplicate? + bool duplicate = sceneIds.TryGetValue(m_SceneId, out NetworkIdentity existing) && existing != null && existing != this; + if (m_SceneId == 0 || duplicate) + { + // clear in any case, because it might have been a duplicate + m_SceneId = 0; + + // if a scene was never opened and we are building it, then a + // sceneId would be assigned to build but not saved in editor, + // resulting in them getting out of sync. + // => don't ever assign temporary ids. they always need to be + // permanent + // => throw an exception to cancel the build and let the user + // know how to fix it! + if (BuildPipeline.isBuildingPlayer) + throw new Exception("Scene " + gameObject.scene.path + " needs to be opened and resaved before building, because the scene object " + name + " has no valid sceneId yet."); + + // if we generate the sceneId then we MUST be sure to set dirty + // in order to save the scene object properly. otherwise it + // would be regenerated every time we reopen the scene, and + // upgrading would be very difficult. + // -> Undo.RecordObject is the new EditorUtility.SetDirty! + // -> we need to call it before changing. + Undo.RecordObject(this, "Generated SceneId"); + + // generate random sceneId part (0x00000000FFFFFFFF) + uint randomId = GetRandomUInt(); + + // only assign if not a duplicate of an existing scene id + // (small chance, but possible) + duplicate = sceneIds.TryGetValue(randomId, out existing) && existing != null && existing != this; + if (!duplicate) + { + m_SceneId = randomId; + //Debug.Log(name + " in scene=" + gameObject.scene.name + " sceneId assigned to: " + m_SceneId.ToString("X")); + } + } + + // add to sceneIds dict no matter what + // -> even if we didn't generate anything new, because we still need + // existing sceneIds in there to check duplicates + sceneIds[m_SceneId] = this; + } + + // copy scene path hash into sceneId for scene objects. + // this is the only way for scene file duplication to not contain + // duplicate sceneIds as it seems. + // -> sceneId before: 0x00000000AABBCCDD + // -> then we clear the left 4 bytes, so that our 'OR' uses 0x00000000 + // -> then we OR the hash into the 0x00000000 part + // -> buildIndex is not enough, because Editor and Build have different + // build indices if there are disabled scenes in build settings, and + // if no scene is in build settings then Editor and Build have + // different indices too (Editor=0, Build=-1) + // => ONLY USE THIS FROM POSTPROCESSSCENE! + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetSceneIdSceneHashPartInternal() + { + // get deterministic scene hash + uint pathHash = (uint)gameObject.scene.path.GetStableHashCode(); + + // shift hash from 0x000000FFFFFFFF to 0xFFFFFFFF00000000 + ulong shiftedHash = (ulong)pathHash << 32; + + // OR into scene id + m_SceneId = (m_SceneId & 0xFFFFFFFF) | shiftedHash; + + // log it. this is incredibly useful to debug sceneId issues. + if (LogFilter.Debug) Debug.Log(name + " in scene=" + gameObject.scene.name + " scene index hash(" + pathHash.ToString("X") + ") copied into sceneId: " + m_SceneId.ToString("X")); + } + + void SetupIDs() + { + if (ThisIsAPrefab()) + { + m_SceneId = 0; // force 0 for prefabs + AssignAssetID(gameObject); + } + // check prefabstage BEFORE SceneObjectWithPrefabParent + // (fixes https://github.com/vis2k/Mirror/issues/976) + else if (PrefabStageUtility.GetCurrentPrefabStage() != null) + { + m_SceneId = 0; // force 0 for prefabs + string path = PrefabStageUtility.GetCurrentPrefabStage().prefabAssetPath; + AssignAssetID(path); + } + else if (ThisIsASceneObjectWithPrefabParent(out GameObject prefab)) + { + AssignSceneID(); + AssignAssetID(prefab); + } + else + { + AssignSceneID(); + m_AssetId = ""; + } + } +#endif + + void OnDestroy() + { + // remove from sceneIds + // -> remove with (0xFFFFFFFFFFFFFFFF) and without (0x00000000FFFFFFFF) + // sceneHash to be 100% safe. + sceneIds.Remove(sceneId); + sceneIds.Remove(sceneId & 0x00000000FFFFFFFF); + + if (m_IsServer && NetworkServer.active) + { + NetworkServer.Destroy(gameObject); + } + } + + internal void OnStartServer(bool allowNonZeroNetId) + { + if (m_IsServer) + { + return; + } + m_IsServer = true; + hasAuthority = !localPlayerAuthority; + + observers = new Dictionary(); + + // 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 + if (netId == 0) + { + netId = GetNextNetworkId(); + } + else + { + if (!allowNonZeroNetId) + { + Debug.LogError("Object has non-zero netId " + netId + " for " + gameObject); + return; + } + } + + if (LogFilter.Debug) Debug.Log("OnStartServer " + this + " NetId:" + netId + " SceneId:" + sceneId); + + // add to spawned (note: the original EnableIsServer isn't needed + // because we already set m_isServer=true above) + spawned[netId] = this; + + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + try + { + comp.OnStartServer(); + } + catch (Exception e) + { + Debug.LogError("Exception in OnStartServer:" + e.Message + " " + e.StackTrace); + } + } + + if (NetworkClient.active && NetworkServer.localClientActive) + { + // there will be no spawn message, so start the client here too + OnStartClient(); + } + + if (hasAuthority) + { + OnStartAuthority(); + } + } + + internal void OnStartClient() + { + isClient = true; + + if (LogFilter.Debug) Debug.Log("OnStartClient " + gameObject + " netId:" + netId + " localPlayerAuthority:" + localPlayerAuthority); + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + try + { + comp.OnStartClient(); // user implemented startup + } + catch (Exception e) + { + Debug.LogError("Exception in OnStartClient:" + e.Message + " " + e.StackTrace); + } + } + } + + void OnStartAuthority() + { + if (networkBehavioursCache == null) + { + Debug.LogError("Network object " + name + " not initialized properly. Do you have more than one NetworkIdentity in the same object? Did you forget to spawn this object with NetworkServer?", this); + return; + } + + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + try + { + comp.OnStartAuthority(); + } + catch (Exception e) + { + Debug.LogError("Exception in OnStartAuthority:" + e.Message + " " + e.StackTrace); + } + } + } + + void OnStopAuthority() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + try + { + comp.OnStopAuthority(); + } + catch (Exception e) + { + Debug.LogError("Exception in OnStopAuthority:" + e.Message + " " + e.StackTrace); + } + } + } + + internal void OnSetLocalVisibility(bool vis) + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + try + { + comp.OnSetLocalVisibility(vis); + } + catch (Exception e) + { + Debug.LogError("Exception in OnSetLocalVisibility:" + e.Message + " " + e.StackTrace); + } + } + } + + internal bool OnCheckObserver(NetworkConnection conn) + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + try + { + if (!comp.OnCheckObserver(conn)) + return false; + } + catch (Exception e) + { + Debug.LogError("Exception in OnCheckObserver:" + e.Message + " " + e.StackTrace); + } + } + return true; + } + + // vis2k: readstring bug prevention: https://issuetracker.unity3d.com/issues/unet-networkwriter-dot-write-causing-readstring-slash-readbytes-out-of-range-errors-in-clients + // -> 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) + { + // 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; + writer.WriteInt32(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=" + m_SceneId.ToString("X") + "\n\n" + e); + } + int endPosition = writer.Position; + + // fill in length now + writer.Position = headerPosition; + writer.WriteInt32(endPosition - contentPosition); + writer.Position = endPosition; + + if (LogFilter.Debug) Debug.Log("OnSerializeSafely written for object=" + comp.name + " component=" + comp.GetType() + " sceneId=" + m_SceneId.ToString("X") + "header@" + headerPosition + " content@" + contentPosition + " end@" + endPosition + " contentSize=" + (endPosition - contentPosition)); + + return result; + } + + // serialize all components (or only dirty ones if not initial state) + // -> check ownerWritten/observersWritten to know if anything was written + internal void OnSerializeAllSafely(bool initialState, NetworkWriter ownerWriter, out int ownerWritten, NetworkWriter observersWriter, out int observersWritten) + { + // clear 'written' variables + ownerWritten = observersWritten = 0; + + if (NetworkBehaviours.Length > 64) + { + Debug.LogError("Only 64 NetworkBehaviour components are allowed for NetworkIdentity: " + name + " because of the dirtyComponentMask"); + return; + } + ulong dirtyComponentsMask = GetDirtyMask(initialState); + + if (dirtyComponentsMask == 0L) + return; + + // calculate syncMode mask at runtime. this allows users to change + // component.syncMode while the game is running, which can be a huge + // advantage over syncvar-based sync modes. e.g. if a player decides + // to share or not share his inventory, or to go invisible, etc. + // + // (this also lets the TestSynchronizingObjects test pass because + // otherwise if we were to cache it in Awake, then we would call + // GetComponents before all the test behaviours + // were added) + ulong syncModeObserversMask = GetSyncModeObserversMask(); + + // write regular dirty mask for owner, + // writer 'dirty mask & syncMode==Everyone' for everyone else + // (WritePacked64 so we don't write full 8 bytes if we don't have to) + ownerWriter.WritePackedUInt64(dirtyComponentsMask); + observersWriter.WritePackedUInt64(dirtyComponentsMask & syncModeObserversMask); + + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + // 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 + if (initialState || comp.IsDirty()) + { + if (LogFilter.Debug) Debug.Log("OnSerializeAllSafely: " + name + " -> " + comp.GetType() + " initial=" + initialState); + + // serialize into ownerWriter first + // (owner always gets everything!) + int startPosition = ownerWriter.Position; + OnSerializeSafely(comp, ownerWriter, initialState); + ++ownerWritten; + + // 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) + { + ArraySegment segment = ownerWriter.ToArraySegment(); + int length = ownerWriter.Position - startPosition; + observersWriter.WriteBytes(segment.Array, startPosition, length); + ++observersWritten; + } + } + } + } + + internal ulong GetDirtyMask(bool initialState) + { + // loop through all components only once and then write dirty+payload into the writer afterwards + ulong dirtyComponentsMask = 0L; + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < components.Length; ++i) + { + NetworkBehaviour comp = components[i]; + if (initialState || comp.IsDirty()) + { + dirtyComponentsMask |= (ulong)(1L << i); + } + } + + return dirtyComponentsMask; + } + + // a mask that contains all the components with SyncMode.Observers + internal ulong GetSyncModeObserversMask() + { + // loop through all components + ulong mask = 0UL; + NetworkBehaviour[] components = NetworkBehaviours; + for (int i = 0; i < NetworkBehaviours.Length; ++i) + { + NetworkBehaviour comp = components[i]; + if (comp.syncMode == SyncMode.Observers) + { + mask |= 1UL << i; + } + } + + return mask; + } + + void OnDeserializeSafely(NetworkBehaviour comp, NetworkReader reader, bool initialState) + { + // read header as 4 bytes and calculate this chunk's start+end + int contentSize = reader.ReadInt32(); + 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 + { + if (LogFilter.Debug) Debug.Log("OnDeserializeSafely: " + comp.name + " component=" + comp.GetType() + " sceneId=" + m_SceneId.ToString("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 for: object=" + name + " component=" + comp.GetType() + " sceneId=" + m_SceneId.ToString("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" + e); + } + + // now the reader should be EXACTLY at 'before + size'. + // otherwise the component read too much / too less data. + if (reader.Position != chunkEnd) + { + // 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=" + m_SceneId.ToString("X") + ". Make sure that OnSerialize and OnDeserialize write/read the same amount of data in all cases."); + + // fix the position, so the following components don't all fail + reader.Position = chunkEnd; + } + } + + internal void OnDeserializeAllSafely(NetworkReader reader, bool initialState) + { + // read component dirty mask + ulong dirtyComponentsMask = reader.ReadPackedUInt64(); + + NetworkBehaviour[] components = NetworkBehaviours; + // loop through all components and deserialize the dirty ones + for (int i = 0; i < components.Length; ++i) + { + // is the dirty bit at position 'i' set to 1? + ulong dirtyBit = (ulong)(1L << i); + if ((dirtyComponentsMask & dirtyBit) != 0L) + { + OnDeserializeSafely(components[i], reader, initialState); + } + } + } + + // happens on client + internal void HandleClientAuthority(bool authority) + { + if (!localPlayerAuthority) + { + Debug.LogError("HandleClientAuthority " + gameObject + " does not have localPlayerAuthority"); + return; + } + + ForceAuthority(authority); + } + + // helper function to handle SyncEvent/Command/Rpc + void HandleRemoteCall(int componentIndex, int functionHash, MirrorInvokeType invokeType, NetworkReader reader) + { + if (gameObject == null) + { + Debug.LogWarning(invokeType + " [" + functionHash + "] received for deleted object [netId=" + netId + "]"); + return; + } + + // find the right component to invoke the function on + if (0 <= componentIndex && componentIndex < networkBehavioursCache.Length) + { + NetworkBehaviour invokeComponent = networkBehavioursCache[componentIndex]; + if (!invokeComponent.InvokeHandlerDelegate(functionHash, invokeType, reader)) + { + Debug.LogError("Found no receiver for incoming " + invokeType + " [" + functionHash + "] on " + gameObject + ", the server and client should have the same NetworkBehaviour instances [netId=" + netId + "]."); + } + } + else + { + Debug.LogWarning("Component [" + componentIndex + "] not found for [netId=" + netId + "]"); + } + } + + // happens on client + internal void HandleSyncEvent(int componentIndex, int eventHash, NetworkReader reader) + { + HandleRemoteCall(componentIndex, eventHash, MirrorInvokeType.SyncEvent, reader); + } + + // happens on server + internal void HandleCommand(int componentIndex, int cmdHash, NetworkReader reader) + { + HandleRemoteCall(componentIndex, cmdHash, MirrorInvokeType.Command, reader); + } + + // happens on client + internal void HandleRPC(int componentIndex, int rpcHash, NetworkReader reader) + { + HandleRemoteCall(componentIndex, rpcHash, MirrorInvokeType.ClientRpc, reader); + } + + internal void OnUpdateVars(NetworkReader reader, bool initialState) + { + OnDeserializeAllSafely(reader, initialState); + } + + internal void SetLocalPlayer() + { + isLocalPlayer = true; + + // there is an ordering issue here that originAuthority solves. OnStartAuthority should only be called if m_HasAuthority was false when this function began, + // or it will be called twice for this object. But that state is lost by the time OnStartAuthority is called below, so the original value is cached + // here to be checked below. + bool originAuthority = hasAuthority; + if (localPlayerAuthority) + { + hasAuthority = true; + } + + foreach (NetworkBehaviour comp in networkBehavioursCache) + { + comp.OnStartLocalPlayer(); + + if (localPlayerAuthority && !originAuthority) + { + comp.OnStartAuthority(); + } + } + } + + internal void OnNetworkDestroy() + { + for (int i = 0; networkBehavioursCache != null && i < networkBehavioursCache.Length; i++) + { + NetworkBehaviour comp = networkBehavioursCache[i]; + comp.OnNetworkDestroy(); + } + m_IsServer = false; + } + + internal void ClearObservers() + { + if (observers != null) + { + foreach (NetworkConnection conn in observers.Values) + { + conn.RemoveFromVisList(this, true); + } + observers.Clear(); + } + } + + internal void AddObserver(NetworkConnection 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 + // we may have generated one that was already in use. + return; + } + + if (LogFilter.Debug) Debug.Log("Added observer " + conn.address + " added for " + gameObject); + + observers[conn.connectionId] = conn; + conn.AddToVisList(this); + } + + static readonly HashSet newObservers = new HashSet(); + + /// + /// This causes the set of players that can see this object to be rebuild. The OnRebuildObservers callback function will be invoked on each NetworkBehaviour. + /// + /// True if this is the first time. + public void RebuildObservers(bool initialize) + { + if (observers == null) + return; + + bool changed = false; + bool result = false; + + newObservers.Clear(); + + // call OnRebuildObservers function in components + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + result |= comp.OnRebuildObservers(newObservers, initialize); + } + + // if player connection: ensure player always see himself no matter what. + // -> 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 (connectionToClient != null && connectionToClient.isReady) + { + newObservers.Add(connectionToClient); + } + + // if no component implemented OnRebuildObservers, then add all + // connections. + if (!result) + { + if (initialize) + { + foreach (NetworkConnection conn in NetworkServer.connections.Values) + { + if (conn.isReady) + AddObserver(conn); + } + + if (NetworkServer.localConnection != null && NetworkServer.localConnection.isReady) + { + AddObserver(NetworkServer.localConnection); + } + } + return; + } + + // apply changes from rebuild + foreach (NetworkConnection conn in newObservers) + { + if (conn == null) + { + continue; + } + + if (!conn.isReady) + { + if (LogFilter.Debug) Debug.Log("Observer is not ready for " + gameObject + " " + conn); + continue; + } + + if (initialize || !observers.ContainsKey(conn.connectionId)) + { + // new observer + conn.AddToVisList(this); + if (LogFilter.Debug) Debug.Log("New Observer for " + gameObject + " " + conn); + changed = true; + } + } + + foreach (NetworkConnection conn in observers.Values) + { + if (!newObservers.Contains(conn)) + { + // removed observer + conn.RemoveFromVisList(this, false); + if (LogFilter.Debug) Debug.Log("Removed Observer for " + gameObject + " " + conn); + changed = true; + } + } + + // special case for local client. + if (initialize) + { + if (!newObservers.Contains(NetworkServer.localConnection)) + { + OnSetLocalVisibility(false); + } + } + + if (changed) + { + observers.Clear(); + foreach (NetworkConnection conn in newObservers) + { + if (conn.isReady) + observers.Add(conn.connectionId, conn); + } + } + } + + /// + /// Removes ownership for an object for a client by its connection. + /// This applies to objects that had authority set by AssignClientAuthority, or NetworkServer.SpawnWithClientAuthority. Authority cannot be removed for player objects. + /// + /// The connection of the client to remove authority for. + /// True if authority is removed. + public bool RemoveClientAuthority(NetworkConnection conn) + { + if (!isServer) + { + Debug.LogError("RemoveClientAuthority can only be call on the server for spawned objects."); + return false; + } + + if (connectionToClient != null) + { + Debug.LogError("RemoveClientAuthority cannot remove authority for a player object"); + return false; + } + + if (clientAuthorityOwner == null) + { + Debug.LogError("RemoveClientAuthority for " + gameObject + " has no clientAuthority owner."); + return false; + } + + if (clientAuthorityOwner != conn) + { + Debug.LogError("RemoveClientAuthority for " + gameObject + " has different owner."); + return false; + } + + clientAuthorityOwner.RemoveOwnedObject(this); + clientAuthorityOwner = null; + + // server now has authority (this is only called on server) + ForceAuthority(true); + + // send msg to that client + ClientAuthorityMessage msg = new ClientAuthorityMessage + { + netId = netId, + authority = false + }; + conn.Send(msg); + + clientAuthorityCallback?.Invoke(conn, this, false); + return true; + } + + /// + /// Assign control of an object to a client via the client's NetworkConnection. + /// This causes hasAuthority to be set on the client that owns the object, and NetworkBehaviour.OnStartAuthority will be called on that client. This object then will be in the NetworkConnection.clientOwnedObjects list for the connection. + /// Authority can be removed with RemoveClientAuthority. Only one client can own an object at any time. Only NetworkIdentities with localPlayerAuthority set can have client authority assigned. This does not need to be called for player objects, as their authority is setup automatically. + /// + /// The connection of the client to assign authority to. + /// True if authority was assigned. + public bool AssignClientAuthority(NetworkConnection conn) + { + if (!isServer) + { + Debug.LogError("AssignClientAuthority can only be called on the server for spawned objects."); + return false; + } + if (!localPlayerAuthority) + { + Debug.LogError("AssignClientAuthority can only be used for NetworkIdentity components with LocalPlayerAuthority set."); + return false; + } + + if (clientAuthorityOwner != null && conn != clientAuthorityOwner) + { + Debug.LogError("AssignClientAuthority for " + gameObject + " already has an owner. Use RemoveClientAuthority() first."); + return false; + } + + if (conn == null) + { + Debug.LogError("AssignClientAuthority for " + gameObject + " owner cannot be null. Use RemoveClientAuthority() instead."); + return false; + } + + clientAuthorityOwner = conn; + clientAuthorityOwner.AddOwnedObject(this); + + // server no longer has authority (this is called on server). Note that local client could re-acquire authority below + ForceAuthority(false); + + // send msg to that client + ClientAuthorityMessage msg = new ClientAuthorityMessage + { + netId = netId, + authority = true + }; + conn.Send(msg); + + clientAuthorityCallback?.Invoke(conn, this, true); + return true; + } + + // 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 MarkForReset() => m_Reset = true; + + // if we have marked an identity for reset we do the actual reset. + internal void Reset() + { + if (!m_Reset) + return; + + m_Reset = false; + m_IsServer = false; + isClient = false; + hasAuthority = false; + + netId = 0; + isLocalPlayer = false; + connectionToServer = null; + connectionToClient = null; + networkBehavioursCache = null; + + ClearObservers(); + clientAuthorityOwner = null; + } + + // MirrorUpdate is a hot path. Caching the vars msg is really worth it to + // avoid large amounts of allocations. + static UpdateVarsMessage varsMessage = new UpdateVarsMessage(); + + // invoked by NetworkServer during Update() + internal void MirrorUpdate() + { + if (observers != null && observers.Count > 0) + { + // one writer for owner, one for observers + NetworkWriter ownerWriter = NetworkWriterPool.GetWriter(); + NetworkWriter observersWriter = NetworkWriterPool.GetWriter(); + + // serialize all the dirty components and send (if any were dirty) + OnSerializeAllSafely(false, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten); + if (ownerWritten > 0 || observersWritten > 0) + { + // populate cached UpdateVarsMessage and send + varsMessage.netId = netId; + + // send ownerWriter to owner + // (only if we serialized anything for owner) + // (only if there is a connection (e.g. if not a monster), + // and if connection is ready because we use SendToReady + // below too) + if (ownerWritten > 0) + { + varsMessage.payload = ownerWriter.ToArraySegment(); + if (connectionToClient != null && connectionToClient.isReady) + NetworkServer.SendToClientOfPlayer(this, varsMessage); + } + + // send observersWriter to everyone but owner + // (only if we serialized anything for observers) + if (observersWritten > 0) + { + varsMessage.payload = observersWriter.ToArraySegment(); + NetworkServer.SendToReady(this, varsMessage, false); + } + + // only clear bits if we sent something + ClearDirtyBits(); + } + NetworkWriterPool.Recycle(ownerWriter); + NetworkWriterPool.Recycle(observersWriter); + } + else + { + ClearDirtyBits(); + } + } + + private void ClearDirtyBits() + { + foreach (NetworkBehaviour comp in NetworkBehaviours) + { + comp.ClearAllDirtyBits(); + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkIdentity.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkIdentity.cs.meta new file mode 100644 index 0000000..85a8007 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkIdentity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b91ecbcc199f4492b9a91e820070131 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkManager.cs b/Assets/Packages/Mirror/Runtime/NetworkManager.cs new file mode 100644 index 0000000..53e4534 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkManager.cs @@ -0,0 +1,1102 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.SceneManagement; +using UnityEngine.Serialization; + +namespace Mirror +{ + /// + /// Enumeration of methods of where to spawn player objects in multiplayer games. + /// + public enum PlayerSpawnMethod + { + Random, + RoundRobin + } + + [AddComponentMenu("Network/NetworkManager")] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkManager.html")] + public class NetworkManager : MonoBehaviour + { + [Header("Configuration")] + + /// + /// A flag to control whether the NetworkManager object is destroyed when the scene changes. + /// This should be set if your game has a single NetworkManager that exists for the lifetime of the process. If there is a NetworkManager in each scene, then this should not be set. + /// + [FormerlySerializedAs("m_DontDestroyOnLoad")] + public bool dontDestroyOnLoad = true; + + /// + /// Controls whether the program runs when it is in the background. + /// This is required when multiple instances of a program using networking are running on the same machine, such as when testing using localhost. But this is not recommended when deploying to mobile platforms. + /// + [FormerlySerializedAs("m_RunInBackground")] + public bool runInBackground = true; + + /// + /// Automatically invoke StartServer() + /// If the application is a Server Build or run with the -batchMode command line arguement, StartServer is automatically invoked. + /// + public bool startOnHeadless = true; + + /// + /// 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; + + /// + /// Enables verbose debug messages in the console + /// + [FormerlySerializedAs("m_ShowDebugMessages")] + public bool showDebugMessages; + + /// + /// The scene to switch to when offline. + /// Setting this makes the NetworkManager do scene management. This scene will be switched to when a network session is completed - such as a client disconnect, or a server shutdown. + /// + [Scene] + [FormerlySerializedAs("m_OfflineScene")] + public string offlineScene = ""; + + /// + /// The scene to switch to when online. + /// Setting this makes the NetworkManager do scene management. This scene will be switched to when a network session is started - such as a client connect, or a server listen. + /// + [Scene] + [FormerlySerializedAs("m_OnlineScene")] + public string onlineScene = ""; + + [Header("Network Info")] + + // transport layer + [SerializeField] + protected Transport transport; + + /// + /// The network address currently in use. + /// For clients, this is the address of the server that is connected to. For servers, this is the local address. + /// + [FormerlySerializedAs("m_NetworkAddress")] + public string networkAddress = "localhost"; + + /// + /// The maximum number of concurrent network connections to support. + /// This effects the memory usage of the network layer. + /// + [FormerlySerializedAs("m_MaxConnections")] + public int maxConnections = 4; + + [Header("Spawn Info")] + + /// + /// The default prefab to be used to create player objects on the server. + /// Player objects are created in the default handler for AddPlayer() on the server. Implementing OnServerAddPlayer overrides this behaviour. + /// + [FormerlySerializedAs("m_PlayerPrefab")] + public GameObject playerPrefab; + + /// + /// A flag to control whether or not player objects are automatically created on connect, and on scene change. + /// + [FormerlySerializedAs("m_AutoCreatePlayer")] + public bool autoCreatePlayer = true; + + /// + /// The current method of spawning players used by the NetworkManager. + /// + [FormerlySerializedAs("m_PlayerSpawnMethod")] + public PlayerSpawnMethod playerSpawnMethod; + + /// + /// List of prefabs that will be registered with the spawning system. + /// For each of these prefabs, ClientManager.RegisterPrefab() will be automatically invoke. + /// + [FormerlySerializedAs("m_SpawnPrefabs"), HideInInspector] + public List spawnPrefabs = new List(); + + /// + /// List of transforms populted by NetworkStartPosition components found in the scene. + /// + public static List startPositions = new List(); + + /// + /// This is true if the client loaded a new scene when connecting to the server. + /// This is set before OnClientConnect is called, so it can be checked there to perform different logic if a scene load occurred. + /// + [NonSerialized] + public bool clientLoadedScene; + + /// + /// Number of active player objects across all connections on the server. + /// This is only valid on the host / server. + /// + public int numPlayers => NetworkServer.connections.Count(kv => kv.Value.playerController != null); + + /// + /// The name of the current network scene. + /// + /// + /// This is populated if the NetworkManager is doing scene management. This should not be changed directly. Calls to ServerChangeScene() cause this to change. New clients that connect to a server will automatically load this scene. + /// This is used to make sure that all scene changes are initialized by Mirror. + /// Loading a scene manually wont set networkSceneName, so Mirror would still load it again on start. + /// + public static string networkSceneName = ""; + + /// + /// True if the server or client is started and running + /// This is set True in StartServer / StartClient, and set False in StopServer / StopClient + /// + [NonSerialized] + public bool isNetworkActive; + + /// + /// Obsolete: Use directly + /// For example, use NetworkClient.Send(message) instead of NetworkManager.client.Send(message) + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkClient directly, it will be made static soon. For example, use NetworkClient.Send(message) instead of NetworkManager.client.Send(message)")] + public NetworkClient client => NetworkClient.singleton; + + static int startPositionIndex; + + /// + /// NetworkManager singleton + /// + public static NetworkManager singleton; + + static UnityEngine.AsyncOperation loadingSceneAsync; + static NetworkConnection clientReadyConnection; + + /// + /// virtual so that inheriting classes' Awake() can call base.Awake() too + /// + public virtual void Awake() + { + Debug.Log("Thank you for using Mirror! https://mirror-networking.com"); + + // Set the networkSceneName to prevent a scene reload + // if client connection to server fails. + networkSceneName = offlineScene; + + InitializeSingleton(); + + // setup OnSceneLoaded callback + SceneManager.sceneLoaded += OnSceneLoaded; + } + + /// + /// headless mode detection + /// + public static bool isHeadless => SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null; + + /// + /// Obsolete: Use instead. + /// This is a static property now. This method will be removed by summer 2019. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use isHeadless instead of IsHeadless()")] + public static bool IsHeadless() + { + return isHeadless; + } + + void InitializeSingleton() + { + if (singleton != null && singleton == this) + { + return; + } + + // do this early + LogFilter.Debug = showDebugMessages; + + if (dontDestroyOnLoad) + { + if (singleton != null) + { + Debug.LogWarning("Multiple NetworkManagers detected in the scene. Only one NetworkManager can exist at a time. The duplicate NetworkManager will be destroyed."); + Destroy(gameObject); + return; + } + if (LogFilter.Debug) Debug.Log("NetworkManager created singleton (DontDestroyOnLoad)"); + singleton = this; + if (Application.isPlaying) DontDestroyOnLoad(gameObject); + } + else + { + if (LogFilter.Debug) Debug.Log("NetworkManager created singleton (ForScene)"); + singleton = this; + } + + // set active transport AFTER setting singleton. + // so only if we didn't destroy ourselves. + Transport.activeTransport = transport; + } + + /// + /// 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. + // + // (tick rate is applied in StartServer!) + if (isHeadless && startOnHeadless) + { + StartServer(); + } + } + + // support additive scene loads: + // NetworkScenePostProcess disables all scene objects on load, and + // * NetworkServer.SpawnObjects enables them again on the server when + // calling OnStartServer + // * ClientScene.PrepareToSpawnSceneObjects enables them again on the + // client after the server sends ObjectSpawnStartedMessage to client + // in SpawnObserversForConnection. this is only called when the + // client joins, so we need to rebuild scene objects manually again + // TODO merge this with FinishLoadScene()? + void OnSceneLoaded(Scene scene, LoadSceneMode mode) + { + if (mode == LoadSceneMode.Additive) + { + if (NetworkServer.active) + { + // TODO only respawn the server objects from that scene later! + NetworkServer.SpawnObjects(); + Debug.Log("Respawned Server objects after additive scene load: " + scene.name); + } + if (NetworkClient.active) + { + ClientScene.PrepareToSpawnSceneObjects(); + Debug.Log("Rebuild Client spawnableObjects after additive scene load: " + scene.name); + } + } + } + + // NetworkIdentity.UNetStaticUpdate is called from UnityEngine while LLAPI network is active. + // If we want TCP then we need to call it manually. Probably best from NetworkManager, although this means that we can't use NetworkServer/NetworkClient without a NetworkManager invoking Update anymore. + /// + /// virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too + /// + public virtual void LateUpdate() + { + // call it while the NetworkManager exists. + // -> we don't only call while Client/Server.Connected, because then we would stop if disconnected and the + // NetworkClient wouldn't receive the last Disconnect event, result in all kinds of issues + NetworkServer.Update(); + NetworkClient.Update(); + UpdateScene(); + } + + /// + /// 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 + /// + public virtual void OnApplicationQuit() + { + // stop client first + // (we want to send the quit packet to the server instead of waiting + // for a timeout) + if (NetworkClient.isConnected) + { + StopClient(); + print("OnApplicationQuit: stopped client"); + } + + // stop server after stopping client (for proper host mode stopping) + if (NetworkServer.active) + { + StopServer(); + print("OnApplicationQuit: stopped server"); + } + + // stop transport (e.g. to shut down threads) + // (when pressing Stop in the Editor, Unity keeps threads alive + // until we press Start again. so if Transports use threads, we + // really want them to end now and not after next start) + Transport.activeTransport.Shutdown(); + } + + /// + /// virtual so that inheriting classes' OnValidate() can call base.OnValidate() too + /// + public virtual void OnValidate() + { + // add transport if there is none yet. makes upgrading easier. + if (transport == null) + { + // was a transport added yet? if not, add one + transport = GetComponent(); + if (transport == null) + { + transport = gameObject.AddComponent(); + Debug.Log("NetworkManager: added default Transport because there was none yet."); + } +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(gameObject); +#endif + } + + maxConnections = Mathf.Max(maxConnections, 0); // always >= 0 + + if (playerPrefab != null && playerPrefab.GetComponent() == null) + { + Debug.LogError("NetworkManager - playerPrefab must have a NetworkIdentity."); + playerPrefab = null; + } + } + + void RegisterServerMessages() + { + NetworkServer.RegisterHandler(OnServerConnectInternal); + NetworkServer.RegisterHandler(OnServerDisconnectInternal); + NetworkServer.RegisterHandler(OnServerReadyMessageInternal); + NetworkServer.RegisterHandler(OnServerAddPlayerInternal); + NetworkServer.RegisterHandler(OnServerRemovePlayerMessageInternal); + NetworkServer.RegisterHandler(OnServerErrorInternal); + } + + /// + /// Set the frame rate for a headless server. + /// Override if you wish to disable the behavior or set your own tick rate. + /// + public virtual void ConfigureServerFrameRate() + { + // set a fixed tick rate instead of updating as often as possible + // * if not in Editor (it doesn't work in the Editor) + // * if not in Host mode +#if !UNITY_EDITOR + if (!NetworkClient.active && isHeadless) + { + Application.targetFrameRate = serverTickRate; + Debug.Log("Server Tick Rate set to: " + Application.targetFrameRate + " Hz."); + } +#endif + } + + /// + /// This starts a new server. + /// This uses the networkPort property as the listen port. + /// + /// + public bool StartServer() + { + InitializeSingleton(); + + if (runInBackground) + Application.runInBackground = true; + + ConfigureServerFrameRate(); + + if (!NetworkServer.Listen(maxConnections)) + { + Debug.LogError("StartServer listen failed."); + return false; + } + + // 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(); + + if (LogFilter.Debug) Debug.Log("NetworkManager StartServer"); + isNetworkActive = true; + + // Only change scene if the requested online scene is not blank, and is not already loaded + string loadedSceneName = SceneManager.GetActiveScene().name; + if (!string.IsNullOrEmpty(onlineScene) && onlineScene != loadedSceneName && onlineScene != offlineScene) + { + ServerChangeScene(onlineScene); + } + else + { + NetworkServer.SpawnObjects(); + } + return true; + } + + void RegisterClientMessages() + { + NetworkClient.RegisterHandler(OnClientConnectInternal); + NetworkClient.RegisterHandler(OnClientDisconnectInternal); + NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal); + NetworkClient.RegisterHandler(OnClientErrorInternal); + NetworkClient.RegisterHandler(OnClientSceneInternal); + + if (playerPrefab != null) + { + ClientScene.RegisterPrefab(playerPrefab); + } + for (int i = 0; i < spawnPrefabs.Count; i++) + { + GameObject prefab = spawnPrefabs[i]; + if (prefab != null) + { + ClientScene.RegisterPrefab(prefab); + } + } + } + + /// + /// This starts a network client. It uses the networkAddress and networkPort properties as the address to connect to. + /// This makes the newly created client connect to the server immediately. + /// + public void StartClient() + { + InitializeSingleton(); + + if (runInBackground) + Application.runInBackground = true; + + isNetworkActive = true; + + RegisterClientMessages(); + + if (string.IsNullOrEmpty(networkAddress)) + { + Debug.LogError("Must set the Network Address field in the manager"); + return; + } + if (LogFilter.Debug) Debug.Log("NetworkManager StartClient address:" + networkAddress); + + NetworkClient.Connect(networkAddress); + + OnStartClient(); + } + + /// + /// This starts a network "host" - a server and client in the same application. + /// The client returned from StartHost() is a special "local" client that communicates to the in-process server using a message queue instead of the real network. But in almost all other cases, it can be treated as a normal client. + /// + public virtual void StartHost() + { + OnStartHost(); + if (StartServer()) + { + ConnectLocalClient(); + OnStartClient(); + } + } + + void ConnectLocalClient() + { + if (LogFilter.Debug) Debug.Log("NetworkManager StartHost"); + networkAddress = "localhost"; + NetworkServer.ActivateLocalClientScene(); + NetworkClient.ConnectLocalServer(); + RegisterClientMessages(); + } + + /// + /// This stops both the client and the server that the manager is using. + /// + public void StopHost() + { + OnStopHost(); + + StopServer(); + StopClient(); + } + + /// + /// Stops the server that the manager is using. + /// + public void StopServer() + { + if (!NetworkServer.active) + return; + + OnStopServer(); + + if (LogFilter.Debug) Debug.Log("NetworkManager StopServer"); + isNetworkActive = false; + NetworkServer.Shutdown(); + if (!string.IsNullOrEmpty(offlineScene)) + { + ServerChangeScene(offlineScene); + } + CleanupNetworkIdentities(); + } + + /// + /// Stops the client that the manager is using. + /// + public void StopClient() + { + OnStopClient(); + + if (LogFilter.Debug) Debug.Log("NetworkManager StopClient"); + isNetworkActive = false; + + // shutdown client + NetworkClient.Disconnect(); + NetworkClient.Shutdown(); + + if (!string.IsNullOrEmpty(offlineScene)) + { + ClientChangeScene(offlineScene, LoadSceneMode.Single, LocalPhysicsMode.None); + } + CleanupNetworkIdentities(); + } + + /// + /// 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 autmatically 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 virtual void ServerChangeScene(string newSceneName) + { + ServerChangeScene(newSceneName, LoadSceneMode.Single, LocalPhysicsMode.None); + } + + /// + /// 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 autmatically 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 virtual void ServerChangeScene(string newSceneName, LoadSceneMode sceneMode, LocalPhysicsMode physicsMode) + { + if (string.IsNullOrEmpty(newSceneName)) + { + Debug.LogError("ServerChangeScene empty scene name"); + return; + } + + if (LogFilter.Debug) Debug.Log("ServerChangeScene " + newSceneName); + NetworkServer.SetAllClientsNotReady(); + networkSceneName = newSceneName; + + // Let server prepare for scene change + OnServerChangeScene(newSceneName); + + LoadSceneParameters loadSceneParameters = new LoadSceneParameters(sceneMode, physicsMode); + + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName, loadSceneParameters); + + SceneMessage msg = new SceneMessage() + { + sceneName = newSceneName, + sceneMode = loadSceneParameters.loadSceneMode, + physicsMode = loadSceneParameters.localPhysicsMode + }; + + NetworkServer.SendToAll(msg); + + startPositionIndex = 0; + startPositions.Clear(); + } + + void CleanupNetworkIdentities() + { + foreach (NetworkIdentity identity in Resources.FindObjectsOfTypeAll()) + { + identity.MarkForReset(); + } + } + + internal void ClientChangeScene(string newSceneName, LoadSceneMode sceneMode, LocalPhysicsMode physicsMode) + { + if (string.IsNullOrEmpty(newSceneName)) + { + Debug.LogError("ClientChangeScene empty scene name"); + return; + } + + if (LogFilter.Debug) Debug.Log("ClientChangeScene newSceneName:" + newSceneName + " networkSceneName:" + networkSceneName); + + // vis2k: pause message handling while loading scene. otherwise we will process messages and then lose all + // the state as soon as the load is finishing, causing all kinds of bugs because of missing state. + // (client may be null after StopClient etc.) + if (LogFilter.Debug) Debug.Log("ClientChangeScene: pausing handlers while scene is loading to avoid data loss after scene was loaded."); + Transport.activeTransport.enabled = false; + + // Let client prepare for scene change + OnClientChangeScene(newSceneName); + + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName, new LoadSceneParameters() + { + loadSceneMode = sceneMode, + localPhysicsMode = physicsMode, + }); + networkSceneName = newSceneName; //This should probably not change if additive is used + } + + void FinishLoadScene() + { + // NOTE: this cannot use NetworkClient.allClients[0] - that client may be for a completely different purpose. + + // process queued messages that we received while loading the scene + if (LogFilter.Debug) Debug.Log("FinishLoadScene: resuming handlers after scene was loading."); + Transport.activeTransport.enabled = true; + + if (clientReadyConnection != null) + { + clientLoadedScene = true; + OnClientConnect(clientReadyConnection); + clientReadyConnection = null; + } + + if (NetworkServer.active) + { + NetworkServer.SpawnObjects(); + OnServerSceneChanged(networkSceneName); + } + + if (NetworkClient.isConnected) + { + RegisterClientMessages(); + OnClientSceneChanged(NetworkClient.connection); + } + } + + static void UpdateScene() + { + if (singleton != null && loadingSceneAsync != null && loadingSceneAsync.isDone) + { + if (LogFilter.Debug) Debug.Log("ClientChangeScene done readyCon:" + clientReadyConnection); + singleton.FinishLoadScene(); + loadingSceneAsync.allowSceneActivation = true; + loadingSceneAsync = null; + } + } + + /// + /// virtual so that inheriting classes' OnDestroy() can call base.OnDestroy() too + /// + public virtual void OnDestroy() + { + if (LogFilter.Debug) Debug.Log("NetworkManager destroyed"); + } + + /// + /// Registers the transform of a game object as a player spawn location. + /// This is done automatically by NetworkStartPosition components, but can be done manually from user script code. + /// + /// Transform to register. + public static void RegisterStartPosition(Transform start) + { + if (LogFilter.Debug) Debug.Log("RegisterStartPosition: (" + start.gameObject.name + ") " + start.position); + startPositions.Add(start); + + // reorder the list so that round-robin spawning uses the start positions + // in hierarchy order. This assumes all objects with NetworkStartPosition + // component are siblings, either in the scene root or together as children + // under a single parent in the scene. + startPositions = startPositions.OrderBy(transform => transform.GetSiblingIndex()).ToList(); + } + + /// + /// Unregisters the transform of a game object as a player spawn location. + /// This is done automatically by the NetworkStartPosition component, but can be done manually from user code. + /// + /// Transform to unregister. + public static void UnRegisterStartPosition(Transform start) + { + if (LogFilter.Debug) Debug.Log("UnRegisterStartPosition: (" + start.gameObject.name + ") " + start.position); + startPositions.Remove(start); + } + + /// + /// Obsolete: Use instead + /// + /// Returns True if NetworkClient.isConnected + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkClient.isConnected instead")] + public bool IsClientConnected() + { + return NetworkClient.isConnected; + } + + /// + /// This is the only way to clear the singleton, so another instance can be created. + /// + public static void Shutdown() + { + if (singleton == null) + return; + + startPositions.Clear(); + startPositionIndex = 0; + clientReadyConnection = null; + + singleton.StopHost(); + singleton = null; + } + + #region Server Internal Message Handlers + + void OnServerConnectInternal(NetworkConnection conn, ConnectMessage connectMsg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerConnectInternal"); + + if (networkSceneName != "" && networkSceneName != offlineScene) + { + SceneMessage msg = new SceneMessage() { sceneName = networkSceneName }; + conn.Send(msg); + } + + OnServerConnect(conn); + } + + void OnServerDisconnectInternal(NetworkConnection conn, DisconnectMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerDisconnectInternal"); + OnServerDisconnect(conn); + } + + void OnServerReadyMessageInternal(NetworkConnection conn, ReadyMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerReadyMessageInternal"); + OnServerReady(conn); + } + + void OnServerAddPlayerInternal(NetworkConnection conn, AddPlayerMessage extraMessage) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerAddPlayer"); + + if (autoCreatePlayer && playerPrefab == null) + { + Debug.LogError("The PlayerPrefab is empty on the NetworkManager. Please setup a PlayerPrefab object."); + return; + } + + if (autoCreatePlayer && playerPrefab.GetComponent() == null) + { + Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); + return; + } + + if (conn.playerController != null) + { + Debug.LogError("There is already a player for this connection."); + return; + } + + OnServerAddPlayer(conn, extraMessage); + } + + void OnServerRemovePlayerMessageInternal(NetworkConnection conn, RemovePlayerMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerRemovePlayerMessageInternal"); + + if (conn.playerController != null) + { + OnServerRemovePlayer(conn, conn.playerController); + conn.playerController = null; + } + } + + void OnServerErrorInternal(NetworkConnection conn, ErrorMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerErrorInternal"); + OnServerError(conn, msg.value); + } + + #endregion + + #region Client Internal Message Handlers + + void OnClientConnectInternal(NetworkConnection conn, ConnectMessage message) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnClientConnectInternal"); + + string loadedSceneName = SceneManager.GetActiveScene().name; + if (string.IsNullOrEmpty(onlineScene) || onlineScene == offlineScene || loadedSceneName == onlineScene) + { + clientLoadedScene = false; + OnClientConnect(conn); + } + else + { + // will wait for scene id to come from the server. + clientReadyConnection = conn; + } + } + + void OnClientDisconnectInternal(NetworkConnection conn, DisconnectMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnClientDisconnectInternal"); + OnClientDisconnect(conn); + } + + void OnClientNotReadyMessageInternal(NetworkConnection conn, NotReadyMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnClientNotReadyMessageInternal"); + + ClientScene.ready = false; + OnClientNotReady(conn); + + // NOTE: clientReadyConnection is not set here! don't want OnClientConnect to be invoked again after scene changes. + } + + void OnClientErrorInternal(NetworkConnection conn, ErrorMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager:OnClientErrorInternal"); + OnClientError(conn, msg.value); + } + + void OnClientSceneInternal(NetworkConnection conn, SceneMessage msg) + { + if (LogFilter.Debug) Debug.Log("NetworkManager.OnClientSceneInternal"); + + if (NetworkClient.isConnected && !NetworkServer.active) + { + ClientChangeScene(msg.sceneName, msg.sceneMode, msg.physicsMode); + } + } + + #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 virtual void OnServerConnect(NetworkConnection 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 virtual void OnServerDisconnect(NetworkConnection conn) + { + NetworkServer.DestroyPlayerForConnection(conn); + if (LogFilter.Debug) Debug.Log("OnServerDisconnect: Client disconnected."); + } + + /// + /// 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 virtual void OnServerReady(NetworkConnection conn) + { + if (conn.playerController == null) + { + // this is now allowed (was not for a while) + if (LogFilter.Debug) Debug.Log("Ready with no player object"); + } + NetworkServer.SetClientReady(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. + /// An extra message object passed for the new player. + public virtual void OnServerAddPlayer(NetworkConnection conn, AddPlayerMessage extraMessage) + { + Transform startPos = GetStartPosition(); + GameObject player = startPos != null + ? Instantiate(playerPrefab, startPos.position, startPos.rotation) + : Instantiate(playerPrefab); + + NetworkServer.AddPlayerForConnection(conn, player); + } + + /// + /// This finds a spawn position based on NetworkStartPosition objects in the scene. + /// This is used by the default implementation of OnServerAddPlayer. + /// + /// Returns the transform to spawn a player at, or null. + public Transform GetStartPosition() + { + // first remove any dead transforms + startPositions.RemoveAll(t => t == null); + + if (startPositions.Count == 0) + return null; + + if (playerSpawnMethod == PlayerSpawnMethod.Random) + { + return startPositions[UnityEngine.Random.Range(0, startPositions.Count)]; + } + else + { + Transform startPosition = startPositions[startPositionIndex]; + startPositionIndex = (startPositionIndex + 1) % startPositions.Count; + return startPosition; + } + } + + /// + /// Called on the server when a client removes a player. + /// The default implementation of this function destroys the corresponding player object. + /// + /// The connection to remove the player from. + /// The player controller to remove. + public virtual void OnServerRemovePlayer(NetworkConnection conn, NetworkIdentity player) + { + if (player.gameObject != null) + { + NetworkServer.Destroy(player.gameObject); + } + } + + /// + /// Called on the server when a network error occurs for a client connection. + /// + /// Connection from client. + /// Error code. + public virtual void OnServerError(NetworkConnection conn, int errorCode) { } + + /// + /// 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 virtual void OnServerChangeScene(string 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 virtual void OnServerSceneChanged(string sceneName) { } + + #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 virtual void OnClientConnect(NetworkConnection conn) + { + if (!clientLoadedScene) + { + // Ready/AddPlayer is usually triggered by a scene load completing. if no scene was loaded, then Ready/AddPlayer it here instead. + if (!ClientScene.ready) ClientScene.Ready(conn); + if (autoCreatePlayer) + { + ClientScene.AddPlayer(); + } + } + } + + /// + /// 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. + /// + /// Connection to the server. + public virtual void OnClientDisconnect(NetworkConnection conn) + { + StopClient(); + } + + /// + /// Called on clients when a network error occurs. + /// + /// Connection to a server. + /// Error code. + public virtual void OnClientError(NetworkConnection conn, int errorCode) { } + + /// + /// Called on clients when a servers tells the client it is no longer ready. + /// This is commonly used when switching scenes. + /// + /// Connection to a server. + public virtual void OnClientNotReady(NetworkConnection conn) { } + + /// + /// 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 + public virtual void OnClientChangeScene(string newSceneName) { } + + /// + /// 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. + /// + /// The network connection that the scene change message arrived on. + public virtual void OnClientSceneChanged(NetworkConnection conn) + { + // always become ready. + if (!ClientScene.ready) ClientScene.Ready(conn); + + if (autoCreatePlayer && ClientScene.localPlayer == null) + { + // add player if existing one is null + ClientScene.AddPlayer(); + } + } + + #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 host is started. + /// StartHost has multiple signatures, but they all cause this hook to be called. + /// + public virtual void OnStartHost() { } + + /// + /// 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 virtual void OnStartServer() { } + + /// + /// Obsolete: Use instead of OnStartClient(NetworkClient client). + /// All NetworkClient functions are static now, so you can use NetworkClient.Send(message) instead of client.Send(message) directly now. + /// + /// The NetworkClient object that was started. + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use OnStartClient() instead of OnStartClient(NetworkClient client). All NetworkClient functions are static now, so you can use NetworkClient.Send(message) instead of client.Send(message) directly now.")] + public virtual void OnStartClient(NetworkClient client) { } + + /// + /// This is invoked when the client is started. + /// + public virtual void OnStartClient() + { +#pragma warning disable CS0618 // Type or member is obsolete + OnStartClient(NetworkClient.singleton); +#pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// This is called when a server is stopped - including when a host is stopped. + /// + public virtual void OnStopServer() { } + + /// + /// This is called when a client is stopped. + /// + public virtual void OnStopClient() { } + + /// + /// This is called when a host is stopped. + /// + public virtual void OnStopHost() { } + + #endregion + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkManager.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkManager.cs.meta new file mode 100644 index 0000000..3ca7c55 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs b/Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs new file mode 100644 index 0000000..dab55d8 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs @@ -0,0 +1,128 @@ +// vis2k: GUILayout instead of spacey += ...; removed Update hotkeys to avoid +// confusion if someone accidentally presses one. +using System.ComponentModel; +using UnityEngine; + +namespace Mirror +{ + /// + /// An extension for the NetworkManager that displays a default HUD for controlling the network state of the game. + /// This component also shows useful internal state for the networking system in the inspector window of the editor. It allows users to view connections, networked objects, message handlers, and packet statistics. This information can be helpful when debugging networked games. + /// + [AddComponentMenu("Network/NetworkManagerHUD")] + [RequireComponent(typeof(NetworkManager))] + [EditorBrowsable(EditorBrowsableState.Never)] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkManagerHUD.html")] + public class NetworkManagerHUD : MonoBehaviour + { + NetworkManager manager; + + /// + /// Whether to show the default control HUD at runtime. + /// + public bool showGUI = true; + + /// + /// The horizontal offset in pixels to draw the HUD runtime GUI at. + /// + public int offsetX; + + /// + /// The vertical offset in pixels to draw the HUD runtime GUI at. + /// + public int offsetY; + + void Awake() + { + manager = GetComponent(); + } + + void OnGUI() + { + if (!showGUI) + return; + + GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, 215, 9999)); + if (!NetworkClient.isConnected && !NetworkServer.active) + { + if (!NetworkClient.active) + { + // LAN Host + if (Application.platform != RuntimePlatform.WebGLPlayer) + { + if (GUILayout.Button("LAN Host")) + { + manager.StartHost(); + } + } + + // LAN Client + IP + GUILayout.BeginHorizontal(); + if (GUILayout.Button("LAN Client")) + { + manager.StartClient(); + } + manager.networkAddress = GUILayout.TextField(manager.networkAddress); + GUILayout.EndHorizontal(); + + // LAN Server Only + if (Application.platform == RuntimePlatform.WebGLPlayer) + { + // cant be a server in webgl build + GUILayout.Box("( WebGL cannot be server )"); + } + else + { + if (GUILayout.Button("LAN Server Only")) manager.StartServer(); + } + } + else + { + // Connecting + GUILayout.Label("Connecting to " + manager.networkAddress + ".."); + if (GUILayout.Button("Cancel Connection Attempt")) + { + manager.StopClient(); + } + } + } + else + { + // server / client status message + if (NetworkServer.active) + { + GUILayout.Label("Server: active. Transport: " + Transport.activeTransport); + } + if (NetworkClient.isConnected) + { + GUILayout.Label("Client: address=" + manager.networkAddress); + } + } + + // client ready + if (NetworkClient.isConnected && !ClientScene.ready) + { + if (GUILayout.Button("Client Ready")) + { + ClientScene.Ready(NetworkClient.connection); + + if (ClientScene.localPlayer == null) + { + ClientScene.AddPlayer(); + } + } + } + + // stop + if (NetworkServer.active || NetworkClient.isConnected) + { + if (GUILayout.Button("Stop")) + { + manager.StopHost(); + } + } + + GUILayout.EndArea(); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs.meta new file mode 100644 index 0000000..fa08c3d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkManagerHUD.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6442dc8070ceb41f094e44de0bf87274 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkMessage.cs b/Assets/Packages/Mirror/Runtime/NetworkMessage.cs new file mode 100644 index 0000000..9e9613c --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkMessage.cs @@ -0,0 +1,25 @@ +namespace Mirror +{ + public struct NetworkMessage + { + public int msgType; + public NetworkConnection conn; + public NetworkReader reader; + + public TMsg ReadMessage() where TMsg : IMessageBase, new() + { + // Normally I would just do: + // TMsg msg = new TMsg(); + // but mono calls an expensive method Activator.CreateInstance + // For value types this is unnecesary, just use the default value + TMsg msg = typeof(TMsg).IsValueType ? default(TMsg) : new TMsg(); + msg.Deserialize(reader); + return msg; + } + + public void ReadMessage(TMsg msg) where TMsg : IMessageBase + { + msg.Deserialize(reader); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkMessage.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkMessage.cs.meta new file mode 100644 index 0000000..370b0a6 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkMessage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eb04e4848a2e4452aa2dbd7adb801c51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkReader.cs b/Assets/Packages/Mirror/Runtime/NetworkReader.cs new file mode 100644 index 0000000..7df6ad4 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkReader.cs @@ -0,0 +1,355 @@ +// Custom NetworkReader that doesn't use C#'s built in MemoryStream in order to +// avoid allocations. +// +// Benchmark: 100kb byte[] passed to NetworkReader constructor 1000x +// before with MemoryStream +// 0.8% CPU time, 250KB memory, 3.82ms +// now: +// 0.0% CPU time, 32KB memory, 0.02ms +using System; +using System.IO; +using System.Text; +using UnityEngine; + +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. + public class NetworkReader + { + // internal buffer + // byte[] pointer would work, but we use ArraySegment to also support + // the ArraySegment constructor + ArraySegment 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; + public int Length => buffer.Count; + + + public NetworkReader(byte[] bytes) + { + buffer = new ArraySegment(bytes); + } + + public NetworkReader(ArraySegment segment) + { + buffer = segment; + } + + public byte ReadByte() + { + if (Position + 1 > buffer.Count) + { + throw new EndOfStreamException("ReadByte out of range:" + ToString()); + } + return buffer.Array[buffer.Offset + Position++]; + } + public int ReadInt32() => (int)ReadUInt32(); + public uint ReadUInt32() + { + uint value = 0; + value |= ReadByte(); + value |= (uint)(ReadByte() << 8); + value |= (uint)(ReadByte() << 16); + value |= (uint)(ReadByte() << 24); + return value; + } + public long ReadInt64() => (long)ReadUInt64(); + public ulong ReadUInt64() + { + ulong value = 0; + value |= ReadByte(); + value |= ((ulong)ReadByte()) << 8; + value |= ((ulong)ReadByte()) << 16; + value |= ((ulong)ReadByte()) << 24; + value |= ((ulong)ReadByte()) << 32; + value |= ((ulong)ReadByte()) << 40; + value |= ((ulong)ReadByte()) << 48; + value |= ((ulong)ReadByte()) << 56; + return value; + } + + // read bytes into the passed buffer + public byte[] ReadBytes(byte[] bytes, int count) + { + // 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); + } + + ArraySegment data = ReadBytesSegment(count); + Array.Copy(data.Array, data.Offset, bytes, 0, count); + return bytes; + } + + // useful to parse payloads etc. without allocating + public ArraySegment ReadBytesSegment(int count) + { + // check if within buffer limits + if (Position + count > buffer.Count) + { + throw new EndOfStreamException("ReadBytesSegment can't read " + count + " bytes because it would read past the end of the stream. " + ToString()); + } + + // return the segment + ArraySegment result = new ArraySegment(buffer.Array, buffer.Offset + Position, count); + Position += count; + return result; + } + + public override string ToString() + { + return "NetworkReader pos=" + Position + " len=" + Length + " buffer=" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count); + } + } + + // Mirror's Weaver automatically detects all NetworkReader function types, + // 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); + + public static byte ReadByte(this NetworkReader reader) => reader.ReadByte(); + public static sbyte ReadSByte(this NetworkReader reader) => (sbyte)reader.ReadByte(); + public static char ReadChar(this NetworkReader reader) => (char)reader.ReadUInt16(); + public static bool ReadBoolean(this NetworkReader reader) => reader.ReadByte() != 0; + public static short ReadInt16(this NetworkReader reader) => (short)reader.ReadUInt16(); + public static ushort ReadUInt16(this NetworkReader reader) + { + ushort value = 0; + value |= reader.ReadByte(); + value |= (ushort)(reader.ReadByte() << 8); + return value; + } + public static int ReadInt32(this NetworkReader reader) => (int)reader.ReadUInt32(); + public static uint ReadUInt32(this NetworkReader reader) + { + uint value = 0; + value |= reader.ReadByte(); + value |= (uint)(reader.ReadByte() << 8); + value |= (uint)(reader.ReadByte() << 16); + value |= (uint)(reader.ReadByte() << 24); + return value; + } + public static long ReadInt64(this NetworkReader reader) => (long)reader.ReadUInt64(); + public static ulong ReadUInt64(this NetworkReader reader) + { + ulong value = 0; + value |= reader.ReadByte(); + value |= ((ulong)reader.ReadByte()) << 8; + value |= ((ulong)reader.ReadByte()) << 16; + value |= ((ulong)reader.ReadByte()) << 24; + value |= ((ulong)reader.ReadByte()) << 32; + value |= ((ulong)reader.ReadByte()) << 40; + value |= ((ulong)reader.ReadByte()) << 48; + value |= ((ulong)reader.ReadByte()) << 56; + return value; + } + public static float ReadSingle(this NetworkReader reader) + { + UIntFloat converter = new UIntFloat(); + converter.intValue = reader.ReadUInt32(); + return converter.floatValue; + } + public static double ReadDouble(this NetworkReader reader) + { + UIntDouble converter = new UIntDouble(); + converter.longValue = reader.ReadUInt64(); + return converter.doubleValue; + } + public static decimal ReadDecimal(this NetworkReader reader) + { + UIntDecimal converter = new UIntDecimal(); + converter.longValue1 = reader.ReadUInt64(); + converter.longValue2 = reader.ReadUInt64(); + return converter.decimalValue; + } + + // note: this will throw an ArgumentException if an invalid utf8 string is sent + // null support, see NetworkWriter + public static string ReadString(this NetworkReader reader) + { + // read number of bytes + ushort size = reader.ReadUInt16(); + + if (size == 0) + return null; + + int realSize = 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); + } + + ArraySegment data = reader.ReadBytesSegment(realSize); + + // convert directly from buffer to string via encoding + return encoding.GetString(data.Array, data.Offset, data.Count); + } + + // Use checked() to force it to throw OverflowException if data is invalid + // null support, see NetworkWriter + 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.ReadPackedUInt32(); + return count == 0 ? null : reader.ReadBytes(checked((int)(count - 1u))); + } + + public static ArraySegment ReadBytesAndSizeSegment(this NetworkReader reader) + { + // count = 0 means the array was null + // otherwise count - 1 is the length of the array + uint count = reader.ReadPackedUInt32(); + return count == 0 ? default : reader.ReadBytesSegment(checked((int)(count - 1u))); + } + + // zigzag decoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba + public static int ReadPackedInt32(this NetworkReader reader) + { + uint data = reader.ReadPackedUInt32(); + return (int)((data >> 1) ^ -(data & 1)); + } + + // http://sqlite.org/src4/doc/trunk/www/varint.wiki + // NOTE: big endian. + // Use checked() to force it to throw OverflowException if data is invalid + public static uint ReadPackedUInt32(this NetworkReader reader) => checked((uint)reader.ReadPackedUInt64()); + + // zigzag decoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba + public static long ReadPackedInt64(this NetworkReader reader) + { + ulong data = reader.ReadPackedUInt64(); + return ((long)(data >> 1)) ^ -((long)data & 1); + } + + public static ulong ReadPackedUInt64(this NetworkReader reader) + { + byte a0 = reader.ReadByte(); + if (a0 < 241) + { + return a0; + } + + byte a1 = reader.ReadByte(); + if (a0 >= 241 && a0 <= 248) + { + return 240 + ((a0 - (ulong)241) << 8) + a1; + } + + byte a2 = reader.ReadByte(); + if (a0 == 249) + { + return 2288 + ((ulong)a1 << 8) + a2; + } + + byte a3 = reader.ReadByte(); + if (a0 == 250) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16); + } + + byte a4 = reader.ReadByte(); + if (a0 == 251) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24); + } + + byte a5 = reader.ReadByte(); + if (a0 == 252) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32); + } + + byte a6 = reader.ReadByte(); + if (a0 == 253) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40); + } + + byte a7 = reader.ReadByte(); + if (a0 == 254) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40) + (((ulong)a7) << 48); + } + + byte a8 = reader.ReadByte(); + if (a0 == 255) + { + return a1 + (((ulong)a2) << 8) + (((ulong)a3) << 16) + (((ulong)a4) << 24) + (((ulong)a5) << 32) + (((ulong)a6) << 40) + (((ulong)a7) << 48) + (((ulong)a8) << 56); + } + + throw new IndexOutOfRangeException("ReadPackedUInt64() failure: " + a0); + } + + public static Vector2 ReadVector2(this NetworkReader reader) => new Vector2(reader.ReadSingle(), reader.ReadSingle()); + public static Vector3 ReadVector3(this NetworkReader reader) => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Vector4 ReadVector4(this NetworkReader reader) => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Vector2Int ReadVector2Int(this NetworkReader reader) => new Vector2Int(reader.ReadPackedInt32(), reader.ReadPackedInt32()); + public static Vector3Int ReadVector3Int(this NetworkReader reader) => new Vector3Int(reader.ReadPackedInt32(), reader.ReadPackedInt32(), reader.ReadPackedInt32()); + public static Color ReadColor(this NetworkReader reader) => new Color(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Color32 ReadColor32(this NetworkReader reader) => new Color32(reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()); + public static Quaternion ReadQuaternion(this NetworkReader reader) => new Quaternion(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Rect ReadRect(this NetworkReader reader) => new Rect(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + public static Plane ReadPlane(this NetworkReader reader) => new Plane(reader.ReadVector3(), reader.ReadSingle()); + public static Ray ReadRay(this NetworkReader reader) => new Ray(reader.ReadVector3(), reader.ReadVector3()); + + public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader) + { + return new Matrix4x4 + { + m00 = reader.ReadSingle(), + m01 = reader.ReadSingle(), + m02 = reader.ReadSingle(), + m03 = reader.ReadSingle(), + m10 = reader.ReadSingle(), + m11 = reader.ReadSingle(), + m12 = reader.ReadSingle(), + m13 = reader.ReadSingle(), + m20 = reader.ReadSingle(), + m21 = reader.ReadSingle(), + m22 = reader.ReadSingle(), + m23 = reader.ReadSingle(), + m30 = reader.ReadSingle(), + m31 = reader.ReadSingle(), + m32 = reader.ReadSingle(), + m33 = reader.ReadSingle() + }; + } + + public static byte[] ReadBytes(this NetworkReader reader, int count) + { + byte[] bytes = new byte[count]; + reader.ReadBytes(bytes, count); + return bytes; + } + + public static Guid ReadGuid(this NetworkReader reader) => new Guid(reader.ReadBytes(16)); + public static Transform ReadTransform(this NetworkReader reader) => reader.ReadNetworkIdentity()?.transform; + public static GameObject ReadGameObject(this NetworkReader reader) => reader.ReadNetworkIdentity()?.gameObject; + + public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader) + { + uint netId = reader.ReadPackedUInt32(); + if (netId == 0) return null; + + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity)) + { + return identity; + } + + if (LogFilter.Debug) Debug.Log("ReadNetworkIdentity netId:" + netId + " not found in spawned"); + return null; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkReader.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkReader.cs.meta new file mode 100644 index 0000000..f5b0c1e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1610f05ec5bd14d6882e689f7372596a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkServer.cs b/Assets/Packages/Mirror/Runtime/NetworkServer.cs new file mode 100644 index 0000000..a413675 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkServer.cs @@ -0,0 +1,1414 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using UnityEngine; + +namespace Mirror +{ + /// + /// The NetworkServer uses a NetworkServerSimple for basic network functionality and adds more game-like functionality. + /// + /// + /// NetworkServer handles remote connections from remote clients via a NetworkServerSimple instance, and also has a local connection for a local client. + /// The NetworkServer is a singleton. It has static convenience functions such as NetworkServer.SendToAll() and NetworkServer.Spawn() which automatically use the singleton instance. + /// The NetworkManager uses the NetworkServer, but it can be used without the NetworkManager. + /// The set of networked objects that have been spawned is managed by NetworkServer. Objects are spawned with NetworkServer.Spawn() which adds them to this set, and makes them be created on clients. Spawned objects are removed automatically when they are destroyed, or than they can be removed from the spawned set by calling NetworkServer.UnSpawn() - this does not destroy the object. + /// There are a number of internal messages used by NetworkServer, these are setup when NetworkServer.Listen() is called. + /// + public static class NetworkServer + { + static bool initialized; + static int maxConnections; + + // original HLAPI has .localConnections list with only m_LocalConnection in it + // (for backwards compatibility because they removed the real localConnections list a while ago) + // => removed it for easier code. use .localConnection now! + public static NetworkConnection localConnection { get; private set; } + + /// + /// A list of local connections on the server. + /// + public static Dictionary connections = new Dictionary(); + + /// + /// Dictionary of the message handlers registered with the server. + /// The key to the dictionary is the message Id. + /// + public static Dictionary handlers = new Dictionary(); + + /// + /// If you enable this, the server will not listen for incoming connections on the regular network port. + /// This can be used if the game is running in host mode and does not want external players to be able to connect - making it like a single-player game. Also this can be useful when using AddExternalConnection(). + /// + public static bool dontListen; + + /// + /// Checks if the server has been started. + /// This will be true after NetworkServer.Listen() has been called. + /// + public static bool active { get; private set; } + + /// + /// True is a local client is currently active on the server. + /// This will be true for "Hosts" on hosted server games. + /// + public static bool localClientActive { get; private set; } + + /// + /// Reset the NetworkServer singleton. + /// + public static void Reset() + { + active = false; + } + + /// + /// This shuts down the server and disconnects all clients. + /// + public static void Shutdown() + { + if (initialized) + { + DisconnectAll(); + + if (dontListen) + { + // was never started, so dont stop + } + else + { + Transport.activeTransport.ServerStop(); + } + + Transport.activeTransport.OnServerDisconnected.RemoveListener(OnDisconnected); + Transport.activeTransport.OnServerConnected.RemoveListener(OnConnected); + Transport.activeTransport.OnServerDataReceived.RemoveListener(OnDataReceived); + Transport.activeTransport.OnServerError.RemoveListener(OnError); + + initialized = false; + } + dontListen = false; + active = false; + + NetworkIdentity.ResetNextNetworkId(); + } + + static void Initialize() + { + if (initialized) + return; + + initialized = true; + if (LogFilter.Debug) Debug.Log("NetworkServer Created version " + Version.Current); + + //Make sure connections are cleared in case any old connections references exist from previous sessions + connections.Clear(); + Transport.activeTransport.OnServerDisconnected.AddListener(OnDisconnected); + Transport.activeTransport.OnServerConnected.AddListener(OnConnected); + Transport.activeTransport.OnServerDataReceived.AddListener(OnDataReceived); + Transport.activeTransport.OnServerError.AddListener(OnError); + } + + internal static void RegisterMessageHandlers() + { + RegisterHandler(OnClientReadyMessage); + RegisterHandler(OnCommandMessage); + RegisterHandler(OnRemovePlayerMessage); + RegisterHandler(NetworkTime.OnServerPing); + } + + /// + /// Start the server, setting the maximum number of connections. + /// + /// Maximum number of allowed connections + /// + public static bool Listen(int maxConns) + { + Initialize(); + maxConnections = maxConns; + + // only start server if we want to listen + if (!dontListen) + { + Transport.activeTransport.ServerStart(); + if (LogFilter.Debug) Debug.Log("Server started listening"); + } + + active = true; + RegisterMessageHandlers(); + return true; + } + + /// + /// This accepts a network connection and adds it to the server. + /// This connection will use the callbacks registered with the server. + /// + /// Network connection to add. + /// True if added. + public static bool AddConnection(NetworkConnection conn) + { + if (!connections.ContainsKey(conn.connectionId)) + { + // connection cannot be null here or conn.connectionId + // would throw NRE + connections[conn.connectionId] = conn; + conn.SetHandlers(handlers); + return true; + } + // already a connection with this id + return false; + } + + /// + /// This removes an external connection added with AddExternalConnection(). + /// + /// The id of the connection to remove. + /// True if the removal succeeded + public static bool RemoveConnection(int connectionId) + { + return connections.Remove(connectionId); + } + + // called by LocalClient to add itself. dont call directly. + internal static void SetLocalConnection(ULocalConnectionToClient conn) + { + if (localConnection != null) + { + Debug.LogError("Local Connection already exists"); + return; + } + + localConnection = conn; + OnConnected(localConnection); + } + + internal static void RemoveLocalConnection() + { + if (localConnection != null) + { + localConnection.Disconnect(); + localConnection.Dispose(); + localConnection = null; + } + localClientActive = false; + RemoveConnection(0); + } + + internal static void ActivateLocalClientScene() + { + if (localClientActive) + return; + + // ClientScene for a local connection is becoming active. any spawned objects need to be started as client objects + localClientActive = true; + foreach (NetworkIdentity identity in NetworkIdentity.spawned.Values) + { + if (!identity.isClient) + { + if (LogFilter.Debug) Debug.Log("ActivateClientScene " + identity.netId + " " + identity); + + identity.OnStartClient(); + } + } + } + + // this is like SendToReady - but it doesn't check the ready flag on the connection. + // this is used for ObjectDestroy messages. + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("use SendToObservers instead")] + static bool SendToObservers(NetworkIdentity identity, short msgType, MessageBase msg) + { + if (LogFilter.Debug) Debug.Log("Server.SendToObservers id:" + msgType); + + if (identity != null && identity.observers != null) + { + // pack message into byte[] once + byte[] bytes = MessagePacker.PackMessage((ushort)msgType, msg); + + // send to all observers + bool result = true; + foreach (KeyValuePair kvp in identity.observers) + { + result &= kvp.Value.SendBytes(bytes); + } + return result; + } + return false; + } + + // this is like SendToReady - but it doesn't check the ready flag on the connection. + // this is used for ObjectDestroy messages. + static bool SendToObservers(NetworkIdentity identity, T msg) where T: IMessageBase + { + if (LogFilter.Debug) Debug.Log("Server.SendToObservers id:" + typeof(T)); + + if (identity != null && identity.observers != null) + { + // pack message into byte[] once + byte[] bytes = MessagePacker.Pack(msg); + + bool result = true; + foreach (KeyValuePair kvp in identity.observers) + { + result &= kvp.Value.SendBytes(bytes); + } + return result; + } + return false; + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use SendToAll instead.")] + public static bool SendToAll(int msgType, MessageBase msg, int channelId = Channels.DefaultReliable) + { + if (LogFilter.Debug) Debug.Log("Server.SendToAll id:" + msgType); + + // pack message into byte[] once + byte[] bytes = MessagePacker.PackMessage((ushort)msgType, msg); + + // send to all + bool result = true; + foreach (KeyValuePair kvp in connections) + { + result &= kvp.Value.SendBytes(bytes, channelId); + } + return result; + } + + /// + /// Send a message structure with the given type number to all connected clients. + /// This applies to clients that are ready and not-ready. + /// + /// Message type. + /// Message structure. + /// Transport channel to use + /// + public static bool SendToAll(T msg, int channelId = Channels.DefaultReliable) where T : IMessageBase + { + if (LogFilter.Debug) Debug.Log("Server.SendToAll id:" + typeof(T)); + + // pack message into byte[] once + byte[] bytes = MessagePacker.Pack(msg); + + bool result = true; + foreach (KeyValuePair kvp in connections) + { + result &= kvp.Value.SendBytes(bytes, channelId); + } + return result; + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use SendToReady instead.")] + public static bool SendToReady(NetworkIdentity identity, short msgType, MessageBase msg, int channelId = Channels.DefaultReliable) + { + if (LogFilter.Debug) Debug.Log("Server.SendToReady msgType:" + msgType); + + if (identity != null && identity.observers != null) + { + // pack message into byte[] once + byte[] bytes = MessagePacker.PackMessage((ushort)msgType, msg); + + // send to all ready observers + bool result = true; + foreach (KeyValuePair kvp in identity.observers) + { + if (kvp.Value.isReady) + { + result &= kvp.Value.SendBytes(bytes, channelId); + } + } + return result; + } + return false; + } + + /// + /// Send a message structure with the given type number to only clients which are ready. + /// See Networking.NetworkClient.Ready. + /// + /// Message type. + /// + /// Message structure. + /// Send to observers including self.. + /// Transport channel to use + /// + public static bool SendToReady(NetworkIdentity identity, T msg, bool includeSelf = true, int channelId = Channels.DefaultReliable) where T : IMessageBase + { + if (LogFilter.Debug) Debug.Log("Server.SendToReady msgType:" + typeof(T)); + + if (identity != null && identity.observers != null) + { + // pack message into byte[] once + byte[] bytes = MessagePacker.Pack(msg); + + bool result = true; + foreach (KeyValuePair kvp in identity.observers) + { + bool isSelf = kvp.Value == identity.connectionToClient; + if ((!isSelf || includeSelf) && + kvp.Value.isReady) + { + result &= kvp.Value.SendBytes(bytes, channelId); + } + } + return result; + } + return false; + } + + /// + /// Send a message structure with the given type number to only clients which are ready. + /// See Networking.NetworkClient.Ready. + /// + /// Message type. + /// + /// Message structure. + /// Transport channel to use + /// + public static bool SendToReady(NetworkIdentity identity, T msg, int channelId = Channels.DefaultReliable) where T : IMessageBase + { + return SendToReady(identity, msg, true, channelId); + } + + /// + /// Disconnect all currently connected clients, including the local connection. + /// This can only be called on the server. Clients will receive the Disconnect message. + /// + public static void DisconnectAll() + { + DisconnectAllConnections(); + localConnection = null; + + active = false; + localClientActive = false; + } + + /// + /// Disconnect all currently connected clients except the local connection. + /// This can only be called on the server. Clients will receive the Disconnect message. + /// + public static void DisconnectAllConnections() + { + foreach (NetworkConnection conn in connections.Values) + { + conn.Disconnect(); + // call OnDisconnected unless local player in host mode + if (conn.connectionId != 0) + OnDisconnected(conn); + conn.Dispose(); + } + connections.Clear(); + } + + // The user should never need to pump the update loop manually + internal static void Update() + { + if (!active) + return; + + // update all server objects + foreach (KeyValuePair kvp in NetworkIdentity.spawned) + { + if (kvp.Value != null && kvp.Value.gameObject != null) + { + kvp.Value.MirrorUpdate(); + } + else + { + // spawned list should have no null entries because we + // always call Remove in OnObjectDestroy everywhere. + Debug.LogWarning("Found 'null' entry in spawned list for netId=" + kvp.Key + ". Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy."); + } + } + } + + static void OnConnected(int connectionId) + { + if (LogFilter.Debug) Debug.Log("Server accepted client:" + connectionId); + + // connectionId needs to be > 0 because 0 is reserved for local player + 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; + } + + // connectionId not in use yet? + if (connections.ContainsKey(connectionId)) + { + Transport.activeTransport.ServerDisconnect(connectionId); + if (LogFilter.Debug) Debug.Log("Server connectionId " + connectionId + " already in use. kicked client:" + connectionId); + return; + } + + // are more connections allowed? if not, kick + // (it's easier to handle this in Mirror, so Transports can have + // 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) + { + // get ip address from connection + string address = Transport.activeTransport.ServerGetClientAddress(connectionId); + + // add player info + NetworkConnection conn = new NetworkConnection(address, connectionId); + OnConnected(conn); + } + else + { + // kick + Transport.activeTransport.ServerDisconnect(connectionId); + if (LogFilter.Debug) Debug.Log("Server full, kicked client:" + connectionId); + } + } + + static void OnConnected(NetworkConnection conn) + { + if (LogFilter.Debug) Debug.Log("Server accepted client:" + conn.connectionId); + + // add connection and invoke connected event + AddConnection(conn); + conn.InvokeHandler(new ConnectMessage()); + } + + static void OnDisconnected(int connectionId) + { + if (LogFilter.Debug) Debug.Log("Server disconnect client:" + connectionId); + + if (connections.TryGetValue(connectionId, out NetworkConnection conn)) + { + conn.Disconnect(); + RemoveConnection(connectionId); + if (LogFilter.Debug) Debug.Log("Server lost client:" + connectionId); + + OnDisconnected(conn); + } + } + + static void OnDisconnected(NetworkConnection conn) + { + conn.InvokeHandler(new DisconnectMessage()); + if (LogFilter.Debug) Debug.Log("Server lost client:" + conn.connectionId); + } + + static void OnDataReceived(int connectionId, ArraySegment data) + { + if (connections.TryGetValue(connectionId, out NetworkConnection conn)) + { + conn.TransportReceive(data); + } + else + { + Debug.LogError("HandleData Unknown connectionId:" + connectionId); + } + } + + static void OnError(int connectionId, Exception exception) + { + // TODO Let's discuss how we will handle errors + Debug.LogException(exception); + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use RegisterHandler instead.")] + public static void RegisterHandler(int msgType, NetworkMessageDelegate handler) + { + if (handlers.ContainsKey(msgType)) + { + if (LogFilter.Debug) Debug.Log("NetworkServer.RegisterHandler replacing " + msgType); + } + handlers[msgType] = handler; + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use RegisterHandler instead.")] + public static void RegisterHandler(MsgType msgType, NetworkMessageDelegate handler) + { + RegisterHandler((int)msgType, handler); + } + + /// + /// Register a handler for a particular message type. + /// There are several system message types which you can add handlers for. You can also add your own message types. + /// + /// Message type + /// Function handler which will be invoked for when this message type is received. + public static void RegisterHandler(Action handler) where T: IMessageBase, new() + { + int msgType = MessagePacker.GetId(); + if (handlers.ContainsKey(msgType)) + { + if (LogFilter.Debug) Debug.Log("NetworkServer.RegisterHandler replacing " + msgType); + } + handlers[msgType] = MessagePacker.MessageHandler(handler); + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use UnregisterHandler instead.")] + public static void UnregisterHandler(int msgType) + { + handlers.Remove(msgType); + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use UnregisterHandler instead.")] + public static void UnregisterHandler(MsgType msgType) + { + UnregisterHandler((int)msgType); + } + + /// + /// Unregisters a handler for a particular message type. + /// + /// Message type + public static void UnregisterHandler() where T : IMessageBase + { + int msgType = MessagePacker.GetId(); + handlers.Remove(msgType); + } + + /// + /// Clear all registered callback handlers. + /// + public static void ClearHandlers() + { + handlers.Clear(); + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use SendToClient instead.")] + public static void SendToClient(int connectionId, int msgType, MessageBase msg) + { + if (connections.TryGetValue(connectionId, out NetworkConnection conn)) + { + conn.Send(msgType, msg); + return; + } + Debug.LogError("Failed to send message to connection ID '" + connectionId + ", not found in connection list"); + } + + /// + /// Send a message to the client which owns the given connection ID. + /// It accepts the connection ID as a parameter as well as a message and MsgType. Remember to set the client up for receiving the messages by using NetworkClient.RegisterHandler. Also, for user messages you must use a MsgType with a higher ID number than MsgType.Highest. + /// + /// Message type + /// Client connection ID. + /// Message struct to send + public static void SendToClient(int connectionId, T msg) where T : IMessageBase + { + if (connections.TryGetValue(connectionId, out NetworkConnection conn)) + { + conn.Send(msg); + return; + } + Debug.LogError("Failed to send message to connection ID '" + connectionId + ", not found in connection list"); + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use SendToClientOfPlayer instead.")] + public static void SendToClientOfPlayer(NetworkIdentity identity, int msgType, MessageBase msg) + { + if (identity != null) + { + identity.connectionToClient.Send(msgType, msg); + } + else + { + Debug.LogError("SendToClientOfPlayer: player has no NetworkIdentity: " + identity.name); + } + } + + /// + /// send this message to the player only + /// + /// Message type + /// + /// + public static void SendToClientOfPlayer(NetworkIdentity identity, T msg) where T: IMessageBase + { + if (identity != null) + { + identity.connectionToClient.Send(msg); + } + else + { + Debug.LogError("SendToClientOfPlayer: player has no NetworkIdentity: " + identity.name); + } + } + + /// + /// This replaces the player object for a connection with a different player object. The old player object is not destroyed. + /// If a connection already has a player object, this can be used to replace that object with a different player object. This does NOT change the ready state of the connection, so it can safely be used while changing scenes. + /// + /// Connection which is adding the player. + /// Player object spawned for the player. + /// + /// + public static bool ReplacePlayerForConnection(NetworkConnection conn, GameObject player, Guid assetId) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return InternalReplacePlayerForConnection(conn, player); + } + + /// + /// This replaces the player object for a connection with a different player object. The old player object is not destroyed. + /// If a connection already has a player object, this can be used to replace that object with a different player object. This does NOT change the ready state of the connection, so it can safely be used while changing scenes. + /// + /// Connection which is adding the player. + /// Player object spawned for the player. + /// + public static bool ReplacePlayerForConnection(NetworkConnection conn, GameObject player) + { + return InternalReplacePlayerForConnection(conn, player); + } + + /// + /// When an AddPlayer message handler has received a request from a player, the server calls this to associate the player object with 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. + /// + /// Connection which is adding the player. + /// Player object spawned for the player. + /// + /// + public static bool AddPlayerForConnection(NetworkConnection conn, GameObject player, Guid assetId) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return AddPlayerForConnection(conn, player); + } + + static void SpawnObserversForConnection(NetworkConnection conn) + { + if (LogFilter.Debug) Debug.Log("Spawning " + NetworkIdentity.spawned.Count + " objects for conn " + conn.connectionId); + + 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 NetworkIdentity.spawned.Values) + { + if (identity.gameObject.activeSelf) //TODO this is different // try with far away ones in ummorpg! + { + if (LogFilter.Debug) Debug.Log("Sending spawn message for current server objects name='" + identity.name + "' netId=" + identity.netId + " sceneId=" + identity.sceneId); + + bool visible = identity.OnCheckObserver(conn); + if (visible) + { + 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()); + } + + /// + /// When an AddPlayer message handler has received a request from a player, the server calls this to associate the player object with 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. + /// + /// Connection which is adding the player. + /// Player object spawned for the player. + /// + public static bool AddPlayerForConnection(NetworkConnection conn, GameObject player) + { + NetworkIdentity identity = player.GetComponent(); + if (identity == null) + { + Debug.Log("AddPlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to " + player); + return false; + } + identity.Reset(); + + // cannot have a player object in "Add" version + if (conn.playerController != null) + { + Debug.Log("AddPlayer: player object already exists"); + return false; + } + + // make sure we have a controller before we call SetClientReady + // because the observers will be rebuilt only if we have a controller + conn.playerController = identity; + + // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) + identity.connectionToClient = conn; + + // set ready if not set yet + SetClientReady(conn); + + if (SetupLocalPlayerForConnection(conn, identity)) + { + return true; + } + + if (LogFilter.Debug) Debug.Log("Adding new playerGameObject object netId: " + identity.netId + " asset ID " + identity.assetId); + + FinishPlayerForConnection(identity, player); + if (identity.localPlayerAuthority) + { + identity.SetClientOwner(conn); + } + return true; + } + + static bool SetupLocalPlayerForConnection(NetworkConnection conn, NetworkIdentity identity) + { + if (LogFilter.Debug) Debug.Log("NetworkServer SetupLocalPlayerForConnection netID:" + identity.netId); + + if (conn is ULocalConnectionToClient) + { + if (LogFilter.Debug) Debug.Log("NetworkServer AddPlayer handling ULocalConnectionToClient"); + + // Spawn this player for other players, instead of SpawnObject: + if (identity.netId == 0) + { + // it is allowed to provide an already spawned object as the new player object. + // so dont spawn it again. + identity.OnStartServer(true); + } + identity.RebuildObservers(true); + SendSpawnMessage(identity, null); + + // Set up local player instance on the client instance and update local object map + NetworkClient.AddLocalPlayer(identity); + identity.SetClientOwner(conn); + + // Trigger OnAuthority + identity.ForceAuthority(true); + + // Trigger OnStartLocalPlayer + identity.SetLocalPlayer(); + return true; + } + return false; + } + + static void FinishPlayerForConnection(NetworkIdentity identity, GameObject playerGameObject) + { + if (identity.netId == 0) + { + // it is allowed to provide an already spawned object as the new player object. + // so dont spawn it again. + Spawn(playerGameObject); + } + } + + internal static bool InternalReplacePlayerForConnection(NetworkConnection conn, GameObject player) + { + NetworkIdentity identity = player.GetComponent(); + if (identity == null) + { + Debug.LogError("ReplacePlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to " + player); + return false; + } + + //NOTE: there can be an existing player + if (LogFilter.Debug) Debug.Log("NetworkServer ReplacePlayer"); + + // is there already an owner that is a different object?? + if (conn.playerController != null) + { + conn.playerController.SetNotLocalPlayer(); + conn.playerController.clientAuthorityOwner = null; + } + + conn.playerController = identity; + + // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) + identity.connectionToClient = conn; + + //NOTE: DONT set connection ready. + + // add connection to observers AFTER the playerController was set. + // by definition, there is nothing to observe if there is no player + // controller. + // + // IMPORTANT: do this in AddPlayerForConnection & ReplacePlayerForConnection! + SpawnObserversForConnection(conn); + + if (LogFilter.Debug) Debug.Log("NetworkServer ReplacePlayer setup local"); + + if (SetupLocalPlayerForConnection(conn, identity)) + { + return true; + } + + if (LogFilter.Debug) Debug.Log("Replacing playerGameObject object netId: " + player.GetComponent().netId + " asset ID " + player.GetComponent().assetId); + + FinishPlayerForConnection(identity, player); + if (identity.localPlayerAuthority) + { + identity.SetClientOwner(conn); + } + return true; + } + + static bool GetNetworkIdentity(GameObject go, out NetworkIdentity identity) + { + identity = go.GetComponent(); + if (identity == null) + { + Debug.LogError("GameObject " + go.name + " doesn't have NetworkIdentity."); + return false; + } + return true; + } + + /// + /// Sets the client to be ready. + /// When a client has signaled that it is ready, this method tells the server that the client is ready to receive spawned objects and state synchronization updates. This is usually called in a handler for the SYSTEM_READY message. If there is not specific action a game needs to take for this message, relying on the default ready handler function is probably fine, so this call wont be needed. + /// + /// The connection of the client to make ready. + public static void SetClientReady(NetworkConnection conn) + { + if (LogFilter.Debug) Debug.Log("SetClientReadyInternal for conn:" + conn.connectionId); + + // set ready + conn.isReady = true; + + // client is ready to start spawning objects + if (conn.playerController != null) + SpawnObserversForConnection(conn); + } + + internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn) + { + if (conn.isReady) + SendSpawnMessage(identity, conn); + } + + internal static void HideForConnection(NetworkIdentity identity, NetworkConnection conn) + { + ObjectHideMessage msg = new ObjectHideMessage + { + netId = identity.netId + }; + conn.Send(msg); + } + + /// + /// Marks all connected clients as no longer ready. + /// All clients will no longer be sent state synchronization updates. The player's clients can call ClientManager.Ready() again to re-enter the ready state. This is useful when switching scenes. + /// + public static void SetAllClientsNotReady() + { + foreach (NetworkConnection conn in connections.Values) + { + SetClientNotReady(conn); + } + } + + /// + /// Sets 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 calling SetClientReady(). + /// + /// The connection of the client to make not ready. + public static void SetClientNotReady(NetworkConnection conn) + { + if (conn.isReady) + { + if (LogFilter.Debug) Debug.Log("PlayerNotReady " + conn); + conn.isReady = false; + conn.RemoveObservers(); + + conn.Send(new NotReadyMessage()); + } + } + + // default ready handler. + static void OnClientReadyMessage(NetworkConnection conn, ReadyMessage msg) + { + if (LogFilter.Debug) Debug.Log("Default handler for ready message from " + conn); + SetClientReady(conn); + } + + // default remove player handler + static void OnRemovePlayerMessage(NetworkConnection conn, RemovePlayerMessage msg) + { + if (conn.playerController != null) + { + Destroy(conn.playerController.gameObject); + conn.playerController = null; + } + else + { + Debug.LogError("Received remove player message but connection has no player"); + } + } + + // Handle command from specific player, this could be one of multiple players on a single client + static void OnCommandMessage(NetworkConnection conn, CommandMessage msg) + { + if (!NetworkIdentity.spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + 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 + if (conn.playerController != null && conn.playerController.netId != identity.netId) + { + if (identity.clientAuthorityOwner != conn) + { + Debug.LogWarning("Command for object without authority [netId=" + msg.netId + "]"); + return; + } + } + + if (LogFilter.Debug) Debug.Log("OnCommandMessage for netId=" + msg.netId + " conn=" + conn); + identity.HandleCommand(msg.componentIndex, msg.functionHash, new NetworkReader(msg.payload)); + } + + internal static void SpawnObject(GameObject obj) + { + if (!active) + { + Debug.LogError("SpawnObject for " + obj + ", NetworkServer is not active. Cannot spawn objects without an active server."); + return; + } + + NetworkIdentity identity = obj.GetComponent(); + if (identity == null) + { + Debug.LogError("SpawnObject " + obj + " has no NetworkIdentity. Please add a NetworkIdentity to " + obj); + return; + } + identity.Reset(); + + identity.OnStartServer(false); + + if (LogFilter.Debug) Debug.Log("SpawnObject instance ID " + identity.netId + " asset ID " + identity.assetId); + + identity.RebuildObservers(true); + //SendSpawnMessage(objNetworkIdentity, null); + } + + internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn) + { + if (identity.serverOnly) + return; + + if (LogFilter.Debug) Debug.Log("Server SendSpawnMessage: name=" + identity.name + " sceneId=" + identity.sceneId.ToString("X") + " netid=" + identity.netId); // for easier debugging + + // one writer for owner, one for observers + NetworkWriter ownerWriter = NetworkWriterPool.GetWriter(); + NetworkWriter observersWriter = NetworkWriterPool.GetWriter(); + + + // serialize all components with initialState = true + // (can be null if has none) + identity.OnSerializeAllSafely(true, ownerWriter, out int ownerWritten, observersWriter, out int observersWritten); + + // convert to ArraySegment to avoid reader allocations + // (need to handle null case too) + ArraySegment ownerSegment = ownerWritten > 0 ? ownerWriter.ToArraySegment() : default; + ArraySegment observersSegment = observersWritten > 0 ? observersWriter.ToArraySegment() : default; + + // 'identity' is a prefab that should be spawned + if (identity.sceneId == 0) + { + SpawnPrefabMessage msg = new SpawnPrefabMessage + { + netId = identity.netId, + owner = conn?.playerController == identity, + assetId = identity.assetId, + // use local values for VR support + position = identity.transform.localPosition, + rotation = identity.transform.localRotation, + scale = identity.transform.localScale + }; + + // conn is != null when spawning it for a client + if (conn != null) + { + // use owner segment if 'conn' owns this identity, otherwise + // use observers segment + bool isOwner = identity.connectionToClient == conn; + msg.payload = isOwner ? ownerSegment : observersSegment; + + conn.Send(msg); + } + // conn is == null when spawning it for the local player + else + { + // send ownerWriter to owner + // (spawn no matter what, even if no components were + // serialized because the spawn message contains more data. + // components might still be updated later on.) + msg.payload = ownerSegment; + SendToClientOfPlayer(identity, msg); + + // send observersWriter to everyone but owner + // (spawn no matter what, even if no components were + // serialized because the spawn message contains more data. + // components might still be updated later on.) + msg.payload = observersSegment; + SendToReady(identity, msg, false); + } + } + // 'identity' is a scene object that should be spawned again + else + { + SpawnSceneObjectMessage msg = new SpawnSceneObjectMessage + { + netId = identity.netId, + owner = conn?.playerController == identity, + sceneId = identity.sceneId, + // use local values for VR support + position = identity.transform.localPosition, + rotation = identity.transform.localRotation, + scale = identity.transform.localScale + }; + + // conn is != null when spawning it for a client + if (conn != null) + { + // use owner segment if 'conn' owns this identity, otherwise + // use observers segment + bool isOwner = identity.connectionToClient == conn; + msg.payload = isOwner ? ownerSegment : observersSegment; + + conn.Send(msg); + } + // conn is == null when spawning it for the local player + else + { + // send ownerWriter to owner + // (spawn no matter what, even if no components were + // serialized because the spawn message contains more data. + // components might still be updated later on.) + msg.payload = ownerSegment; + SendToClientOfPlayer(identity, msg); + + // send observersWriter to everyone but owner + // (spawn no matter what, even if no components were + // serialized because the spawn message contains more data. + // components might still be updated later on.) + msg.payload = observersSegment; + SendToReady(identity, msg, false); + } + } + + NetworkWriterPool.Recycle(ownerWriter); + NetworkWriterPool.Recycle(observersWriter); + } + + /// + /// This destroys all the player objects associated with a NetworkConnections on a 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. + /// + /// The connections object to clean up for. + public static void DestroyPlayerForConnection(NetworkConnection conn) + { + // => destroy what we can destroy. + HashSet tmp = new HashSet(conn.clientOwnedObjects); + foreach (uint netId in tmp) + { + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity)) + { + Destroy(identity.gameObject); + } + } + + if (conn.playerController != null) + { + DestroyObject(conn.playerController, true); + conn.playerController = null; + } + } + + /// + /// 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. + /// + /// Game object with NetworkIdentity to spawn. + public static void Spawn(GameObject obj) + { + if (VerifyCanSpawn(obj)) + { + SpawnObject(obj); + } + } + + static bool CheckForPrefab(GameObject obj) + { +#if UNITY_EDITOR +#if UNITY_2018_3_OR_NEWER + return UnityEditor.PrefabUtility.IsPartOfPrefabAsset(obj); +#elif UNITY_2018_2_OR_NEWER + return (UnityEditor.PrefabUtility.GetCorrespondingObjectFromSource(obj) == null) && (UnityEditor.PrefabUtility.GetPrefabObject(obj) != null); +#else + return (UnityEditor.PrefabUtility.GetPrefabParent(obj) == null) && (UnityEditor.PrefabUtility.GetPrefabObject(obj) != null); +#endif +#else + return false; +#endif + } + + static bool VerifyCanSpawn(GameObject obj) + { + if (CheckForPrefab(obj)) + { + Debug.LogErrorFormat("GameObject {0} is a prefab, it can't be spawned. This will cause errors in builds.", obj.name); + return false; + } + + return true; + } + + /// + /// This spawns an object like NetworkServer.Spawn() but also assigns Client Authority to the specified client. + /// This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + /// + /// The object to spawn. + /// The player object to set Client Authority to. + /// + public static bool SpawnWithClientAuthority(GameObject obj, GameObject player) + { + NetworkIdentity identity = player.GetComponent(); + if (identity == null) + { + Debug.LogError("SpawnWithClientAuthority player object has no NetworkIdentity"); + return false; + } + + if (identity.connectionToClient == null) + { + Debug.LogError("SpawnWithClientAuthority player object is not a player."); + return false; + } + + return SpawnWithClientAuthority(obj, identity.connectionToClient); + } + + /// + /// This spawns an object like NetworkServer.Spawn() but also assigns Client Authority to the specified client. + /// This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + /// + /// The object to spawn. + /// The connection to set Client Authority to. + /// + public static bool SpawnWithClientAuthority(GameObject obj, NetworkConnection conn) + { + if (!conn.isReady) + { + Debug.LogError("SpawnWithClientAuthority NetworkConnection is not ready!"); + return false; + } + + Spawn(obj); + + NetworkIdentity identity = obj.GetComponent(); + if (identity == null || !identity.isServer) + { + // spawning the object failed. + return false; + } + + return identity.AssignClientAuthority(conn); + } + + /// + /// This spawns an object like NetworkServer.Spawn() but also assigns Client Authority to the specified client. + /// This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + /// + /// The object to spawn. + /// The assetId of the object to spawn. Used for custom spawn handlers. + /// The connection to set Client Authority to. + /// + public static bool SpawnWithClientAuthority(GameObject obj, Guid assetId, NetworkConnection conn) + { + Spawn(obj, assetId); + + NetworkIdentity identity = obj.GetComponent(); + if (identity == null || !identity.isServer) + { + // spawning the object failed. + return false; + } + + return identity.AssignClientAuthority(conn); + } + + /// + /// This spawns an object like NetworkServer.Spawn() but also assigns Client Authority to the specified client. + /// This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + /// + /// The object to spawn. + /// The assetId of the object to spawn. Used for custom spawn handlers. + public static void Spawn(GameObject obj, Guid assetId) + { + if (VerifyCanSpawn(obj)) + { + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + SpawnObject(obj); + } + } + + static void DestroyObject(NetworkIdentity identity, bool destroyServerObject) + { + if (LogFilter.Debug) Debug.Log("DestroyObject instance:" + identity.netId); + NetworkIdentity.spawned.Remove(identity.netId); + + identity.clientAuthorityOwner?.RemoveOwnedObject(identity); + + ObjectDestroyMessage msg = new ObjectDestroyMessage + { + netId = identity.netId + }; + SendToObservers(identity, msg); + + identity.ClearObservers(); + if (NetworkClient.active && localClientActive) + { + identity.OnNetworkDestroy(); + } + + // when unspawning, dont destroy the server's object + if (destroyServerObject) + { + UnityEngine.Object.Destroy(identity.gameObject); + } + identity.MarkForReset(); + } + + /// + /// 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(). + /// + /// Game object to destroy. + public static void Destroy(GameObject obj) + { + if (obj == null) + { + if (LogFilter.Debug) Debug.Log("NetworkServer DestroyObject is null"); + return; + } + + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + DestroyObject(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. + /// + /// The spawned object to be unspawned. + public static void UnSpawn(GameObject obj) + { + if (obj == null) + { + if (LogFilter.Debug) Debug.Log("NetworkServer UnspawnObject is null"); + return; + } + + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + DestroyObject(identity, false); + } + } + + /// + /// Obsolete: Use instead. + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use NetworkIdentity.spawned[netId] instead.")] + public static GameObject FindLocalObject(uint netId) + { + if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity identity)) + { + return identity.gameObject; + } + return null; + } + + static bool ValidateSceneObject(NetworkIdentity identity) + { + 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; + } + + /// + /// This causes NetworkIdentity objects in a scene to be spawned on a 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. + /// + /// Success if objects where spawned. + public static bool SpawnObjects() + { + if (!active) + return true; + + NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); + foreach (NetworkIdentity identity in identities) + { + if (ValidateSceneObject(identity)) + { + if (LogFilter.Debug) Debug.Log("SpawnObjects sceneId:" + identity.sceneId.ToString("X") + " name:" + identity.gameObject.name); + identity.Reset(); + identity.gameObject.SetActive(true); + } + } + + foreach (NetworkIdentity identity in identities) + { + if (ValidateSceneObject(identity)) + { + Spawn(identity.gameObject); + + // these objects are server authority - even if "localPlayerAuthority" is set on them + identity.ForceAuthority(true); + } + } + return true; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkServer.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkServer.cs.meta new file mode 100644 index 0000000..4d44a26 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkServer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5f5ec068f5604c32b160bc49ee97b75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs b/Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs new file mode 100644 index 0000000..c7483b5 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs @@ -0,0 +1,24 @@ +using UnityEngine; + +namespace Mirror +{ + /// + /// This component is used to make a gameObject a starting position for spawning player objects in multiplayer games. + /// This object's transform will be automatically registered and unregistered with the NetworkManager as a starting position. + /// + [DisallowMultipleComponent] + [AddComponentMenu("Network/NetworkStartPosition")] + [HelpURL("https://mirror-networking.com/xmldocs/articles/Components/NetworkStartPosition.html")] + public class NetworkStartPosition : MonoBehaviour + { + public void Awake() + { + NetworkManager.RegisterStartPosition(transform); + } + + public void OnDestroy() + { + NetworkManager.UnRegisterStartPosition(transform); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs.meta new file mode 100644 index 0000000..97f5445 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkStartPosition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41f84591ce72545258ea98cb7518d8b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkTime.cs b/Assets/Packages/Mirror/Runtime/NetworkTime.cs new file mode 100644 index 0000000..222d354 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkTime.cs @@ -0,0 +1,177 @@ +using System; +using UnityEngine; +using Stopwatch = System.Diagnostics.Stopwatch; + +namespace Mirror +{ + /// + /// Synchronize time between the server and the clients + /// + public static class NetworkTime + { + /// + /// how often are we sending ping messages + /// 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; + + + // Date and time when the application started + static readonly Stopwatch stopwatch = new Stopwatch(); + + static NetworkTime() + { + stopwatch.Start(); + } + + 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 the clock time _in this system_ + static double LocalTime() + { + return stopwatch.Elapsed.TotalSeconds; + } + + public static void Reset() + { + _rtt = new ExponentialMovingAverage(PingWindowSize); + _offset = new ExponentialMovingAverage(PingWindowSize); + offsetMin = double.MinValue; + offsetMax = double.MaxValue; + } + + internal static void UpdateClient() + { + if (Time.time - lastPingTime >= PingFrequency) + { + NetworkPingMessage pingMessage = new NetworkPingMessage(LocalTime()); + NetworkClient.Send(pingMessage); + lastPingTime = Time.time; + } + } + + // 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(NetworkConnection conn, NetworkPingMessage msg) + { + if (LogFilter.Debug) Debug.Log("OnPingServerMessage conn=" + conn); + + NetworkPongMessage pongMsg = new NetworkPongMessage + { + clientTime = msg.clientTime, + serverTime = LocalTime() + }; + + conn.Send(pongMsg); + } + + // 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(NetworkConnection _, NetworkPongMessage msg) + { + double now = LocalTime(); + + // how long did this message take to come back + double newRtt = now - msg.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 - msg.serverTime; + + double newOffsetMin = now - newRtt - msg.serverTime; + double newOffsetMax = now - msg.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); + } + } + + /// + /// The time in seconds since the server started. + /// + /// + /// + /// Note this value works in the client and the server + /// the value is synchronized accross the network with high accuracy + /// + /// You should not cast this down to a float because the it loses too much accuracy + /// when the server is up for a while + /// 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 => LocalTime() - _offset.Value; + + /// + /// Measurement of the variance of time. + /// The higher the variance, the less accurate the time is + /// + public static double timeVar => _offset.Var; + + /// + /// standard deviation of time. + /// The higher the variance, the less accurate the time is + /// + public static double timeSd => Math.Sqrt(timeVar); + + /// + /// Clock difference in seconds between the client and the server + /// + /// + /// Note this value is always 0 at the server + /// + public static double offset => _offset.Value; + + /// + /// how long in seconds does it take for a message to go + /// to the server and come back + /// + public static double rtt => _rtt.Value; + + /// + /// measure variance of rtt + /// the higher the number, the less accurate rtt is + /// + public static double rttVar => _rtt.Var; + + /// + /// Measure the standard deviation of rtt + /// the higher the number, the less accurate rtt is + /// + public static double rttSd => Math.Sqrt(rttVar); + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkTime.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkTime.cs.meta new file mode 100644 index 0000000..f5c2b6c --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkTime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 09a0c241fc4a5496dbf4a0ab6e9a312c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkWriter.cs b/Assets/Packages/Mirror/Runtime/NetworkWriter.cs new file mode 100644 index 0000000..1766b5d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkWriter.cs @@ -0,0 +1,564 @@ +using System; +using System.IO; +using System.Text; +using UnityEngine; + +namespace Mirror +{ + // Binary stream Writer. Supports simple types, buffers, arrays, structs, and nested types + public class NetworkWriter + { + public const int MaxStringLength = 1024 * 32; + + // 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 + readonly MemoryStream stream = new MemoryStream(); + + // '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 { get { return (int)stream.Position; } set { stream.Position = value; } } + + // MemoryStream has 3 values: Position, Length and Capacity. + // Position is used to indicate where we are writing + // Length is how much data we have written + // capacity is how much memory we have allocated + // ToArray returns all the data we have written, regardless of the current position + public byte[] ToArray() + { + stream.Flush(); + return stream.ToArray(); + } + + // Gets the serialized data in an ArraySegment + // this is similar to ToArray(), but it gets the data in O(1) + // and without allocations. + // Do not write anything else or modify the NetworkWriter + // while you are using the ArraySegment + public ArraySegment ToArraySegment() + { + stream.Flush(); + if (stream.TryGetBuffer(out ArraySegment data)) + { + return data; + } + throw new Exception("Cannot expose contents of memory stream. Make sure that MemoryStream buffer is publicly visible (see MemoryStream source code)."); + } + + // reset both the position and length of the stream, but leaves the capacity the same + // so that we can reuse this writer without extra allocations + public void SetLength(long value) + { + stream.SetLength(value); + } + + public void WriteByte(byte value) => stream.WriteByte(value); + + // for byte arrays with consistent size, where the reader knows how many to read + // (like a packet opcode that's always the same) + public void WriteBytes(byte[] buffer, int offset, int count) + { + // no null check because we would need to write size info for that too (hence WriteBytesAndSize) + stream.Write(buffer, offset, count); + } + + public void WriteUInt32(uint value) + { + WriteByte((byte)(value & 0xFF)); + WriteByte((byte)((value >> 8) & 0xFF)); + WriteByte((byte)((value >> 16) & 0xFF)); + WriteByte((byte)((value >> 24) & 0xFF)); + } + + public void WriteInt32(int value) => WriteUInt32((uint)value); + + public void WriteUInt64(ulong value) + { + WriteByte((byte)(value & 0xFF)); + WriteByte((byte)((value >> 8) & 0xFF)); + WriteByte((byte)((value >> 16) & 0xFF)); + WriteByte((byte)((value >> 24) & 0xFF)); + WriteByte((byte)((value >> 32) & 0xFF)); + WriteByte((byte)((value >> 40) & 0xFF)); + WriteByte((byte)((value >> 48) & 0xFF)); + WriteByte((byte)((value >> 56) & 0xFF)); + } + + public void WriteInt64(long value) => WriteUInt64((ulong)value); + + #region Obsoletes + [Obsolete("Use WriteUInt16 instead")] + public void Write(ushort value) => this.WriteUInt16(value); + + [Obsolete("Use WriteUInt32 instead")] + public void Write(uint value) => WriteUInt32(value); + + [Obsolete("Use WriteUInt64 instead")] + public void Write(ulong value) => WriteUInt64(value); + + [Obsolete("Use WriteByte instead")] + public void Write(byte value) => stream.WriteByte(value); + + [Obsolete("Use WriteSByte instead")] + public void Write(sbyte value) => WriteByte((byte)value); + + // write char the same way that NetworkReader reads it (2 bytes) + [Obsolete("Use WriteChar instead")] + public void Write(char value) => this.WriteUInt16((ushort)value); + + [Obsolete("Use WriteBoolean instead")] + public void Write(bool value) => WriteByte((byte)(value ? 1 : 0)); + + [Obsolete("Use WriteInt16 instead")] + public void Write(short value) => this.WriteUInt16((ushort)value); + + [Obsolete("Use WriteInt32 instead")] + public void Write(int value) => WriteUInt32((uint)value); + + [Obsolete("Use WriteInt64 instead")] + public void Write(long value) => WriteUInt64((ulong)value); + + [Obsolete("Use WriteSingle instead")] + public void Write(float value) => this.WriteSingle(value); + + [Obsolete("Use WriteDouble instead")] + public void Write(double value) => this.WriteDouble(value); + + [Obsolete("Use WriteDecimal instead")] + public void Write(decimal value) => this.WriteDecimal(value); + + [Obsolete("Use WriteString instead")] + public void Write(string value) => this.WriteString(value); + + [Obsolete("Use WriteBytes instead")] + public void Write(byte[] buffer, int offset, int count) => WriteBytes(buffer, offset, count); + + [Obsolete("Use WriteVector2 instead")] + public void Write(Vector2 value) => this.WriteVector2(value); + + [Obsolete("Use WriteVector3 instead")] + public void Write(Vector3 value) => this.WriteVector3(value); + + [Obsolete("Use WriteVector4 instead")] + public void Write(Vector4 value) => this.WriteVector4(value); + + [Obsolete("Use WriteVector2Int instead")] + public void Write(Vector2Int value) => this.WriteVector2Int(value); + + [Obsolete("Use WriteVector3Int instead")] + public void Write(Vector3Int value) => this.WriteVector3Int(value); + + [Obsolete("Use WriteColor instead")] + public void Write(Color value) => this.WriteColor(value); + + [Obsolete("Use WriteColor32 instead")] + public void Write(Color32 value) => this.WriteColor32(value); + + [Obsolete("Use WriteQuaternion instead")] + public void Write(Quaternion value) => this.WriteQuaternion(value); + + [Obsolete("Use WriteRect instead")] + public void Write(Rect value) => this.WriteRect(value); + + [Obsolete("Use WritePlane instead")] + public void Write(Plane value) => this.WritePlane(value); + + [Obsolete("Use WriteRay instead")] + public void Write(Ray value) => this.WriteRay(value); + + [Obsolete("Use WriteMatrix4x4 instead")] + public void Write(Matrix4x4 value) => this.WriteMatrix4x4(value); + + [Obsolete("Use WriteGuid instead")] + public void Write(Guid value) => this.WriteGuid(value); + + [Obsolete("Use WriteNetworkIdentity instead")] + public void Write(NetworkIdentity value) => this.WriteNetworkIdentity(value); + + [Obsolete("Use WriteTransform instead")] + public void Write(Transform value) => this.WriteTransform(value); + + [Obsolete("Use WriteGameObject instead")] + public void Write(GameObject value) => this.WriteGameObject(value); + + #endregion + } + + + // 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]; + + public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteByte(value); + + public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteByte((byte)value); + + public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteUInt16((ushort)value); + + public static void WriteBoolean(this NetworkWriter writer, bool value) => writer.WriteByte((byte)(value ? 1 : 0)); + + public static void WriteUInt16(this NetworkWriter writer, ushort value) + { + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)(value >> 8)); + } + + public static void WriteInt16(this NetworkWriter writer, short value) => writer.WriteUInt16((ushort)value); + + public static void WriteSingle(this NetworkWriter writer, float value) + { + UIntFloat converter = new UIntFloat + { + floatValue = value + }; + writer.WriteUInt32(converter.intValue); + } + + public static void WriteDouble(this NetworkWriter writer, double value) + { + UIntDouble converter = new UIntDouble + { + doubleValue = value + }; + writer.WriteUInt64(converter.longValue); + } + + public static void WriteDecimal(this NetworkWriter writer, decimal value) + { + // the only way to read it without allocations is to both read and + // write it with the FloatConverter (which is not binary compatible + // to writer.Write(decimal), hence why we use it here too) + UIntDecimal converter = new UIntDecimal + { + decimalValue = value + }; + writer.WriteUInt64(converter.longValue1); + writer.WriteUInt64(converter.longValue2); + } + + 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.WriteUInt16((ushort)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.WriteUInt16(checked((ushort)(size + 1))); + writer.WriteBytes(stringBuffer, 0, size); + } + + // 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 + // write 0 for null array, increment normal size by 1 to save bandwith + // (using size=-1 for null would limit max size to 32kb instead of 64kb) + if (buffer == null) + { + writer.WritePackedUInt32(0u); + return; + } + writer.WritePackedUInt32(checked((uint)count) + 1u); + writer.WriteBytes(buffer, offset, 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) + 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); + } + + public static void WriteBytesAndSizeSegment(this NetworkWriter writer, ArraySegment buffer) + { + writer.WriteBytesAndSize(buffer.Array, buffer.Offset, buffer.Count); + } + + // zigzag encoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba + public static void WritePackedInt32(this NetworkWriter writer, int i) + { + uint zigzagged = (uint)((i >> 31) ^ (i << 1)); + writer.WritePackedUInt32(zigzagged); + } + + // http://sqlite.org/src4/doc/trunk/www/varint.wiki + public static void WritePackedUInt32(this NetworkWriter writer, uint value) + { + // for 32 bit values WritePackedUInt64 writes the + // same exact thing bit by bit + writer.WritePackedUInt64(value); + } + + // zigzag encoding https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba + public static void WritePackedInt64(this NetworkWriter writer, long i) + { + ulong zigzagged = (ulong)((i >> 63) ^ (i << 1)); + writer.WritePackedUInt64(zigzagged); + } + + public static void WritePackedUInt64(this NetworkWriter writer, ulong value) + { + if (value <= 240) + { + writer.WriteByte((byte)value); + return; + } + if (value <= 2287) + { + writer.WriteByte((byte)(((value - 240) >> 8) + 241)); + writer.WriteByte((byte)((value - 240) & 0xFF)); + return; + } + if (value <= 67823) + { + writer.WriteByte((byte)249); + writer.WriteByte((byte)((value - 2288) >> 8)); + writer.WriteByte((byte)((value - 2288) & 0xFF)); + return; + } + if (value <= 16777215) + { + writer.WriteByte((byte)250); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + return; + } + if (value <= 4294967295) + { + writer.WriteByte((byte)251); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + return; + } + if (value <= 1099511627775) + { + writer.WriteByte((byte)252); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + return; + } + if (value <= 281474976710655) + { + writer.WriteByte((byte)253); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + writer.WriteByte((byte)((value >> 40) & 0xFF)); + return; + } + if (value <= 72057594037927935) + { + writer.WriteByte((byte)254); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + writer.WriteByte((byte)((value >> 40) & 0xFF)); + writer.WriteByte((byte)((value >> 48) & 0xFF)); + return; + } + + // all others + { + writer.WriteByte((byte)255); + writer.WriteByte((byte)(value & 0xFF)); + writer.WriteByte((byte)((value >> 8) & 0xFF)); + writer.WriteByte((byte)((value >> 16) & 0xFF)); + writer.WriteByte((byte)((value >> 24) & 0xFF)); + writer.WriteByte((byte)((value >> 32) & 0xFF)); + writer.WriteByte((byte)((value >> 40) & 0xFF)); + writer.WriteByte((byte)((value >> 48) & 0xFF)); + writer.WriteByte((byte)((value >> 56) & 0xFF)); + } + } + + public static void WriteVector2(this NetworkWriter writer, Vector2 value) + { + writer.WriteSingle(value.x); + writer.WriteSingle(value.y); + } + + public static void WriteVector3(this NetworkWriter writer, Vector3 value) + { + writer.WriteSingle(value.x); + writer.WriteSingle(value.y); + writer.WriteSingle(value.z); + } + + public static void WriteVector4(this NetworkWriter writer, Vector4 value) + { + writer.WriteSingle(value.x); + writer.WriteSingle(value.y); + writer.WriteSingle(value.z); + writer.WriteSingle(value.w); + } + + public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) + { + writer.WritePackedInt32(value.x); + writer.WritePackedInt32(value.y); + } + + public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) + { + writer.WritePackedInt32(value.x); + writer.WritePackedInt32(value.y); + writer.WritePackedInt32(value.z); + } + + public static void WriteColor(this NetworkWriter writer, Color value) + { + writer.WriteSingle(value.r); + writer.WriteSingle(value.g); + writer.WriteSingle(value.b); + writer.WriteSingle(value.a); + } + + public static void WriteColor32(this NetworkWriter writer, Color32 value) + { + writer.WriteByte(value.r); + writer.WriteByte(value.g); + writer.WriteByte(value.b); + writer.WriteByte(value.a); + } + + public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) + { + writer.WriteSingle(value.x); + writer.WriteSingle(value.y); + writer.WriteSingle(value.z); + writer.WriteSingle(value.w); + } + + public static void WriteRect(this NetworkWriter writer, Rect value) + { + writer.WriteSingle(value.xMin); + writer.WriteSingle(value.yMin); + writer.WriteSingle(value.width); + writer.WriteSingle(value.height); + } + + public static void WritePlane(this NetworkWriter writer, Plane value) + { + writer.WriteVector3(value.normal); + writer.WriteSingle(value.distance); + } + + public static void WriteRay(this NetworkWriter writer, Ray value) + { + writer.WriteVector3(value.origin); + writer.WriteVector3(value.direction); + } + + public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) + { + writer.WriteSingle(value.m00); + writer.WriteSingle(value.m01); + writer.WriteSingle(value.m02); + writer.WriteSingle(value.m03); + writer.WriteSingle(value.m10); + writer.WriteSingle(value.m11); + writer.WriteSingle(value.m12); + writer.WriteSingle(value.m13); + writer.WriteSingle(value.m20); + writer.WriteSingle(value.m21); + writer.WriteSingle(value.m22); + writer.WriteSingle(value.m23); + writer.WriteSingle(value.m30); + writer.WriteSingle(value.m31); + writer.WriteSingle(value.m32); + writer.WriteSingle(value.m33); + } + + public static void WriteGuid(this NetworkWriter writer, Guid value) + { + byte[] data = value.ToByteArray(); + writer.WriteBytes(data, 0, data.Length); + } + + public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value) + { + if (value == null) + { + writer.WritePackedUInt32(0); + return; + } + writer.WritePackedUInt32(value.netId); + } + + public static void WriteTransform(this NetworkWriter writer, Transform value) + { + if (value == null || value.gameObject == null) + { + writer.WritePackedUInt32(0); + return; + } + NetworkIdentity identity = value.GetComponent(); + if (identity != null) + { + writer.WritePackedUInt32(identity.netId); + } + else + { + Debug.LogWarning("NetworkWriter " + value + " has no NetworkIdentity"); + writer.WritePackedUInt32(0); + } + } + + public static void WriteGameObject(this NetworkWriter writer, GameObject value) + { + if (value == null) + { + writer.WritePackedUInt32(0); + return; + } + NetworkIdentity identity = value.GetComponent(); + if (identity != null) + { + writer.WritePackedUInt32(identity.netId); + } + else + { + Debug.LogWarning("NetworkWriter " + value + " has no NetworkIdentity"); + writer.WritePackedUInt32(0); + } + } + + public static void Write(this NetworkWriter writer, T msg) where T : IMessageBase + { + msg.Serialize(writer); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/NetworkWriter.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkWriter.cs.meta new file mode 100644 index 0000000..240f74a --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 48d2207bcef1f4477b624725f075f9bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs b/Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs new file mode 100644 index 0000000..b3a32a5 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Mirror +{ + + public static class NetworkWriterPool + { + static readonly Stack pool = new Stack(); + + public static NetworkWriter GetWriter() + { + if (pool.Count != 0) + { + NetworkWriter writer = pool.Pop(); + // reset cached writer length and position + writer.SetLength(0); + return writer; + } + + return new NetworkWriter(); + } + + public static void Recycle(NetworkWriter writer) + { + pool.Push(writer); + } + } +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs.meta b/Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs.meta new file mode 100644 index 0000000..d383901 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/NetworkWriterPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3f34b53bea38e4f259eb8dc211e4fdb6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/StringHash.cs b/Assets/Packages/Mirror/Runtime/StringHash.cs new file mode 100644 index 0000000..95e9b07 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/StringHash.cs @@ -0,0 +1,18 @@ +namespace Mirror +{ + public static class StringHash + { + // 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; + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/StringHash.cs.meta b/Assets/Packages/Mirror/Runtime/StringHash.cs.meta new file mode 100644 index 0000000..6198581 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/StringHash.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 733f020f9b76d453da841089579fd7a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/SyncDictionary.cs b/Assets/Packages/Mirror/Runtime/SyncDictionary.cs new file mode 100644 index 0000000..930b8e5 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncDictionary.cs @@ -0,0 +1,308 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Mirror +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class SyncDictionary : IDictionary, SyncObject + { + public delegate void SyncDictionaryChanged(Operation op, TKey key, TValue item); + + readonly IDictionary objects; + + public int Count => objects.Count; + public bool IsReadOnly { get; private set; } + public event SyncDictionaryChanged Callback; + + public enum Operation : byte + { + OP_ADD, + OP_CLEAR, + OP_REMOVE, + OP_SET, + OP_DIRTY + } + + struct Change + { + internal Operation operation; + internal TKey key; + internal TValue item; + } + + readonly List changes = new List(); + // how many changes we need to ignore + // this is needed because when we initialize the list, + // we might later receive changes that have already been applied + // so we need to skip them + int changesAhead; + + protected virtual void SerializeKey(NetworkWriter writer, TKey item) { } + protected virtual void SerializeItem(NetworkWriter writer, TValue item) { } + protected virtual TKey DeserializeKey(NetworkReader reader) => default; + protected virtual TValue DeserializeItem(NetworkReader reader) => default; + + public bool IsDirty => changes.Count > 0; + + public ICollection Keys => objects.Keys; + + public ICollection Values => objects.Values; + + // throw away all the changes + // this should be called after a successfull sync + public void Flush() => changes.Clear(); + + protected SyncDictionary() + { + objects = new Dictionary(); + } + + protected SyncDictionary(IEqualityComparer eq) + { + objects = new Dictionary(eq); + } + + protected SyncDictionary(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 + }; + + changes.Add(change); + + Callback?.Invoke(op, key, item); + } + + public void OnSerializeAll(NetworkWriter writer) + { + // if init, write the full list content + writer.WritePackedUInt32((uint)objects.Count); + + foreach (KeyValuePair syncItem in objects) + { + SerializeKey(writer, syncItem.Key); + SerializeItem(writer, syncItem.Value); + } + + // all changes have been applied already + // thus the client will need to skip all the pending changes + // or they would be applied again. + // So we write how many changes are pending + writer.WritePackedUInt32((uint)changes.Count); + } + + public void OnSerializeDelta(NetworkWriter writer) + { + // write all the queued up changes + writer.WritePackedUInt32((uint)changes.Count); + + for (int i = 0; i < changes.Count; i++) + { + Change change = changes[i]; + writer.WriteByte((byte)change.operation); + + switch (change.operation) + { + case Operation.OP_ADD: + case Operation.OP_REMOVE: + case Operation.OP_SET: + case Operation.OP_DIRTY: + SerializeKey(writer, change.key); + SerializeItem(writer, change.item); + break; + case Operation.OP_CLEAR: + break; + } + } + } + + public 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.ReadPackedUInt32(); + + objects.Clear(); + changes.Clear(); + + for (int i = 0; i < count; i++) + { + TKey key = DeserializeKey(reader); + TValue obj = DeserializeItem(reader); + objects.Add(key, obj); + } + + // We will need to skip all these changes + // the next time the list is synchronized + // because they have already been applied + changesAhead = (int)reader.ReadPackedUInt32(); + } + + public void OnDeserializeDelta(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + int changesCount = (int)reader.ReadPackedUInt32(); + + for (int i = 0; i < changesCount; i++) + { + Operation operation = (Operation)reader.ReadByte(); + + // apply the operation only if it is a new change + // that we have not applied yet + bool apply = changesAhead == 0; + TKey key = default; + TValue item = default; + + switch (operation) + { + case Operation.OP_ADD: + case Operation.OP_SET: + case Operation.OP_DIRTY: + key = DeserializeKey(reader); + item = DeserializeItem(reader); + if (apply) + { + objects[key] = item; + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + objects.Clear(); + } + break; + + case Operation.OP_REMOVE: + key = DeserializeKey(reader); + item = DeserializeItem(reader); + if (apply) + { + objects.Remove(key); + } + break; + } + + if (apply) + { + Callback?.Invoke(operation, key, item); + } + // we just skipped this change + else + { + changesAhead--; + } + } + } + + public void Clear() + { + objects.Clear(); + AddOperation(Operation.OP_CLEAR, default, default); + } + + public bool ContainsKey(TKey key) => objects.ContainsKey(key); + + public bool Remove(TKey key) + { + if (objects.TryGetValue(key, out TValue item) && objects.Remove(key)) + { + AddOperation(Operation.OP_REMOVE, key, item); + return true; + } + return false; + } + + public void Dirty(TKey index) + { + AddOperation(Operation.OP_DIRTY, index, objects[index]); + } + + public TValue this[TKey i] + { + get => objects[i]; + set + { + if (ContainsKey(i)) + { + AddOperation(Operation.OP_SET, i, value); + } + else + { + AddOperation(Operation.OP_ADD, i, value); + } + objects[i] = value; + } + } + + 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 Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out TValue val) && EqualityComparer.Default.Equals(val, item.Value); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array == null) + { + throw new System.ArgumentNullException("Array Is Null"); + } + if (arrayIndex < 0 || arrayIndex > array.Length) + { + throw new System.ArgumentOutOfRangeException("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) + { + array[i] = item; + i++; + } + } + + public bool Remove(KeyValuePair item) + { + bool result = objects.Remove(item.Key); + if (result) + { + AddOperation(Operation.OP_REMOVE, item.Key, item.Value); + } + return result; + } + + public IEnumerator> GetEnumerator() => ((IDictionary)objects).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IDictionary)objects).GetEnumerator(); + } +} diff --git a/Assets/Packages/Mirror/Runtime/SyncDictionary.cs.meta b/Assets/Packages/Mirror/Runtime/SyncDictionary.cs.meta new file mode 100644 index 0000000..9b4ff53 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncDictionary.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b346c49cfdb668488a364c3023590e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/SyncList.cs b/Assets/Packages/Mirror/Runtime/SyncList.cs new file mode 100644 index 0000000..83ddc02 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncList.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Mirror +{ + public class SyncListString : SyncList + { + protected override void SerializeItem(NetworkWriter writer, string item) => writer.WriteString(item); + protected override string DeserializeItem(NetworkReader reader) => reader.ReadString(); + } + + public class SyncListFloat : SyncList + { + protected override void SerializeItem(NetworkWriter writer, float item) => writer.WriteSingle(item); + protected override float DeserializeItem(NetworkReader reader) => reader.ReadSingle(); + } + + public class SyncListInt : SyncList + { + protected override void SerializeItem(NetworkWriter writer, int item) => writer.WritePackedInt32(item); + protected override int DeserializeItem(NetworkReader reader) => reader.ReadPackedInt32(); + } + + public class SyncListUInt : SyncList + { + protected override void SerializeItem(NetworkWriter writer, uint item) => writer.WritePackedUInt32(item); + protected override uint DeserializeItem(NetworkReader reader) => reader.ReadPackedUInt32(); + } + + public class SyncListBool : SyncList + { + protected override void SerializeItem(NetworkWriter writer, bool item) => writer.WriteBoolean(item); + protected override bool DeserializeItem(NetworkReader reader) => reader.ReadBoolean(); + } + + // Original UNET name is SyncListStruct and original Weaver weavers anything + // that contains the name 'SyncListStruct', without considering the name- + // space. + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use SyncList instead")] + public class SyncListSTRUCT : SyncList where T : struct + { + public T GetItem(int i) => base[i]; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class SyncList : IList, IReadOnlyList, SyncObject + { + public delegate void SyncListChanged(Operation op, int itemIndex, T item); + + readonly IList objects; + + public int Count => objects.Count; + public bool IsReadOnly { get; private set; } + public event SyncListChanged Callback; + + public enum Operation : byte + { + OP_ADD, + OP_CLEAR, + OP_INSERT, + OP_REMOVE, + OP_REMOVEAT, + OP_SET, + OP_DIRTY + } + + struct Change + { + internal Operation operation; + internal int index; + internal T item; + } + + readonly List changes = new List(); + // how many changes we need to ignore + // this is needed because when we initialize the list, + // we might later receive changes that have already been applied + // so we need to skip them + int changesAhead; + + protected virtual void SerializeItem(NetworkWriter writer, T item) { } + protected virtual T DeserializeItem(NetworkReader reader) => default; + + + protected SyncList() + { + objects = new List(); + } + + protected SyncList(IList objects) + { + this.objects = objects; + } + + public bool IsDirty => changes.Count > 0; + + // throw away all the changes + // this should be called after a successfull sync + public void Flush() => changes.Clear(); + + void AddOperation(Operation op, int itemIndex, T item) + { + if (IsReadOnly) + { + throw new InvalidOperationException("Synclists can only be modified at the server"); + } + + Change change = new Change + { + operation = op, + index = itemIndex, + item = item + }; + + changes.Add(change); + + Callback?.Invoke(op, itemIndex, item); + } + + void AddOperation(Operation op, int itemIndex) => AddOperation(op, itemIndex, default); + + public void OnSerializeAll(NetworkWriter writer) + { + // if init, write the full list content + writer.WritePackedUInt32((uint)objects.Count); + + for (int i = 0; i < objects.Count; i++) + { + T obj = objects[i]; + SerializeItem(writer, obj); + } + + // all changes have been applied already + // thus the client will need to skip all the pending changes + // or they would be applied again. + // So we write how many changes are pending + writer.WritePackedUInt32((uint)changes.Count); + } + + public void OnSerializeDelta(NetworkWriter writer) + { + // write all the queued up changes + writer.WritePackedUInt32((uint)changes.Count); + + for (int i = 0; i < changes.Count; i++) + { + Change change = changes[i]; + writer.WriteByte((byte)change.operation); + + switch (change.operation) + { + case Operation.OP_ADD: + case Operation.OP_REMOVE: + SerializeItem(writer, change.item); + break; + + case Operation.OP_CLEAR: + break; + + case Operation.OP_REMOVEAT: + writer.WritePackedUInt32((uint)change.index); + break; + + case Operation.OP_INSERT: + case Operation.OP_SET: + case Operation.OP_DIRTY: + writer.WritePackedUInt32((uint)change.index); + SerializeItem(writer, change.item); + break; + } + } + } + + public 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.ReadPackedUInt32(); + + objects.Clear(); + changes.Clear(); + + for (int i = 0; i < count; i++) + { + T obj = DeserializeItem(reader); + objects.Add(obj); + } + + // We will need to skip all these changes + // the next time the list is synchronized + // because they have already been applied + changesAhead = (int)reader.ReadPackedUInt32(); + } + + public void OnDeserializeDelta(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + int changesCount = (int)reader.ReadPackedUInt32(); + + for (int i = 0; i < changesCount; i++) + { + Operation operation = (Operation)reader.ReadByte(); + + // apply the operation only if it is a new change + // that we have not applied yet + bool apply = changesAhead == 0; + int index = 0; + T item = default; + + switch (operation) + { + case Operation.OP_ADD: + item = DeserializeItem(reader); + if (apply) + { + index = objects.Count; + objects.Add(item); + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + objects.Clear(); + } + break; + + case Operation.OP_INSERT: + index = (int)reader.ReadPackedUInt32(); + item = DeserializeItem(reader); + if (apply) + { + objects.Insert(index, item); + } + break; + + case Operation.OP_REMOVE: + item = DeserializeItem(reader); + if (apply) + { + objects.Remove(item); + } + break; + + case Operation.OP_REMOVEAT: + index = (int)reader.ReadPackedUInt32(); + if (apply) + { + item = objects[index]; + objects.RemoveAt(index); + } + break; + + case Operation.OP_SET: + case Operation.OP_DIRTY: + index = (int)reader.ReadPackedUInt32(); + item = DeserializeItem(reader); + if (apply) + { + objects[index] = item; + } + break; + } + + if (apply) + { + Callback?.Invoke(operation, index, item); + } + // we just skipped this change + else + { + changesAhead--; + } + } + } + + public void Add(T item) + { + objects.Add(item); + AddOperation(Operation.OP_ADD, objects.Count - 1, item); + } + + public void Clear() + { + objects.Clear(); + AddOperation(Operation.OP_CLEAR, 0); + } + + public bool Contains(T item) => objects.Contains(item); + + public void CopyTo(T[] array, int index) => objects.CopyTo(array, index); + + public int IndexOf(T item) => objects.IndexOf(item); + + public int FindIndex(Predicate match) + { + for (int i = 0; i < objects.Count; ++i) + if (match(objects[i])) + return i; + return -1; + } + + public void Insert(int index, T item) + { + objects.Insert(index, item); + AddOperation(Operation.OP_INSERT, index, item); + } + + public bool Remove(T item) + { + bool result = objects.Remove(item); + if (result) + { + AddOperation(Operation.OP_REMOVE, 0, item); + } + return result; + } + + public void RemoveAt(int index) + { + objects.RemoveAt(index); + AddOperation(Operation.OP_REMOVEAT, index); + } + + public void Dirty(int index) + { + AddOperation(Operation.OP_DIRTY, index, objects[index]); + } + + public T this[int i] + { + get => objects[i]; + set + { + if (!EqualityComparer.Default.Equals(objects[i], value)) + { + objects[i] = value; + AddOperation(Operation.OP_SET, i, value); + } + } + } + + public IEnumerator GetEnumerator() => objects.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/Assets/Packages/Mirror/Runtime/SyncList.cs.meta b/Assets/Packages/Mirror/Runtime/SyncList.cs.meta new file mode 100644 index 0000000..9b9387d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncList.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 744fc71f748fe40d5940e04bf42b29f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/SyncObject.cs b/Assets/Packages/Mirror/Runtime/SyncObject.cs new file mode 100644 index 0000000..afaa508 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncObject.cs @@ -0,0 +1,26 @@ +namespace Mirror +{ + // A sync object is an object that can synchronize it's state + // between server and client, such as a SyncList + public interface SyncObject + { + // true if there are changes since the last flush + bool IsDirty { get; } + + // Discard all the queued changes + // Consider the object fully synchronized with clients + void Flush(); + + // Write a full copy of the object + void OnSerializeAll(NetworkWriter writer); + + // Write the changes made to the object + void OnSerializeDelta(NetworkWriter writer); + + // deserialize all the data in the object + void OnDeserializeAll(NetworkReader reader); + + // deserialize changes since last sync + void OnDeserializeDelta(NetworkReader reader); + } +} diff --git a/Assets/Packages/Mirror/Runtime/SyncObject.cs.meta b/Assets/Packages/Mirror/Runtime/SyncObject.cs.meta new file mode 100644 index 0000000..a67485d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae226d17a0c844041aa24cc2c023dd49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/SyncSet.cs b/Assets/Packages/Mirror/Runtime/SyncSet.cs new file mode 100644 index 0000000..0a06352 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncSet.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Mirror +{ + + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class SyncSet : ISet, SyncObject + { + public delegate void SyncSetChanged(Operation op, T item); + + readonly ISet objects; + + public int Count => objects.Count; + public bool IsReadOnly { get; private set; } + public event SyncSetChanged Callback; + + public enum Operation : byte + { + OP_ADD, + OP_CLEAR, + OP_REMOVE + } + + struct Change + { + internal Operation operation; + internal T item; + } + + readonly List changes = new List(); + // how many changes we need to ignore + // this is needed because when we initialize the list, + // we might later receive changes that have already been applied + // so we need to skip them + int changesAhead; + + protected SyncSet(ISet objects) + { + this.objects = objects; + } + + protected virtual void SerializeItem(NetworkWriter writer, T item) { } + protected virtual T DeserializeItem(NetworkReader reader) => default; + + public bool IsDirty => changes.Count > 0; + + // throw away all the changes + // this should be called after a successfull sync + public void Flush() => changes.Clear(); + + void AddOperation(Operation op, T item) + { + if (IsReadOnly) + { + throw new InvalidOperationException("SyncSets can only be modified at the server"); + } + + Change change = new Change + { + operation = op, + item = item + }; + + changes.Add(change); + + Callback?.Invoke(op, item); + } + + void AddOperation(Operation op) => AddOperation(op, default); + + public void OnSerializeAll(NetworkWriter writer) + { + // if init, write the full list content + writer.WritePackedUInt32((uint)objects.Count); + + foreach (T obj in objects) + { + SerializeItem(writer, obj); + } + + // all changes have been applied already + // thus the client will need to skip all the pending changes + // or they would be applied again. + // So we write how many changes are pending + writer.WritePackedUInt32((uint)changes.Count); + } + + public void OnSerializeDelta(NetworkWriter writer) + { + // write all the queued up changes + writer.WritePackedUInt32((uint)changes.Count); + + for (int i = 0; i < changes.Count; i++) + { + Change change = changes[i]; + writer.WriteByte((byte)change.operation); + + switch (change.operation) + { + case Operation.OP_ADD: + SerializeItem(writer, change.item); + break; + + case Operation.OP_CLEAR: + break; + + case Operation.OP_REMOVE: + SerializeItem(writer, change.item); + break; + } + } + } + + public 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.ReadPackedUInt32(); + + objects.Clear(); + changes.Clear(); + + for (int i = 0; i < count; i++) + { + T obj = DeserializeItem(reader); + objects.Add(obj); + } + + // We will need to skip all these changes + // the next time the list is synchronized + // because they have already been applied + changesAhead = (int)reader.ReadPackedUInt32(); + } + + public void OnDeserializeDelta(NetworkReader reader) + { + // This list can now only be modified by synchronization + IsReadOnly = true; + + int changesCount = (int)reader.ReadPackedUInt32(); + + for (int i = 0; i < changesCount; i++) + { + Operation operation = (Operation)reader.ReadByte(); + + // apply the operation only if it is a new change + // that we have not applied yet + bool apply = changesAhead == 0; + T item = default; + + switch (operation) + { + case Operation.OP_ADD: + item = DeserializeItem(reader); + if (apply) + { + objects.Add(item); + } + break; + + case Operation.OP_CLEAR: + if (apply) + { + objects.Clear(); + } + break; + + case Operation.OP_REMOVE: + item = DeserializeItem(reader); + if (apply) + { + objects.Remove(item); + } + break; + } + + if (apply) + { + Callback?.Invoke(operation, item); + } + // we just skipped this change + else + { + changesAhead--; + } + } + } + + public bool Add(T item) + { + if (objects.Add(item)) + { + AddOperation(Operation.OP_ADD, item); + return true; + } + return false; + } + + void ICollection.Add(T item) + { + if (objects.Add(item)) + { + AddOperation(Operation.OP_ADD, item); + } + } + + public void Clear() + { + objects.Clear(); + AddOperation(Operation.OP_CLEAR); + } + + public bool Contains(T item) => objects.Contains(item); + + public void CopyTo(T[] array, int index) => objects.CopyTo(array, index); + + public bool Remove(T item) + { + if (objects.Remove(item)) + { + AddOperation(Operation.OP_REMOVE, item); + return true; + } + return false; + } + + public IEnumerator GetEnumerator() => objects.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void ExceptWith(IEnumerable other) + { + if (other == this) + { + Clear(); + return; + } + + // 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); + IntersectWithSet(otherAsSet); + } + } + + 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); + + public bool IsProperSupersetOf(IEnumerable other) => objects.IsProperSupersetOf(other); + + public bool IsSubsetOf(IEnumerable other) => objects.IsSubsetOf(other); + + public bool IsSupersetOf(IEnumerable other) => objects.IsSupersetOf(other); + + public bool Overlaps(IEnumerable other) => objects.Overlaps(other); + + public bool SetEquals(IEnumerable other) => objects.SetEquals(other); + + public void SymmetricExceptWith(IEnumerable other) + { + if (other == this) + { + Clear(); + } + else + { + foreach (T element in other) + { + if (!Remove(element)) + { + Add(element); + } + } + } + } + + public void UnionWith(IEnumerable other) + { + if (other != this) + { + foreach (T element in other) + { + Add(element); + } + } + } + } + + public abstract class SyncHashSet : SyncSet + { + protected SyncHashSet() : base(new HashSet()) { } + } + + public abstract class SyncSortedSet : SyncSet + { + protected SyncSortedSet() : base(new SortedSet()) { } + + protected SyncSortedSet(IComparer comparer) : base(new SortedSet(comparer)) { } + } +} diff --git a/Assets/Packages/Mirror/Runtime/SyncSet.cs.meta b/Assets/Packages/Mirror/Runtime/SyncSet.cs.meta new file mode 100644 index 0000000..173523c --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/SyncSet.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8a31599d9f9dd4ef9999f7b9707c832c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport.meta b/Assets/Packages/Mirror/Runtime/Transport.meta new file mode 100644 index 0000000..fc29442 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7825d46cd73fe47938869eb5427b40fa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs b/Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs new file mode 100644 index 0000000..bb6cfd8 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs @@ -0,0 +1,323 @@ +// Coburn: LLAPI is not available on UWP. There are a lot of compile directives here that we're checking against. +// Checking all of them may be overkill, but it's better to cover all the possible UWP directives. Sourced from +// https://docs.unity3d.com/Manual/PlatformDependentCompilation.html +// TODO: Check if LLAPI is supported on Xbox One? + +// LLAPITransport wraps UNET's LLAPI for use as a HLAPI TransportLayer, only if you're not on a UWP platform. +#if !(UNITY_WSA || UNITY_WSA_10_0 || UNITY_WINRT || UNITY_WINRT_10_0 || NETFX_CORE) + +using System; +using System.ComponentModel; +using UnityEngine; +using UnityEngine.Networking; +using UnityEngine.Networking.Types; + +namespace Mirror +{ + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("LLAPI is obsolete and will be removed from future versions of Unity")] + public class LLAPITransport : Transport + { + public ushort port = 7777; + + [Tooltip("Enable for WebGL games. Can only do either WebSockets or regular Sockets, not both (yet).")] + public bool useWebsockets; + + // settings copied from uMMORPG configuration for best results + public ConnectionConfig connectionConfig = new ConnectionConfig + { + PacketSize = 1500, + FragmentSize = 500, + ResendTimeout = 1200, + DisconnectTimeout = 6000, + ConnectTimeout = 6000, + MinUpdateTimeout = 1, + PingTimeout = 2000, + ReducedPingTimeout = 100, + AllCostTimeout = 20, + NetworkDropThreshold = 80, + OverflowDropThreshold = 80, + MaxConnectionAttempt = 10, + AckDelay = 33, + SendDelay = 10, + MaxCombinedReliableMessageSize = 100, + MaxCombinedReliableMessageCount = 10, + MaxSentMessageQueueSize = 512, + AcksType = ConnectionAcksType.Acks128, + InitialBandwidth = 0, + BandwidthPeakFactor = 2, + WebSocketReceiveBufferMaxSize = 0, + UdpSocketReceiveBufferMaxSize = 0 + }; + + // settings copied from uMMORPG configuration for best results + public GlobalConfig globalConfig = new GlobalConfig + { + ReactorModel = ReactorModel.SelectReactor, + ThreadAwakeTimeout = 1, + ReactorMaximumSentMessages = 4096, + ReactorMaximumReceivedMessages = 4096, + MaxPacketSize = 2000, + MaxHosts = 16, + ThreadPoolSize = 3, + MinTimerTimeout = 1, + MaxTimerTimeout = 12000 + }; + + readonly int channelId; // always use first channel + byte error; + + int clientId = -1; + int clientConnectionId = -1; + readonly byte[] clientReceiveBuffer = new byte[4096]; + + int serverHostId = -1; + readonly byte[] serverReceiveBuffer = new byte[4096]; + + void OnValidate() + { + // add connectionconfig channels if none + if (connectionConfig.Channels.Count == 0) + { + // channel 0 is reliable fragmented sequenced + connectionConfig.AddChannel(QosType.ReliableFragmentedSequenced); + // channel 1 is unreliable + connectionConfig.AddChannel(QosType.Unreliable); + } + } + + void Awake() + { + NetworkTransport.Init(globalConfig); + Debug.Log("LLAPITransport initialized!"); + } + + #region client + public override bool ClientConnected() + { + return clientConnectionId != -1; + } + + public override void ClientConnect(string address) + { + // LLAPI can't handle 'localhost' + if (address.ToLower() == "localhost") address = "127.0.0.1"; + + HostTopology hostTopology = new HostTopology(connectionConfig, 1); + + // important: + // AddHost(topology) doesn't work in WebGL. + // AddHost(topology, port) works in standalone and webgl if port=0 + clientId = NetworkTransport.AddHost(hostTopology, 0); + + clientConnectionId = NetworkTransport.Connect(clientId, address, port, 0, out error); + NetworkError networkError = (NetworkError)error; + if (networkError != NetworkError.Ok) + { + Debug.LogWarning("NetworkTransport.Connect failed: clientId=" + clientId + " address= " + address + " port=" + port + " error=" + error); + clientConnectionId = -1; + } + } + + public override bool ClientSend(int channelId, byte[] data) + { + return NetworkTransport.Send(clientId, clientConnectionId, channelId, data, data.Length, out error); + } + + public bool ProcessClientMessage() + { + if (clientId == -1) return false; + + NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(clientId, out int connectionId, out int channel, clientReceiveBuffer, clientReceiveBuffer.Length, out int receivedSize, out error); + + // note: 'error' is used for extra information, e.g. the reason for + // a disconnect. we don't necessarily have to throw an error if + // error != 0. but let's log it for easier debugging. + // + // DO NOT return after error != 0. otherwise Disconnect won't be + // registered. + NetworkError networkError = (NetworkError)error; + if (networkError != NetworkError.Ok) + { + string message = "NetworkTransport.Receive failed: hostid=" + clientId + " connId=" + connectionId + " channelId=" + channel + " error=" + networkError; + OnClientError.Invoke(new Exception(message)); + } + + // raise events + switch (networkEvent) + { + case NetworkEventType.ConnectEvent: + OnClientConnected.Invoke(); + break; + case NetworkEventType.DataEvent: + ArraySegment data = new ArraySegment(clientReceiveBuffer, 0, receivedSize); + OnClientDataReceived.Invoke(data); + break; + case NetworkEventType.DisconnectEvent: + OnClientDisconnected.Invoke(); + break; + default: + return false; + } + + return true; + } + + public string ClientGetAddress() + { + NetworkTransport.GetConnectionInfo(serverHostId, clientId, out string address, out int port, out NetworkID networkId, out NodeID node, out error); + return address; + } + + public override void ClientDisconnect() + { + if (clientId != -1) + { + NetworkTransport.RemoveHost(clientId); + clientId = -1; + } + } + #endregion + + #region server + public override bool ServerActive() + { + return serverHostId != -1; + } + + public override void ServerStart() + { + if (useWebsockets) + { + HostTopology topology = new HostTopology(connectionConfig, ushort.MaxValue - 1); + serverHostId = NetworkTransport.AddWebsocketHost(topology, port); + //Debug.Log("LLAPITransport.ServerStartWebsockets port=" + port + " max=" + maxConnections + " hostid=" + serverHostId); + } + else + { + HostTopology topology = new HostTopology(connectionConfig, ushort.MaxValue - 1); + serverHostId = NetworkTransport.AddHost(topology, port); + //Debug.Log("LLAPITransport.ServerStart port=" + port + " max=" + maxConnections + " hostid=" + serverHostId); + } + } + + public override bool ServerSend(int connectionId, int channelId, byte[] data) + { + return NetworkTransport.Send(serverHostId, connectionId, channelId, data, data.Length, out error); + } + + public bool ProcessServerMessage() + { + if (serverHostId == -1) return false; + + NetworkEventType networkEvent = NetworkTransport.ReceiveFromHost(serverHostId, out int connectionId, out int channel, serverReceiveBuffer, serverReceiveBuffer.Length, out int receivedSize, out error); + + // note: 'error' is used for extra information, e.g. the reason for + // a disconnect. we don't necessarily have to throw an error if + // error != 0. but let's log it for easier debugging. + // + // DO NOT return after error != 0. otherwise Disconnect won't be + // registered. + NetworkError networkError = (NetworkError)error; + if (networkError != NetworkError.Ok) + { + string message = "NetworkTransport.Receive failed: hostid=" + serverHostId + " connId=" + connectionId + " channelId=" + channel + " error=" + networkError; + + // TODO write a TransportException or better + OnServerError.Invoke(connectionId, new Exception(message)); + } + + // LLAPI client sends keep alive messages (75-6C-6C) on channel=110. + // ignore all messages that aren't for our selected channel. + /*if (channel != channelId) + { + return false; + }*/ + + switch (networkEvent) + { + case NetworkEventType.ConnectEvent: + OnServerConnected.Invoke(connectionId); + break; + case NetworkEventType.DataEvent: + ArraySegment data = new ArraySegment(serverReceiveBuffer, 0, receivedSize); + OnServerDataReceived.Invoke(connectionId, data); + break; + case NetworkEventType.DisconnectEvent: + OnServerDisconnected.Invoke(connectionId); + break; + default: + // nothing or a message we don't recognize + return false; + } + + return true; + } + + public override bool ServerDisconnect(int connectionId) + { + return NetworkTransport.Disconnect(serverHostId, connectionId, out error); + } + + public override string ServerGetClientAddress(int connectionId) + { + NetworkTransport.GetConnectionInfo(serverHostId, connectionId, out string address, out int port, out NetworkID networkId, out NodeID node, out error); + return address; + } + + public override void ServerStop() + { + NetworkTransport.RemoveHost(serverHostId); + serverHostId = -1; + Debug.Log("LLAPITransport.ServerStop"); + } + #endregion + + #region common + // IMPORTANT: set script execution order to >1000 to call Transport's + // LateUpdate after all others. Fixes race condition where + // e.g. in uSurvival Transport would apply Cmds before + // ShoulderRotation.LateUpdate, resulting in projectile + // spawns at the point before shoulder rotation. + public void LateUpdate() + { + // process all messages + while (ProcessClientMessage()) {} + while (ProcessServerMessage()) {} + } + + public override bool Available() + { + // websocket is available in all platforms (including webgl) + return useWebsockets || base.Available(); + } + + public override void Shutdown() + { + NetworkTransport.Shutdown(); + serverHostId = -1; + clientConnectionId = -1; + Debug.Log("LLAPITransport.Shutdown"); + } + + public override int GetMaxPacketSize(int channelId) + { + return globalConfig.MaxPacketSize; + } + + public override string ToString() + { + if (ServerActive()) + { + return "LLAPI Server port: " + port; + } + else if (ClientConnected()) + { + string ip = ClientGetAddress(); + return "LLAPI Client ip: " + ip + " port: " + port; + } + return "LLAPI (inactive/disconnected)"; + } + #endregion + } +} +#endif diff --git a/Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs.meta new file mode 100644 index 0000000..2ddc7da --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/LLAPITransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d333dcc8c7bd34f35896f5a9b4c9e759 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 1001 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs b/Assets/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs new file mode 100644 index 0000000..c59a6a1 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs @@ -0,0 +1,191 @@ +using System; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Mirror +{ + // a transport that can listen to multiple underlying transport at the same time + public class MultiplexTransport : Transport + { + public Transport[] transports; + + public void Awake() + { + if (transports == null || transports.Length == 0) + { + Debug.LogError("Multiplex transport requires at least 1 underlying transport"); + } + InitClient(); + InitServer(); + } + + #region Client + // clients always pick the first transport + void InitClient() + { + // wire all the base transports to my events + foreach (Transport transport in transports) + { + transport.OnClientConnected.AddListener(OnClientConnected.Invoke ); + transport.OnClientDataReceived.AddListener(OnClientDataReceived.Invoke); + transport.OnClientError.AddListener(OnClientError.Invoke ); + transport.OnClientDisconnected.AddListener(OnClientDisconnected.Invoke); + } + } + + // The client just uses the first transport available + Transport GetAvailableTransport() + { + foreach (Transport transport in transports) + { + if (transport.Available()) + { + return transport; + } + } + throw new Exception("No transport suitable for this platform"); + } + + public override void ClientConnect(string address) + { + GetAvailableTransport().ClientConnect(address); + } + + public override bool ClientConnected() + { + return GetAvailableTransport().ClientConnected(); + } + + public override void ClientDisconnect() + { + GetAvailableTransport().ClientDisconnect(); + } + + public override bool ClientSend(int channelId, byte[] data) + { + return GetAvailableTransport().ClientSend(channelId, data); + } + + public override int GetMaxPacketSize(int channelId = 0) + { + return GetAvailableTransport().GetMaxPacketSize(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 InitServer() + { + // 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.AddListener(baseConnectionId => + { + OnServerConnected.Invoke(FromBaseId(locali, baseConnectionId)); + }); + + transport.OnServerDataReceived.AddListener((baseConnectionId, data) => + { + OnServerDataReceived.Invoke(FromBaseId(locali, baseConnectionId), data); + }); + + transport.OnServerError.AddListener((baseConnectionId, error) => + { + OnServerError.Invoke(FromBaseId(locali, baseConnectionId), error); + }); + transport.OnServerDisconnected.AddListener(baseConnectionId => + { + OnServerDisconnected.Invoke(FromBaseId(locali, baseConnectionId)); + }); + } + } + + public override bool ServerActive() + { + return transports.All(t => t.ServerActive()); + } + + public override string ServerGetClientAddress(int connectionId) + { + int baseConnectionId = ToBaseId(connectionId); + int transportId = ToTransportId(connectionId); + return transports[transportId].ServerGetClientAddress(baseConnectionId); + } + + public override bool ServerDisconnect(int connectionId) + { + int baseConnectionId = ToBaseId(connectionId); + int transportId = ToTransportId(connectionId); + return transports[transportId].ServerDisconnect(baseConnectionId); + } + + public override bool ServerSend(int connectionId, int channelId, byte[] data) + { + int baseConnectionId = ToBaseId(connectionId); + int transportId = ToTransportId(connectionId); + return transports[transportId].ServerSend(baseConnectionId, channelId, data); + } + + public override void ServerStart() + { + foreach (Transport transport in transports) + { + transport.ServerStart(); + } + } + + public override void ServerStop() + { + foreach (Transport transport in transports) + { + transport.ServerStop(); + } + } + #endregion + + 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/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs.meta new file mode 100644 index 0000000..40394aa --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/MultiplexTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 929e3234c7db540b899f00183fc2b1fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy.meta new file mode 100644 index 0000000..ede2d0e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 552b3d8382916438d81fe7f39e18db72 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs new file mode 100644 index 0000000..50be684 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Telepathy +{ + public class Client : Common + { + public TcpClient client; + Thread receiveThread; + Thread sendThread; + + // TcpClient.Connected doesn't check if socket != null, which + // results in NullReferenceExceptions if connection was closed. + // -> let's check it manually instead + public bool Connected => client != null && + client.Client != null && + client.Client.Connected; + + // TcpClient has no 'connecting' state to check. We need to keep track + // of it manually. + // -> checking 'thread.IsAlive && !Connected' is not enough because the + // thread is alive and connected is false for a short moment after + // disconnecting, so this would cause race conditions. + // -> we use a threadsafe bool wrapper so that ThreadFunction can remain + // static (it needs a common lock) + // => Connecting is true from first Connect() call in here, through the + // thread start, until TcpClient.Connect() returns. Simple and clear. + // => bools are atomic according to + // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables + // made volatile so the compiler does not reorder access to it + volatile bool _Connecting; + public bool Connecting => _Connecting; + + // send queue + // => SafeQueue is twice as fast as ConcurrentQueue, see SafeQueue.cs! + SafeQueue sendQueue = new SafeQueue(); + + // ManualResetEvent to wake up the send thread. better than Thread.Sleep + // -> call Set() if everything was sent + // -> call Reset() if there is something to send again + // -> call WaitOne() to block until Reset was called + ManualResetEvent sendPending = new ManualResetEvent(false); + + // the thread function + void ReceiveThreadFunction(string ip, int port) + { + // absolutely must wrap with try/catch, otherwise thread + // exceptions are silent + try + { + // connect (blocking) + client.Connect(ip, port); + _Connecting = false; + + // set socket options after the socket was created in Connect() + // (not after the constructor because we clear the socket there) + client.NoDelay = NoDelay; + client.SendTimeout = SendTimeout; + + // start send thread only after connected + sendThread = new Thread(() => { SendLoop(0, client, sendQueue, sendPending); }); + sendThread.IsBackground = true; + sendThread.Start(); + + // run the receive loop + ReceiveLoop(0, client, receiveQueue, MaxMessageSize); + } + catch (SocketException exception) + { + // this happens if (for example) the ip address is correct + // but there is no server running on that ip/port + Logger.Log("Client Recv: failed to connect to ip=" + ip + " port=" + port + " reason=" + exception); + + // add 'Disconnected' event to message queue so that the caller + // knows that the Connect failed. otherwise they will never know + receiveQueue.Enqueue(new Message(0, EventType.Disconnected, null)); + } + catch (Exception exception) + { + // something went wrong. probably important. + Logger.LogError("Client Recv Exception: " + exception); + } + + // sendthread might be waiting on ManualResetEvent, + // so let's make sure to end it if the connection + // closed. + // otherwise the send thread would only end if it's + // actually sending data while the connection is + // closed. + sendThread?.Interrupt(); + + // Connect might have failed. thread might have been closed. + // let's reset connecting state no matter what. + _Connecting = false; + + // if we got here then we are done. ReceiveLoop cleans up already, + // but we may never get there if connect fails. so let's clean up + // here too. + client.Close(); + } + + public void Connect(string ip, int port) + { + // not if already started + if (Connecting || Connected) return; + + // We are connecting from now until Connect succeeds or fails + _Connecting = true; + + // create a TcpClient with perfect IPv4, IPv6 and hostname resolving + // support. + // + // * TcpClient(hostname, port): works but would connect (and block) + // already + // * TcpClient(AddressFamily.InterNetworkV6): takes Ipv4 and IPv6 + // addresses but only connects to IPv6 servers (e.g. Telepathy). + // does NOT connect to IPv4 servers (e.g. Mirror Booster), even + // with DualMode enabled. + // * TcpClient(): creates IPv4 socket internally, which would force + // Connect() to only use IPv4 sockets. + // + // => the trick is to clear the internal IPv4 socket so that Connect + // resolves the hostname and creates either an IPv4 or an IPv6 + // socket as needed (see TcpClient source) + client = new TcpClient(); // creates IPv4 socket + client.Client = null; // clear internal IPv4 socket until Connect() + + // clear old messages in queue, just to be sure that the caller + // doesn't receive data from last time and gets out of sync. + // -> calling this in Disconnect isn't smart because the caller may + // still want to process all the latest messages afterwards + receiveQueue = new ConcurrentQueue(); + sendQueue.Clear(); + + // client.Connect(ip, port) is blocking. let's call it in the thread + // and return immediately. + // -> this way the application doesn't hang for 30s if connect takes + // too long, which is especially good in games + // -> this way we don't async client.BeginConnect, which seems to + // fail sometimes if we connect too many clients too fast + receiveThread = new Thread(() => { ReceiveThreadFunction(ip, port); }); + receiveThread.IsBackground = true; + receiveThread.Start(); + } + + public void Disconnect() + { + // only if started + if (Connecting || Connected) + { + // close client + client.Close(); + + // wait until thread finished. this is the only way to guarantee + // that we can call Connect() again immediately after Disconnect + receiveThread?.Join(); + + // clear send queues. no need to hold on to them. + // (unlike receiveQueue, which is still needed to process the + // latest Disconnected message, etc.) + sendQueue.Clear(); + + // let go of this one completely. the thread ended, no one uses + // it anymore and this way Connected is false again immediately. + client = null; + } + } + + public bool Send(byte[] data) + { + if (Connected) + { + // respect max message size to avoid allocation attacks. + if (data.Length <= MaxMessageSize) + { + // add to send queue and return immediately. + // calling Send here would be blocking (sometimes for long times + // if other side lags or wire was disconnected) + sendQueue.Enqueue(data); + sendPending.Set(); // interrupt SendThread WaitOne() + return true; + } + Logger.LogError("Client.Send: message too big: " + data.Length + ". Limit: " + MaxMessageSize); + return false; + } + Logger.LogWarning("Client.Send: not connected!"); + return false; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs.meta new file mode 100644 index 0000000..1b6d222 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Client.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a5b95294cc4ec4b15aacba57531c7985 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs new file mode 100644 index 0000000..2d68162 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs @@ -0,0 +1,289 @@ +// common code used by server and client +using System; +using System.Collections.Concurrent; +using System.Net.Sockets; +using System.Threading; + +namespace Telepathy +{ + public abstract class Common + { + // common code ///////////////////////////////////////////////////////// + // incoming message queue of + // (not a HashSet because one connection can have multiple new messages) + protected ConcurrentQueue receiveQueue = new ConcurrentQueue(); + + // queue count, useful for debugging / benchmarks + public int ReceiveQueueCount => receiveQueue.Count; + + // warning if message queue gets too big + // if the average message is about 20 bytes then: + // - 1k messages are 20KB + // - 10k messages are 200KB + // - 100k messages are 1.95MB + // 2MB are not that much, but it is a bad sign if the caller process + // can't call GetNextMessage faster than the incoming messages. + public static int messageQueueSizeWarning = 100000; + + // removes and returns the oldest message from the message queue. + // (might want to call this until it doesn't return anything anymore) + // -> Connected, Data, Disconnected events are all added here + // -> bool return makes while (GetMessage(out Message)) easier! + // -> no 'is client connected' check because we still want to read the + // Disconnected message after a disconnect + public bool GetNextMessage(out Message message) + { + return receiveQueue.TryDequeue(out message); + } + + // NoDelay disables nagle algorithm. lowers CPU% and latency but + // increases bandwidth + public bool NoDelay = true; + + // Prevent allocation attacks. Each packet is prefixed with a length + // header, so an attacker could send a fake packet with length=2GB, + // causing the server to allocate 2GB and run out of memory quickly. + // -> simply increase max packet size if you want to send around bigger + // files! + // -> 16KB per message should be more than enough. + public int MaxMessageSize = 16 * 1024; + + // Send would stall forever if the network is cut off during a send, so + // we need a timeout (in milliseconds) + public int SendTimeout = 5000; + + // avoid header[4] allocations but don't use one buffer for all threads + [ThreadStatic] static byte[] header; + + // avoid payload[packetSize] allocations but don't use one buffer for + // all threads + [ThreadStatic] static byte[] payload; + + // static helper functions ///////////////////////////////////////////// + // send message (via stream) with the message structure + // this function is blocking sometimes! + // (e.g. if someone has high latency or wire was cut off) + protected static bool SendMessagesBlocking(NetworkStream stream, byte[][] messages) + { + // stream.Write throws exceptions if client sends with high + // frequency and the server stops + try + { + // we might have multiple pending messages. merge into one + // packet to avoid TCP overheads and improve performance. + int packetSize = 0; + for (int i = 0; i < messages.Length; ++i) + packetSize += sizeof(int) + messages[i].Length; // header + content + + // create payload buffer if not created yet or previous one is + // too small + // IMPORTANT: payload.Length might be > packetSize! don't use it! + if (payload == null || payload.Length < packetSize) + payload = new byte[packetSize]; + + // create the packet + int position = 0; + for (int i = 0; i < messages.Length; ++i) + { + // create header buffer if not created yet + if (header == null) + header = new byte[4]; + + // construct header (size) + Utils.IntToBytesBigEndianNonAlloc(messages[i].Length, header); + + // copy header + message into buffer + Array.Copy(header, 0, payload, position, header.Length); + Array.Copy(messages[i], 0, payload, position + header.Length, messages[i].Length); + position += header.Length + messages[i].Length; + } + + // write the whole thing + stream.Write(payload, 0, packetSize); + + return true; + } + catch (Exception exception) + { + // log as regular message because servers do shut down sometimes + Logger.Log("Send: stream.Write exception: " + exception); + return false; + } + } + + // read message (via stream) with the message structure + protected static bool ReadMessageBlocking(NetworkStream stream, int MaxMessageSize, out byte[] content) + { + content = null; + + // create header buffer if not created yet + if (header == null) + header = new byte[4]; + + // read exactly 4 bytes for header (blocking) + if (!stream.ReadExactly(header, 4)) + return false; + + // convert to int + int size = Utils.BytesToIntBigEndian(header); + + // protect against allocation attacks. an attacker might send + // multiple fake '2GB header' packets in a row, causing the server + // to allocate multiple 2GB byte arrays and run out of memory. + if (size <= MaxMessageSize) + { + // read exactly 'size' bytes for content (blocking) + content = new byte[size]; + return stream.ReadExactly(content, size); + } + Logger.LogWarning("ReadMessageBlocking: possible allocation attack with a header of: " + size + " bytes."); + return false; + } + + // thread receive function is the same for client and server's clients + // (static to reduce state for maximum reliability) + protected static void ReceiveLoop(int connectionId, TcpClient client, ConcurrentQueue receiveQueue, int MaxMessageSize) + { + // get NetworkStream from client + NetworkStream stream = client.GetStream(); + + // keep track of last message queue warning + DateTime messageQueueLastWarning = DateTime.Now; + + // absolutely must wrap with try/catch, otherwise thread exceptions + // are silent + try + { + // add connected event to queue with ip address as data in case + // it's needed + receiveQueue.Enqueue(new Message(connectionId, EventType.Connected, null)); + + // let's talk about reading data. + // -> normally we would read as much as possible and then + // extract as many , messages + // as we received this time. this is really complicated + // and expensive to do though + // -> instead we use a trick: + // Read(2) -> size + // Read(size) -> content + // repeat + // Read is blocking, but it doesn't matter since the + // best thing to do until the full message arrives, + // is to wait. + // => this is the most elegant AND fast solution. + // + no resizing + // + no extra allocations, just one for the content + // + no crazy extraction logic + while (true) + { + // read the next message (blocking) or stop if stream closed + byte[] content; + if (!ReadMessageBlocking(stream, MaxMessageSize, out content)) + break; // break instead of return so stream close still happens! + + // queue it + receiveQueue.Enqueue(new Message(connectionId, EventType.Data, content)); + + // and show a warning if the queue gets too big + // -> we don't want to show a warning every single time, + // because then a lot of processing power gets wasted on + // logging, which will make the queue pile up even more. + // -> instead we show it every 10s, so that the system can + // use most it's processing power to hopefully process it. + if (receiveQueue.Count > messageQueueSizeWarning) + { + TimeSpan elapsed = DateTime.Now - messageQueueLastWarning; + if (elapsed.TotalSeconds > 10) + { + Logger.LogWarning("ReceiveLoop: messageQueue is getting big(" + receiveQueue.Count + "), try calling GetNextMessage more often. You can call it more than once per frame!"); + messageQueueLastWarning = DateTime.Now; + } + } + } + } + catch (Exception exception) + { + // something went wrong. the thread was interrupted or the + // connection closed or we closed our own connection or ... + // -> either way we should stop gracefully + Logger.Log("ReceiveLoop: finished receive function for connectionId=" + connectionId + " reason: " + exception); + } + finally + { + // clean up no matter what + stream.Close(); + client.Close(); + + // add 'Disconnected' message after disconnecting properly. + // -> always AFTER closing the streams to avoid a race condition + // where Disconnected -> Reconnect wouldn't work because + // Connected is still true for a short moment before the stream + // would be closed. + receiveQueue.Enqueue(new Message(connectionId, EventType.Disconnected, null)); + } + } + + // thread send function + // note: we really do need one per connection, so that if one connection + // blocks, the rest will still continue to get sends + protected static void SendLoop(int connectionId, TcpClient client, SafeQueue sendQueue, ManualResetEvent sendPending) + { + // get NetworkStream from client + NetworkStream stream = client.GetStream(); + + try + { + while (client.Connected) // try this. client will get closed eventually. + { + // reset ManualResetEvent before we do anything else. this + // way there is no race condition. if Send() is called again + // while in here then it will be properly detected next time + // -> otherwise Send might be called right after dequeue but + // before .Reset, which would completely ignore it until + // the next Send call. + sendPending.Reset(); // WaitOne() blocks until .Set() again + + // dequeue all + // SafeQueue.TryDequeueAll is twice as fast as + // ConcurrentQueue, see SafeQueue.cs! + byte[][] messages; + if (sendQueue.TryDequeueAll(out messages)) + { + // send message (blocking) or stop if stream is closed + if (!SendMessagesBlocking(stream, messages)) + break; // break instead of return so stream close still happens! + } + + // don't choke up the CPU: wait until queue not empty anymore + sendPending.WaitOne(); + } + } + catch (ThreadAbortException) + { + // happens on stop. don't log anything. + } + catch (ThreadInterruptedException) + { + // happens if receive thread interrupts send thread. + } + catch (Exception exception) + { + // something went wrong. the thread was interrupted or the + // connection closed or we closed our own connection or ... + // -> either way we should stop gracefully + Logger.Log("SendLoop Exception: connectionId=" + connectionId + " reason: " + exception); + } + finally + { + // clean up no matter what + // we might get SocketExceptions when sending if the 'host has + // failed to respond' - in which case we should close the connection + // which causes the ReceiveLoop to end and fire the Disconnected + // message. otherwise the connection would stay alive forever even + // though we can't send anymore. + stream.Close(); + client.Close(); + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs.meta new file mode 100644 index 0000000..5d8ab5b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Common.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4d56322cf0e248a89103c002a505dab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs new file mode 100644 index 0000000..f07baf2 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs @@ -0,0 +1,9 @@ +namespace Telepathy +{ + public enum EventType + { + Connected, + Data, + Disconnected + } +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs.meta new file mode 100644 index 0000000..ac88c1b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/EventType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 49f1a330755814803be5f27f493e1910 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE new file mode 100644 index 0000000..680deef --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018, vis2k + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE.meta new file mode 100644 index 0000000..4d7664e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0ba11103b95fd4721bffbb08440d5b8e +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs new file mode 100644 index 0000000..e97704d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs @@ -0,0 +1,15 @@ +// A simple logger class that uses Console.WriteLine by default. +// Can also do Logger.LogMethod = Debug.Log for Unity etc. +// (this way we don't have to depend on UnityEngine.DLL and don't need a +// different version for every UnityEngine version here) +using System; + +namespace Telepathy +{ + public static class Logger + { + public static Action Log = Console.WriteLine; + public static Action LogWarning = Console.WriteLine; + public static Action LogError = Console.Error.WriteLine; + } +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs.meta new file mode 100644 index 0000000..304866f --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Logger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa8d703f0b73f4d6398b76812719b68b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs new file mode 100644 index 0000000..529a594 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs @@ -0,0 +1,18 @@ +// incoming message queue of +// (not a HashSet because one connection can have multiple new messages) +// -> a struct to minimize GC +namespace Telepathy +{ + public struct Message + { + public readonly int connectionId; + public readonly EventType eventType; + public readonly byte[] data; + public Message(int connectionId, EventType eventType, byte[] data) + { + this.connectionId = connectionId; + this.eventType = eventType; + this.data = data; + } + } +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs.meta new file mode 100644 index 0000000..5937bb9 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Message.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aedf812e9637b4f92a35db1aedca8c92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs new file mode 100644 index 0000000..9b5bd4e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs @@ -0,0 +1,55 @@ +using System.IO; +using System.Net.Sockets; + +public static class NetworkStreamExtensions +{ + // .Read returns '0' if remote closed the connection but throws an + // IOException if we voluntarily closed our own connection. + // + // let's add a ReadSafely method that returns '0' in both cases so we don't + // have to worry about exceptions, since a disconnect is a disconnect... + public static int ReadSafely(this NetworkStream stream, byte[] buffer, int offset, int size) + { + try + { + return stream.Read(buffer, offset, size); + } + catch (IOException) + { + return 0; + } + } + + // helper function to read EXACTLY 'n' bytes + // -> default .Read reads up to 'n' bytes. this function reads exactly 'n' + // bytes + // -> this is blocking until 'n' bytes were received + // -> immediately returns false in case of disconnects + public static bool ReadExactly(this NetworkStream stream, byte[] buffer, int amount) + { + // there might not be enough bytes in the TCP buffer for .Read to read + // the whole amount at once, so we need to keep trying until we have all + // the bytes (blocking) + // + // note: this just is a faster version of reading one after another: + // for (int i = 0; i < amount; ++i) + // if (stream.Read(buffer, i, 1) == 0) + // return false; + // return true; + int bytesRead = 0; + while (bytesRead < amount) + { + // read up to 'remaining' bytes with the 'safe' read extension + int remaining = amount - bytesRead; + int result = stream.ReadSafely(buffer, bytesRead, remaining); + + // .Read returns 0 if disconnected + if (result == 0) + return false; + + // otherwise add to bytes read + bytesRead += result; + } + return true; + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs.meta new file mode 100644 index 0000000..e7e5744 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/NetworkStreamExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a8076c43fa8d4d45831adae232d4d3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs new file mode 100644 index 0000000..9ebdf8a --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs @@ -0,0 +1,75 @@ +// Net 4.X has ConcurrentQueue, but ConcurrentQueue has no TryDequeueAll method, +// which makes SafeQueue twice as fast for the send thread. +// +// uMMORPG 450 CCU +// SafeQueue: 900-1440ms latency +// ConcurrentQueue: 2000ms latency +// +// It's also noticeable in the LoadTest project, which hardly handles 300 CCU +// with ConcurrentQueue! +using System.Collections.Generic; + +namespace Telepathy +{ + public class SafeQueue + { + readonly Queue queue = new Queue(); + + // for statistics. don't call Count and assume that it's the same after the + // call. + public int Count + { + get + { + lock(queue) + { + return queue.Count; + } + } + } + + public void Enqueue(T item) + { + lock(queue) + { + queue.Enqueue(item); + } + } + + // can't check .Count before doing Dequeue because it might change inbetween, + // so we need a TryDequeue + public bool TryDequeue(out T result) + { + lock(queue) + { + result = default; + if (queue.Count > 0) + { + result = queue.Dequeue(); + return true; + } + return false; + } + } + + // for when we want to dequeue and remove all of them at once without + // locking every single TryDequeue. + public bool TryDequeueAll(out T[] result) + { + lock(queue) + { + result = queue.ToArray(); + queue.Clear(); + return result.Length > 0; + } + } + + public void Clear() + { + lock(queue) + { + queue.Clear(); + } + } + } +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs.meta new file mode 100644 index 0000000..f3a9310 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/SafeQueue.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8fc06e2fb29854a0c9e90c0188d36a08 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs new file mode 100644 index 0000000..e8b4aa0 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Telepathy +{ + public class Server : Common + { + // listener + public TcpListener listener; + Thread listenerThread; + + // class with all the client's data. let's call it Token for consistency + // with the async socket methods. + class ClientToken + { + public TcpClient client; + + // send queue + // SafeQueue is twice as fast as ConcurrentQueue, see SafeQueue.cs! + public SafeQueue sendQueue = new SafeQueue(); + + // ManualResetEvent to wake up the send thread. better than Thread.Sleep + // -> call Set() if everything was sent + // -> call Reset() if there is something to send again + // -> call WaitOne() to block until Reset was called + public ManualResetEvent sendPending = new ManualResetEvent(false); + + public ClientToken(TcpClient client) + { + this.client = client; + } + } + + // clients with + readonly ConcurrentDictionary clients = new ConcurrentDictionary(); + + // connectionId counter + int counter; + + // public next id function in case someone needs to reserve an id + // (e.g. if hostMode should always have 0 connection and external + // connections should start at 1, etc.) + public int NextConnectionId() + { + int id = Interlocked.Increment(ref counter); + + // it's very unlikely that we reach the uint limit of 2 billion. + // even with 1 new connection per second, this would take 68 years. + // -> but if it happens, then we should throw an exception because + // the caller probably should stop accepting clients. + // -> it's hardly worth using 'bool Next(out id)' for that case + // because it's just so unlikely. + if (id == int.MaxValue) + { + throw new Exception("connection id limit reached: " + id); + } + + return id; + } + + // check if the server is running + public bool Active => listenerThread != null && listenerThread.IsAlive; + + // the listener thread's listen function + // note: no maxConnections parameter. high level API should handle that. + // (Transport can't send a 'too full' message anyway) + void Listen(int port) + { + // absolutely must wrap with try/catch, otherwise thread + // exceptions are silent + try + { + // start listener on all IPv4 and IPv6 address via .Create + listener = TcpListener.Create(port); + listener.Server.NoDelay = NoDelay; + listener.Server.SendTimeout = SendTimeout; + listener.Start(); + Logger.Log("Server: listening port=" + port); + + // keep accepting new clients + while (true) + { + // wait and accept new client + // note: 'using' sucks here because it will try to + // dispose after thread was started but we still need it + // in the thread + TcpClient client = listener.AcceptTcpClient(); + + // set socket options + client.NoDelay = NoDelay; + client.SendTimeout = SendTimeout; + + // generate the next connection id (thread safely) + int connectionId = NextConnectionId(); + + // add to dict immediately + ClientToken token = new ClientToken(client); + clients[connectionId] = token; + + // spawn a send thread for each client + Thread sendThread = new Thread(() => + { + // wrap in try-catch, otherwise Thread exceptions + // are silent + try + { + // run the send loop + SendLoop(connectionId, client, token.sendQueue, token.sendPending); + } + catch (ThreadAbortException) + { + // happens on stop. don't log anything. + // (we catch it in SendLoop too, but it still gets + // through to here when aborting. don't show an + // error.) + } + catch (Exception exception) + { + Logger.LogError("Server send thread exception: " + exception); + } + }); + sendThread.IsBackground = true; + sendThread.Start(); + + // spawn a receive thread for each client + Thread receiveThread = new Thread(() => + { + // wrap in try-catch, otherwise Thread exceptions + // are silent + try + { + // run the receive loop + ReceiveLoop(connectionId, client, receiveQueue, MaxMessageSize); + + // remove client from clients dict afterwards + clients.TryRemove(connectionId, out ClientToken _); + + // sendthread might be waiting on ManualResetEvent, + // so let's make sure to end it if the connection + // closed. + // otherwise the send thread would only end if it's + // actually sending data while the connection is + // closed. + sendThread.Interrupt(); + } + catch (Exception exception) + { + Logger.LogError("Server client thread exception: " + exception); + } + }); + receiveThread.IsBackground = true; + receiveThread.Start(); + } + } + catch (ThreadAbortException exception) + { + // UnityEditor causes AbortException if thread is still + // running when we press Play again next time. that's okay. + Logger.Log("Server thread aborted. That's okay. " + exception); + } + catch (SocketException exception) + { + // calling StopServer will interrupt this thread with a + // 'SocketException: interrupted'. that's okay. + Logger.Log("Server Thread stopped. That's okay. " + exception); + } + catch (Exception exception) + { + // something went wrong. probably important. + Logger.LogError("Server Exception: " + exception); + } + } + + // start listening for new connections in a background thread and spawn + // a new thread for each one. + public bool Start(int port) + { + // not if already started + if (Active) return false; + + // clear old messages in queue, just to be sure that the caller + // doesn't receive data from last time and gets out of sync. + // -> calling this in Stop isn't smart because the caller may + // still want to process all the latest messages afterwards + receiveQueue = new ConcurrentQueue(); + + // start the listener thread + // (on low priority. if main thread is too busy then there is not + // much value in accepting even more clients) + Logger.Log("Server: Start port=" + port); + listenerThread = new Thread(() => { Listen(port); }); + listenerThread.IsBackground = true; + listenerThread.Priority = ThreadPriority.BelowNormal; + listenerThread.Start(); + return true; + } + + public void Stop() + { + // only if started + if (!Active) return; + + Logger.Log("Server: stopping..."); + + // stop listening to connections so that no one can connect while we + // close the client connections + // (might be null if we call Stop so quickly after Start that the + // thread was interrupted before even creating the listener) + listener?.Stop(); + + // kill listener thread at all costs. only way to guarantee that + // .Active is immediately false after Stop. + // -> calling .Join would sometimes wait forever + listenerThread?.Interrupt(); + listenerThread = null; + + // close all client connections + foreach (KeyValuePair kvp in clients) + { + TcpClient client = kvp.Value.client; + // close the stream if not closed yet. it may have been closed + // by a disconnect already, so use try/catch + try { client.GetStream().Close(); } catch {} + client.Close(); + } + + // clear clients list + clients.Clear(); + } + + // send message to client using socket connection. + public bool Send(int connectionId, byte[] data) + { + // respect max message size to avoid allocation attacks. + if (data.Length <= MaxMessageSize) + { + // find the connection + ClientToken token; + if (clients.TryGetValue(connectionId, out token)) + { + // add to send queue and return immediately. + // calling Send here would be blocking (sometimes for long times + // if other side lags or wire was disconnected) + token.sendQueue.Enqueue(data); + token.sendPending.Set(); // interrupt SendThread WaitOne() + return true; + } + Logger.Log("Server.Send: invalid connectionId: " + connectionId); + return false; + } + Logger.LogError("Client.Send: message too big: " + data.Length + ". Limit: " + MaxMessageSize); + return false; + } + + // client's ip is sometimes needed by the server, e.g. for bans + public string GetClientAddress(int connectionId) + { + // find the connection + ClientToken token; + if (clients.TryGetValue(connectionId, out token)) + { + return ((IPEndPoint)token.client.Client.RemoteEndPoint).Address.ToString(); + } + return ""; + } + + // disconnect (kick) a client + public bool Disconnect(int connectionId) + { + // find the connection + ClientToken token; + if (clients.TryGetValue(connectionId, out token)) + { + // just close it. client thread will take care of the rest. + token.client.Close(); + Logger.Log("Server.Disconnect connectionId:" + connectionId); + return true; + } + return false; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs.meta new file mode 100644 index 0000000..9cee8b7 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Server.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb98a16841ccc4338a7e0b4e59136563 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Telepathy.dll b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Telepathy.dll new file mode 100644 index 0000000000000000000000000000000000000000..dc984f0972c54205d410ffd7c34d458a40b453ac GIT binary patch literal 4096 zcmeHJO>9(E6h60qv=k}?B;XG|Y!Qk2rnLnmqV{K68TxByT2zSi&GdHq@XdSUeQ%(n ziJ;L13lkOw;zD%c#>7MxBqGs`t})RS#0|oZjU>7-8pZG2_ukCVRu;w>7TlS8?)ksx zocs4iUp`6oMAU$O^Cr=GjNE!PygfJ$aoa-|+UQ)%#VzN>$i*$0dD}ON%F8ROVB{>< z^#UU+4OMat+clEoX`|rHN^|w2uvjT=}4PO*I|O}_%?rE7nFDwV|806{a`_B&n{ zG&h)w73+ywnD||E(}pm1P8~8`4~~QL;ZF3n2*)P+jlL00v`;YHL7$6z=og5ZP`-I^ zy2NR~uZZD~8kRLYrQt>`O=)}`U4>3v-AU~@^v*SSORS+51hE^qK!l%S=Cy!Lv51j6MYHrq2MMrY`{p z=o;WK-2hB$=>ZLM^anK@k1K+r=+=floux5Z3Y6tc zO5ZD~ob*R5H(#>yvM<1LW=nyjq|BD`c`NJG@cMjT7P3w`V+XbDjHU827{a|Fk9z9W zs*hNI$adtkRKD%GONA+S&Qk>|fY5UOQER|+=j?n*X}zURlG4v9yU0$e!U3;Pv>k0R zDIIG;V}4a0hc}^6o?Nh0zz!_8Top`~+`ukK%@6FX?btz$G+p)sSui6dGs*?0gEG>Q zMJt#uo3oBXp-mA5^J7-*IZRBdv{XlItX@SPFK4uR zm>Me&`FQp)rl4f0P$*Yo(Oc!u4>n&t@onqyg;eg^gBLD+PYs3;&GiO}Mj&g~GTGMr z#p?&BH}zaOffLMk9noe!6`8sX`;}E3^W4FOoYck7%q#DxFQCTo19x#pSeuH1e!40Y(PTQA-tVmTwl%(yI(Pm3S4#I?KFv2y zV#>$iNW6Abs-T=$NX*J32|UESWQQv0CMIRk^KER9a-z!gvWFqiB}lk3=-Jg(6`IA_ zET#AMJ-K5iMbnn!X*spwpXv{Mr1lP79&eR?^ptS4?=fyLwzS1hPAEAv;HjkJj9RuE zUM*7U4X&j(x5CNd5-jz2m-8Q0KxQ5I3Qs}!ZpLrYbCDx{kHTvp_DqxnD&W;31wS^v zDI|QlG)JCR>d>!P22u-LA94$4&xX{EoSbcV2TlcI3ZP|!pI^q)jMnL-_}4>O-~_l8 zwgd22z)!+Dz%2ZII6xEFuvyIKgFg$L{fAK$p|3z2I!=YLq_qq^zd=**V?&PH1+52f zZ#+j)dnlu7mY$F08TjYCSr*W|w%Vg@;)*08^(m+GD?+E5|8lmSxRgbu2Gg)ru&&y4 z;R~X((uwxq<5K9V2U^!f98OJyOXo0A4+lS@N3o)UMqroMR$R# test with 100k conversions: + // BitConverter.GetBytes(ushort): 144ms + // bit shifting: 11ms + // -> 10x speed improvement makes this optimization actually worth it + // -> this way we don't need to allocate BinaryWriter/Reader either + // -> 4 bytes because some people may want to send messages larger than + // 64K bytes + // => big endian is standard for network transmissions, and necessary + // for compatibility with erlang + public static byte[] IntToBytesBigEndian(int value) + { + return new byte[] { + (byte)(value >> 24), + (byte)(value >> 16), + (byte)(value >> 8), + (byte)value + }; + } + + // IntToBytes version that doesn't allocate a new byte[4] each time. + // -> important for MMO scale networking performance. + public static void IntToBytesBigEndianNonAlloc(int value, byte[] bytes) + { + bytes[0] = (byte)(value >> 24); + bytes[1] = (byte)(value >> 16); + bytes[2] = (byte)(value >> 8); + bytes[3] = (byte)value; + } + + public static int BytesToIntBigEndian(byte[] bytes) + { + return + (bytes[0] << 24) | + (bytes[1] << 16) | + (bytes[2] << 8) | + bytes[3]; + } + } +} \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Utils.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Utils.cs.meta new file mode 100644 index 0000000..0a9253b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Telepathy/Utils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 951d08c05297f4b3e8feb5bfcab86531 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs b/Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs new file mode 100644 index 0000000..f5ed0f6 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs @@ -0,0 +1,180 @@ +// wraps Telepathy for use as HLAPI TransportLayer +using System; +using System.ComponentModel; +using System.Net.Sockets; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Mirror +{ + [HelpURL("https://github.com/vis2k/Telepathy/blob/master/README.md")] + public class TelepathyTransport : Transport + { + public ushort port = 7777; + + [Tooltip("Nagle Algorithm can be disabled by enabling NoDelay")] + public bool NoDelay = true; + + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use MaxMessageSizeFromClient or MaxMessageSizeFromServer instead.")] + public int MaxMessageSize + { + get => serverMaxMessageSize; + set => serverMaxMessageSize = clientMaxMessageSize = value; + } + + [Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker might send multiple fake packets with 2GB headers, causing the server to run out of memory after allocating multiple large packets.")] + [FormerlySerializedAs("MaxMessageSize")] public int serverMaxMessageSize = 16 * 1024; + + [Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker host might send multiple fake packets with 2GB headers, causing the connected clients to run out of memory after allocating multiple large packets.")] + [FormerlySerializedAs("MaxMessageSize")] public int clientMaxMessageSize = 16 * 1024; + + protected Telepathy.Client client = new Telepathy.Client(); + protected Telepathy.Server server = new Telepathy.Server(); + + void Awake() + { + // tell Telepathy to use Unity's Debug.Log + Telepathy.Logger.Log = Debug.Log; + Telepathy.Logger.LogWarning = Debug.LogWarning; + Telepathy.Logger.LogError = Debug.LogError; + + // configure + client.NoDelay = NoDelay; + client.MaxMessageSize = clientMaxMessageSize; + server.NoDelay = NoDelay; + server.MaxMessageSize = serverMaxMessageSize; + + Debug.Log("TelepathyTransport initialized!"); + } + + // client + public override bool ClientConnected() => client.Connected; + public override void ClientConnect(string address) => client.Connect(address, port); + public override bool ClientSend(int channelId, byte[] data) => client.Send(data); + + bool ProcessClientMessage() + { + if (client.GetNextMessage(out Telepathy.Message message)) + { + switch (message.eventType) + { + case Telepathy.EventType.Connected: + OnClientConnected.Invoke(); + break; + case Telepathy.EventType.Data: + OnClientDataReceived.Invoke(new ArraySegment(message.data)); + break; + case Telepathy.EventType.Disconnected: + OnClientDisconnected.Invoke(); + break; + default: + // TODO: Telepathy does not report errors at all + // it just disconnects, should be fixed + OnClientDisconnected.Invoke(); + break; + } + return true; + } + return false; + } + public override void ClientDisconnect() => client.Disconnect(); + + // IMPORTANT: set script execution order to >1000 to call Transport's + // LateUpdate after all others. Fixes race condition where + // e.g. in uSurvival Transport would apply Cmds before + // ShoulderRotation.LateUpdate, resulting in projectile + // spawns at the point before shoulder rotation. + public void LateUpdate() + { + // note: we need to check enabled in case we set it to false + // when LateUpdate already started. + // (https://github.com/vis2k/Mirror/pull/379) + while (enabled && ProcessClientMessage()) {} + while (enabled && ProcessServerMessage()) {} + } + + // server + public override bool ServerActive() => server.Active; + public override void ServerStart() => server.Start(port); + public override bool ServerSend(int connectionId, int channelId, byte[] data) => server.Send(connectionId, data); + public bool ProcessServerMessage() + { + if (server.GetNextMessage(out Telepathy.Message message)) + { + switch (message.eventType) + { + case Telepathy.EventType.Connected: + OnServerConnected.Invoke(message.connectionId); + break; + case Telepathy.EventType.Data: + OnServerDataReceived.Invoke(message.connectionId, new ArraySegment(message.data)); + break; + case Telepathy.EventType.Disconnected: + OnServerDisconnected.Invoke(message.connectionId); + break; + default: + // TODO handle errors from Telepathy when telepathy can report errors + OnServerDisconnected.Invoke(message.connectionId); + break; + } + return true; + } + return false; + } + public override bool ServerDisconnect(int connectionId) => server.Disconnect(connectionId); + public override string ServerGetClientAddress(int connectionId) + { + try + { + return server.GetClientAddress(connectionId); + } + catch (SocketException) + { + // using server.listener.LocalEndpoint causes an Exception + // in UWP + Unity 2019: + // Exception thrown at 0x00007FF9755DA388 in UWF.exe: + // Microsoft C++ exception: Il2CppExceptionWrapper at memory + // location 0x000000E15A0FCDD0. SocketException: An address + // incompatible with the requested protocol was used at + // System.Net.Sockets.Socket.get_LocalEndPoint () + // so let's at least catch it and recover + return "unknown"; + } + } + public override void ServerStop() => server.Stop(); + + // common + public override void Shutdown() + { + Debug.Log("TelepathyTransport Shutdown()"); + client.Disconnect(); + server.Stop(); + } + + public override int GetMaxPacketSize(int channelId) + { + return serverMaxMessageSize; + } + + public override string ToString() + { + if (server.Active && server.listener != null) + { + // printing server.listener.LocalEndpoint causes an Exception + // in UWP + Unity 2019: + // Exception thrown at 0x00007FF9755DA388 in UWF.exe: + // Microsoft C++ exception: Il2CppExceptionWrapper at memory + // location 0x000000E15A0FCDD0. SocketException: An address + // incompatible with the requested protocol was used at + // System.Net.Sockets.Socket.get_LocalEndPoint () + // so let's use the regular port instead. + return "Telepathy Server port: " + port; + } + else if (client.Connecting || client.Connected) + { + return "Telepathy Client ip: " + client.client.Client.RemoteEndPoint; + } + return "Telepathy (inactive/disconnected)"; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs.meta new file mode 100644 index 0000000..a89b0d3 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/TelepathyTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7424c1070fad4ba2a7a96b02fbeb4bb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 1000 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Transport.cs b/Assets/Packages/Mirror/Runtime/Transport/Transport.cs new file mode 100644 index 0000000..6a706e8 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Transport.cs @@ -0,0 +1,190 @@ +// abstract transport layer component +// note: not all transports need a port, so add it to yours if needed. +using System; +using System.ComponentModel; +using UnityEngine; +using UnityEngine.Events; + +namespace Mirror +{ + // UnityEvent definitions + [Serializable] public class UnityEventArraySegment : UnityEvent> {} + [Serializable] public class UnityEventException : UnityEvent {} + [Serializable] public class UnityEventInt : UnityEvent {} + [Serializable] public class UnityEventIntArraySegment : UnityEvent> {} + [Serializable] public class UnityEventIntException : UnityEvent {} + + public abstract class Transport : MonoBehaviour + { + /// + /// The current transport used by Mirror. + /// + public static Transport activeTransport; + + /// + /// Is this transport available in the current platform? + /// Some transports might only be available in mobile + /// Many will not work in webgl + /// + /// True if this transport works in the current platform + public virtual bool Available() + { + return Application.platform != RuntimePlatform.WebGLPlayer; + } + + #region Client + /// + /// Notify subscribers when when this client establish a successful connection to the server + /// + [HideInInspector] public UnityEvent OnClientConnected = new UnityEvent(); + + /// + /// Notify subscribers when this client receive data from the server + /// + [HideInInspector] public UnityEventArraySegment OnClientDataReceived = new UnityEventArraySegment(); + + /// + /// Notify subscribers when this clianet encounters an error communicating with the server + /// + [HideInInspector] public UnityEventException OnClientError = new UnityEventException(); + + /// + /// Notify subscribers when this client disconnects from the server + /// + [HideInInspector] public UnityEvent OnClientDisconnected = new UnityEvent(); + + /// + /// Determines if we are currently connected to the server + /// + /// True if a connection has been established to the server + public abstract bool ClientConnected(); + + /// + /// Establish a connecion to a server + /// + /// The IP address or FQDN of the server we are trying to connect to + public abstract void ClientConnect(string address); + + /// + /// Send data to the server + /// + /// The channel to use. 0 is the default channel, + /// but some transports might want to provide unreliable, encrypted, compressed, or any other feature + /// as new channels + /// The data to send to the server + /// true if the send was successful + public abstract bool ClientSend(int channelId, byte[] data); + + /// + /// Disconnect this client from the server + /// + public abstract void ClientDisconnect(); + + #endregion + + #region Server + + /// + /// Notify subscribers when a client connects to this server + /// + [HideInInspector] public UnityEventInt OnServerConnected = new UnityEventInt(); + + /// + /// Notify subscribers when this server receives data from the client + /// + [HideInInspector] public UnityEventIntArraySegment OnServerDataReceived = new UnityEventIntArraySegment(); + + /// + /// Notify subscribers when this server has some problem communicating with the client + /// + [HideInInspector] public UnityEventIntException OnServerError = new UnityEventIntException(); + + /// + /// Notify subscribers when a client disconnects from this server + /// + [HideInInspector] public UnityEventInt OnServerDisconnected = new UnityEventInt(); + + /// + /// Determines if the server is up and running + /// + /// true if the transport is ready for connections from clients + public abstract bool ServerActive(); + + /// + /// Start listening for clients + /// + public abstract void ServerStart(); + + /// + /// Send data to a client + /// + /// The id of the client to send the data to + /// The channel to be used. Transports can use channels to implement + /// other features such as unreliable, encryption, compression, etc... + /// + /// true if the data was sent + public abstract bool ServerSend(int connectionId, int channelId, byte[] data); + + /// + /// Disconnect a client from this server. Useful to kick people out. + /// + /// the id of the client to disconnect + /// true if the client was kicked + public abstract bool ServerDisconnect(int connectionId); + + /// + /// Deprecated: Use ServerGetClientAddress(int connectionId) instead + /// + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use ServerGetClientAddress(int connectionId) instead")] + public virtual bool GetConnectionInfo(int connectionId, out string address) + { + address = ServerGetClientAddress(connectionId); + return true; + } + + /// + /// Get the client address + /// + /// id of the client + /// address of the client + public abstract string ServerGetClientAddress(int connectionId); + + /// + /// Stop listening for clients and disconnect all existing clients + /// + public abstract void ServerStop(); + + + #endregion + + /// + /// Shut down the transport, both as client and server + /// + public abstract void Shutdown(); + + /// + /// The maximum packet size for a given channel. Unreliable transports + /// usually can only deliver small packets. Reliable fragmented channels + /// can usually deliver large ones. + /// + /// channel id + /// the size in bytes that can be sent via the provided channel + public abstract int GetMaxPacketSize(int channelId = Channels.DefaultReliable); + + // block Update() to force Transports to use LateUpdate to avoid race + // conditions. messages should be processed after all the game state + // was processed in Update. + // -> in other words: use LateUpdate! + // -> uMMORPG 480 CCU stress test: when bot machine stops, it causes + // 'Observer not ready for ...' log messages when using Update + // -> occupying a public Update() function will cause Warnings if a + // transport uses Update. + // + // IMPORTANT: set script execution order to >1000 to call Transport's + // LateUpdate after all others. Fixes race condition where + // e.g. in uSurvival Transport would apply Cmds before + // ShoulderRotation.LateUpdate, resulting in projectile + // spawns at the point before shoulder rotation. + public void Update() {} + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Transport.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Transport.cs.meta new file mode 100644 index 0000000..2d451cf --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Transport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cfffcac25d6d64ced9de620159e221b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket.meta new file mode 100644 index 0000000..5797569 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 75b15785adf2d4b7c8d779f5ba6a6326 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs new file mode 100644 index 0000000..535975b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs @@ -0,0 +1,191 @@ +#if !UNITY_WEBGL || UNITY_EDITOR + +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Ninja.WebSockets; +using UnityEngine; + +namespace Mirror.Websocket +{ + + public class Client + { + public event Action Connected; + public event Action> ReceivedData; + public event Action Disconnected; + public event Action ReceivedError; + + const int MaxMessageSize = 1024 * 256; + WebSocket webSocket; + CancellationTokenSource cancellation; + + public bool NoDelay = true; + + public bool Connecting { get; set; } + public bool IsConnected { get; set; } + + Uri uri; + + public async void Connect(Uri uri) + { + // not if already started + if (webSocket != null) + { + // paul: exceptions are better than silence + ReceivedError?.Invoke(new Exception("Client already connected")); + return; + } + this.uri = uri; + // We are connecting from now until Connect succeeds or fails + Connecting = true; + + WebSocketClientOptions options = new WebSocketClientOptions() + { + NoDelay = true, + KeepAliveInterval = TimeSpan.Zero, + SecWebSocketProtocol = "binary" + }; + + cancellation = new CancellationTokenSource(); + + WebSocketClientFactory clientFactory = new WebSocketClientFactory(); + + try + { + using (webSocket = await clientFactory.ConnectAsync(uri, options, cancellation.Token)) + { + CancellationToken token = cancellation.Token; + IsConnected = true; + Connecting = false; + Connected?.Invoke(); + + await ReceiveLoop(webSocket, token); + } + } + catch (ObjectDisposedException) + { + // No error, the client got closed + } + catch (Exception ex) + { + ReceivedError?.Invoke(ex); + } + finally + { + Disconnect(); + Disconnected?.Invoke(); + } + } + + async Task ReceiveLoop(WebSocket webSocket, CancellationToken token) + { + byte[] buffer = new byte[MaxMessageSize]; + + while (true) + { + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + + + if (result == null) + break; + if (result.MessageType == WebSocketMessageType.Close) + break; + + // we got a text or binary message, need the full message + ArraySegment data = await ReadFrames(result, webSocket, buffer); + + if (data.Count == 0) + break; + + try + { + ReceivedData?.Invoke(data); + } + catch (Exception exception) + { + ReceivedError?.Invoke(exception); + } + } + } + + // a message might come splitted in multiple frames + // collect all frames + async Task> ReadFrames(WebSocketReceiveResult result, WebSocket webSocket, byte[] buffer) + { + int count = result.Count; + + while (!result.EndOfMessage) + { + if (count >= MaxMessageSize) + { + string closeMessage = string.Format("Maximum message size: {0} bytes.", MaxMessageSize); + await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, CancellationToken.None); + ReceivedError?.Invoke(new WebSocketException(WebSocketError.HeaderError)); + return new ArraySegment(); + } + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer, count, MaxMessageSize - count), CancellationToken.None); + count += result.Count; + + } + return new ArraySegment(buffer, 0, count); + } + + public void Disconnect() + { + cancellation?.Cancel(); + + // only if started + if (webSocket != null) + { + // close client + webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,"", CancellationToken.None); + webSocket = null; + Connecting = false; + IsConnected = false; + } + } + + // send the data or throw exception + public async void Send(byte[] data) + { + if (webSocket == null) + { + ReceivedError?.Invoke(new SocketException((int)SocketError.NotConnected)); + return; + } + + try + { + await webSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, cancellation.Token); + } + catch (Exception ex) + { + Disconnect(); + ReceivedError?.Invoke(ex); + } + } + + + public override string ToString() + { + if (IsConnected ) + { + return $"Websocket connected to {uri}"; + } + if (Connecting) + { + return $"Websocket connecting to {uri}"; + } + return ""; + } + } + +} + +#endif diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs.meta new file mode 100644 index 0000000..70e22d3 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Client.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f69ff0981a33445a89b5e37b245806f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs new file mode 100644 index 0000000..73d10cd --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs @@ -0,0 +1,118 @@ +#if UNITY_WEBGL && !UNITY_EDITOR + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using AOT; +using Ninja.WebSockets; +using UnityEngine; + +namespace Mirror.Websocket +{ + // this is the client implementation used by browsers + public class Client + { + static int idGenerator = 0; + static readonly Dictionary clients = new Dictionary(); + + public bool NoDelay = true; + + public event Action Connected; + public event Action> ReceivedData; + public event Action Disconnected; + public event Action ReceivedError; + + public bool Connecting { get; set; } + public bool IsConnected + { + get + { + return SocketState(m_NativeRef) != 0; + } + } + + int m_NativeRef = 0; + readonly int id; + + public Client() + { + id = Interlocked.Increment(ref idGenerator); + } + + public void Connect(Uri uri) + { + clients[id] = this; + + Connecting = true; + + m_NativeRef = SocketCreate(uri.ToString(), id, OnOpen, OnData, OnClose); + } + + public void Disconnect() + { + SocketClose(m_NativeRef); + } + + // send the data or throw exception + public void Send(byte[] data) + { + SocketSend(m_NativeRef, data, data.Length); + } + + + #region Javascript native functions + [DllImport("__Internal")] + static extern int SocketCreate( + string url, + int id, + Action onpen, + Action ondata, + Action onclose); + + [DllImport("__Internal")] + static extern int SocketState(int socketInstance); + + [DllImport("__Internal")] + static extern void SocketSend(int socketInstance, byte[] ptr, int length); + + [DllImport("__Internal")] + static extern void SocketClose(int socketInstance); + + #endregion + + #region Javascript callbacks + + [MonoPInvokeCallback(typeof(Action))] + public static void OnOpen(int id) + { + clients[id].Connecting = false; + clients[id].Connected?.Invoke(); + } + + [MonoPInvokeCallback(typeof(Action))] + public static void OnClose(int id) + { + clients[id].Connecting = false; + clients[id].Disconnected?.Invoke(); + clients.Remove(id); + } + + [MonoPInvokeCallback(typeof(Action))] + public static void OnData(int id, IntPtr ptr, int length) + { + byte[] data = new byte[length]; + Marshal.Copy(ptr, data, 0, length); + + clients[id].ReceivedData(new ArraySegment(data)); + } + #endregion + } +} + +#endif diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs.meta new file mode 100644 index 0000000..2fdca98 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/ClientJs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 278e08c90f2324e2e80a0fe8984b0590 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets.meta new file mode 100644 index 0000000..e156578 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 100fd42034e0d46db8980db4cc0cd178 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs new file mode 100644 index 0000000..74a0c87 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Ninja.WebSockets +{ + /// + /// This buffer pool is instance thread safe + /// Use GetBuffer to get a MemoryStream (with a publically accessible buffer) + /// Calling Close on this MemoryStream will clear its internal buffer and return the buffer to the pool for reuse + /// MemoryStreams can grow larger than the DEFAULT_BUFFER_SIZE (or whatever you passed in) + /// and the underlying buffers will be returned to the pool at their larger sizes + /// + public class BufferPool : IBufferPool + { + const int DEFAULT_BUFFER_SIZE = 16384; + readonly ConcurrentStack _bufferPoolStack; + readonly int _bufferSize; + + public BufferPool() : this(DEFAULT_BUFFER_SIZE) + { + } + + public BufferPool(int bufferSize) + { + _bufferSize = bufferSize; + _bufferPoolStack = new ConcurrentStack(); + } + + /// + /// This memory stream is not instance thread safe (not to be confused with the BufferPool which is instance thread safe) + /// + protected class PublicBufferMemoryStream : MemoryStream + { + readonly BufferPool _bufferPoolInternal; + byte[] _buffer; + MemoryStream _ms; + + public PublicBufferMemoryStream(byte[] buffer, BufferPool bufferPool) : base(new byte[0]) + { + _bufferPoolInternal = bufferPool; + _buffer = buffer; + _ms = new MemoryStream(buffer, 0, buffer.Length, true, true); + } + + public override long Length => base.Length; + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _ms.BeginRead(buffer, offset, count, callback, state); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _ms.BeginWrite(buffer, offset, count, callback, state); + } + + public override bool CanRead => _ms.CanRead; + public override bool CanSeek => _ms.CanSeek; + public override bool CanTimeout => _ms.CanTimeout; + public override bool CanWrite => _ms.CanWrite; + public override int Capacity + { + get { return _ms.Capacity; } + set { _ms.Capacity = value; } + } + + public override void Close() + { + // clear the buffer - we only need to clear up to the number of bytes we have already written + Array.Clear(_buffer, 0, (int)_ms.Position); + + _ms.Close(); + + // return the buffer to the pool + _bufferPoolInternal.ReturnBuffer(_buffer); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _ms.CopyToAsync(destination, bufferSize, cancellationToken); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return _ms.EndRead(asyncResult); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + _ms.EndWrite(asyncResult); + } + + public override void Flush() + { + _ms.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _ms.FlushAsync(cancellationToken); + } + + public override byte[] GetBuffer() + { + return _buffer; + } + + public override long Position + { + get { return _ms.Position; } + set { _ms.Position = value; } + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _ms.Read(buffer, offset, count); + } + + void EnlargeBufferIfRequired(int count) + { + // we cannot fit the data into the existing buffer, time for a new buffer + if (count > (_buffer.Length - _ms.Position)) + { + int position = (int)_ms.Position; + + // double the buffer size + int newSize = _buffer.Length * 2; + + // make sure the new size is big enough + int requiredSize = count + _buffer.Length - position; + if (requiredSize > newSize) + { + // compute the power of two larger than requiredSize. so 40000 => 65536 + newSize = (int)Math.Pow(2, Math.Ceiling(Math.Log(requiredSize) / Math.Log(2))); ; + } + + byte[] newBuffer = new byte[newSize]; + Buffer.BlockCopy(_buffer, 0, newBuffer, 0, position); + _ms = new MemoryStream(newBuffer, 0, newBuffer.Length, true, true) + { + Position = position + }; + + _buffer = newBuffer; + } + } + + public override void WriteByte(byte value) + { + EnlargeBufferIfRequired(1); + _ms.WriteByte(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + EnlargeBufferIfRequired(count); + _ms.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + EnlargeBufferIfRequired(count); + return _ms.WriteAsync(buffer, offset, count); + } + + public override object InitializeLifetimeService() + { + return _ms.InitializeLifetimeService(); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _ms.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override int ReadByte() + { + return _ms.ReadByte(); + } + + public override int ReadTimeout { + get { return _ms.ReadTimeout; } + set { _ms.ReadTimeout = value; } + } + + public override long Seek(long offset, SeekOrigin loc) + { + return _ms.Seek(offset, loc); + } + + /// + /// Note: This will not make the MemoryStream any smaller, only larger + /// + public override void SetLength(long value) + { + EnlargeBufferIfRequired((int)value); + } + + public override byte[] ToArray() + { + // you should never call this + return _ms.ToArray(); + } + + public override int WriteTimeout + { + get { return _ms.WriteTimeout; } + set { _ms.WriteTimeout = value; } + } + +#if !NET45 + public override bool TryGetBuffer(out ArraySegment buffer) + { + buffer = new ArraySegment(_buffer, 0, (int)_ms.Position); + return true; + } +#endif + + public override void WriteTo(Stream stream) + { + _ms.WriteTo(stream); + } + } + + /// + /// Gets a MemoryStream built from a buffer plucked from a thread safe pool + /// The pool grows automatically. + /// Closing the memory stream clears the buffer and returns it to the pool + /// + public MemoryStream GetBuffer() + { + if (!_bufferPoolStack.TryPop(out byte[] buffer)) + { + buffer = new byte[_bufferSize]; + } + + return new PublicBufferMemoryStream(buffer, this); + } + + protected void ReturnBuffer(byte[] buffer) + { + _bufferPoolStack.Push(buffer); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs.meta new file mode 100644 index 0000000..2bfda08 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/BufferPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e362254f82b2a4627bfd83394d093715 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions.meta new file mode 100644 index 0000000..210aa6b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ca217a8fd870444d0ae623d3905d603f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs new file mode 100644 index 0000000..5a1724f --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs @@ -0,0 +1,26 @@ +using System; + +namespace Ninja.WebSockets.Exceptions +{ + [Serializable] + public class EntityTooLargeException : Exception + { + public EntityTooLargeException() : base() + { + + } + + /// + /// Http header too large to fit in buffer + /// + public EntityTooLargeException(string message) : base(message) + { + + } + + public EntityTooLargeException(string message, Exception inner) : base(message, inner) + { + + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs.meta new file mode 100644 index 0000000..b54345b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/EntityTooLargeException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7ef87d4a0822c4d2da3e8daa392e61d3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs new file mode 100644 index 0000000..73354dd --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ninja.WebSockets.Exceptions +{ + [Serializable] + public class InvalidHttpResponseCodeException : Exception + { + public string ResponseCode { get; private set; } + + public string ResponseHeader { get; private set; } + + public string ResponseDetails { get; private set; } + + public InvalidHttpResponseCodeException() : base() + { + } + + public InvalidHttpResponseCodeException(string message) : base(message) + { + } + + public InvalidHttpResponseCodeException(string responseCode, string responseDetails, string responseHeader) : base(responseCode) + { + ResponseCode = responseCode; + ResponseDetails = responseDetails; + ResponseHeader = responseHeader; + } + + public InvalidHttpResponseCodeException(string message, Exception inner) : base(message, inner) + { + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs.meta new file mode 100644 index 0000000..7e772d5 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/InvalidHttpResponseCodeException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c2d7303a1a324f0ebe15cb3faf9b0d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt new file mode 100644 index 0000000..a1d28e4 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt @@ -0,0 +1 @@ +Make sure that exceptions follow the microsoft standards \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt.meta new file mode 100644 index 0000000..2ac5059 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/README.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3488f8d8c73d64a12bc24930c0210a41 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs new file mode 100644 index 0000000..8247625 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ninja.WebSockets.Exceptions +{ + [Serializable] + public class SecWebSocketKeyMissingException : Exception + { + public SecWebSocketKeyMissingException() : base() + { + + } + + public SecWebSocketKeyMissingException(string message) : base(message) + { + + } + + public SecWebSocketKeyMissingException(string message, Exception inner) : base(message, inner) + { + + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs.meta new file mode 100644 index 0000000..ebb2ec0 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/SecWebSocketKeyMissingException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 66d99fe90f4cb4471bf01c6f391ffc29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs new file mode 100644 index 0000000..69d5360 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Net.Sockets; + +namespace Ninja.WebSockets.Exceptions +{ + [Serializable] + public class ServerListenerSocketException : Exception + { + public ServerListenerSocketException() : base() + { + } + + public ServerListenerSocketException(string message) : base(message) + { + } + + public ServerListenerSocketException(string message, Exception inner) : base(message, inner) + { + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs.meta new file mode 100644 index 0000000..10b3384 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/ServerListenerSocketException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b188b3dddee84decadd5913a535746b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs new file mode 100644 index 0000000..682143f --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ninja.WebSockets.Exceptions +{ + [Serializable] + public class WebSocketBufferOverflowException : Exception + { + public WebSocketBufferOverflowException() : base() + { + } + + public WebSocketBufferOverflowException(string message) : base(message) + { + } + + public WebSocketBufferOverflowException(string message, Exception inner) : base(message, inner) + { + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs.meta new file mode 100644 index 0000000..419f9d0 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketBufferOverflowException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c3188abcb6c441759011da01fa342f4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs new file mode 100644 index 0000000..624dc77 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Ninja.WebSockets.Exceptions +{ + [Serializable] + public class WebSocketHandshakeFailedException : Exception + { + public WebSocketHandshakeFailedException() : base() + { + } + + public WebSocketHandshakeFailedException(string message) : base(message) + { + } + + public WebSocketHandshakeFailedException(string message, Exception inner) : base(message, inner) + { + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs.meta new file mode 100644 index 0000000..8bd4271 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketHandshakeFailedException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7dd72ca0a28054d258c1d1ad8254a16e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs new file mode 100644 index 0000000..f23a85a --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Ninja.WebSockets.Exceptions +{ + [Serializable] + public class WebSocketVersionNotSupportedException : Exception + { + public WebSocketVersionNotSupportedException() : base() + { + } + + public WebSocketVersionNotSupportedException(string message) : base(message) + { + } + + public WebSocketVersionNotSupportedException(string message, Exception inner) : base(message, inner) + { + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs.meta new file mode 100644 index 0000000..6f14394 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Exceptions/WebSocketVersionNotSupportedException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dc12d4b9cfe854aeeaab3c9f2f3d165e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs new file mode 100644 index 0000000..88fc9a6 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs @@ -0,0 +1,202 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Ninja.WebSockets.Exceptions; +using System.Linq; + +namespace Ninja.WebSockets +{ + public class HttpHelper + { + const string HTTP_GET_HEADER_REGEX = @"^GET(.*)HTTP\/1\.1"; + + /// + /// Calculates a random WebSocket key that can be used to initiate a WebSocket handshake + /// + /// A random websocket key + public static string CalculateWebSocketKey() + { + // this is not used for cryptography so doing something simple like he code below is op + Random rand = new Random((int)DateTime.Now.Ticks); + byte[] keyAsBytes = new byte[16]; + rand.NextBytes(keyAsBytes); + return Convert.ToBase64String(keyAsBytes); + } + + /// + /// Computes a WebSocket accept string from a given key + /// + /// The web socket key to base the accept string on + /// A web socket accept string + public static string ComputeSocketAcceptString(string secWebSocketKey) + { + // this is a guid as per the web socket spec + const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + string concatenated = secWebSocketKey + webSocketGuid; + byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated); + + // note an instance of SHA1 is not threadsafe so we have to create a new one every time here + byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes); + string secWebSocketAccept = Convert.ToBase64String(sha1Hash); + return secWebSocketAccept; + } + + /// + /// Reads an http header as per the HTTP spec + /// + /// The stream to read UTF8 text from + /// The cancellation token + /// The HTTP header + public static async Task ReadHttpHeaderAsync(Stream stream, CancellationToken token) + { + int length = 1024*16; // 16KB buffer more than enough for http header + byte[] buffer = new byte[length]; + int offset = 0; + int bytesRead = 0; + + do + { + if (offset >= length) + { + throw new EntityTooLargeException("Http header message too large to fit in buffer (16KB)"); + } + + bytesRead = await stream.ReadAsync(buffer, offset, length - offset, token); + offset += bytesRead; + string header = Encoding.UTF8.GetString(buffer, 0, offset); + + // as per http specification, all headers should end this this + if (header.Contains("\r\n\r\n")) + { + return header; + } + + } while (bytesRead > 0); + + return string.Empty; + } + + /// + /// Decodes the header to detect is this is a web socket upgrade response + /// + /// The HTTP header + /// True if this is an http WebSocket upgrade response + public static bool IsWebSocketUpgradeRequest(String header) + { + Regex getRegex = new Regex(HTTP_GET_HEADER_REGEX, RegexOptions.IgnoreCase); + Match getRegexMatch = getRegex.Match(header); + + if (getRegexMatch.Success) + { + // check if this is a web socket upgrade request + Regex webSocketUpgradeRegex = new Regex("Upgrade: websocket", RegexOptions.IgnoreCase); + Match webSocketUpgradeRegexMatch = webSocketUpgradeRegex.Match(header); + return webSocketUpgradeRegexMatch.Success; + } + + return false; + } + + /// + /// Gets the path from the HTTP header + /// + /// The HTTP header to read + /// The path + public static string GetPathFromHeader(string httpHeader) + { + Regex getRegex = new Regex(HTTP_GET_HEADER_REGEX, RegexOptions.IgnoreCase); + Match getRegexMatch = getRegex.Match(httpHeader); + + if (getRegexMatch.Success) + { + // extract the path attribute from the first line of the header + return getRegexMatch.Groups[1].Value.Trim(); + } + + return null; + } + + public static IList GetSubProtocols(string httpHeader) + { + Regex regex = new Regex(@"Sec-WebSocket-Protocol:(?.+)", RegexOptions.IgnoreCase); + Match match = regex.Match(httpHeader); + + if (match.Success) + { + const int MAX_LEN = 2048; + if (match.Length > MAX_LEN) + { + throw new EntityTooLargeException($"Sec-WebSocket-Protocol exceeded the maximum of length of {MAX_LEN}"); + } + + // extract a csv list of sub protocols (in order of highest preference first) + string csv = match.Groups["protocols"].Value.Trim(); + return csv.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .ToList(); + } + + return new List(); + } + + /// + /// Reads the HTTP response code from the http response string + /// + /// The response string + /// the response code + public static string ReadHttpResponseCode(string response) + { + Regex getRegex = new Regex(@"HTTP\/1\.1 (.*)", RegexOptions.IgnoreCase); + Match getRegexMatch = getRegex.Match(response); + + if (getRegexMatch.Success) + { + // extract the path attribute from the first line of the header + return getRegexMatch.Groups[1].Value.Trim(); + } + + return null; + } + + /// + /// Writes an HTTP response string to the stream + /// + /// The response (without the new line characters) + /// The stream to write to + /// The cancellation token + public static async Task WriteHttpHeaderAsync(string response, Stream stream, CancellationToken token) + { + response = response.Trim() + "\r\n\r\n"; + Byte[] bytes = Encoding.UTF8.GetBytes(response); + await stream.WriteAsync(bytes, 0, bytes.Length, token); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs.meta new file mode 100644 index 0000000..808420c --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/HttpHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3c90b5378af58402987e8a2938d763f2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs new file mode 100644 index 0000000..395a281 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Ninja.WebSockets +{ + public interface IBufferPool + { + /// + /// Gets a MemoryStream built from a buffer plucked from a thread safe pool + /// The pool grows automatically. + /// Closing the memory stream clears the buffer and returns it to the pool + /// + MemoryStream GetBuffer(); + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs.meta new file mode 100644 index 0000000..cb3da58 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IBufferPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 894bb86ff9cbe4179a6764904f9dd742 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs new file mode 100644 index 0000000..e87dbb6 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ninja.WebSockets +{ + /// + /// Ping Pong Manager used to facilitate ping pong WebSocket messages + /// + interface IPingPongManager + { + /// + /// Raised when a Pong frame is received + /// + event EventHandler Pong; + + /// + /// Sends a ping frame + /// + /// The payload (must be 125 bytes of less) + /// The cancellation token + Task SendPing(ArraySegment payload, CancellationToken cancellation); + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs.meta new file mode 100644 index 0000000..9ac6ac5 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IPingPongManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1432676d0074e4a7ca123228797fe0ac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs new file mode 100644 index 0000000..7eb1071 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Ninja.WebSockets +{ + /// + /// Web socket client factory used to open web socket client connections + /// + public interface IWebSocketClientFactory + { + /// + /// Connect with default options + /// + /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) + /// The optional cancellation token + /// A connected web socket instance + Task ConnectAsync(Uri uri, CancellationToken token = default(CancellationToken)); + + /// + /// Connect with options specified + /// + /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) + /// The WebSocket client options + /// The optional cancellation token + /// A connected web socket instance + Task ConnectAsync(Uri uri, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)); + + /// + /// Connect with a stream that has already been opened and HTTP websocket upgrade request sent + /// This function will check the handshake response from the server and proceed if successful + /// Use this function if you have specific requirements to open a conenction like using special http headers and cookies + /// You will have to build your own HTTP websocket upgrade request + /// You may not even choose to use TCP/IP and this function will allow you to do that + /// + /// The full duplex response stream from the server + /// The secWebSocketKey you used in the handshake request + /// The WebSocket client options + /// The optional cancellation token + /// + Task ConnectAsync(Stream responseStream, string secWebSocketKey, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)); + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs.meta new file mode 100644 index 0000000..7039071 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketClientFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 290e337adc73544809ef6db4d09ec5bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs new file mode 100644 index 0000000..6915463 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Ninja.WebSockets +{ + /// + /// Web socket server factory used to open web socket server connections + /// + public interface IWebSocketServerFactory + { + /// + /// Reads a http header information from a stream and decodes the parts relating to the WebSocket protocot upgrade + /// + /// The network stream + /// The optional cancellation token + /// Http data read from the stream + Task ReadHttpHeaderFromStreamAsync(Stream stream, CancellationToken token = default(CancellationToken)); + + /// + /// Accept web socket with default options + /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext + /// + /// The http context used to initiate this web socket request + /// The optional cancellation token + /// A connected web socket + Task AcceptWebSocketAsync(WebSocketHttpContext context, CancellationToken token = default(CancellationToken)); + + /// + /// Accept web socket with options specified + /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext + /// + /// The http context used to initiate this web socket request + /// The web socket options + /// The optional cancellation token + /// A connected web socket + Task AcceptWebSocketAsync(WebSocketHttpContext context, WebSocketServerOptions options, CancellationToken token = default(CancellationToken)); + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs.meta new file mode 100644 index 0000000..391cb7f --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/IWebSocketServerFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 33e062311740342db82c2f6e53fbce73 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal.meta new file mode 100644 index 0000000..b6bf8b2 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 58fcf4fbb7c9b47eeaf267adb27810d3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs new file mode 100644 index 0000000..e31c222 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs @@ -0,0 +1,149 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Ninja.WebSockets.Internal +{ + internal class BinaryReaderWriter + { + public static async Task ReadExactly(int length, Stream stream, ArraySegment buffer, CancellationToken cancellationToken) + { + if (buffer.Count < length) + { + // This will happen if the calling function supplied a buffer that was too small to fit the payload of the websocket frame. + // Note that this can happen on the close handshake where the message size can be larger than the regular payload + throw new InternalBufferOverflowException($"Unable to read {length} bytes into buffer (offset: {buffer.Offset} size: {buffer.Count}). Use a larger read buffer"); + } + + int offset = 0; + while (offset < length) + { + int bytesRead = 0; + + NetworkStream networkStream = stream as NetworkStream; + if (networkStream != null && networkStream.DataAvailable) + { + // paul: if data is available read it immediatelly. + // in my tests this performed a lot better, because ReadAsync always waited until + // the next frame. + bytesRead = stream.Read(buffer.Array, buffer.Offset + offset, length - offset); + } + else + { + bytesRead = await stream.ReadAsync(buffer.Array, buffer.Offset + offset, length - offset, cancellationToken); + } + + if (bytesRead == 0) + { + throw new EndOfStreamException(string.Format("Unexpected end of stream encountered whilst attempting to read {0:#,##0} bytes", length)); + } + + offset += bytesRead; + } + } + + public static async Task ReadUShortExactly(Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) + { + await ReadExactly(2, stream, buffer, cancellationToken); + + if (!isLittleEndian) + { + Array.Reverse(buffer.Array, buffer.Offset, 2); // big endian + } + + return BitConverter.ToUInt16(buffer.Array, buffer.Offset); + } + + public static async Task ReadULongExactly(Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) + { + await ReadExactly(8, stream, buffer, cancellationToken); + + if (!isLittleEndian) + { + Array.Reverse(buffer.Array, buffer.Offset, 8); // big endian + } + + return BitConverter.ToUInt64(buffer.Array, buffer.Offset); + } + + public static async Task ReadLongExactly(Stream stream, bool isLittleEndian, ArraySegment buffer, CancellationToken cancellationToken) + { + await ReadExactly(8, stream, buffer, cancellationToken); + + if (!isLittleEndian) + { + Array.Reverse(buffer.Array, buffer.Offset, 8); // big endian + } + + return BitConverter.ToInt64(buffer.Array, buffer.Offset); + } + + public static void WriteInt(int value, Stream stream, bool isLittleEndian) + { + byte[] buffer = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian && !isLittleEndian) + { + Array.Reverse(buffer); + } + + stream.Write(buffer, 0, buffer.Length); + } + + public static void WriteULong(ulong value, Stream stream, bool isLittleEndian) + { + byte[] buffer = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian && ! isLittleEndian) + { + Array.Reverse(buffer); + } + + stream.Write(buffer, 0, buffer.Length); + } + + public static void WriteLong(long value, Stream stream, bool isLittleEndian) + { + byte[] buffer = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian && !isLittleEndian) + { + Array.Reverse(buffer); + } + + stream.Write(buffer, 0, buffer.Length); + } + + public static void WriteUShort(ushort value, Stream stream, bool isLittleEndian) + { + byte[] buffer = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian && !isLittleEndian) + { + Array.Reverse(buffer); + } + + stream.Write(buffer, 0, buffer.Length); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs.meta new file mode 100644 index 0000000..8671b81 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/BinaryReaderWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2379de83fde9446689e7fd94d8cefca1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs new file mode 100644 index 0000000..310092e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs @@ -0,0 +1,393 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.Diagnostics.Tracing; +using System.Net.Security; +using System.Net.WebSockets; + +namespace Ninja.WebSockets.Internal +{ + /// + /// Use the Guid to locate this EventSource in PerfView using the Additional Providers box (without wildcard characters) + /// + [EventSource(Name = "Ninja-WebSockets", Guid = "7DE1A071-4F85-4DBD-8FB1-EE8D3845E087")] + internal sealed class Events : EventSource + { + public static Events Log = new Events(); + + [Event(1, Level = EventLevel.Informational)] + public void ClientConnectingToIpAddress(Guid guid, string ipAddress, int port) + { + if (this.IsEnabled()) + { + WriteEvent(1, guid, ipAddress, port); + } + } + + [Event(2, Level = EventLevel.Informational)] + public void ClientConnectingToHost(Guid guid, string host, int port) + { + if (this.IsEnabled()) + { + WriteEvent(2, guid, host, port); + } + } + + [Event(3, Level = EventLevel.Informational)] + public void AttemtingToSecureSslConnection(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(3, guid); + } + } + + [Event(4, Level = EventLevel.Informational)] + public void ConnectionSecured(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(4, guid); + } + } + + [Event(5, Level = EventLevel.Informational)] + public void ConnectionNotSecure(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(5, guid); + } + } + + [Event(6, Level = EventLevel.Error)] + public void SslCertificateError(SslPolicyErrors sslPolicyErrors) + { + if (this.IsEnabled()) + { + WriteEvent(6, sslPolicyErrors); + } + } + + [Event(7, Level = EventLevel.Informational)] + public void HandshakeSent(Guid guid, string httpHeader) + { + if (this.IsEnabled()) + { + WriteEvent(7, guid, httpHeader ?? string.Empty); + } + } + + [Event(8, Level = EventLevel.Informational)] + public void ReadingHttpResponse(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(8, guid); + } + } + + [Event(9, Level = EventLevel.Error)] + public void ReadHttpResponseError(Guid guid, string exception) + { + if (this.IsEnabled()) + { + WriteEvent(9, guid, exception ?? string.Empty); + } + } + + [Event(10, Level = EventLevel.Warning)] + public void InvalidHttpResponseCode(Guid guid, string response) + { + if (this.IsEnabled()) + { + WriteEvent(10, guid, response ?? string.Empty); + } + } + + [Event(11, Level = EventLevel.Error)] + public void HandshakeFailure(Guid guid, string message) + { + if (this.IsEnabled()) + { + WriteEvent(11, guid, message ?? string.Empty); + } + } + + [Event(12, Level = EventLevel.Informational)] + public void ClientHandshakeSuccess(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(12, guid); + } + } + + [Event(13, Level = EventLevel.Informational)] + public void ServerHandshakeSuccess(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(13, guid); + } + } + + [Event(14, Level = EventLevel.Informational)] + public void AcceptWebSocketStarted(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(14, guid); + } + } + + [Event(15, Level = EventLevel.Informational)] + public void SendingHandshakeResponse(Guid guid, string response) + { + if (this.IsEnabled()) + { + WriteEvent(15, guid, response ?? string.Empty); + } + } + + [Event(16, Level = EventLevel.Error)] + public void WebSocketVersionNotSupported(Guid guid, string exception) + { + if (this.IsEnabled()) + { + WriteEvent(16, guid, exception ?? string.Empty); + } + } + + [Event(17, Level = EventLevel.Error)] + public void BadRequest(Guid guid, string exception) + { + if (this.IsEnabled()) + { + WriteEvent(17, guid, exception ?? string.Empty); + } + } + + [Event(18, Level = EventLevel.Informational)] + public void UsePerMessageDeflate(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(18, guid); + } + } + + [Event(19, Level = EventLevel.Informational)] + public void NoMessageCompression(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(19, guid); + } + } + + [Event(20, Level = EventLevel.Informational)] + public void KeepAliveIntervalZero(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(20, guid); + } + } + + [Event(21, Level = EventLevel.Informational)] + public void PingPongManagerStarted(Guid guid, int keepAliveIntervalSeconds) + { + if (this.IsEnabled()) + { + WriteEvent(21, guid, keepAliveIntervalSeconds); + } + } + + [Event(22, Level = EventLevel.Informational)] + public void PingPongManagerEnded(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(22, guid); + } + } + + [Event(23, Level = EventLevel.Warning)] + public void KeepAliveIntervalExpired(Guid guid, int keepAliveIntervalSeconds) + { + if (this.IsEnabled()) + { + WriteEvent(23, guid, keepAliveIntervalSeconds); + } + } + + [Event(24, Level = EventLevel.Warning)] + public void CloseOutputAutoTimeout(Guid guid, WebSocketCloseStatus closeStatus, string statusDescription, string exception) + { + if (this.IsEnabled()) + { + WriteEvent(24, guid, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty); + } + } + + [Event(25, Level = EventLevel.Error)] + public void CloseOutputAutoTimeoutCancelled(Guid guid, int timeoutSeconds, WebSocketCloseStatus closeStatus, string statusDescription, string exception) + { + if (this.IsEnabled()) + { + WriteEvent(25, guid, timeoutSeconds, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty); + } + } + + [Event(26, Level = EventLevel.Error)] + public void CloseOutputAutoTimeoutError(Guid guid, string closeException, WebSocketCloseStatus closeStatus, string statusDescription, string exception) + { + if (this.IsEnabled()) + { + WriteEvent(26, guid, closeException ?? string.Empty, closeStatus, statusDescription ?? string.Empty, exception ?? string.Empty); + } + } + + [Event(27, Level = EventLevel.Warning)] + public void TryGetBufferNotSupported(Guid guid, string streamType) + { + if (this.IsEnabled()) + { + WriteEvent(27, guid, streamType ?? string.Empty); + } + } + + [Event(28, Level = EventLevel.Verbose)] + public void SendingFrame(Guid guid, WebSocketOpCode webSocketOpCode, bool isFinBitSet, int numBytes, bool isPayloadCompressed) + { + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.None)) + { + WriteEvent(28, guid, webSocketOpCode, isFinBitSet, numBytes, isPayloadCompressed); + } + } + + [Event(29, Level = EventLevel.Verbose)] + public void ReceivedFrame(Guid guid, WebSocketOpCode webSocketOpCode, bool isFinBitSet, int numBytes) + { + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.None)) + { + WriteEvent(29, guid, webSocketOpCode, isFinBitSet, numBytes); + } + } + + [Event(30, Level = EventLevel.Informational)] + public void CloseOutputNoHandshake(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription) + { + if (this.IsEnabled()) + { + string closeStatusDesc = $"{closeStatus}"; + WriteEvent(30, guid, closeStatusDesc, statusDescription ?? string.Empty); + } + } + + [Event(31, Level = EventLevel.Informational)] + public void CloseHandshakeStarted(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription) + { + if (this.IsEnabled()) + { + string closeStatusDesc = $"{closeStatus}"; + WriteEvent(31, guid, closeStatusDesc, statusDescription ?? string.Empty); + } + } + + [Event(32, Level = EventLevel.Informational)] + public void CloseHandshakeRespond(Guid guid, WebSocketCloseStatus? closeStatus, string statusDescription) + { + if (this.IsEnabled()) + { + string closeStatusDesc = $"{closeStatus}"; + WriteEvent(32, guid, closeStatusDesc, statusDescription ?? string.Empty); + } + } + + [Event(33, Level = EventLevel.Informational)] + public void CloseHandshakeComplete(Guid guid) + { + if (this.IsEnabled()) + { + WriteEvent(33, guid); + } + } + + [Event(34, Level = EventLevel.Warning)] + public void CloseFrameReceivedInUnexpectedState(Guid guid, WebSocketState webSocketState, WebSocketCloseStatus? closeStatus, string statusDescription) + { + if (this.IsEnabled()) + { + string closeStatusDesc = $"{closeStatus}"; + WriteEvent(34, guid, webSocketState, closeStatusDesc, statusDescription ?? string.Empty); + } + } + + [Event(35, Level = EventLevel.Informational)] + public void WebSocketDispose(Guid guid, WebSocketState webSocketState) + { + if (this.IsEnabled()) + { + WriteEvent(35, guid, webSocketState); + } + } + + [Event(36, Level = EventLevel.Warning)] + public void WebSocketDisposeCloseTimeout(Guid guid, WebSocketState webSocketState) + { + if (this.IsEnabled()) + { + WriteEvent(36, guid, webSocketState); + } + } + + [Event(37, Level = EventLevel.Error)] + public void WebSocketDisposeError(Guid guid, WebSocketState webSocketState, string exception) + { + if (this.IsEnabled()) + { + WriteEvent(37, guid, webSocketState, exception ?? string.Empty); + } + } + + [Event(38, Level = EventLevel.Warning)] + public void InvalidStateBeforeClose(Guid guid, WebSocketState webSocketState) + { + if (this.IsEnabled()) + { + WriteEvent(38, guid, webSocketState); + } + } + + [Event(39, Level = EventLevel.Warning)] + public void InvalidStateBeforeCloseOutput(Guid guid, WebSocketState webSocketState) + { + if (this.IsEnabled()) + { + WriteEvent(39, guid, webSocketState); + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs.meta new file mode 100644 index 0000000..3e533be --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/Events.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7713ae9d113e145389c0a175d58090be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs new file mode 100644 index 0000000..eb37317 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs @@ -0,0 +1,31 @@ +using System.Net.WebSockets; + +namespace Ninja.WebSockets.Internal +{ + internal class WebSocketFrame + { + public bool IsFinBitSet { get; private set; } + + public WebSocketOpCode OpCode { get; private set; } + + public int Count { get; private set; } + + public WebSocketCloseStatus? CloseStatus { get; private set; } + + public string CloseStatusDescription { get; private set; } + + public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count) + { + IsFinBitSet = isFinBitSet; + OpCode = webSocketOpCode; + Count = count; + } + + public WebSocketFrame(bool isFinBitSet, WebSocketOpCode webSocketOpCode, int count, WebSocketCloseStatus closeStatus, string closeStatusDescription) : this(isFinBitSet, webSocketOpCode, count) + { + CloseStatus = closeStatus; + CloseStatusDescription = closeStatusDescription; + } + + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs.meta new file mode 100644 index 0000000..f27e26d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrame.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4e8a4f1bcb0c49d7a82e5f430c85e2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs new file mode 100644 index 0000000..48022bb --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs @@ -0,0 +1,62 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; + +namespace Ninja.WebSockets.Internal +{ + internal static class WebSocketFrameCommon + { + public const int MaskKeyLength = 4; + + /// + /// Mutate payload with the mask key + /// This is a reversible process + /// If you apply this to masked data it will be unmasked and visa versa + /// + /// The 4 byte mask key + /// The payload to mutate + public static void ToggleMask(ArraySegment maskKey, ArraySegment payload) + { + if (maskKey.Count != MaskKeyLength) + { + throw new Exception($"MaskKey key must be {MaskKeyLength} bytes"); + } + + byte[] buffer = payload.Array; + byte[] maskKeyArray = maskKey.Array; + int payloadOffset = payload.Offset; + int payloadCount = payload.Count; + int maskKeyOffset = maskKey.Offset; + + // apply the mask key (this is a reversible process so no need to copy the payload) + // NOTE: this is a hot function + // TODO: make this faster + for (int i = payloadOffset; i < payloadCount; i++) + { + int payloadIndex = i - payloadOffset; // index should start at zero + int maskKeyIndex = maskKeyOffset + (payloadIndex % MaskKeyLength); + buffer[i] = (Byte)(buffer[i] ^ maskKeyArray[maskKeyIndex]); + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs.meta new file mode 100644 index 0000000..0011ea2 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameCommon.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: adbf6c11b66794c0494acf9c713a6759 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs new file mode 100644 index 0000000..e8ff65f --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs @@ -0,0 +1,168 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Ninja.WebSockets.Internal +{ + /// + /// Reads a WebSocket frame + /// see http://tools.ietf.org/html/rfc6455 for specification + /// + internal static class WebSocketFrameReader + { + /// + /// Read a WebSocket frame from the stream + /// + /// The stream to read from + /// The buffer to read into + /// the cancellation token + /// A websocket frame + public static async Task ReadAsync(Stream fromStream, ArraySegment intoBuffer, CancellationToken cancellationToken) + { + // allocate a small buffer to read small chunks of data from the stream + ArraySegment smallBuffer = new ArraySegment(new byte[8]); + + await BinaryReaderWriter.ReadExactly(2, fromStream, smallBuffer, cancellationToken); + byte byte1 = smallBuffer.Array[0]; + byte byte2 = smallBuffer.Array[1]; + + // process first byte + byte finBitFlag = 0x80; + byte opCodeFlag = 0x0F; + bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag; + WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag); + + // read and process second byte + byte maskFlag = 0x80; + bool isMaskBitSet = (byte2 & maskFlag) == maskFlag; + uint len = await ReadLength(byte2, smallBuffer, fromStream, cancellationToken); + int count = (int)len; + + try + { + // use the masking key to decode the data if needed + if (isMaskBitSet) + { + ArraySegment maskKey = new ArraySegment(smallBuffer.Array, 0, WebSocketFrameCommon.MaskKeyLength); + await BinaryReaderWriter.ReadExactly(maskKey.Count, fromStream, maskKey, cancellationToken); + await BinaryReaderWriter.ReadExactly(count, fromStream, intoBuffer, cancellationToken); + ArraySegment payloadToMask = new ArraySegment(intoBuffer.Array, intoBuffer.Offset, count); + WebSocketFrameCommon.ToggleMask(maskKey, payloadToMask); + } + else + { + await BinaryReaderWriter.ReadExactly(count, fromStream, intoBuffer, cancellationToken); + } + } + catch (InternalBufferOverflowException e) + { + throw new InternalBufferOverflowException($"Supplied buffer too small to read {0} bytes from {Enum.GetName(typeof(WebSocketOpCode), opCode)} frame", e); + } + + if (opCode == WebSocketOpCode.ConnectionClose) + { + return DecodeCloseFrame(isFinBitSet, opCode, count, intoBuffer); + } + else + { + // note that by this point the payload will be populated + return new WebSocketFrame(isFinBitSet, opCode, count); + } + } + + /// + /// Extracts close status and close description information from the web socket frame + /// + static WebSocketFrame DecodeCloseFrame(bool isFinBitSet, WebSocketOpCode opCode, int count, ArraySegment buffer) + { + WebSocketCloseStatus closeStatus; + string closeStatusDescription; + + if (count >= 2) + { + Array.Reverse(buffer.Array, buffer.Offset, 2); // network byte order + int closeStatusCode = (int)BitConverter.ToUInt16(buffer.Array, buffer.Offset); + if (Enum.IsDefined(typeof(WebSocketCloseStatus), closeStatusCode)) + { + closeStatus = (WebSocketCloseStatus)closeStatusCode; + } + else + { + closeStatus = WebSocketCloseStatus.Empty; + } + + int offset = buffer.Offset + 2; + int descCount = count - 2; + + if (descCount > 0) + { + closeStatusDescription = Encoding.UTF8.GetString(buffer.Array, offset, descCount); + } + else + { + closeStatusDescription = null; + } + } + else + { + closeStatus = WebSocketCloseStatus.Empty; + closeStatusDescription = null; + } + + return new WebSocketFrame(isFinBitSet, opCode, count, closeStatus, closeStatusDescription); + } + + /// + /// Reads the length of the payload according to the contents of byte2 + /// + static async Task ReadLength(byte byte2, ArraySegment smallBuffer, Stream fromStream, CancellationToken cancellationToken) + { + byte payloadLenFlag = 0x7F; + uint len = (uint) (byte2 & payloadLenFlag); + + // read a short length or a long length depending on the value of len + if (len == 126) + { + len = await BinaryReaderWriter.ReadUShortExactly(fromStream, false, smallBuffer, cancellationToken); + } + else if (len == 127) + { + len = (uint) await BinaryReaderWriter.ReadULongExactly(fromStream, false, smallBuffer, cancellationToken); + const uint maxLen = 2147483648; // 2GB - not part of the spec but just a precaution. Send large volumes of data in smaller frames. + + // protect ourselves against bad data + if (len > maxLen || len < 0) + { + throw new ArgumentOutOfRangeException($"Payload length out of range. Min 0 max 2GB. Actual {len:#,##0} bytes."); + } + } + + return len; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs.meta new file mode 100644 index 0000000..370baf4 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 092a4b88aabbc4d3b91f6a82c40b47e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs new file mode 100644 index 0000000..7ecf274 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs @@ -0,0 +1,100 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System.IO; +using System; +using System.Net.WebSockets; +using System.Text; + +namespace Ninja.WebSockets.Internal +{ + // see http://tools.ietf.org/html/rfc6455 for specification + // see fragmentation section for sending multi part messages + // EXAMPLE: For a text message sent as three fragments, + // the first fragment would have an opcode of TextFrame and isLastFrame false, + // the second fragment would have an opcode of ContinuationFrame and isLastFrame false, + // the third fragment would have an opcode of ContinuationFrame and isLastFrame true. + internal static class WebSocketFrameWriter + { + /// + /// This is used for data masking so that web proxies don't cache the data + /// Therefore, there are no cryptographic concerns + /// + static readonly Random _random; + + static WebSocketFrameWriter() + { + _random = new Random((int)DateTime.Now.Ticks); + } + + /// + /// No async await stuff here because we are dealing with a memory stream + /// + /// The web socket opcode + /// Array segment to get payload data from + /// Stream to write to + /// True is this is the last frame in this message (usually true) + public static void Write(WebSocketOpCode opCode, ArraySegment fromPayload, MemoryStream toStream, bool isLastFrame, bool isClient) + { + MemoryStream memoryStream = toStream; + byte finBitSetAsByte = isLastFrame ? (byte)0x80 : (byte)0x00; + byte byte1 = (byte)(finBitSetAsByte | (byte)opCode); + memoryStream.WriteByte(byte1); + + // NB, set the mask flag if we are constructing a client frame + byte maskBitSetAsByte = isClient ? (byte)0x80 : (byte)0x00; + + // depending on the size of the length we want to write it as a byte, ushort or ulong + if (fromPayload.Count < 126) + { + byte byte2 = (byte)(maskBitSetAsByte | (byte)fromPayload.Count); + memoryStream.WriteByte(byte2); + } + else if (fromPayload.Count <= ushort.MaxValue) + { + byte byte2 = (byte)(maskBitSetAsByte | 126); + memoryStream.WriteByte(byte2); + BinaryReaderWriter.WriteUShort((ushort)fromPayload.Count, memoryStream, false); + } + else + { + byte byte2 = (byte)(maskBitSetAsByte | 127); + memoryStream.WriteByte(byte2); + BinaryReaderWriter.WriteULong((ulong)fromPayload.Count, memoryStream, false); + } + + // if we are creating a client frame then we MUST mack the payload as per the spec + if (isClient) + { + byte[] maskKey = new byte[WebSocketFrameCommon.MaskKeyLength]; + _random.NextBytes(maskKey); + memoryStream.Write(maskKey, 0, maskKey.Length); + + // mask the payload + ArraySegment maskKeyArraySegment = new ArraySegment(maskKey, 0, maskKey.Length); + WebSocketFrameCommon.ToggleMask(maskKeyArraySegment, fromPayload); + } + + memoryStream.Write(fromPayload.Array, fromPayload.Offset, fromPayload.Count); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs.meta new file mode 100644 index 0000000..acd29fa --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketFrameWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: df337f1f7ba5245f7acadbe2e44b86bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs new file mode 100644 index 0000000..1e6f708 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs @@ -0,0 +1,628 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net.Security; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +#if RELEASESIGNED +[assembly: InternalsVisibleTo("Ninja.WebSockets.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1707056f4761b7846ed503642fcde97fc350c939f78026211304a56ba51e094c9cefde77fadce5b83c0a621c17f032c37c520b6d9ab2da8291a21472175d9caad55bf67bab4bffb46a96f864ea441cf695edc854296e02a44062245a4e09ccd9a77ef6146ecf941ce1d9da078add54bc2d4008decdac2fa2b388e17794ee6a6")] +#else +[assembly: InternalsVisibleTo("Ninja.WebSockets.UnitTests")] +#endif + +namespace Ninja.WebSockets.Internal +{ + /// + /// Main implementation of the WebSocket abstract class + /// + internal class WebSocketImplementation : WebSocket + { + readonly Guid _guid; + readonly Func _recycledStreamFactory; + readonly Stream _stream; + readonly bool _includeExceptionInCloseResponse; + readonly bool _isClient; + readonly string _subProtocol; + CancellationTokenSource _internalReadCts; + WebSocketState _state; + bool _isContinuationFrame; + WebSocketMessageType _continuationFrameMessageType = WebSocketMessageType.Binary; + readonly bool _usePerMessageDeflate = false; + bool _tryGetBufferFailureLogged = false; + const int MAX_PING_PONG_PAYLOAD_LEN = 125; + WebSocketCloseStatus? _closeStatus; + string _closeStatusDescription; + + public event EventHandler Pong; + + Queue> _messageQueue = new Queue>(); + SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1, 1); + + internal WebSocketImplementation(Guid guid, Func recycledStreamFactory, Stream stream, TimeSpan keepAliveInterval, string secWebSocketExtensions, bool includeExceptionInCloseResponse, bool isClient, string subProtocol) + { + _guid = guid; + _recycledStreamFactory = recycledStreamFactory; + _stream = stream; + _isClient = isClient; + _subProtocol = subProtocol; + _internalReadCts = new CancellationTokenSource(); + _state = WebSocketState.Open; + + if (secWebSocketExtensions?.IndexOf("permessage-deflate") >= 0) + { + _usePerMessageDeflate = true; + Events.Log.UsePerMessageDeflate(guid); + } + else + { + Events.Log.NoMessageCompression(guid); + } + + KeepAliveInterval = keepAliveInterval; + _includeExceptionInCloseResponse = includeExceptionInCloseResponse; + if (keepAliveInterval.Ticks < 0) + { + throw new InvalidOperationException("KeepAliveInterval must be Zero or positive"); + } + + if (keepAliveInterval == TimeSpan.Zero) + { + Events.Log.KeepAliveIntervalZero(guid); + } + else + { + // the ping pong manager starts a task + // but we don't have to keep a reference to it +#pragma warning disable 0219 + PingPongManager pingPongManager = new PingPongManager(guid, this, keepAliveInterval, _internalReadCts.Token); +#pragma warning restore 0219 + } + } + + public override WebSocketCloseStatus? CloseStatus => _closeStatus; + + public override string CloseStatusDescription => _closeStatusDescription; + + public override WebSocketState State { get { return _state; } } + + public override string SubProtocol => _subProtocol; + + public TimeSpan KeepAliveInterval { get; private set; } + + /// + /// Receive web socket result + /// + /// The buffer to copy data into + /// The cancellation token + /// The web socket result details + public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + try + { + // we may receive control frames so reading needs to happen in an infinite loop + while (true) + { + // allow this operation to be cancelled from iniside OR outside this instance + using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_internalReadCts.Token, cancellationToken)) + { + WebSocketFrame frame = null; + try + { + frame = await WebSocketFrameReader.ReadAsync(_stream, buffer, linkedCts.Token); + Events.Log.ReceivedFrame(_guid, frame.OpCode, frame.IsFinBitSet, frame.Count); + } + catch (SocketException) + { + // do nothing, the socket has been disconnected + } + catch (InternalBufferOverflowException ex) + { + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.MessageTooBig, "Frame too large to fit in buffer. Use message fragmentation", ex); + throw; + } + catch (ArgumentOutOfRangeException ex) + { + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, "Payload length out of range", ex); + throw; + } + catch (EndOfStreamException ex) + { + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InvalidPayloadData, "Unexpected end of stream encountered", ex); + throw; + } + catch (OperationCanceledException ex) + { + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Operation cancelled", ex); + throw; + } + catch (Exception ex) + { + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Error reading WebSocket frame", ex); + throw; + } + + switch (frame.OpCode) + { + case WebSocketOpCode.ConnectionClose: + return await RespondToCloseFrame(frame, buffer, linkedCts.Token); + case WebSocketOpCode.Ping: + ArraySegment pingPayload = new ArraySegment(buffer.Array, buffer.Offset, frame.Count); + await SendPongAsync(pingPayload, linkedCts.Token); + break; + case WebSocketOpCode.Pong: + ArraySegment pongBuffer = new ArraySegment(buffer.Array, frame.Count, buffer.Offset); + Pong?.Invoke(this, new PongEventArgs(pongBuffer)); + break; + case WebSocketOpCode.TextFrame: + if (!frame.IsFinBitSet) + { + // continuation frames will follow, record the message type Text + _continuationFrameMessageType = WebSocketMessageType.Text; + } + return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Text, frame.IsFinBitSet); + case WebSocketOpCode.BinaryFrame: + if (!frame.IsFinBitSet) + { + // continuation frames will follow, record the message type Binary + _continuationFrameMessageType = WebSocketMessageType.Binary; + } + return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Binary, frame.IsFinBitSet); + case WebSocketOpCode.ContinuationFrame: + return new WebSocketReceiveResult(frame.Count, _continuationFrameMessageType, frame.IsFinBitSet); + default: + Exception ex = new NotSupportedException($"Unknown WebSocket opcode {frame.OpCode}"); + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex); + throw ex; + } + } + } + } + catch (Exception catchAll) + { + // Most exceptions will be caught closer to their source to send an appropriate close message (and set the WebSocketState) + // However, if an unhandled exception is encountered and a close message not sent then send one here + if (_state == WebSocketState.Open) + { + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.InternalServerError, "Unexpected error reading from WebSocket", catchAll); + } + + throw; + } + } + + /// + /// Send data to the web socket + /// + /// the buffer containing data to send + /// The message type. Can be Text or Binary + /// True if this message is a standalone message (this is the norm) + /// If it is a multi-part message then false (and true for the last message) + /// the cancellation token + public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + using (MemoryStream stream = _recycledStreamFactory()) + { + WebSocketOpCode opCode = GetOppCode(messageType); + + if (_usePerMessageDeflate) + { + // NOTE: Compression is currently work in progress and should NOT be used in this library. + // The code below is very inefficient for small messages. Ideally we would like to have some sort of moving window + // of data to get the best compression. And we don't want to create new buffers which is bad for GC. + using (MemoryStream temp = new MemoryStream()) + { + DeflateStream deflateStream = new DeflateStream(temp, CompressionMode.Compress); + deflateStream.Write(buffer.Array, buffer.Offset, buffer.Count); + deflateStream.Flush(); + ArraySegment compressedBuffer = new ArraySegment(temp.ToArray()); + WebSocketFrameWriter.Write(opCode, compressedBuffer, stream, endOfMessage, _isClient); + Events.Log.SendingFrame(_guid, opCode, endOfMessage, compressedBuffer.Count, true); + } + } + else + { + WebSocketFrameWriter.Write(opCode, buffer, stream, endOfMessage, _isClient); + Events.Log.SendingFrame(_guid, opCode, endOfMessage, buffer.Count, false); + } + + await WriteStreamToNetwork(stream, cancellationToken); + _isContinuationFrame = !endOfMessage; // TODO: is this correct?? + } + } + + /// + /// Call this automatically from server side each keepAliveInterval period + /// NOTE: ping payload must be 125 bytes or less + /// + public async Task SendPingAsync(ArraySegment payload, CancellationToken cancellationToken) + { + if (payload.Count > MAX_PING_PONG_PAYLOAD_LEN) + { + throw new InvalidOperationException($"Cannot send Ping: Max ping message size {MAX_PING_PONG_PAYLOAD_LEN} exceeded: {payload.Count}"); + } + + if (_state == WebSocketState.Open) + { + using (MemoryStream stream = _recycledStreamFactory()) + { + WebSocketFrameWriter.Write(WebSocketOpCode.Ping, payload, stream, true, _isClient); + Events.Log.SendingFrame(_guid, WebSocketOpCode.Ping, true, payload.Count, false); + await WriteStreamToNetwork(stream, cancellationToken); + } + } + } + + /// + /// Aborts the WebSocket without sending a Close frame + /// + public override void Abort() + { + _state = WebSocketState.Aborted; + _internalReadCts.Cancel(); + } + + /// + /// Polite close (use the close handshake) + /// + public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + if (_state == WebSocketState.Open) + { + using (MemoryStream stream = _recycledStreamFactory()) + { + ArraySegment buffer = BuildClosePayload(closeStatus, statusDescription); + WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, buffer, stream, true, _isClient); + Events.Log.CloseHandshakeStarted(_guid, closeStatus, statusDescription); + Events.Log.SendingFrame(_guid, WebSocketOpCode.ConnectionClose, true, buffer.Count, true); + await WriteStreamToNetwork(stream, cancellationToken); + _state = WebSocketState.CloseSent; + } + } + else + { + Events.Log.InvalidStateBeforeClose(_guid, _state); + } + } + + /// + /// Fire and forget close + /// + public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + if (_state == WebSocketState.Open) + { + _state = WebSocketState.Closed; // set this before we write to the network because the write may fail + + using (MemoryStream stream = _recycledStreamFactory()) + { + ArraySegment buffer = BuildClosePayload(closeStatus, statusDescription); + WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, buffer, stream, true, _isClient); + Events.Log.CloseOutputNoHandshake(_guid, closeStatus, statusDescription); + Events.Log.SendingFrame(_guid, WebSocketOpCode.ConnectionClose, true, buffer.Count, true); + await WriteStreamToNetwork(stream, cancellationToken); + } + } + else + { + Events.Log.InvalidStateBeforeCloseOutput(_guid, _state); + } + + // cancel pending reads + _internalReadCts.Cancel(); + } + + /// + /// Dispose will send a close frame if the connection is still open + /// + public override void Dispose() + { + Events.Log.WebSocketDispose(_guid, _state); + + try + { + if (_state == WebSocketState.Open) + { + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + try + { + CloseOutputAsync(WebSocketCloseStatus.EndpointUnavailable, "Service is Disposed", cts.Token).Wait(); + } + catch (OperationCanceledException) + { + // log don't throw + Events.Log.WebSocketDisposeCloseTimeout(_guid, _state); + } + } + + // cancel pending reads - usually does nothing + _internalReadCts.Cancel(); + _stream.Close(); + } + catch (Exception ex) + { + // log dont throw + Events.Log.WebSocketDisposeError(_guid, _state, ex.ToString()); + } + } + + /// + /// Called when a Pong frame is received + /// + /// + protected virtual void OnPong(PongEventArgs e) + { + Pong?.Invoke(this, e); + } + + /// + /// As per the spec, write the close status followed by the close reason + /// + /// The close status + /// Optional extra close details + /// The payload to sent in the close frame + ArraySegment BuildClosePayload(WebSocketCloseStatus closeStatus, string statusDescription) + { + byte[] statusBuffer = BitConverter.GetBytes((ushort)closeStatus); + Array.Reverse(statusBuffer); // network byte order (big endian) + + if (statusDescription == null) + { + return new ArraySegment(statusBuffer); + } + else + { + byte[] descBuffer = Encoding.UTF8.GetBytes(statusDescription); + byte[] payload = new byte[statusBuffer.Length + descBuffer.Length]; + Buffer.BlockCopy(statusBuffer, 0, payload, 0, statusBuffer.Length); + Buffer.BlockCopy(descBuffer, 0, payload, statusBuffer.Length, descBuffer.Length); + return new ArraySegment(payload); + } + } + + /// NOTE: pong payload must be 125 bytes or less + /// Pong should contain the same payload as the ping + async Task SendPongAsync(ArraySegment payload, CancellationToken cancellationToken) + { + // as per websocket spec + if (payload.Count > MAX_PING_PONG_PAYLOAD_LEN) + { + Exception ex = new InvalidOperationException($"Max ping message size {MAX_PING_PONG_PAYLOAD_LEN} exceeded: {payload.Count}"); + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.ProtocolError, ex.Message, ex); + throw ex; + } + + try + { + if (_state == WebSocketState.Open) + { + using (MemoryStream stream = _recycledStreamFactory()) + { + WebSocketFrameWriter.Write(WebSocketOpCode.Pong, payload, stream, true, _isClient); + Events.Log.SendingFrame(_guid, WebSocketOpCode.Pong, true, payload.Count, false); + await WriteStreamToNetwork(stream, cancellationToken); + } + } + } + catch (Exception ex) + { + await CloseOutputAutoTimeoutAsync(WebSocketCloseStatus.EndpointUnavailable, "Unable to send Pong response", ex); + throw; + } + } + + /// + /// Called when a Close frame is received + /// Send a response close frame if applicable + /// + async Task RespondToCloseFrame(WebSocketFrame frame, ArraySegment buffer, CancellationToken token) + { + _closeStatus = frame.CloseStatus; + _closeStatusDescription = frame.CloseStatusDescription; + + if (_state == WebSocketState.CloseSent) + { + // this is a response to close handshake initiated by this instance + _state = WebSocketState.Closed; + Events.Log.CloseHandshakeComplete(_guid); + } + else if (_state == WebSocketState.Open) + { + // do not echo the close payload back to the client, there is no requirement for it in the spec. + // However, the same CloseStatus as recieved should be sent back. + ArraySegment closePayload = new ArraySegment(new byte[0], 0, 0); + _state = WebSocketState.CloseReceived; + Events.Log.CloseHandshakeRespond(_guid, frame.CloseStatus, frame.CloseStatusDescription); + + using (MemoryStream stream = _recycledStreamFactory()) + { + WebSocketFrameWriter.Write(WebSocketOpCode.ConnectionClose, closePayload, stream, true, _isClient); + Events.Log.SendingFrame(_guid, WebSocketOpCode.ConnectionClose, true, closePayload.Count, false); + await WriteStreamToNetwork(stream, token); + } + } + else + { + Events.Log.CloseFrameReceivedInUnexpectedState(_guid, _state, frame.CloseStatus, frame.CloseStatusDescription); + } + + return new WebSocketReceiveResult(frame.Count, WebSocketMessageType.Close, frame.IsFinBitSet, frame.CloseStatus, frame.CloseStatusDescription); + } + + /// + /// Note that the way in which the stream buffer is accessed can lead to significant performance problems + /// You want to avoid a call to stream.ToArray to avoid extra memory allocation + /// MemoryStream can be configured to have its internal buffer accessible. + /// + ArraySegment GetBuffer(MemoryStream stream) + { +#if NET45 + // NET45 does not have a TryGetBuffer function on Stream + if (_tryGetBufferFailureLogged) + { + return new ArraySegment(stream.ToArray(), 0, (int)stream.Position); + } + + // note that a MemoryStream will throw an UnuthorizedAccessException if the internal buffer is not public. Set publiclyVisible = true + try + { + return new ArraySegment(stream.GetBuffer(), 0, (int)stream.Position); + } + catch (UnauthorizedAccessException) + { + Events.Log.TryGetBufferNotSupported(_guid, stream?.GetType()?.ToString()); + _tryGetBufferFailureLogged = true; + return new ArraySegment(stream.ToArray(), 0, (int)stream.Position); + } +#else + // Avoid calling ToArray on the MemoryStream because it allocates a new byte array on tha heap + // We avaoid this by attempting to access the internal memory stream buffer + // This works with supported streams like the recyclable memory stream and writable memory streams + if (!stream.TryGetBuffer(out ArraySegment buffer)) + { + if (!_tryGetBufferFailureLogged) + { + Events.Log.TryGetBufferNotSupported(_guid, stream?.GetType()?.ToString()); + _tryGetBufferFailureLogged = true; + } + + // internal buffer not suppoted, fall back to ToArray() + byte[] array = stream.ToArray(); + buffer = new ArraySegment(array, 0, array.Length); + } + + return new ArraySegment(buffer.Array, buffer.Offset, (int)stream.Position); +#endif + } + + /// + /// Puts data on the wire + /// + /// The stream to read data from + async Task WriteStreamToNetwork(MemoryStream stream, CancellationToken cancellationToken) + { + ArraySegment buffer = GetBuffer(stream); + if(_stream is SslStream) + { + _messageQueue.Enqueue(buffer); + await _sendSemaphore.WaitAsync(); + try + { + while (_messageQueue.Count > 0) + { + var _buf = _messageQueue.Dequeue(); + try + { + if (_stream != null && _stream.CanWrite) + { + await _stream.WriteAsync(_buf.Array, _buf.Offset, _buf.Count, cancellationToken).ConfigureAwait(false); + } + } + catch (IOException) + { + // do nothing, the socket is not connected + } + catch (SocketException) + { + // do nothing, the socket is not connected + } + } + } + finally + { + _sendSemaphore.Release(); + } + } + else + { + await _stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Turns a spec websocket frame opcode into a WebSocketMessageType + /// + WebSocketOpCode GetOppCode(WebSocketMessageType messageType) + { + if (_isContinuationFrame) + { + return WebSocketOpCode.ContinuationFrame; + } + else + { + switch (messageType) + { + case WebSocketMessageType.Binary: + return WebSocketOpCode.BinaryFrame; + case WebSocketMessageType.Text: + return WebSocketOpCode.TextFrame; + case WebSocketMessageType.Close: + throw new NotSupportedException("Cannot use Send function to send a close frame. Use Close function."); + default: + throw new NotSupportedException($"MessageType {messageType} not supported"); + } + } + } + + /// + /// Automatic WebSocket close in response to some invalid data from the remote websocket host + /// + /// The close status to use + /// A description of why we are closing + /// The exception (for logging) + async Task CloseOutputAutoTimeoutAsync(WebSocketCloseStatus closeStatus, string statusDescription, Exception ex) + { + TimeSpan timeSpan = TimeSpan.FromSeconds(5); + Events.Log.CloseOutputAutoTimeout(_guid, closeStatus, statusDescription, ex.ToString()); + + try + { + // we may not want to send sensitive information to the client / server + if (_includeExceptionInCloseResponse) + { + statusDescription = statusDescription + "\r\n\r\n" + ex.ToString(); + } + + CancellationTokenSource autoCancel = new CancellationTokenSource(timeSpan); + await CloseOutputAsync(closeStatus, statusDescription, autoCancel.Token); + } + catch (OperationCanceledException) + { + // do not throw an exception because that will mask the original exception + Events.Log.CloseOutputAutoTimeoutCancelled(_guid, (int)timeSpan.TotalSeconds, closeStatus, statusDescription, ex.ToString()); + } + catch (Exception closeException) + { + // do not throw an exception because that will mask the original exception + Events.Log.CloseOutputAutoTimeoutError(_guid, closeException.ToString(), closeStatus, statusDescription, ex.ToString()); + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs.meta new file mode 100644 index 0000000..a573a62 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketImplementation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 087dfa54efe9345b390e0758d42e52cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs new file mode 100644 index 0000000..6ef2ac0 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs @@ -0,0 +1,12 @@ +namespace Ninja.WebSockets.Internal +{ + internal enum WebSocketOpCode + { + ContinuationFrame = 0, + TextFrame = 1, + BinaryFrame = 2, + ConnectionClose = 8, + Ping = 9, + Pong = 10 + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs.meta new file mode 100644 index 0000000..f568d56 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Internal/WebSocketOpCode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f2106e8022034b9180513e27b488b4c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md new file mode 100644 index 0000000..6fd6c11 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright 2018 David Haig + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md.meta new file mode 100644 index 0000000..9a7423e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/LICENCE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 681505da4aa4b48b8b6251521a495d64 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs new file mode 100644 index 0000000..762e625 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs @@ -0,0 +1,139 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Ninja.WebSockets.Internal; + +namespace Ninja.WebSockets +{ + /// + /// Ping Pong Manager used to facilitate ping pong WebSocket messages + /// + public class PingPongManager : IPingPongManager + { + readonly WebSocketImplementation _webSocket; + readonly Guid _guid; + readonly TimeSpan _keepAliveInterval; + readonly Task _pingTask; + readonly CancellationToken _cancellationToken; + Stopwatch _stopwatch; + long _pingSentTicks; + + /// + /// Raised when a Pong frame is received + /// + public event EventHandler Pong; + + /// + /// Initialises a new instance of the PingPongManager to facilitate ping pong WebSocket messages. + /// If you are manually creating an instance of this class then it is advisable to set keepAliveInterval to + /// TimeSpan.Zero when you create the WebSocket instance (using a factory) otherwise you may be automatically + /// be sending duplicate Ping messages (see keepAliveInterval below) + /// + /// The web socket used to listen to ping messages and send pong messages + /// The time between automatically sending ping messages. + /// Set this to TimeSpan.Zero if you with to manually control sending ping messages. + /// + /// The token used to cancel a pending ping send AND the automatic sending of ping messages + /// if keepAliveInterval is positive + public PingPongManager(Guid guid, WebSocket webSocket, TimeSpan keepAliveInterval, CancellationToken cancellationToken) + { + WebSocketImplementation webSocketImpl = webSocket as WebSocketImplementation; + _webSocket = webSocketImpl; + if (_webSocket == null) + throw new InvalidCastException("Cannot cast WebSocket to an instance of WebSocketImplementation. Please use the web socket factories to create a web socket"); + _guid = guid; + _keepAliveInterval = keepAliveInterval; + _cancellationToken = cancellationToken; + webSocketImpl.Pong += WebSocketImpl_Pong; + _stopwatch = Stopwatch.StartNew(); + + if (keepAliveInterval != TimeSpan.Zero) + { + Task.Run(PingForever, cancellationToken); + } + } + + /// + /// Sends a ping frame + /// + /// The payload (must be 125 bytes of less) + /// The cancellation token + public async Task SendPing(ArraySegment payload, CancellationToken cancellation) + { + await _webSocket.SendPingAsync(payload, cancellation); + } + + protected virtual void OnPong(PongEventArgs e) + { + Pong?.Invoke(this, e); + } + + async Task PingForever() + { + Events.Log.PingPongManagerStarted(_guid, (int)_keepAliveInterval.TotalSeconds); + + try + { + while (!_cancellationToken.IsCancellationRequested) + { + await Task.Delay(_keepAliveInterval, _cancellationToken); + + if (_webSocket.State != WebSocketState.Open) + { + break; + } + + if (_pingSentTicks != 0) + { + Events.Log.KeepAliveIntervalExpired(_guid, (int)_keepAliveInterval.TotalSeconds); + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, $"No Pong message received in response to a Ping after KeepAliveInterval {_keepAliveInterval}", _cancellationToken); + break; + } + + if (!_cancellationToken.IsCancellationRequested) + { + _pingSentTicks = _stopwatch.Elapsed.Ticks; + ArraySegment buffer = new ArraySegment(BitConverter.GetBytes(_pingSentTicks)); + await SendPing(buffer, _cancellationToken); + } + } + } + catch (OperationCanceledException) + { + // normal, do nothing + } + + Events.Log.PingPongManagerEnded(_guid); + } + + void WebSocketImpl_Pong(object sender, PongEventArgs e) + { + _pingSentTicks = 0; + OnPong(e); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs.meta new file mode 100644 index 0000000..80bb880 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PingPongManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e69d99db8e5024dda9afb8bb5d494260 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs new file mode 100644 index 0000000..027d06b --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ninja.WebSockets +{ + /// + /// Pong EventArgs + /// + public class PongEventArgs : EventArgs + { + /// + /// The data extracted from a Pong WebSocket frame + /// + public ArraySegment Payload { get; private set; } + + /// + /// Initialises a new instance of the PongEventArgs class + /// + /// The pong payload must be 125 bytes or less (can be zero bytes) + public PongEventArgs(ArraySegment payload) + { + Payload = payload; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs.meta new file mode 100644 index 0000000..6b75b16 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/PongEventArgs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57068e417e3ea4201a8de864e9a223a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties.meta new file mode 100644 index 0000000..afecd37 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c1e6c099dd56f4b30b02f4258c38ff05 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles.meta new file mode 100644 index 0000000..5fc2e66 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 52998228355394f00bf8c88cfad8e362 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..f873eee --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ + + + + + FileSystem + ReleaseSigned + netstandard2.0 + bin\ReleaseSigned\PublishOutput + Any CPU + + \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml.meta new file mode 100644 index 0000000..a829e20 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/Properties/PublishProfiles/FolderProfile.pubxml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5e1a5037feecd49deb7892a8b0b755c6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs new file mode 100644 index 0000000..a6f0546 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs @@ -0,0 +1,288 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Ninja.WebSockets.Exceptions; +using Ninja.WebSockets.Internal; + +namespace Ninja.WebSockets +{ + /// + /// Web socket client factory used to open web socket client connections + /// + public class WebSocketClientFactory : IWebSocketClientFactory + { + readonly Func _bufferFactory; + readonly IBufferPool _bufferPool; + + /// + /// Initialises a new instance of the WebSocketClientFactory class without caring about internal buffers + /// + public WebSocketClientFactory() + { + _bufferPool = new BufferPool(); + _bufferFactory = _bufferPool.GetBuffer; + } + + /// + /// Initialises a new instance of the WebSocketClientFactory class with control over internal buffer creation + /// + /// Used to get a memory stream. Feel free to implement your own buffer pool. MemoryStreams will be disposed when no longer needed and can be returned to the pool. + public WebSocketClientFactory(Func bufferFactory) + { + _bufferFactory = bufferFactory; + } + + /// + /// Connect with default options + /// + /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) + /// The optional cancellation token + /// A connected web socket instance + public async Task ConnectAsync(Uri uri, CancellationToken token = default(CancellationToken)) + { + return await ConnectAsync(uri, new WebSocketClientOptions(), token); + } + + /// + /// Connect with options specified + /// + /// The WebSocket uri to connect to (e.g. ws://example.com or wss://example.com for SSL) + /// The WebSocket client options + /// The optional cancellation token + /// A connected web socket instance + public async Task ConnectAsync(Uri uri, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)) + { + Guid guid = Guid.NewGuid(); + string host = uri.Host; + int port = uri.Port; + TcpClient tcpClient = new TcpClient(AddressFamily.InterNetworkV6); + tcpClient.NoDelay = options.NoDelay; + tcpClient.Client.DualMode = true; + string uriScheme = uri.Scheme.ToLower(); + bool useSsl = uriScheme == "wss" || uriScheme == "https"; + if (IPAddress.TryParse(host, out IPAddress ipAddress)) + { + Events.Log.ClientConnectingToIpAddress(guid, ipAddress.ToString(), port); + await tcpClient.ConnectAsync(ipAddress, port); + } + else + { + Events.Log.ClientConnectingToHost(guid, host, port); + await tcpClient.ConnectAsync(host, port); + } + + token.ThrowIfCancellationRequested(); + Stream stream = GetStream(guid, tcpClient, useSsl, host); + return await PerformHandshake(guid, uri, stream, options, token); + } + + /// + /// Connect with a stream that has already been opened and HTTP websocket upgrade request sent + /// This function will check the handshake response from the server and proceed if successful + /// Use this function if you have specific requirements to open a conenction like using special http headers and cookies + /// You will have to build your own HTTP websocket upgrade request + /// You may not even choose to use TCP/IP and this function will allow you to do that + /// + /// The full duplex response stream from the server + /// The secWebSocketKey you used in the handshake request + /// The WebSocket client options + /// The optional cancellation token + /// + public async Task ConnectAsync(Stream responseStream, string secWebSocketKey, WebSocketClientOptions options, CancellationToken token = default(CancellationToken)) + { + Guid guid = Guid.NewGuid(); + return await ConnectAsync(guid, responseStream, secWebSocketKey, options.KeepAliveInterval, options.SecWebSocketExtensions, options.IncludeExceptionInCloseResponse, token); + } + + async Task ConnectAsync(Guid guid, Stream responseStream, string secWebSocketKey, TimeSpan keepAliveInterval, string secWebSocketExtensions, bool includeExceptionInCloseResponse, CancellationToken token) + { + Events.Log.ReadingHttpResponse(guid); + string response = string.Empty; + + try + { + response = await HttpHelper.ReadHttpHeaderAsync(responseStream, token); + } + catch (Exception ex) + { + Events.Log.ReadHttpResponseError(guid, ex.ToString()); + throw new WebSocketHandshakeFailedException("Handshake unexpected failure", ex); + } + + ThrowIfInvalidResponseCode(response); + ThrowIfInvalidAcceptString(guid, response, secWebSocketKey); + string subProtocol = GetSubProtocolFromHeader(response); + return new WebSocketImplementation(guid, _bufferFactory, responseStream, keepAliveInterval, secWebSocketExtensions, includeExceptionInCloseResponse, true, subProtocol); + } + + string GetSubProtocolFromHeader(string response) + { + // make sure we escape the accept string which could contain special regex characters + string regexPattern = "Sec-WebSocket-Protocol: (.*)"; + Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase); + Match match = regex.Match(response); + if (match.Success) + { + return match.Groups[1].Value.Trim(); + } + + return null; + } + + void ThrowIfInvalidAcceptString(Guid guid, string response, string secWebSocketKey) + { + // make sure we escape the accept string which could contain special regex characters + string regexPattern = "Sec-WebSocket-Accept: (.*)"; + Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase); + string actualAcceptString = regex.Match(response).Groups[1].Value.Trim(); + + // check the accept string + string expectedAcceptString = HttpHelper.ComputeSocketAcceptString(secWebSocketKey); + if (expectedAcceptString != actualAcceptString) + { + string warning = string.Format($"Handshake failed because the accept string from the server '{expectedAcceptString}' was not the expected string '{actualAcceptString}'"); + Events.Log.HandshakeFailure(guid, warning); + throw new WebSocketHandshakeFailedException(warning); + } + else + { + Events.Log.ClientHandshakeSuccess(guid); + } + } + + void ThrowIfInvalidResponseCode(string responseHeader) + { + string responseCode = HttpHelper.ReadHttpResponseCode(responseHeader); + if (!string.Equals(responseCode, "101 Switching Protocols", StringComparison.InvariantCultureIgnoreCase)) + { + string[] lines = responseHeader.Split(new string[] { "\r\n" }, StringSplitOptions.None); + + for (int i = 0; i < lines.Length; i++) + { + // if there is more to the message than just the header + if (string.IsNullOrWhiteSpace(lines[i])) + { + StringBuilder builder = new StringBuilder(); + for (int j = i + 1; j < lines.Length - 1; j++) + { + builder.AppendLine(lines[j]); + } + + string responseDetails = builder.ToString(); + throw new InvalidHttpResponseCodeException(responseCode, responseDetails, responseHeader); + } + } + } + } + + Stream GetStream(Guid guid, TcpClient tcpClient, bool isSecure, string host) + { + Stream stream = tcpClient.GetStream(); + + if (isSecure) + { + SslStream sslStream = new SslStream(stream, false, new RemoteCertificateValidationCallback(ValidateServerCertificate), null); + Events.Log.AttemtingToSecureSslConnection(guid); + + // This will throw an AuthenticationException if the certificate is not valid + sslStream.AuthenticateAsClient(host); + Events.Log.ConnectionSecured(guid); + return sslStream; + } + else + { + Events.Log.ConnectionNotSecure(guid); + return stream; + } + } + + /// + /// Invoked by the RemoteCertificateValidationDelegate + /// If you want to ignore certificate errors (for debugging) then return true + /// + static bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + Events.Log.SslCertificateError(sslPolicyErrors); + + // Do not allow this client to communicate with unauthenticated servers. + return false; + } + + static string GetAdditionalHeaders(Dictionary additionalHeaders) + { + if (additionalHeaders == null || additionalHeaders.Count == 0) + { + return string.Empty; + } + else + { + StringBuilder builder = new StringBuilder(); + foreach (KeyValuePair pair in additionalHeaders) + { + builder.Append($"{pair.Key}: {pair.Value}\r\n"); + } + + return builder.ToString(); + } + } + + async Task PerformHandshake(Guid guid, Uri uri, Stream stream, WebSocketClientOptions options, CancellationToken token) + { + Random rand = new Random(); + byte[] keyAsBytes = new byte[16]; + rand.NextBytes(keyAsBytes); + string secWebSocketKey = Convert.ToBase64String(keyAsBytes); + string additionalHeaders = GetAdditionalHeaders(options.AdditionalHttpHeaders); + string handshakeHttpRequest = $"GET {uri.PathAndQuery} HTTP/1.1\r\n" + + $"Host: {uri.Host}:{uri.Port}\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + $"Sec-WebSocket-Key: {secWebSocketKey}\r\n" + + $"Origin: http://{uri.Host}:{uri.Port}\r\n" + + $"Sec-WebSocket-Protocol: {options.SecWebSocketProtocol}\r\n" + + additionalHeaders + + "Sec-WebSocket-Version: 13\r\n\r\n"; + + byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest); + stream.Write(httpRequest, 0, httpRequest.Length); + Events.Log.HandshakeSent(guid, handshakeHttpRequest); + return await ConnectAsync(stream, secWebSocketKey, options, token); + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs.meta new file mode 100644 index 0000000..a93bf3e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38af07c5cfa1940f2bd0b981bccea293 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs new file mode 100644 index 0000000..b6318a8 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ninja.WebSockets +{ + /// + /// Client WebSocket init options + /// + public class WebSocketClientOptions + { + /// + /// How often to send ping requests to the Server + /// This is done to prevent proxy servers from closing your connection + /// The default is TimeSpan.Zero meaning that it is disabled. + /// WebSocket servers usually send ping messages so it is not normally necessary for the client to send them (hence the TimeSpan.Zero default) + /// You can manually control ping pong messages using the PingPongManager class. + /// If you do that it is advisible to set this KeepAliveInterval to zero for the WebSocketClientFactory + /// + public TimeSpan KeepAliveInterval { get; set; } + + /// + /// Set to true to send a message immediately with the least amount of latency (typical usage for chat) + /// This will disable Nagle's algorithm which can cause high tcp latency for small packets sent infrequently + /// However, if you are streaming large packets or sending large numbers of small packets frequently it is advisable to set NoDelay to false + /// This way data will be bundled into larger packets for better throughput + /// + public bool NoDelay { get; set; } + + /// + /// Add any additional http headers to this dictionary + /// + public Dictionary AdditionalHttpHeaders { get; set; } + + /// + /// Include the full exception (with stack trace) in the close response + /// when an exception is encountered and the WebSocket connection is closed + /// The default is false + /// + public bool IncludeExceptionInCloseResponse { get; set; } + + /// + /// WebSocket Extensions as an HTTP header value + /// + public string SecWebSocketExtensions { get; set; } + + /// + /// A comma separated list of sub protocols in preference order (first one being the most preferred) + /// The server will return the first supported sub protocol (or none if none are supported) + /// Can be null + /// + public string SecWebSocketProtocol { get; set; } + + /// + /// Initialises a new instance of the WebSocketClientOptions class + /// + public WebSocketClientOptions() + { + KeepAliveInterval = TimeSpan.Zero; + NoDelay = true; + AdditionalHttpHeaders = new Dictionary(); + IncludeExceptionInCloseResponse = false; + SecWebSocketProtocol = null; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs.meta new file mode 100644 index 0000000..45c8305 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketClientOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f2911aa44c1154bf88781555542a9de3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs new file mode 100644 index 0000000..f1c85dc --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; + +namespace Ninja.WebSockets +{ + /// + /// The WebSocket HTTP Context used to initiate a WebSocket handshake + /// + public class WebSocketHttpContext + { + /// + /// True if this is a valid WebSocket request + /// + public bool IsWebSocketRequest { get; private set; } + + public IList WebSocketRequestedProtocols { get; private set; } + + /// + /// The raw http header extracted from the stream + /// + public string HttpHeader { get; private set; } + + /// + /// The Path extracted from the http header + /// + public string Path { get; private set; } + + /// + /// The stream AFTER the header has already been read + /// + public Stream Stream { get; private set; } + + /// + /// Initialises a new instance of the WebSocketHttpContext class + /// + /// True if this is a valid WebSocket request + /// The raw http header extracted from the stream + /// The Path extracted from the http header + /// The stream AFTER the header has already been read + public WebSocketHttpContext(bool isWebSocketRequest, IList webSocketRequestedProtocols, string httpHeader, string path, Stream stream) + { + IsWebSocketRequest = isWebSocketRequest; + WebSocketRequestedProtocols = webSocketRequestedProtocols; + HttpHeader = httpHeader; + Path = path; + Stream = stream; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs.meta new file mode 100644 index 0000000..7e38e8a --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketHttpContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: efbbd1ab9e45e4dd3b88faaca1048c7f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs new file mode 100644 index 0000000..a638ce0 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs @@ -0,0 +1,169 @@ +// --------------------------------------------------------------------- +// Copyright 2018 David Haig +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// --------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Ninja.WebSockets.Exceptions; +using Ninja.WebSockets.Internal; + +namespace Ninja.WebSockets +{ + /// + /// Web socket server factory used to open web socket server connections + /// + public class WebSocketServerFactory : IWebSocketServerFactory + { + readonly Func _bufferFactory; + readonly IBufferPool _bufferPool; + + /// + /// Initialises a new instance of the WebSocketServerFactory class without caring about internal buffers + /// + public WebSocketServerFactory() + { + _bufferPool = new BufferPool(); + _bufferFactory = _bufferPool.GetBuffer; + } + + /// + /// Initialises a new instance of the WebSocketClientFactory class with control over internal buffer creation + /// + /// Used to get a memory stream. Feel free to implement your own buffer pool. MemoryStreams will be disposed when no longer needed and can be returned to the pool. + /// + public WebSocketServerFactory(Func bufferFactory) + { + _bufferFactory = bufferFactory; + } + + /// + /// Reads a http header information from a stream and decodes the parts relating to the WebSocket protocot upgrade + /// + /// The network stream + /// The optional cancellation token + /// Http data read from the stream + public async Task ReadHttpHeaderFromStreamAsync(Stream stream, CancellationToken token = default(CancellationToken)) + { + string header = await HttpHelper.ReadHttpHeaderAsync(stream, token); + string path = HttpHelper.GetPathFromHeader(header); + bool isWebSocketRequest = HttpHelper.IsWebSocketUpgradeRequest(header); + IList subProtocols = HttpHelper.GetSubProtocols(header); + return new WebSocketHttpContext(isWebSocketRequest, subProtocols, header, path, stream); + } + + /// + /// Accept web socket with default options + /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext + /// + /// The http context used to initiate this web socket request + /// The optional cancellation token + /// A connected web socket + public async Task AcceptWebSocketAsync(WebSocketHttpContext context, CancellationToken token = default(CancellationToken)) + { + return await AcceptWebSocketAsync(context, new WebSocketServerOptions(), token); + } + + /// + /// Accept web socket with options specified + /// Call ReadHttpHeaderFromStreamAsync first to get WebSocketHttpContext + /// + /// The http context used to initiate this web socket request + /// The web socket options + /// The optional cancellation token + /// A connected web socket + public async Task AcceptWebSocketAsync(WebSocketHttpContext context, WebSocketServerOptions options, CancellationToken token = default(CancellationToken)) + { + Guid guid = Guid.NewGuid(); + Events.Log.AcceptWebSocketStarted(guid); + await PerformHandshakeAsync(guid, context.HttpHeader, options.SubProtocol, context.Stream, token); + Events.Log.ServerHandshakeSuccess(guid); + string secWebSocketExtensions = null; + return new WebSocketImplementation(guid, _bufferFactory, context.Stream, options.KeepAliveInterval, secWebSocketExtensions, options.IncludeExceptionInCloseResponse, false, options.SubProtocol); + } + + static void CheckWebSocketVersion(string httpHeader) + { + Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)", RegexOptions.IgnoreCase); + + // check the version. Support version 13 and above + const int WebSocketVersion = 13; + Match match = webSocketVersionRegex.Match(httpHeader); + if (match.Success) + { + int secWebSocketVersion = Convert.ToInt32(match.Groups[1].Value.Trim()); + if (secWebSocketVersion < WebSocketVersion) + { + throw new WebSocketVersionNotSupportedException(string.Format("WebSocket Version {0} not suported. Must be {1} or above", secWebSocketVersion, WebSocketVersion)); + } + } + else + { + throw new WebSocketVersionNotSupportedException("Cannot find \"Sec-WebSocket-Version\" in http header"); + } + } + + static async Task PerformHandshakeAsync(Guid guid, String httpHeader, string subProtocol, Stream stream, CancellationToken token) + { + try + { + Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)", RegexOptions.IgnoreCase); + CheckWebSocketVersion(httpHeader); + + Match match = webSocketKeyRegex.Match(httpHeader); + if (match.Success) + { + string secWebSocketKey = match.Groups[1].Value.Trim(); + string setWebSocketAccept = HttpHelper.ComputeSocketAcceptString(secWebSocketKey); + string response = ("HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: websocket\r\n" + + (subProtocol != null ? $"Sec-WebSocket-Protocol: {subProtocol}\r\n" : "") + + $"Sec-WebSocket-Accept: {setWebSocketAccept}"); + + Events.Log.SendingHandshakeResponse(guid, response); + await HttpHelper.WriteHttpHeaderAsync(response, stream, token); + } + else + { + throw new SecWebSocketKeyMissingException("Unable to read \"Sec-WebSocket-Key\" from http header"); + } + } + catch (WebSocketVersionNotSupportedException ex) + { + Events.Log.WebSocketVersionNotSupported(guid, ex.ToString()); + string response = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocket-Version: 13" + ex.Message; + await HttpHelper.WriteHttpHeaderAsync(response, stream, token); + throw; + } + catch (Exception ex) + { + Events.Log.BadRequest(guid, ex.ToString()); + await HttpHelper.WriteHttpHeaderAsync("HTTP/1.1 400 Bad Request", stream, token); + throw; + } + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs.meta new file mode 100644 index 0000000..920e27a --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5fcfcd10538542edb4842d81798f4dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs new file mode 100644 index 0000000..c347bd5 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs @@ -0,0 +1,45 @@ +using System; + +namespace Ninja.WebSockets +{ + /// + /// Server WebSocket init options + /// + public class WebSocketServerOptions + { + /// + /// How often to send ping requests to the Client + /// The default is 60 seconds + /// This is done to prevent proxy servers from closing your connection + /// A timespan of zero will disable the automatic ping pong mechanism + /// You can manually control ping pong messages using the PingPongManager class. + /// If you do that it is advisible to set this KeepAliveInterval to zero in the WebSocketServerFactory + /// + public TimeSpan KeepAliveInterval { get; set; } + + /// + /// Include the full exception (with stack trace) in the close response + /// when an exception is encountered and the WebSocket connection is closed + /// The default is false + /// + public bool IncludeExceptionInCloseResponse { get; set; } + + /// + /// Specifies the sub protocol to send back to the client in the opening handshake + /// Can be null (the most common use case) + /// The client can specify multiple preferred protocols in the opening handshake header + /// The server should use the first supported one or set this to null if none of the requested sub protocols are supported + /// + public string SubProtocol { get; set; } + + /// + /// Initialises a new instance of the WebSocketServerOptions class + /// + public WebSocketServerOptions() + { + KeepAliveInterval = TimeSpan.FromSeconds(60); + IncludeExceptionInCloseResponse = false; + SubProtocol = null; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs.meta new file mode 100644 index 0000000..adfcf1d --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Ninja.WebSockets/WebSocketServerOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f8d9d315d665461a94bc139e46cbfce +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins.meta new file mode 100644 index 0000000..6d6de23 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b64fa4674492a4cf1857ecee9f73fcb1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib new file mode 100644 index 0000000..ff1e622 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib @@ -0,0 +1,108 @@ +var LibraryWebSockets = { + $webSocketInstances: [], + + SocketCreate: function(url, id, onopen, ondata, onclose) + { + var str = Pointer_stringify(url); + + var socket = new WebSocket(str, "binary"); + + socket.binaryType = 'arraybuffer'; + + socket.onopen = function(e) { + Runtime.dynCall('vi', onopen, [id]); + } + + socket.onerror = function(e) { + console.log("websocket error " + JSON.stringify(e)); + } + + socket.onmessage = function (e) { + // Todo: handle other data types? + if (e.data instanceof Blob) + { + var reader = new FileReader(); + reader.addEventListener("loadend", function() { + var array = new Uint8Array(reader.result); + }); + reader.readAsArrayBuffer(e.data); + } + else if (e.data instanceof ArrayBuffer) + { + var array = new Uint8Array(e.data); + var ptr = _malloc(array.length); + var dataHeap = new Uint8Array(HEAPU8.buffer, ptr, array.length); + dataHeap.set(array); + Runtime.dynCall('viii', ondata, [id, ptr, array.length]); + _free(ptr); + } + else if(typeof e.data === "string") { + var reader = new FileReader(); + reader.addEventListener("loadend", function() { + var array = new Uint8Array(reader.result); + }); + var blob = new Blob([e.data]); + reader.readAsArrayBuffer(blob); + } + }; + + socket.onclose = function (e) { + Runtime.dynCall('vi', onclose, [id]); + + if (e.code != 1000) + { + if (e.reason != null && e.reason.length > 0) + socket.error = e.reason; + else + { + switch (e.code) + { + case 1001: + socket.error = "Endpoint going away."; + break; + case 1002: + socket.error = "Protocol error."; + break; + case 1003: + socket.error = "Unsupported message."; + break; + case 1005: + socket.error = "No status."; + break; + case 1006: + socket.error = "Abnormal disconnection."; + break; + case 1009: + socket.error = "Data frame too large."; + break; + default: + socket.error = "Error "+e.code; + } + } + } + } + var instance = webSocketInstances.push(socket) - 1; + return instance; + }, + + SocketState: function (socketInstance) + { + var socket = webSocketInstances[socketInstance]; + return socket.readyState; + }, + + SocketSend: function (socketInstance, ptr, length) + { + var socket = webSocketInstances[socketInstance]; + socket.send (HEAPU8.buffer.slice(ptr, ptr+length)); + }, + + SocketClose: function (socketInstance) + { + var socket = webSocketInstances[socketInstance]; + socket.close(); + } +}; + +autoAddDeps(LibraryWebSockets, '$webSocketInstances'); +mergeInto(LibraryManager.library, LibraryWebSockets); \ No newline at end of file diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib.meta new file mode 100644 index 0000000..67daa5e --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Plugins/WebSocket.jslib.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: 3fba16b22ae274c729f6e8f91c425355 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Facebook: WebGL + second: + enabled: 1 + settings: {} + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs new file mode 100644 index 0000000..9afd8c3 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Ninja.WebSockets; +using UnityEngine; + +namespace Mirror.Websocket +{ + public class Server + { + public event Action Connected; + public event Action> ReceivedData; + public event Action Disconnected; + public event Action ReceivedError; + + const int MaxMessageSize = 256 * 1024; + + // listener + TcpListener listener; + readonly IWebSocketServerFactory webSocketServerFactory = new WebSocketServerFactory(); + + CancellationTokenSource cancellation; + + // clients with + Dictionary clients = new Dictionary(); + + public bool NoDelay = true; + + // connectionId counter + // (right now we only use it from one listener thread, but we might have + // multiple threads later in case of WebSockets etc.) + // -> static so that another server instance doesn't start at 0 again. + static int counter = 0; + + // public next id function in case someone needs to reserve an id + // (e.g. if hostMode should always have 0 connection and external + // connections should start at 1, etc.) + public static int NextConnectionId() + { + int id = Interlocked.Increment(ref counter); + + // it's very unlikely that we reach the uint limit of 2 billion. + // even with 1 new connection per second, this would take 68 years. + // -> but if it happens, then we should throw an exception because + // the caller probably should stop accepting clients. + // -> it's hardly worth using 'bool Next(out id)' for that case + // because it's just so unlikely. + if (id == int.MaxValue) + { + throw new Exception("connection id limit reached: " + id); + } + + return id; + } + + // check if the server is running + public bool Active + { + get { return listener != null; } + } + + public WebSocket GetClient(int connectionId) + { + // paul: null is evil, throw exception if not found + return clients[connectionId]; + } + + public bool _secure = false; + + public SslConfiguration _sslConfig; + + public class SslConfiguration + { + public System.Security.Cryptography.X509Certificates.X509Certificate2 Certificate; + public bool ClientCertificateRequired; + public System.Security.Authentication.SslProtocols EnabledSslProtocols; + public bool CheckCertificateRevocation; + } + + public async void Listen(int port) + { + try + { + cancellation = new CancellationTokenSource(); + + listener = TcpListener.Create(port); + listener.Server.NoDelay = this.NoDelay; + listener.Start(); + Debug.Log($"Websocket server started listening on port {port}"); + while (true) + { + TcpClient tcpClient = await listener.AcceptTcpClientAsync(); + ProcessTcpClient(tcpClient, cancellation.Token); + } + } + catch (ObjectDisposedException) + { + // do nothing. This will be thrown if the Listener has been stopped + } + catch (Exception ex) + { + ReceivedError?.Invoke(0, ex); + } + } + + async void ProcessTcpClient(TcpClient tcpClient, CancellationToken token) + { + + try + { + // this worker thread stays alive until either of the following happens: + // Client sends a close conection request OR + // An unhandled exception is thrown OR + // The server is disposed + + // get a secure or insecure stream + Stream stream = tcpClient.GetStream(); + if (_secure) + { + SslStream sslStream = new SslStream(stream, false, CertVerificationCallback); + sslStream.AuthenticateAsServer(_sslConfig.Certificate, _sslConfig.ClientCertificateRequired, _sslConfig.EnabledSslProtocols, _sslConfig.CheckCertificateRevocation); + stream = sslStream; + } + WebSocketHttpContext context = await webSocketServerFactory.ReadHttpHeaderFromStreamAsync(stream, token); + if (context.IsWebSocketRequest) + { + WebSocketServerOptions options = new WebSocketServerOptions() { KeepAliveInterval = TimeSpan.FromSeconds(30), SubProtocol = "binary" }; + + WebSocket webSocket = await webSocketServerFactory.AcceptWebSocketAsync(context, options); + + await ReceiveLoopAsync(webSocket, token); + } + else + { + Debug.Log("Http header contains no web socket upgrade request. Ignoring"); + } + + } + catch(IOException) + { + // do nothing. This will be thrown if the transport is closed + } + catch (ObjectDisposedException) + { + // do nothing. This will be thrown if the Listener has been stopped + } + catch (Exception ex) + { + ReceivedError?.Invoke(0, ex); + } + finally + { + try + { + tcpClient.Client.Close(); + tcpClient.Close(); + } + catch (Exception ex) + { + ReceivedError?.Invoke(0, ex); + } + } + } + + bool CertVerificationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + // Much research has been done on this. When this is initiated from a HTTPS/WSS stream, + // the certificate is null and the SslPolicyErrors is RemoteCertificateNotAvailable. + // Meaning we CAN'T verify this and this is all we can do. + return true; + } + + async Task ReceiveLoopAsync(WebSocket webSocket, CancellationToken token) + { + int connectionId = NextConnectionId(); + clients.Add(connectionId, webSocket); + + byte[] buffer = new byte[MaxMessageSize]; + + try + { + // someone connected, raise event + Connected?.Invoke(connectionId); + + + while (true) + { + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); + + if (result.MessageType == WebSocketMessageType.Close) + { + Debug.Log($"Client initiated close. Status: {result.CloseStatus} Description: {result.CloseStatusDescription}"); + break; + } + + ArraySegment data = await ReadFrames(connectionId, result, webSocket, buffer, token); + + if (data.Count == 0) + break; + + try + { + // we received some data, raise event + ReceivedData?.Invoke(connectionId, data); + } + catch (Exception exception) + { + ReceivedError?.Invoke(connectionId, exception); + } + } + + } + catch (Exception exception) + { + ReceivedError?.Invoke(connectionId, exception); + } + finally + { + clients.Remove(connectionId); + Disconnected?.Invoke(connectionId); + } + } + + // a message might come splitted in multiple frames + // collect all frames + async Task> ReadFrames(int connectionId, WebSocketReceiveResult result, WebSocket webSocket, byte[] buffer, CancellationToken token) + { + int count = result.Count; + + while (!result.EndOfMessage) + { + if (count >= MaxMessageSize) + { + string closeMessage = string.Format("Maximum message size: {0} bytes.", MaxMessageSize); + await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, CancellationToken.None); + ReceivedError?.Invoke(connectionId, new WebSocketException(WebSocketError.HeaderError)); + return new ArraySegment(); + } + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer, count, MaxMessageSize - count), CancellationToken.None); + count += result.Count; + + } + return new ArraySegment(buffer, 0, count); + } + + public void Stop() + { + // only if started + if (!Active) return; + + Debug.Log("Server: stopping..."); + cancellation.Cancel(); + + // stop listening to connections so that no one can connect while we + // close the client connections + listener.Stop(); + + // clear clients list + clients.Clear(); + listener = null; + } + + // send message to client using socket connection or throws exception + public async void Send(int connectionId, byte[] data) + { + // find the connection + if (clients.TryGetValue(connectionId, out WebSocket client)) + { + try + { + await client.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, cancellation.Token); + } + catch (ObjectDisposedException) { + // connection has been closed, swallow exception + Disconnect(connectionId); + } + catch (Exception exception) + { + if (clients.ContainsKey(connectionId)) + { + // paul: If someone unplugs their internet + // we can potentially get hundreds of errors here all at once + // because all the WriteAsync wake up at once and throw exceptions + + // by hiding inside this if, I ensure that we only report the first error + // all other errors are swallowed. + // this prevents a log storm that freezes the server for several seconds + ReceivedError?.Invoke(connectionId, exception); + } + + Disconnect(connectionId); + } + } + else + { + ReceivedError?.Invoke(connectionId, new SocketException((int)SocketError.NotConnected)); + } + } + + // get connection info in case it's needed (IP etc.) + // (we should never pass the TcpClient to the outside) + public string GetClientAddress(int connectionId) + { + // find the connection + if (clients.TryGetValue(connectionId, out WebSocket client)) + { + return ""; + } + return null; + } + + // disconnect (kick) a client + public bool Disconnect(int connectionId) + { + // find the connection + if (clients.TryGetValue(connectionId, out WebSocket client)) + { + clients.Remove(connectionId); + // just close it. client thread will take care of the rest. + client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + Debug.Log("Server.Disconnect connectionId:" + connectionId); + return true; + } + return false; + } + + public override string ToString() + { + if (Active) + { + return $"Websocket server {listener.LocalEndpoint}"; + } + return ""; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs.meta new file mode 100644 index 0000000..44d8af0 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/Server.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b4bf9040513294fa4939bb9f2f0cda4e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs b/Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs new file mode 100644 index 0000000..6dd1a92 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs @@ -0,0 +1,132 @@ +using System; +using UnityEngine; + +namespace Mirror.Websocket +{ + public class WebsocketTransport : Transport + { + + protected Client client = new Client(); + protected Server server = new Server(); + + public int port = 7778; + + public bool Secure; + public string CertificatePath; + public string CertificatePassword; + + [Tooltip("Nagle Algorithm can be disabled by enabling NoDelay")] + public bool NoDelay = true; + + public WebsocketTransport() + { + // dispatch the events from the server + server.Connected += (connectionId) => OnServerConnected.Invoke(connectionId); + server.Disconnected += (connectionId) => OnServerDisconnected.Invoke(connectionId); + server.ReceivedData += (connectionId, data) => OnServerDataReceived.Invoke(connectionId, data); + server.ReceivedError += (connectionId, error) => OnServerError.Invoke(connectionId, error); + + // dispatch events from the client + client.Connected += () => OnClientConnected.Invoke(); + client.Disconnected += () => OnClientDisconnected.Invoke(); + client.ReceivedData += (data) => OnClientDataReceived.Invoke(data); + client.ReceivedError += (error) => OnClientError.Invoke(error); + + // configure + client.NoDelay = NoDelay; + server.NoDelay = NoDelay; + + Debug.Log("Websocket transport initialized!"); + } + + public override bool Available() + { + // WebSockets should be available on all platforms, including WebGL (automatically) using our included JSLIB code + return true; + } + + // client + public override bool ClientConnected() => client.IsConnected; + + public override void ClientConnect(string host) + { + if (Secure) + { + client.Connect(new Uri($"wss://{host}:{port}")); + } + else + { + client.Connect(new Uri($"ws://{host}:{port}")); + } + } + + public override bool ClientSend(int channelId, byte[] data) { client.Send(data); return true; } + + public override void ClientDisconnect() => client.Disconnect(); + + // server + public override bool ServerActive() => server.Active; + + public override void ServerStart() + { + server._secure = Secure; + if (Secure) + { + server._secure = Secure; + server._sslConfig = new Server.SslConfiguration + { + Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2( + System.IO.Path.Combine(Application.dataPath, CertificatePath), + CertificatePassword), + ClientCertificateRequired = false, + CheckCertificateRevocation = false, + EnabledSslProtocols = System.Security.Authentication.SslProtocols.Default + }; + } + server.Listen(port); + } + + public override bool ServerSend(int connectionId, int channelId, byte[] data) + { + server.Send(connectionId, data); + return true; + } + + public override bool ServerDisconnect(int connectionId) + { + return server.Disconnect(connectionId); + } + + public override string ServerGetClientAddress(int connectionId) + { + return server.GetClientAddress(connectionId); + } + public override void ServerStop() => server.Stop(); + + // common + public override void Shutdown() + { + client.Disconnect(); + server.Stop(); + } + + public override int GetMaxPacketSize(int channelId) + { + // Telepathy's limit is Array.Length, which is int + return int.MaxValue; + } + + public override string ToString() + { + if (client.Connecting || client.IsConnected) + { + return client.ToString(); + } + if (server.Active) + { + return server.ToString(); + } + return ""; + } + } +} diff --git a/Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs.meta b/Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs.meta new file mode 100644 index 0000000..aab9380 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/Transport/Websocket/WebsocketTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f039183eda8945448b822a77e2a9d0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Packages/Mirror/Runtime/UNetwork.cs b/Assets/Packages/Mirror/Runtime/UNetwork.cs new file mode 100644 index 0000000..a515c41 --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/UNetwork.cs @@ -0,0 +1,107 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Mirror +{ + // Handles network messages on client and server + public delegate void NetworkMessageDelegate(NetworkMessage netMsg); + + // Handles requests to spawn objects on the client + public delegate GameObject SpawnDelegate(Vector3 position, Guid assetId); + + // Handles requests to unspawn objects on the client + public delegate void UnSpawnDelegate(GameObject spawned); + + // invoke type for Cmd/Rpc/SyncEvents + public enum MirrorInvokeType + { + Command, + ClientRpc, + SyncEvent + } + + // built-in system network messages + // original HLAPI uses short, so let's keep short to not break packet header etc. + // => use .ToString() to get the field name from the field value + // => we specify the short values so it's easier to look up opcodes when debugging packets + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use Send with no message id instead")] + public enum MsgType : short + { + // internal system messages - cannot be replaced by user code + ObjectDestroy = 1, + Rpc = 2, + Owner = 4, + Command = 5, + SyncEvent = 7, + UpdateVars = 8, + SpawnPrefab = 3, + SpawnSceneObject = 10, + SpawnStarted = 11, + SpawnFinished = 12, + ObjectHide = 13, + LocalClientAuthority = 15, + + // public system messages - can be replaced by user code + Connect = 32, + Disconnect = 33, + Error = 34, + Ready = 35, + NotReady = 36, + AddPlayer = 37, + RemovePlayer = 38, + Scene = 39, + + // time synchronization + Ping = 43, + Pong = 44, + + Highest = 47 + } + + public enum Version + { + Current = 1 + } + + public static class Channels + { + public const int DefaultReliable = 0; + public const int DefaultUnreliable = 1; + } + + // -- helpers for float conversion without allocations -- + [StructLayout(LayoutKind.Explicit)] + internal struct UIntFloat + { + [FieldOffset(0)] + public float floatValue; + + [FieldOffset(0)] + public uint intValue; + } + + [StructLayout(LayoutKind.Explicit)] + internal struct UIntDouble + { + [FieldOffset(0)] + public double doubleValue; + + [FieldOffset(0)] + public ulong longValue; + } + + [StructLayout(LayoutKind.Explicit)] + internal struct UIntDecimal + { + [FieldOffset(0)] + public ulong longValue1; + + [FieldOffset(8)] + public ulong longValue2; + + [FieldOffset(0)] + public decimal decimalValue; + } +} diff --git a/Assets/Packages/Mirror/Runtime/UNetwork.cs.meta b/Assets/Packages/Mirror/Runtime/UNetwork.cs.meta new file mode 100644 index 0000000..0ee79ed --- /dev/null +++ b/Assets/Packages/Mirror/Runtime/UNetwork.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b530ce39098b54374a29ad308c8e4554 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Telepathy.dll b/Assets/Telepathy.dll deleted file mode 100644 index 3af32ed5357c7ae1d7291efdf1b4744d7a85fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14336 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